native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(tab-icon): pin Run Script and Custom Command icons over auto-detection

The auto-detected command icon (npm, swift, …) used to clobber the
"play.fill" glyph that Run Script tabs are seeded with: the icon
visibly flashed in for a frame and was immediately overwritten by
whatever command the script kicked off. Custom Commands had no
configured-icon support at all — the user's chosen `systemImage`
never reached the tab.

Introduce a third precedence level between auto-detection and the
user picker by adding `TerminalTabItem.isScriptIconActive`. Auto
detection (`updateIcon` / `applyResolvedIcon`) skips tabs flagged
this way; the user picker (`overrideIcon` / `clearIconOverride`)
clears the flag so manual locks still win.

Wire-up:
- `WorktreeTerminalState.runScript` calls `setScriptIcon` after
creating the tab so the play glyph survives `npm`, `swift`, etc.
- `TerminalClient.Command.{createTabWithInput,createSplitWithInput}`
carry a new `customCommandIcon: String?`. `AppFeature` populates it
from `customCommand.systemImage`, treating the model's "terminal"
placeholder and empty/whitespace as unset so untouched commands
still get auto-detection.
- `WorktreeTerminalManager` forwards the icon into
`WorktreeTerminalState.applyCustomCommandIcon`, which resolves the
surface's tab and pins the icon via `setScriptIcon`.

onevcat 7f673175 ceef897a

