Skip to content

Commit 716435b

Browse files
committed
fix: set isRemote on resume and add mutagen flush + uname preamble
resumeCompleted was missing the isRemote flag, so resumed cards never showed the remote icon. Now mirrors launchCompleted behavior. Both launch and resume now inject a shell preamble for remote sessions that flushes mutagen sync and runs uname on the remote host, giving early visual feedback before claude starts.
1 parent 8f44d27 commit 716435b

File tree

5 files changed

+51
-15
lines changed

5 files changed

+51
-15
lines changed

Sources/KanbanCode/ContentView.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,7 @@ struct ContentView: View {
15671567
let shellOverride: String?
15681568
let extraEnv: [String: String]
15691569
let isRemote: Bool
1570+
let preamble: String?
15701571

15711572
let globalRemote = settings?.remote
15721573
if runRemotely, let remote = globalRemote, projectPath.hasPrefix(remote.localPath) {
@@ -1584,10 +1585,13 @@ struct ContentView: View {
15841585
name: syncName,
15851586
ignores: ignores
15861587
)
1588+
1589+
preamble = Self.remotePreamble(host: remote.host)
15871590
} else {
15881591
shellOverride = nil
15891592
extraEnv = [:]
15901593
isRemote = false
1594+
preamble = nil
15911595
}
15921596

15931597
// Snapshot existing .jsonl files for session detection
@@ -1622,7 +1626,8 @@ struct ContentView: View {
16221626
shellOverride: shellOverride,
16231627
extraEnv: extraEnv,
16241628
commandOverride: commandOverride,
1625-
skipPermissions: skipPermissions
1629+
skipPermissions: skipPermissions,
1630+
preamble: preamble
16261631
)
16271632
KanbanCodeLog.info("launch", "Tmux session created: \(tmuxName)")
16281633

@@ -1718,6 +1723,12 @@ struct ContentView: View {
17181723
return WorktreeLink(path: worktreePath, branch: branchName)
17191724
}
17201725

1726+
/// Build a shell preamble that flushes mutagen and shows remote uname before launching claude.
1727+
private static func remotePreamble(host: String) -> String {
1728+
// Use ; instead of && so a flush failure doesn't block claude from starting
1729+
"printf '\\e[2mSyncing files...\\e[0m' && mutagen sync flush 2>/dev/null; printf '\\e[2mRemote: %s\\e[0m\\n' \"$(ssh -o ConnectTimeout=5 \(host) uname -snr 2>/dev/null || echo 'unavailable')\""
1730+
}
1731+
17211732
@State private var pendingWorktreeCleanup: WorktreeCleanupInfo?
17221733
@State private var pendingDeleteCardId: String?
17231734
@State private var pendingArchiveCardId: String?
@@ -1928,12 +1939,15 @@ struct ContentView: View {
19281939

19291940
let shellOverride: String?
19301941
let extraEnv: [String: String]
1942+
let isRemote: Bool
1943+
let preamble: String?
19311944

19321945
let globalRemote = settings?.remote
19331946
if runRemotely, let remote = globalRemote, projectPath.hasPrefix(remote.localPath) {
19341947
try? RemoteShellManager.deploy()
19351948
shellOverride = RemoteShellManager.shellOverridePath()
19361949
extraEnv = RemoteShellManager.setupEnvironment(remote: remote, projectPath: projectPath)
1950+
isRemote = true
19371951

19381952
let syncName = "kanban-code-\((projectPath as NSString).lastPathComponent)"
19391953
let remoteDest = "\(remote.host):\(remote.remotePath)"
@@ -1944,9 +1958,13 @@ struct ContentView: View {
19441958
name: syncName,
19451959
ignores: ignores
19461960
)
1961+
1962+
preamble = Self.remotePreamble(host: remote.host)
19471963
} else {
19481964
shellOverride = nil
19491965
extraEnv = [:]
1966+
isRemote = false
1967+
preamble = nil
19501968
}
19511969

19521970
let actualTmuxName = try await launcher.resume(
@@ -1955,11 +1973,12 @@ struct ContentView: View {
19551973
shellOverride: shellOverride,
19561974
extraEnv: extraEnv,
19571975
commandOverride: commandOverride,
1958-
skipPermissions: skipPermissions
1976+
skipPermissions: skipPermissions,
1977+
preamble: preamble
19591978
)
19601979
KanbanCodeLog.info("resume", "Resume launched for card=\(cardId.prefix(12)) actualTmux=\(actualTmuxName)")
19611980

1962-
store.dispatch(.resumeCompleted(cardId: cardId, tmuxName: actualTmuxName))
1981+
store.dispatch(.resumeCompleted(cardId: cardId, tmuxName: actualTmuxName, isRemote: isRemote))
19631982
} catch {
19641983
KanbanCodeLog.info("resume", "Resume failed for card=\(cardId.prefix(12)): \(error.localizedDescription)")
19651984
store.dispatch(.resumeFailed(cardId: cardId, error: error.localizedDescription))

Sources/KanbanCodeCore/Domain/Ports/SessionLauncher.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public protocol SessionLauncher: Sendable {
1111
shellOverride: String?,
1212
extraEnv: [String: String],
1313
commandOverride: String?,
14-
skipPermissions: Bool
14+
skipPermissions: Bool,
15+
preamble: String?
1516
) async throws -> String // returns tmux session name
1617

1718
/// Resume an existing session by its ID.
@@ -21,7 +22,8 @@ public protocol SessionLauncher: Sendable {
2122
shellOverride: String?,
2223
extraEnv: [String: String],
2324
commandOverride: String?,
24-
skipPermissions: Bool
25+
skipPermissions: Bool,
26+
preamble: String?
2527
) async throws -> String // returns tmux session name
2628
}
2729

@@ -42,7 +44,8 @@ extension SessionLauncher {
4244
shellOverride: shellOverride,
4345
extraEnv: [:],
4446
commandOverride: nil,
45-
skipPermissions: false
47+
skipPermissions: false,
48+
preamble: nil
4649
)
4750
}
4851

@@ -59,7 +62,8 @@ extension SessionLauncher {
5962
shellOverride: shellOverride,
6063
extraEnv: extraEnv,
6164
commandOverride: commandOverride,
62-
skipPermissions: false
65+
skipPermissions: false,
66+
preamble: nil
6367
)
6468
}
6569
}

