diff --git a/supacode/Features/Terminal/Models/WorktreeTerminalState+Surfaces.swift b/supacode/Features/Terminal/Models/WorktreeTerminalState+Surfaces.swift index a3d96b48..d35aa839 100644 --- a/supacode/Features/Terminal/Models/WorktreeTerminalState+Surfaces.swift +++ b/supacode/Features/Terminal/Models/WorktreeTerminalState+Surfaces.swift @@ -653,9 +653,12 @@ extension WorktreeTerminalState { /// is selected, it is the focused pane of the selected tab (`isFocusedSurface` /// already implies both), and the app window is key and visible. Unknown window /// state (`nil`) is treated as not-viewed so a notification is never silently - /// dropped. + /// dropped. Canvas mode is also treated as not-viewed: the normal-mode window + /// observers are torn down there, so `lastWindowIsKey`/`lastWindowIsVisible` + /// freeze at their pre-canvas values and a backgrounded app would keep muting. func isViewedSurface(_ surfaceId: UUID) -> Bool { - isSelected() && isFocusedSurface(surfaceId) && lastWindowIsKey == true && lastWindowIsVisible == true + guard !isCanvasManaged else { return false } + return isSelected() && isFocusedSurface(surfaceId) && lastWindowIsKey == true && lastWindowIsVisible == true } func updateRunningState(for tabId: TerminalTabID) { diff --git a/supacodeTests/WorktreeTerminalStateViewedSurfaceTests.swift b/supacodeTests/WorktreeTerminalStateViewedSurfaceTests.swift new file mode 100644 index 00000000..65c29d1c --- /dev/null +++ b/supacodeTests/WorktreeTerminalStateViewedSurfaceTests.swift @@ -0,0 +1,65 @@ +import Foundation +import Testing + +@testable import supacode + +@MainActor +struct WorktreeTerminalStateViewedSurfaceTests { + @Test func surfaceIsViewedWhenSelectedFocusedAndWindowActive() { + let (state, surfaceId) = makeViewedState() + + #expect(state.isViewedSurface(surfaceId)) + } + + @Test func surfaceIsNotViewedWhenWindowStateIsUnknown() { + let (state, surfaceId) = makeViewedState() + state.lastWindowIsKey = nil + state.lastWindowIsVisible = nil + + #expect(!state.isViewedSurface(surfaceId)) + } + + @Test func surfaceIsNotViewedWhenWindowIsNotKey() { + let (state, surfaceId) = makeViewedState() + state.lastWindowIsKey = false + + #expect(!state.isViewedSurface(surfaceId)) + } + + @Test func surfaceIsNotViewedWhenCanvasManaged() { + // Canvas mode tears down the normal-mode window observers, so the window + // flags freeze at their pre-canvas values; a stale `true` must not mute + // notifications while the app is in the background. + let (state, surfaceId) = makeViewedState() + state.isCanvasManaged = true + + #expect(!state.isViewedSurface(surfaceId)) + } + + @Test func differentSurfaceIsNotViewed() { + let (state, _) = makeViewedState() + + #expect(!state.isViewedSurface(UUID())) + } + + private func makeViewedState() -> (WorktreeTerminalState, UUID) { + let state = WorktreeTerminalState( + runtime: GhosttyRuntime(), + worktree: Worktree( + id: "/tmp/repo/wt-1", + name: "wt-1", + detail: "", + workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"), + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo") + ) + ) + let tabId = state.tabManager.createTab(title: "tab", icon: nil) + state.tabManager.selectTab(tabId) + let surfaceId = UUID() + state.focusedSurfaceIdByTab[tabId] = surfaceId + state.isSelected = { true } + state.lastWindowIsKey = true + state.lastWindowIsVisible = true + return (state, surfaceId) + } +}