+227 -22
+4 -2
supacode/Clients/Terminal/TerminalClient.swift
··· 13 13 input: String, 14 14 runSetupScriptIfNew: Bool, 15 15 autoCloseOnSuccess: Bool, 16 - customCommandName: String? = nil 16 + customCommandName: String? = nil, 17 + customCommandIcon: String? = nil 17 18 ) 18 19 case createSplitWithInput( 19 20 Worktree, 20 21 direction: UserCustomSplitDirection, 21 22 input: String, 22 23 autoCloseOnSuccess: Bool, 23 - customCommandName: String? = nil 24 + customCommandName: String? = nil, 25 + customCommandIcon: String? = nil 24 26 ) 25 27 case createTabInDirectory(Worktree, directory: URL) 26 28 case ensureInitialTab(Worktree, runSetupScriptIfNew: Bool, focusing: Bool)
+13 -2
supacode/Features/App/Reducer/AppFeature.swift
··· 645 645 let command = customCommand.command 646 646 let closeOnSuccess = customCommand.closeOnSuccess 647 647 let commandName = customCommand.resolvedTitle 648 + // Treat the model's "terminal" placeholder (and an empty value) 649 + // as "no icon configured", so the auto-detector can still brand 650 + // the tab from the command itself. Anything else is a deliberate 651 + // user pick and gets pinned for the duration of the run. 652 + let commandIcon: String? = { 653 + let trimmed = customCommand.systemImage.trimmingCharacters(in: .whitespacesAndNewlines) 654 + guard !trimmed.isEmpty, trimmed != "terminal" else { return nil } 655 + return trimmed 656 + }() 648 657 switch customCommand.execution { 649 658 case .shellScript: 650 659 return .run { _ in ··· 654 663 input: command, 655 664 runSetupScriptIfNew: false, 656 665 autoCloseOnSuccess: closeOnSuccess, 657 - customCommandName: commandName 666 + customCommandName: commandName, 667 + customCommandIcon: commandIcon 658 668 ) 659 669 ) 660 670 } ··· 667 677 direction: direction, 668 678 input: command, 669 679 autoCloseOnSuccess: closeOnSuccess, 670 - customCommandName: commandName 680 + customCommandName: commandName, 681 + customCommandIcon: commandIcon 671 682 ) 672 683 ) 673 684 }
+18 -6
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 53 53 case .createTab(let worktree, let runSetupScriptIfNew): 54 54 Task { createTabAsync(in: worktree, runSetupScriptIfNew: runSetupScriptIfNew) } 55 55 case .createTabWithInput( 56 - let worktree, let input, let runSetupScriptIfNew, let autoCloseOnSuccess, let customCommandName): 56 + let worktree, let input, let runSetupScriptIfNew, let autoCloseOnSuccess, let customCommandName, 57 + let customCommandIcon): 57 58 Task { 58 59 createTabAsync( 59 60 in: worktree, 60 61 runSetupScriptIfNew: runSetupScriptIfNew, 61 62 initialInput: input, 62 63 autoCloseOnSuccess: autoCloseOnSuccess, 63 - customCommandName: customCommandName 64 + customCommandName: customCommandName, 65 + customCommandIcon: customCommandIcon 64 66 ) 65 67 } 66 - case .createSplitWithInput(let worktree, let direction, let input, let autoCloseOnSuccess, let customCommandName): 68 + case .createSplitWithInput( 69 + let worktree, let direction, let input, let autoCloseOnSuccess, let customCommandName, let customCommandIcon): 67 70 Task { 68 71 createSplitAsync( 69 72 in: worktree, 70 73 direction: direction, 71 74 initialInput: input, 72 75 autoCloseOnSuccess: autoCloseOnSuccess, 73 - customCommandName: customCommandName 76 + customCommandName: customCommandName, 77 + customCommandIcon: customCommandIcon 74 78 ) 75 79 } 76 80 case .createTabInDirectory(let worktree, let directory): ··· 268 272 initialInput: String? = nil, 269 273 workingDirectory: URL? = nil, 270 274 autoCloseOnSuccess: Bool = false, 271 - customCommandName: String? = nil 275 + customCommandName: String? = nil, 276 + customCommandIcon: String? = nil 272 277 ) { 273 278 let state = state(for: worktree) { runSetupScriptIfNew } 274 279 let setupScript: String? ··· 292 297 } 293 298 if let customCommandName { 294 299 state.markSurfaceForCustomCommand(surfaceId, name: customCommandName) 300 + } 301 + if let customCommandIcon { 302 + state.applyCustomCommandIcon(customCommandIcon, surfaceId: surfaceId) 295 303 } 296 304 } 297 305 } ··· 301 309 direction: UserCustomSplitDirection, 302 310 initialInput: String, 303 311 autoCloseOnSuccess: Bool, 304 - customCommandName: String? = nil 312 + customCommandName: String? = nil, 313 + customCommandIcon: String? = nil 305 314 ) { 306 315 let state = state(for: worktree) 307 316 guard ··· 317 326 } 318 327 if let customCommandName { 319 328 state.markSurfaceForCustomCommand(newSurfaceId, name: customCommandName) 329 + } 330 + if let customCommandIcon { 331 + state.applyCustomCommandIcon(customCommandIcon, surfaceId: newSurfaceId) 320 332 } 321 333 } 322 334
+9 -1
supacode/Features/Terminal/Models/TerminalTabItem.swift
··· 7 7 var isDirty: Bool 8 8 var isTitleLocked: Bool 9 9 var isIconLocked: Bool 10 + /// `true` while a Run Script or Custom Command's configured icon owns 11 + /// this tab's icon slot. Sits between auto-detected command icons and 12 + /// `isIconLocked` (user picker) in the precedence chain: blocks 13 + /// `CommandIconMap`-driven overrides so the play / configured glyph 14 + /// doesn't get clobbered mid-run, but yields to a user-set lock. 15 + var isScriptIconActive: Bool 10 16 11 17 init( 12 18 id: TerminalTabID = TerminalTabID(), ··· 14 20 icon: String?, 15 21 isDirty: Bool = false, 16 22 isTitleLocked: Bool = false, 17 - isIconLocked: Bool = false 23 + isIconLocked: Bool = false, 24 + isScriptIconActive: Bool = false 18 25 ) { 19 26 self.id = id 20 27 self.title = title ··· 22 29 self.isDirty = isDirty 23 30 self.isTitleLocked = isTitleLocked 24 31 self.isIconLocked = isIconLocked 32 + self.isScriptIconActive = isScriptIconActive 25 33 } 26 34 }
+17 -1
supacode/Features/Terminal/Models/TerminalTabManager.swift
··· 43 43 44 44 func updateIcon(_ id: TerminalTabID, icon: String?) { 45 45 guard let index = tabs.firstIndex(where: { $0.id == id }) else { return } 46 - guard !tabs[index].isIconLocked else { return } 46 + guard !tabs[index].isIconLocked, !tabs[index].isScriptIconActive else { return } 47 47 tabs[index].icon = icon 48 48 } 49 49 ··· 51 51 guard let index = tabs.firstIndex(where: { $0.id == id }) else { return } 52 52 tabs[index].icon = icon 53 53 tabs[index].isIconLocked = true 54 + // User-set lock supersedes any active script-command override so the 55 + // precedence chain (user > script > auto) can't ambiguously stack. 56 + tabs[index].isScriptIconActive = false 54 57 } 55 58 56 59 func clearIconOverride(_ id: TerminalTabID) { 57 60 guard let index = tabs.firstIndex(where: { $0.id == id }) else { return } 58 61 tabs[index].isIconLocked = false 62 + // Reset to default also drops the script-level pin so the next 63 + // auto-detection can take over. 64 + tabs[index].isScriptIconActive = false 65 + } 66 + 67 + /// Apply a Run Script / Custom Command icon and mark it as the active 68 + /// "script" override. No-op when the user has already locked an icon 69 + /// — manual lock always wins. 70 + func setScriptIcon(_ id: TerminalTabID, icon: String) { 71 + guard let index = tabs.firstIndex(where: { $0.id == id }) else { return } 72 + guard !tabs[index].isIconLocked else { return } 73 + tabs[index].icon = icon 74 + tabs[index].isScriptIconActive = true 59 75 } 60 76 61 77 func updateDirty(_ id: TerminalTabID, isDirty: Bool) {
+23 -5
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 259 259 workingDirectoryOverride: nil 260 260 ) 261 261 ) 262 + if let tabId { 263 + // Lock in the play glyph as a script-level override so OSC-2 264 + // titles emitted by the script (e.g. `npm run dev`) can't swap 265 + // the icon out from under it. 266 + tabManager.setScriptIcon(tabId, icon: "play.fill") 267 + } 262 268 setRunScriptTabId(tabId) 263 269 return tabId 264 270 } ··· 560 566 /// so a success toast can be emitted when that surface's next command exits with code 0. 561 567 func markSurfaceForCustomCommand(_ surfaceId: UUID, name: String) { 562 568 pendingCustomCommands[surfaceId] = name 569 + } 570 + 571 + /// Pin a Custom Command's configured icon onto its host tab so the 572 + /// auto-detector can't swap it out when the script's OSC-2 title 573 + /// matches a known command. Yields to a user-set icon lock — manual 574 + /// picker selections always win. 575 + func applyCustomCommandIcon(_ icon: String, surfaceId: UUID) { 576 + let trimmed = icon.trimmingCharacters(in: .whitespacesAndNewlines) 577 + guard !trimmed.isEmpty else { return } 578 + guard let tabId = tabId(containing: surfaceId) else { return } 579 + tabManager.setScriptIcon(tabId, icon: trimmed) 563 580 } 564 581 565 582 // Short delay lets the user see the final output before the pane disappears. ··· 1495 1512 return false 1496 1513 } 1497 1514 1498 - /// Apply an already-resolved icon to the tab. Honours focus and 1499 - /// user-icon-lock; encodes the icon through `storageString` so 1500 - /// `assetName`-bearing entries pick up the `@asset:` marker the 1501 - /// renderers parse via `ResolvedTabIcon`. 1515 + /// Apply an already-resolved icon to the tab. Honours focus, the user 1516 + /// icon lock, and the Run Script / Custom Command override; encodes 1517 + /// the icon through `storageString` so `assetName`-bearing entries 1518 + /// pick up the `@asset:` marker the renderers parse via 1519 + /// `ResolvedTabIcon`. 1502 1520 private func applyResolvedIcon( 1503 1521 _ icon: TabIconSource, 1504 1522 surfaceId: UUID, ··· 1510 1528 // user is currently looking at. 1511 1529 guard focusedSurfaceIdByTab[tabId] == surfaceId else { return } 1512 1530 guard let tab = tabManager.tabs.first(where: { $0.id == tabId }) else { return } 1513 - guard !tab.isIconLocked else { return } 1531 + guard !tab.isIconLocked, !tab.isScriptIconActive else { return } 1514 1532 let serialised = icon.storageString 1515 1533 guard tab.icon != serialised else { return } 1516 1534 tabManager.updateIcon(tabId, icon: serialised)
+97 -5
supacodeTests/AppFeatureCustomCommandTests.swift
··· 42 42 input: "swift test", 43 43 runSetupScriptIfNew: false, 44 44 autoCloseOnSuccess: false, 45 - customCommandName: "Test" 45 + customCommandName: "Test", 46 + customCommandIcon: "checkmark.circle" 46 47 ), 47 48 ], 48 49 ) ··· 119 120 direction: .down, 120 121 input: "tail -f logs", 121 122 autoCloseOnSuccess: false, 122 - customCommandName: "Tail" 123 + customCommandName: "Tail", 124 + customCommandIcon: "doc.text" 123 125 ), 124 126 ], 125 127 ) ··· 171 173 input: "make build", 172 174 runSetupScriptIfNew: false, 173 175 autoCloseOnSuccess: true, 174 - customCommandName: "Build" 176 + customCommandName: "Build", 177 + customCommandIcon: "hammer" 175 178 ), 176 179 .createSplitWithInput( 177 180 worktree, 178 181 direction: .right, 179 182 input: "make lint", 180 183 autoCloseOnSuccess: true, 181 - customCommandName: "Lint" 184 + customCommandName: "Lint", 185 + customCommandIcon: "checkmark" 182 186 ), 183 187 ], 184 188 ) ··· 286 290 input: "echo five", 287 291 runSetupScriptIfNew: false, 288 292 autoCloseOnSuccess: false, 289 - customCommandName: "Five" 293 + customCommandName: "Five", 294 + customCommandIcon: "5.circle" 295 + ), 296 + ], 297 + ) 298 + } 299 + 300 + @Test(.dependencies) func defaultTerminalIconIsTreatedAsUnsetForAutoDetection() async { 301 + let worktree = makeWorktree() 302 + let sent = LockIsolated<[TerminalClient.Command]>([]) 303 + var state = AppFeature.State( 304 + repositories: makeRepositoriesState(worktree: worktree), 305 + settings: SettingsFeature.State() 306 + ) 307 + state.selectedCustomCommands = [ 308 + UserCustomCommand( 309 + title: "Default", 310 + systemImage: "terminal", 311 + command: "npm test", 312 + execution: .shellScript, 313 + shortcut: nil, 314 + ), 315 + ] 316 + 317 + let store = TestStore(initialState: state) { 318 + AppFeature() 319 + } withDependencies: { 320 + $0.terminalClient.send = { command in 321 + sent.withValue { $0.append(command) } 322 + } 323 + } 324 + 325 + await store.send(.runCustomCommand(0)) 326 + await store.finish() 327 + 328 + // The model's "terminal" placeholder should not be pinned as a 329 + // script icon — the auto-detector should remain free to brand the 330 + // tab from the executed command itself. 331 + #expect( 332 + sent.value == [ 333 + .createTabWithInput( 334 + worktree, 335 + input: "npm test", 336 + runSetupScriptIfNew: false, 337 + autoCloseOnSuccess: false, 338 + customCommandName: "Default", 339 + customCommandIcon: nil 340 + ), 341 + ], 342 + ) 343 + } 344 + 345 + @Test(.dependencies) func emptyIconIsTreatedAsUnsetForAutoDetection() async { 346 + let worktree = makeWorktree() 347 + let sent = LockIsolated<[TerminalClient.Command]>([]) 348 + var state = AppFeature.State( 349 + repositories: makeRepositoriesState(worktree: worktree), 350 + settings: SettingsFeature.State() 351 + ) 352 + state.selectedCustomCommands = [ 353 + UserCustomCommand( 354 + title: "Blank", 355 + systemImage: " ", 356 + command: "swift test", 357 + execution: .shellScript, 358 + shortcut: nil, 359 + ), 360 + ] 361 + 362 + let store = TestStore(initialState: state) { 363 + AppFeature() 364 + } withDependencies: { 365 + $0.terminalClient.send = { command in 366 + sent.withValue { $0.append(command) } 367 + } 368 + } 369 + 370 + await store.send(.runCustomCommand(0)) 371 + await store.finish() 372 + 373 + #expect( 374 + sent.value == [ 375 + .createTabWithInput( 376 + worktree, 377 + input: "swift test", 378 + runSetupScriptIfNew: false, 379 + autoCloseOnSuccess: false, 380 + customCommandName: "Blank", 381 + customCommandIcon: nil 290 382 ), 291 383 ], 292 384 )
+46
supacodeTests/TerminalTabManagerTests.swift
··· 89 89 manager.updateIcon(tabId, icon: "play.fill") 90 90 #expect(manager.tabs.first?.icon == "play.fill") 91 91 } 92 + 93 + @Test func setScriptIconAppliesAndFlags() { 94 + let manager = TerminalTabManager() 95 + let tabId = manager.createTab(title: "one", icon: "terminal") 96 + manager.setScriptIcon(tabId, icon: "play.fill") 97 + #expect(manager.tabs.first?.icon == "play.fill") 98 + #expect(manager.tabs.first?.isScriptIconActive == true) 99 + #expect(manager.tabs.first?.isIconLocked == false) 100 + } 101 + 102 + @Test func updateIconRespectsScriptIconActive() { 103 + let manager = TerminalTabManager() 104 + let tabId = manager.createTab(title: "one", icon: "terminal") 105 + manager.setScriptIcon(tabId, icon: "play.fill") 106 + manager.updateIcon(tabId, icon: "@asset:Npm") 107 + #expect(manager.tabs.first?.icon == "play.fill") 108 + } 109 + 110 + @Test func userOverrideClearsScriptIconActive() { 111 + let manager = TerminalTabManager() 112 + let tabId = manager.createTab(title: "one", icon: "terminal") 113 + manager.setScriptIcon(tabId, icon: "play.fill") 114 + manager.overrideIcon(tabId, icon: "sparkles") 115 + #expect(manager.tabs.first?.icon == "sparkles") 116 + #expect(manager.tabs.first?.isIconLocked == true) 117 + #expect(manager.tabs.first?.isScriptIconActive == false) 118 + } 119 + 120 + @Test func setScriptIconYieldsToUserLock() { 121 + let manager = TerminalTabManager() 122 + let tabId = manager.createTab(title: "one", icon: "terminal") 123 + manager.overrideIcon(tabId, icon: "sparkles") 124 + manager.setScriptIcon(tabId, icon: "play.fill") 125 + #expect(manager.tabs.first?.icon == "sparkles") 126 + #expect(manager.tabs.first?.isScriptIconActive == false) 127 + } 128 + 129 + @Test func clearIconOverrideAlsoClearsScriptIconActive() { 130 + let manager = TerminalTabManager() 131 + let tabId = manager.createTab(title: "one", icon: "terminal") 132 + manager.setScriptIcon(tabId, icon: "play.fill") 133 + manager.clearIconOverride(tabId) 134 + #expect(manager.tabs.first?.isScriptIconActive == false) 135 + manager.updateIcon(tabId, icon: "@asset:Npm") 136 + #expect(manager.tabs.first?.icon == "@asset:Npm") 137 + } 92 138 }