native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #37 from onevcat/feature/canvas-mark-read-on-input

Mark canvas notifications read on key input

authored by

Wei Wang and committed by
GitHub
2364dcbc 75699651

+55 -1
+1
supacode/Clients/Terminal/TerminalClient.swift
··· 24 24 case prune(Set<Worktree.ID>) 25 25 case setNotificationsEnabled(Bool) 26 26 case setCommandFinishedNotification(enabled: Bool, threshold: Int) 27 + case setCanvasMode(Bool) 27 28 case setSelectedWorktreeID(Worktree.ID?) 28 29 } 29 30
+3 -1
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 608 608 state.preCanvasWorktreeID = state.selectedWorktreeID 609 609 state.selection = .canvas 610 610 state.sidebarSelectedWorktreeIDs = [] 611 - return .none 611 + return .run { _ in 612 + await terminalClient.send(.setCanvasMode(true)) 613 + } 612 614 613 615 case .toggleCanvas: 614 616 if state.isShowingCanvas {
+4
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 107 107 setNotificationsEnabled(enabled) 108 108 case .setCommandFinishedNotification(let enabled, let threshold): 109 109 setCommandFinishedNotification(enabled: enabled, threshold: threshold) 110 + case .setCanvasMode(let enabled): 111 + if enabled { 112 + selectedWorktreeID = nil 113 + } 110 114 case .setSelectedWorktreeID(let id): 111 115 guard id != selectedWorktreeID else { return } 112 116 if let previousID = selectedWorktreeID, let previousState = states[previousID] {
+19
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 31 31 var notificationsEnabled = true 32 32 private var commandFinishedNotificationEnabled = true 33 33 private var commandFinishedNotificationThreshold = 10 34 + private var lastKeyInputTimeBySurface: [UUID: ContinuousClock.Instant] = [:] 34 35 var hasUnseenNotification: Bool { 35 36 notifications.contains { !$0.isRead } 36 37 } ··· 684 685 self.emitFocusChangedIfNeeded(view.id) 685 686 self.emitTaskStatusIfChanged() 686 687 } 688 + view.onKeyInput = { [weak self, weak view] in 689 + guard let self, let view else { return } 690 + self.recordKeyInput(forSurfaceID: view.id) 691 + self.markNotificationsRead(forSurfaceID: view.id) 692 + } 687 693 surfaces[view.id] = view 688 694 return view 689 695 } ··· 821 827 onNotificationReceived?(trimmedTitle, trimmedBody) 822 828 } 823 829 830 + /// How recently the user must have typed for us to consider the exit user-initiated. 831 + static let recentInteractionWindow: Duration = .seconds(3) 832 + 833 + func recordKeyInput(forSurfaceID surfaceId: UUID) { 834 + lastKeyInputTimeBySurface[surfaceId] = .now 835 + } 836 + 824 837 func handleCommandFinished(exitCode: Int?, durationNs: UInt64, surfaceId: UUID) { 825 838 guard commandFinishedNotificationEnabled else { return } 826 839 let durationSeconds = Int(durationNs / 1_000_000_000) 827 840 guard durationSeconds >= commandFinishedNotificationThreshold else { return } 828 841 // Skip user-initiated termination (Ctrl+C / kill signal) 829 842 if let code = exitCode, code == 130 || code == 143 { return } 843 + // Skip if the user was recently typing in this surface (e.g. /exit, quit) 844 + if let lastInput = lastKeyInputTimeBySurface[surfaceId], 845 + ContinuousClock.now - lastInput < Self.recentInteractionWindow 846 + { 847 + return 848 + } 830 849 831 850 let title = (exitCode == nil || exitCode == 0) ? "Command finished" : "Command failed" 832 851 let formattedDuration = Self.formatDuration(durationSeconds)
+2
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 96 96 } 97 97 } 98 98 var onFocusChange: ((Bool) -> Void)? 99 + var onKeyInput: (() -> Void)? 99 100 100 101 private var accessibilityPaneIndexHelp: String? 101 102 ··· 532 533 return 533 534 } 534 535 bridge.state.bellCount = 0 536 + onKeyInput?() 535 537 let (translationEvent, translationMods) = translationState(event, surface: surface) 536 538 let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS 537 539 keyTextAccumulator = []
+26
supacodeTests/CommandFinishedNotificationTests.swift
··· 89 89 #expect(state.notifications.count == 1) 90 90 } 91 91 92 + // MARK: - Recent Interaction Suppression 93 + 94 + @Test func doesNotGenerateNotificationAfterRecentKeyInput() { 95 + let state = makeState() 96 + state.recordKeyInput(forSurfaceID: surfaceId) 97 + state.handleCommandFinished(exitCode: 0, durationNs: 60_000_000_000, surfaceId: surfaceId) 98 + 99 + #expect(state.notifications.isEmpty) 100 + } 101 + 102 + @Test func recentKeyInputOnDifferentSurfaceDoesNotSuppressNotification() { 103 + let state = makeState() 104 + let otherSurfaceId = UUID() 105 + state.recordKeyInput(forSurfaceID: otherSurfaceId) 106 + state.handleCommandFinished(exitCode: 0, durationNs: 60_000_000_000, surfaceId: surfaceId) 107 + 108 + #expect(state.notifications.count == 1) 109 + } 110 + 111 + @Test func generatesNotificationWithNoKeyInputHistory() { 112 + let state = makeState() 113 + state.handleCommandFinished(exitCode: 0, durationNs: 60_000_000_000, surfaceId: surfaceId) 114 + 115 + #expect(state.notifications.count == 1) 116 + } 117 + 92 118 // MARK: - Duration Formatting 93 119 94 120 @Test func formatDurationSeconds() {