Skip to content

Commit 7ade80a

Browse files
authored
Merge pull request #200 from supabitapp/sbertix/script-exit
Keep terminal alive after blocking script completes
2 parents 6f8a9aa + 700871f commit 7ade80a

File tree

8 files changed

+411
-292
lines changed

8 files changed

+411
-292
lines changed

supacode/Clients/Terminal/TerminalClient.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct TerminalClient {
1010
case createTabWithInput(Worktree, input: String, runSetupScriptIfNew: Bool)
1111
case ensureInitialTab(Worktree, runSetupScriptIfNew: Bool, focusing: Bool)
1212
case stopRunScript(Worktree)
13+
case selectTab(Worktree, tabId: TerminalTabID)
1314
case runBlockingScript(Worktree, kind: BlockingScriptKind, script: String)
1415
case closeFocusedTab(Worktree)
1516
case closeFocusedSurface(Worktree)
@@ -31,7 +32,8 @@ struct TerminalClient {
3132
case tabClosed(worktreeID: Worktree.ID)
3233
case focusChanged(worktreeID: Worktree.ID, surfaceID: UUID)
3334
case taskStatusChanged(worktreeID: Worktree.ID, status: WorktreeTaskStatus)
34-
case blockingScriptCompleted(worktreeID: Worktree.ID, kind: BlockingScriptKind, exitCode: Int?)
35+
case blockingScriptCompleted(
36+
worktreeID: Worktree.ID, kind: BlockingScriptKind, exitCode: Int?, tabId: TerminalTabID?)
3537
case commandPaletteToggleRequested(worktreeID: Worktree.ID)
3638
case setupScriptConsumed(worktreeID: Worktree.ID)
3739
}

supacode/Features/App/Reducer/AppFeature.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ struct AppFeature {
228228
await terminalClient.send(.runBlockingScript(worktree, kind: kind, script: script))
229229
}
230230

231+
case .repositories(.delegate(.selectTerminalTab(let worktreeID, let tabId))):
232+
guard let worktree = state.repositories.worktree(for: worktreeID) else { return .none }
233+
return .run { _ in
234+
await terminalClient.send(.selectTab(worktree, tabId: tabId))
235+
}
236+
231237
case .settings(.setSelection(let selection)):
232238
let resolvedSelection = selection ?? .general
233239
switch resolvedSelection {
@@ -675,14 +681,14 @@ struct AppFeature {
675681
case .terminalEvent(.setupScriptConsumed(let worktreeID)):
676682
return .send(.repositories(.consumeSetupScript(worktreeID)))
677683

678-
case .terminalEvent(.blockingScriptCompleted(let worktreeID, let kind, let exitCode)):
684+
case .terminalEvent(.blockingScriptCompleted(let worktreeID, let kind, let exitCode, let tabId)):
679685
switch kind {
680686
case .run:
681-
return .send(.repositories(.runScriptCompleted(worktreeID: worktreeID, exitCode: exitCode)))
687+
return .send(.repositories(.runScriptCompleted(worktreeID: worktreeID, exitCode: exitCode, tabId: tabId)))
682688
case .archive:
683-
return .send(.repositories(.archiveScriptCompleted(worktreeID: worktreeID, exitCode: exitCode)))
689+
return .send(.repositories(.archiveScriptCompleted(worktreeID: worktreeID, exitCode: exitCode, tabId: tabId)))
684690
case .delete:
685-
return .send(.repositories(.deleteScriptCompleted(worktreeID: worktreeID, exitCode: exitCode)))
691+
return .send(.repositories(.deleteScriptCompleted(worktreeID: worktreeID, exitCode: exitCode, tabId: tabId)))
686692
}
687693

688694
case .terminalEvent:

supacode/Features/Repositories/Reducer/RepositoriesFeature.swift

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,17 @@ struct RepositoriesFeature {
198198
)
199199
case consumeSetupScript(Worktree.ID)
200200
case consumeTerminalFocus(Worktree.ID)
201-
case runScriptCompleted(worktreeID: Worktree.ID, exitCode: Int?)
201+
case runScriptCompleted(worktreeID: Worktree.ID, exitCode: Int?, tabId: TerminalTabID?)
202202
case requestArchiveWorktree(Worktree.ID, Repository.ID)
203203
case requestArchiveWorktrees([ArchiveWorktreeTarget])
204204
case archiveWorktreeConfirmed(Worktree.ID, Repository.ID)
205-
case archiveScriptCompleted(worktreeID: Worktree.ID, exitCode: Int?)
205+
case archiveScriptCompleted(worktreeID: Worktree.ID, exitCode: Int?, tabId: TerminalTabID?)
206206
case archiveWorktreeApply(Worktree.ID, Repository.ID)
207207
case unarchiveWorktree(Worktree.ID)
208208
case requestDeleteWorktree(Worktree.ID, Repository.ID)
209209
case requestDeleteWorktrees([DeleteWorktreeTarget])
210210
case deleteWorktreeConfirmed(Worktree.ID, Repository.ID)
211-
case deleteScriptCompleted(worktreeID: Worktree.ID, exitCode: Int?)
211+
case deleteScriptCompleted(worktreeID: Worktree.ID, exitCode: Int?, tabId: TerminalTabID?)
212212
case deleteWorktreeApply(Worktree.ID, Repository.ID)
213213
case worktreeDeleted(
214214
Worktree.ID,
@@ -285,6 +285,7 @@ struct RepositoriesFeature {
285285
case confirmDeleteWorktree(Worktree.ID, Repository.ID)
286286
case confirmDeleteWorktrees([DeleteWorktreeTarget])
287287
case confirmRemoveRepository(Repository.ID)
288+
case viewTerminalTab(Worktree.ID, tabId: TerminalTabID)
288289
}
289290

290291
enum PullRequestAction: Equatable {
@@ -305,6 +306,7 @@ struct RepositoriesFeature {
305306
case openRepositorySettings(Repository.ID)
306307
case worktreeCreated(Worktree)
307308
case runBlockingScript(Worktree, repositoryID: Repository.ID, kind: BlockingScriptKind, script: String)
309+
case selectTerminalTab(Worktree.ID, tabId: TerminalTabID)
308310
}
309311

310312
@Dependency(AnalyticsClient.self) private var analyticsClient
@@ -1371,12 +1373,16 @@ struct RepositoriesFeature {
13711373
}
13721374
)
13731375

1374-
case .runScriptCompleted(let worktreeID, _):
1376+
case .runScriptCompleted(let worktreeID, let exitCode, let tabId):
13751377
guard state.runScriptWorktreeIDs.contains(worktreeID) else {
13761378
repositoriesLogger.debug("Ignoring runScriptCompleted for \(worktreeID): not in runScriptWorktreeIDs")
13771379
return .none
13781380
}
13791381
state.runScriptWorktreeIDs.remove(worktreeID)
1382+
guard let exitCode, exitCode != 0 else { return .none }
1383+
state.alert = blockingScriptFailureAlert(
1384+
kind: .run, exitCode: exitCode, worktreeID: worktreeID, tabId: tabId, state: state
1385+
)
13801386
return .none
13811387

13821388
case .archiveWorktreeConfirmed(let worktreeID, let repositoryID):
@@ -1400,7 +1406,7 @@ struct RepositoriesFeature {
14001406
return .send(
14011407
.delegate(.runBlockingScript(worktree, repositoryID: repositoryID, kind: .archive, script: script)))
14021408

1403-
case .archiveScriptCompleted(let worktreeID, let exitCode):
1409+
case .archiveScriptCompleted(let worktreeID, let exitCode, let tabId):
14041410
guard state.archivingWorktreeIDs.contains(worktreeID) else {
14051411
repositoriesLogger.debug("Ignoring archiveScriptCompleted for \(worktreeID): not in archivingWorktreeIDs")
14061412
return .none
@@ -1424,9 +1430,8 @@ struct RepositoriesFeature {
14241430
repositoriesLogger.debug("Archive script cancelled or tab closed for worktree \(worktreeID)")
14251431
return .none
14261432
case let code?:
1427-
state.alert = messageAlert(
1428-
title: "Archive script failed",
1429-
message: "\(blockingScriptExitMessage(code))\nCheck the Archive Script tab for details."
1433+
state.alert = blockingScriptFailureAlert(
1434+
kind: .archive, exitCode: code, worktreeID: worktreeID, tabId: tabId, state: state
14301435
)
14311436
return .none
14321437
}
@@ -1652,7 +1657,7 @@ struct RepositoriesFeature {
16521657
return .send(
16531658
.delegate(.runBlockingScript(worktree, repositoryID: repositoryID, kind: .delete, script: script)))
16541659

1655-
case .deleteScriptCompleted(let worktreeID, let exitCode):
1660+
case .deleteScriptCompleted(let worktreeID, let exitCode, let tabId):
16561661
guard state.deleteScriptWorktreeIDs.contains(worktreeID) else {
16571662
repositoriesLogger.debug("Ignoring deleteScriptCompleted for \(worktreeID): not in deleteScriptWorktreeIDs")
16581663
return .none
@@ -1676,9 +1681,8 @@ struct RepositoriesFeature {
16761681
repositoriesLogger.debug("Delete script cancelled or tab closed for worktree \(worktreeID)")
16771682
return .none
16781683
case let code?:
1679-
state.alert = messageAlert(
1680-
title: "Delete script failed",
1681-
message: "\(blockingScriptExitMessage(code))\nCheck the Delete Script tab for details."
1684+
state.alert = blockingScriptFailureAlert(
1685+
kind: .delete, exitCode: code, worktreeID: worktreeID, tabId: tabId, state: state
16821686
)
16831687
return .none
16841688
}
@@ -2661,6 +2665,12 @@ struct RepositoriesFeature {
26612665
case .openRepositorySettings(let repositoryID):
26622666
return .send(.delegate(.openRepositorySettings(repositoryID)))
26632667

2668+
case .alert(.presented(.viewTerminalTab(let worktreeID, let tabId))):
2669+
return .merge(
2670+
.send(.selectWorktree(worktreeID, focusTerminal: true)),
2671+
.send(.delegate(.selectTerminalTab(worktreeID, tabId: tabId)))
2672+
)
2673+
26642674
case .alert(.dismiss):
26652675
state.alert = nil
26662676
return .none
@@ -2891,6 +2901,37 @@ struct RepositoriesFeature {
28912901
)
28922902
}
28932903

2904+
private func blockingScriptFailureAlert(
2905+
kind: BlockingScriptKind,
2906+
exitCode: Int,
2907+
worktreeID: Worktree.ID,
2908+
tabId: TerminalTabID?,
2909+
state: State
2910+
) -> AlertState<Alert> {
2911+
let worktreeName = state.worktree(for: worktreeID)?.name
2912+
let repoName = state.repositoryID(containing: worktreeID)
2913+
.flatMap { state.repositories[id: $0]?.name }
2914+
let parts = [repoName, worktreeName].compactMap(\.self)
2915+
if parts.isEmpty {
2916+
repositoriesLogger.debug("blockingScriptFailureAlert: worktree \(worktreeID) not found in state")
2917+
}
2918+
let subtitle = parts.isEmpty ? "Unknown worktree" : parts.joined(separator: "")
2919+
return AlertState {
2920+
TextState("\(kind.tabTitle) failed")
2921+
} actions: {
2922+
if let tabId {
2923+
ButtonState(action: .viewTerminalTab(worktreeID, tabId: tabId)) {
2924+
TextState("View Terminal")
2925+
}
2926+
}
2927+
ButtonState(role: .cancel) {
2928+
TextState("Dismiss")
2929+
}
2930+
} message: {
2931+
TextState("\(subtitle)\n\n\(blockingScriptExitMessage(exitCode))")
2932+
}
2933+
}
2934+
28942935
private func messageAlert(title: String, message: String) -> AlertState<Alert> {
28952936
AlertState {
28962937
TextState(title)

supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ final class WorktreeTerminalManager {
4444
state.ensureInitialTab(focusing: focusing)
4545
case .stopRunScript(let worktree):
4646
_ = state(for: worktree).stopRunScript()
47+
case .selectTab(let worktree, let tabId):
48+
state(for: worktree).selectTab(tabId)
4749
case .runBlockingScript(let worktree, let kind, let script):
4850
_ = state(for: worktree).runBlockingScript(kind: kind, script)
4951
case .closeFocusedTab(let worktree):
@@ -159,8 +161,8 @@ final class WorktreeTerminalManager {
159161
state.onTaskStatusChanged = { [weak self] status in
160162
self?.emit(.taskStatusChanged(worktreeID: worktree.id, status: status))
161163
}
162-
state.onBlockingScriptCompleted = { [weak self] kind, exitCode in
163-
self?.emit(.blockingScriptCompleted(worktreeID: worktree.id, kind: kind, exitCode: exitCode))
164+
state.onBlockingScriptCompleted = { [weak self] kind, exitCode, tabId in
165+
self?.emit(.blockingScriptCompleted(worktreeID: worktree.id, kind: kind, exitCode: exitCode, tabId: tabId))
164166
}
165167
state.onCommandPaletteToggle = { [weak self] in
166168
self?.emit(.commandPaletteToggleRequested(worktreeID: worktree.id))

0 commit comments

Comments
 (0)