Skip to content

Commit 7ce22b4

Browse files
author
itsdodobitch
committed
Fix first host selection
1 parent 44b6982 commit 7ce22b4

3 files changed

Lines changed: 137 additions & 3 deletions

File tree

Sources/HermesDesktop/App/AppState.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ final class AppState: ObservableObject {
159159
}
160160
.store(in: &cancellables)
161161

162-
self.activeConnectionID = connectionStore.lastConnectionID
162+
self.activeConnectionID = resolvedActiveConnectionID(preferred: connectionStore.lastConnectionID)
163+
persistActiveConnectionSelectionIfNeeded()
163164

164165
selectedSection = .connections
165166
}
@@ -474,6 +475,7 @@ final class AppState: ObservableObject {
474475
let normalized = profile.updated()
475476
let previous = connectionStore.connections.first(where: { $0.id == normalized.id })
476477
let isActiveConnection = activeConnectionID == normalized.id
478+
let shouldActivateSavedConnection = activeConnection == nil
477479
let isChangingWorkspaceScope = previous?.workspaceScopeFingerprint != normalized.workspaceScopeFingerprint
478480

479481
if isActiveConnection && isChangingWorkspaceScope && hasUnsavedFileChanges {
@@ -486,6 +488,11 @@ final class AppState: ObservableObject {
486488

487489
connectionStore.upsert(normalized)
488490

491+
if shouldActivateSavedConnection {
492+
activeConnectionID = normalized.id
493+
connectionStore.lastConnectionID = normalized.id
494+
}
495+
489496
guard isActiveConnection else { return }
490497
guard isChangingWorkspaceScope else { return }
491498

@@ -2321,10 +2328,12 @@ final class AppState: ObservableObject {
23212328
}
23222329

23232330
func deleteConnection(_ profile: ConnectionProfile) {
2331+
let isDeletingActiveConnection = activeConnectionID == profile.id
23242332
connectionStore.delete(profile)
23252333
terminalWorkspace.closeTabs(forConnectionID: profile.id)
2326-
if activeConnectionID == profile.id {
2327-
activeConnectionID = nil
2334+
if isDeletingActiveConnection {
2335+
activeConnectionID = resolvedActiveConnectionID(preferred: connectionStore.lastConnectionID)
2336+
persistActiveConnectionSelectionIfNeeded()
23282337
resetWorkspaceStateForConnectionChange(closeTerminalTabs: false)
23292338
selectedSection = .connections
23302339
}
@@ -2342,6 +2351,20 @@ final class AppState: ObservableObject {
23422351
setStatusMessage(L10n.string("New Terminal tab opened"))
23432352
}
23442353

2354+
private func resolvedActiveConnectionID(preferred preferredID: UUID?) -> UUID? {
2355+
if let preferredID,
2356+
connectionStore.connections.contains(where: { $0.id == preferredID }) {
2357+
return preferredID
2358+
}
2359+
2360+
return connectionStore.connections.first?.id
2361+
}
2362+
2363+
private func persistActiveConnectionSelectionIfNeeded() {
2364+
guard connectionStore.lastConnectionID != activeConnectionID else { return }
2365+
connectionStore.lastConnectionID = activeConnectionID
2366+
}
2367+
23452368
func resumeSessionInTerminal(_ session: SessionSummary) {
23462369
guard let profile = activeConnection else {
23472370
sessionsError = L10n.string("Select a connection before resuming a session in Terminal.")
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import HermesDesktop
5+
6+
@MainActor
7+
struct AppStateConnectionSelectionTests {
8+
@Test
9+
func launchSelectsSavedHostWhenPreferenceIsMissing() throws {
10+
let root = try makeTemporaryDirectory()
11+
defer { try? FileManager.default.removeItem(at: root) }
12+
13+
let paths = makeTestAppPaths(root: root)
14+
let savedConnection = ConnectionProfile(
15+
label: "Prod",
16+
sshHost: "example.com"
17+
)
18+
let store = ConnectionStore(paths: paths)
19+
store.upsert(savedConnection)
20+
21+
let appState = AppState(paths: paths)
22+
23+
#expect(appState.activeConnectionID == savedConnection.id)
24+
#expect(appState.activeConnection?.id == savedConnection.id)
25+
#expect(appState.connectionStore.lastConnectionID == savedConnection.id)
26+
}
27+
28+
@Test
29+
func launchReplacesStalePreferenceWithSavedHost() throws {
30+
let root = try makeTemporaryDirectory()
31+
defer { try? FileManager.default.removeItem(at: root) }
32+
33+
let paths = makeTestAppPaths(root: root)
34+
let savedConnection = ConnectionProfile(
35+
label: "Prod",
36+
sshHost: "example.com"
37+
)
38+
let store = ConnectionStore(paths: paths)
39+
store.upsert(savedConnection)
40+
store.lastConnectionID = UUID()
41+
42+
let appState = AppState(paths: paths)
43+
44+
#expect(appState.activeConnectionID == savedConnection.id)
45+
#expect(appState.activeConnection?.id == savedConnection.id)
46+
#expect(appState.connectionStore.lastConnectionID == savedConnection.id)
47+
}
48+
49+
@Test
50+
func savingFirstHostMakesItActive() throws {
51+
let root = try makeTemporaryDirectory()
52+
defer { try? FileManager.default.removeItem(at: root) }
53+
54+
let appState = AppState(paths: makeTestAppPaths(root: root))
55+
let savedConnection = ConnectionProfile(
56+
label: "Prod",
57+
sshHost: "example.com"
58+
)
59+
60+
appState.saveConnection(savedConnection)
61+
62+
#expect(appState.activeConnectionID == savedConnection.id)
63+
#expect(appState.activeConnection?.id == savedConnection.id)
64+
#expect(appState.connectionStore.lastConnectionID == savedConnection.id)
65+
}
66+
67+
@Test
68+
func deletingActiveHostSelectsRemainingHost() throws {
69+
let root = try makeTemporaryDirectory()
70+
defer { try? FileManager.default.removeItem(at: root) }
71+
72+
let appState = AppState(paths: makeTestAppPaths(root: root))
73+
let firstConnection = ConnectionProfile(
74+
label: "Alpha",
75+
sshHost: "alpha.example.com"
76+
)
77+
let secondConnection = ConnectionProfile(
78+
label: "Beta",
79+
sshHost: "beta.example.com"
80+
)
81+
appState.saveConnection(firstConnection)
82+
appState.saveConnection(secondConnection)
83+
84+
appState.deleteConnection(firstConnection)
85+
86+
#expect(appState.activeConnectionID == secondConnection.id)
87+
#expect(appState.activeConnection?.id == secondConnection.id)
88+
#expect(appState.connectionStore.lastConnectionID == secondConnection.id)
89+
}
90+
}

docs/releases/RELEASE-v1.0.1.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Hermes Desktop v1.0.1
2+
3+
Hermes Desktop 1.0.1 fixes a host selection regression introduced in the
4+
v1.0.0 Settings refresh.
5+
6+
On a fresh install, saving the first host could write `connections.json`
7+
successfully but leave `preferences.json` without an active `lastConnectionID`.
8+
The host was saved locally, but the Settings UI still behaved as if no host was
9+
available.
10+
11+
Fixes:
12+
13+
- select the first saved host automatically when no active host preference
14+
exists
15+
- repair stale active-host preferences on launch
16+
- make a newly saved first host active immediately
17+
- select the next available host after deleting the active host
18+
19+
This release keeps the same distribution model as v1.0.0: a universal macOS app
20+
bundle, ad-hoc signed, not notarized by Apple, with zip checksum and release
21+
manifest assets.

0 commit comments

Comments
 (0)