Sources/KanbanCodeCore/UseCases/BoardStore.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public enum Action: Sendable {
156156
case launchCompleted(cardId: String, tmuxName: String, sessionLink: SessionLink?, worktreeLink: WorktreeLink?, isRemote: Bool)
157157
case launchTmuxReady(cardId: String)
158158
case launchFailed(cardId: String, error: String)
159-
case resumeCompleted(cardId: String, tmuxName: String)
159+
case resumeCompleted(cardId: String, tmuxName: String, isRemote: Bool)
160160
case resumeFailed(cardId: String, error: String)
161161
case terminalCreated(cardId: String, tmuxName: String)
162162
case terminalFailed(cardId: String, error: String)
@@ -639,10 +639,11 @@ public enum Reducer {
639639
state.error = "Launch failed: \(error)"
640640
return [.upsertLink(link)]
641641

642-
case .resumeCompleted(let cardId, let tmuxName):
642+
case .resumeCompleted(let cardId, let tmuxName, let isRemote):
643643
guard var link = state.links[cardId] else { return [] }
644644
let existingExtras = link.tmuxLink?.extraSessions
645645
link.tmuxLink = TmuxLink(sessionName: tmuxName, extraSessions: existingExtras)
646+
link.isRemote = isRemote
646647
link.isLaunching = nil
647648
link.lastActivity = .now
648649
link.updatedAt = .now

Sources/KanbanCodeCore/UseCases/LaunchSession.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public final class LaunchSession: SessionLauncher, @unchecked Sendable {
1717
shellOverride: String?,
1818
extraEnv: [String: String] = [:],
1919
commandOverride: String? = nil,
20-
skipPermissions: Bool = false
20+
skipPermissions: Bool = false,
21+
preamble: String? = nil
2122
) async throws -> String {
2223

2324
let cmd: String
@@ -47,7 +48,11 @@ public final class LaunchSession: SessionLauncher, @unchecked Sendable {
4748
}
4849

4950
// Prepend cd to ensure we're in the right directory even if zshrc changes it
50-
let fullCmd = "cd \(shellEscape(projectPath)) && \(cmd)"
51+
var fullCmd = "cd \(shellEscape(projectPath))"
52+
if let preamble, !preamble.isEmpty {
53+
fullCmd += " && \(preamble)"
54+
}
55+
fullCmd += " && \(cmd)"
5156

5257
// Kill any stale session with the same name before launching.
5358
// This handles disconnected cards where the old tmux session lingers.
@@ -63,7 +68,8 @@ public final class LaunchSession: SessionLauncher, @unchecked Sendable {
6368
shellOverride: String?,
6469
extraEnv: [String: String] = [:],
6570
commandOverride: String? = nil,
66-
skipPermissions: Bool = false
71+
skipPermissions: Bool = false,
72+
preamble: String? = nil
6773
) async throws -> String {
6874
// Check if there's already a tmux session for this
6975
let existing = try await tmux.listSessions()
@@ -88,7 +94,11 @@ public final class LaunchSession: SessionLauncher, @unchecked Sendable {
8894
}
8995

9096
// Prepend cd to ensure we're in the right directory even if zshrc changes it
91-
let fullCmd = "cd \(shellEscape(projectPath)) && \(cmd)"
97+
var fullCmd = "cd \(shellEscape(projectPath))"
98+
if let preamble, !preamble.isEmpty {
99+
fullCmd += " && \(preamble)"
100+
}
101+
fullCmd += " && \(cmd)"
92102

93103
try await tmux.createSession(name: sessionName, path: projectPath, command: fullCmd)
94104
return sessionName

Tests/KanbanCodeCoreTests/ReducerTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,14 @@ struct ReducerTests {
168168
var state = stateWith([link])
169169

170170
let _ = Reducer.reduce(state: &state, action: .resumeCompleted(
171-
cardId: "card_r3", tmuxName: "claude-sess_abc"
171+
cardId: "card_r3", tmuxName: "claude-sess_abc", isRemote: false
172172
))
173173

174174
// isLaunching cleared immediately — terminal shows without waiting for reconciliation
175175
#expect(state.links["card_r3"]?.isLaunching == nil)
176176
#expect(state.links["card_r3"]?.column == .inProgress)
177177
#expect(state.links["card_r3"]?.lastActivity != nil)
178+
#expect(state.links["card_r3"]?.isRemote == false)
178179
}
179180

180181
// MARK: - Launch Failure
@@ -1130,11 +1131,12 @@ struct ReducerTests {
11301131
var state = stateWith([link])
11311132

11321133
let _ = Reducer.reduce(state: &state, action: .resumeCompleted(
1133-
cardId: "card_ti7", tmuxName: "claude-sess_abc"
1134+
cardId: "card_ti7", tmuxName: "claude-sess_abc", isRemote: true
11341135
))
11351136

11361137
#expect(state.links["card_ti7"]?.tmuxLink?.extraSessions == ["claude-sess_abc-sh1"])
11371138
#expect(state.links["card_ti7"]?.isLaunching == nil)
1139+
#expect(state.links["card_ti7"]?.isRemote == true)
11381140
}
11391141

11401142
@Test("launchCompleted preserves extras")

0 commit comments

Comments
 (0)