fix(connections): show mobile Vault status for selected workspace

This commit is contained in:
Kaede Akino committed 2026-09-27 16:13:02 +08:00
1 parent 6d8bd1dd6b
commit 6140bc3ebe
7 files changed
+65 -21

No files matched your search

@@ -1009,11 +1009,10 @@ public final class HostLibraryModel: RemoteSessionHostStore {
statuses[id] = status
}
/// Records one live connection's state and projects terminals and File
/// Transfer browsers for the same host into the status dot shown by Hosts
/// and host pickers. A connected source wins over a connecting or failed
/// sibling, so closing one pane cannot make an otherwise-online host appear
/// offline.
/// Records one live connection's state for Host-level consumers, including
/// the Vault and Recents dots and the agent Host listing. A connected source
/// wins over a connecting or failed sibling, so closing one pane cannot make
/// an otherwise-online Host appear offline.
public func setConnectionStatus(
_ status: ConnectionStatus, connectionID: UUID, hostID: Host.ID
) {
+15 -8
View File
@@ -37,7 +37,7 @@ final class MobileSceneModel {
@ObservationIgnored private var pendingWorkspaceID: MobileWorkspace.ID?
@ObservationIgnored private var pendingBatchStartup:
(hostIDs: [Host.ID], connectionProtocol: MobileConnectionProtocol)?
@ObservationIgnored private var recentWorkspaceIDs: [MobileWorkspace.ID] = []
private var recentWorkspaceIDs: [MobileWorkspace.ID] = []
init(app: MobileAppModel, sessionID: String) {
workspaceSession = app.workspaceSession(for: sessionID)
@@ -179,21 +179,28 @@ final class MobileSceneModel {
return
}
let kind: ConnectionHistoryEntry.Kind = connectionProtocol == .ssh ? .ssh : .telnet
guard let workspace = workspaceActivatedByHostRow(for: host) else {
open(hostID: hostID, kind: kind)
return
}
selectWorkspace(workspace.id)
}
/// The connection a Vault Host card will reopen in this window.
func workspaceActivatedByHostRow(for host: Host) -> MobileWorkspace? {
guard let connectionProtocol = host.preferredTerminalProtocol else { return nil }
let kind: ConnectionHistoryEntry.Kind = connectionProtocol == .ssh ? .ssh : .telnet
let matchingIDs = Set(
workspaceSession.workspaces.lazy
.filter { $0.descriptor.hostID == hostID && $0.descriptor.kind == kind }
.filter { $0.descriptor.hostID == host.id && $0.descriptor.kind == kind }
.map(\.id)
)
let workspaceID =
recentWorkspaceIDs.first(where: matchingIDs.contains)
?? workspaceSession.workspaces.last(where: {
$0.descriptor.hostID == hostID && $0.descriptor.kind == kind
$0.descriptor.hostID == host.id && $0.descriptor.kind == kind
})?.id
guard let workspaceID else {
open(hostID: hostID, kind: kind)
return
}
selectWorkspace(workspaceID)
return workspaceID.flatMap { id in workspaceSession.workspaces.first { $0.id == id } }
}
func openSameDirectory(_ launch: MobileWorkspaceDirectoryLaunch) {
+16 -2
View File
@@ -784,8 +784,22 @@ struct MobileVaultView: View {
} content: {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .top, spacing: 12) {
HostSystemIcon(asset: entry?.asset, size: 36)
.tint(tint)
ZStack(alignment: .bottomTrailing) {
HostSystemIcon(asset: entry?.asset, size: 36)
.tint(tint)
if let workspace = reviewScene?.workspaceActivatedByHostRow(for: host),
let indicator = MobileWorkspaceConnectionIndicator.resolve(
connected: workspace.connected,
busy: workspace.busy,
reconnecting: workspace.reconnectState != nil
)
{
Circle()
.fill(indicator.tint)
.frame(width: 8, height: 8)
.overlay { Circle().stroke(Color(uiColor: .systemBackground), lineWidth: 1.5) }
}
}
VStack(alignment: .leading, spacing: 4) {
Text(host.name).font(.headline).foregroundStyle(.primary).lineLimit(1)
Text(app.library.preferredTerminalLoginAddress(for: host)).font(.caption.monospaced())
@@ -171,6 +171,32 @@ struct MobileWorkspaceSessionTests {
#expect(second.isWorkspaceUnavailable)
}
@Test func vaultHostIndicatorFollowsTheWorkspaceOpenedByItsCard() throws {
let fixture = try Fixture()
defer { fixture.cleanUp() }
let app = try fixture.makeApp()
let scene = MobileSceneModel(app: app, sessionID: "vault-card")
let other = MobileSceneModel(app: app, sessionID: "other-window")
let host = fixture.host
other.open(hostID: host.id, kind: .ssh)
#expect(scene.workspaceActivatedByHostRow(for: host) == nil)
scene.open(hostID: host.id, kind: .sftp)
#expect(scene.workspaceActivatedByHostRow(for: host) == nil)
scene.open(hostID: host.id, kind: .ssh)
let first = try #require(scene.workspaceActivatedByHostRow(for: host))
scene.open(hostID: host.id, kind: .ssh)
let second = try #require(scene.workspaceActivatedByHostRow(for: host))
#expect(first !== second)
scene.selectWorkspace(first.id)
#expect(scene.workspaceActivatedByHostRow(for: host) === first)
scene.activateHost(host.id)
#expect(scene.activeWorkspace === first)
}
@Test func everyCreationEntryPointUsesTheReceivingWindow() throws {
let fixture = try Fixture()
defer { fixture.cleanUp() }
+2 -3
View File
@@ -1035,12 +1035,11 @@ final class AppModel {
return !unsafeExtensions.contains((file.name as NSString).pathExtension.lowercased())
}
/// Mark as recently used, and flip the status dot.
/// Optimistically shows connecting until the session reports its own state.
///
/// Its own method because three code paths now open hosts — one, a batch of
/// workspaces, and a batch into one Workspace — and *where it lands* is the only
/// thing that should differ between them. Inlining this was fine while there
/// were two.
/// thing that should differ between them.
private func beginOpening(_ host: Host, as target: HostOpenTarget) {
switch target {
case .preferredTerminal, .sshSession, .telnetSession:
@@ -237,8 +237,8 @@ extension HostLibraryModel {
knownHosts: knownHosts
)
// A spread of connection states so the sidebar, workspace list, and status bar
// can be checked against every dot color without a server.
// A spread of connection states so Vault and Welcome Host cards can be
// checked against every dot color without a server.
store.setStatus(.connected, for: web01.id)
store.setStatus(.connecting, for: db.id)
if let staging = hosts.first(where: { $0.name == "staging-app" }) {
@@ -231,7 +231,6 @@ struct StatusBarView: View {
sshSession?.status
?? sftpChannel?.status
?? serialSession?.status
?? pane?.hostID.map { app.hostLibrary.status(of: $0) }
?? .disconnected
}