Show SSH heartbeat status on blank browser tabs
Propagate workspace remote state + heartbeat into browser panels, add heartbeat emission in remote session controller, extend docker bootstrap regression for heartbeat continuity after blank browser open, and scrub workspace/surface env vars in tagged reload launches.
This commit is contained in:
parent
803912a9e3
commit
0d3d71661d
5 changed files with 279 additions and 9 deletions
|
|
@ -10,6 +10,13 @@ struct BrowserProxyEndpoint: Equatable {
|
|||
let port: Int
|
||||
}
|
||||
|
||||
struct BrowserRemoteWorkspaceStatus: Equatable {
|
||||
let target: String
|
||||
let connectionState: WorkspaceRemoteConnectionState
|
||||
let heartbeatCount: Int
|
||||
let lastHeartbeatAt: Date?
|
||||
}
|
||||
|
||||
enum BrowserSearchEngine: String, CaseIterable, Identifiable {
|
||||
case google
|
||||
case duckduckgo
|
||||
|
|
@ -1305,6 +1312,9 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
/// Published loading state
|
||||
@Published private(set) var isLoading: Bool = false
|
||||
|
||||
/// Snapshot of remote SSH connection status for this panel's workspace.
|
||||
@Published private(set) var remoteWorkspaceStatus: BrowserRemoteWorkspaceStatus?
|
||||
|
||||
/// Published download state for browser downloads (navigation + context menu).
|
||||
@Published private(set) var isDownloading: Bool = false
|
||||
|
||||
|
|
@ -1513,6 +1523,11 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
applyRemoteProxyConfigurationIfAvailable()
|
||||
}
|
||||
|
||||
func setRemoteWorkspaceStatus(_ status: BrowserRemoteWorkspaceStatus?) {
|
||||
guard remoteWorkspaceStatus != status else { return }
|
||||
remoteWorkspaceStatus = status
|
||||
}
|
||||
|
||||
private func applyRemoteProxyConfigurationIfAvailable() {
|
||||
guard #available(macOS 14.0, *) else { return }
|
||||
|
||||
|
|
|
|||
|
|
@ -729,19 +729,101 @@ struct BrowserPanelView: View {
|
|||
}
|
||||
})
|
||||
} else {
|
||||
Color(nsColor: browserChromeBackgroundColor)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onRequestPanelFocus()
|
||||
if addressBarFocused {
|
||||
addressBarFocused = false
|
||||
}
|
||||
ZStack(alignment: .topLeading) {
|
||||
Color(nsColor: browserChromeBackgroundColor)
|
||||
if let status = panel.remoteWorkspaceStatus {
|
||||
remoteWorkspaceBlankStateIndicator(status)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 12)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onRequestPanelFocus()
|
||||
if addressBarFocused {
|
||||
addressBarFocused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.zIndex(0)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func remoteWorkspaceBlankStateIndicator(_ status: BrowserRemoteWorkspaceStatus) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: remoteStatusSymbolName(status.connectionState))
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(remoteStatusColor(status.connectionState))
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("SSH \(status.target)")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(Color.primary.opacity(0.92))
|
||||
.lineLimit(1)
|
||||
Text(remoteStatusDetailText(status))
|
||||
.font(.system(size: 10, weight: .regular))
|
||||
.foregroundStyle(Color.primary.opacity(0.72))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(nsColor: NSColor.controlBackgroundColor).opacity(0.85))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.stroke(Color.primary.opacity(0.12), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func remoteStatusSymbolName(_ state: WorkspaceRemoteConnectionState) -> String {
|
||||
switch state {
|
||||
case .connected:
|
||||
return "network"
|
||||
case .connecting:
|
||||
return "arrow.triangle.2.circlepath"
|
||||
case .error:
|
||||
return "exclamationmark.triangle.fill"
|
||||
case .disconnected:
|
||||
return "network.slash"
|
||||
}
|
||||
}
|
||||
|
||||
private func remoteStatusColor(_ state: WorkspaceRemoteConnectionState) -> Color {
|
||||
switch state {
|
||||
case .connected:
|
||||
return .green
|
||||
case .connecting:
|
||||
return .orange
|
||||
case .error:
|
||||
return .red
|
||||
case .disconnected:
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
private func remoteStatusDetailText(_ status: BrowserRemoteWorkspaceStatus) -> String {
|
||||
switch status.connectionState {
|
||||
case .connected:
|
||||
guard let lastHeartbeat = status.lastHeartbeatAt else {
|
||||
return "Connected, waiting for heartbeat"
|
||||
}
|
||||
let ageSeconds = max(0, Int(Date().timeIntervalSince(lastHeartbeat)))
|
||||
return "Connected, heartbeat #\(status.heartbeatCount) \(ageSeconds)s ago"
|
||||
case .connecting:
|
||||
return "Connecting..."
|
||||
case .error:
|
||||
return "Connection error"
|
||||
case .disconnected:
|
||||
return "Disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
private func triggerFocusFlashAnimation() {
|
||||
focusFlashAnimationGeneration &+= 1
|
||||
let generation = focusFlashAnimationGeneration
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue