···11+# Canvas Exit Terminal Blank Tracking
22+33+Last updated: 2026-04-11
44+Status: Open, intermittent, likely long-lived native state issue
55+66+## Symptom
77+88+When leaving Canvas and returning to the normal worktree terminal view, the terminal area can appear blank.
99+1010+Typical behavior:
1111+1212+- The normal terminal view is visible in SwiftUI.
1313+- The selected worktree and tab are correct.
1414+- The terminal becomes visible again only after switching to another tab and back.
1515+1616+## Reproduction Profile
1717+1818+Current evidence suggests this is not a fresh-session deterministic bug.
1919+2020+- In a newly launched Prowl session, the bug is difficult to reproduce.
2121+- After the app has been running for a long time, especially across system sleep/wake, the bug may start happening.
2222+- Once it starts happening in a given app session, it tends to reproduce reliably on every Canvas exit until the app is restarted.
2323+2424+This strongly suggests a sticky stale state in the long-lived terminal/native view stack rather than a simple `toggleCanvas` reducer bug.
2525+2626+## Relevant Areas
2727+2828+- `supacode/Features/Repositories/Reducer/RepositoriesFeature.swift`
2929+- `supacode/Features/App/Reducer/AppFeature.swift`
3030+- `supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift`
3131+- `supacode/Features/Terminal/Models/WorktreeTerminalState.swift`
3232+- `supacode/Features/Terminal/Views/WorktreeTerminalTabsView.swift`
3333+- `supacode/Features/Terminal/Views/WindowFocusObserverView.swift`
3434+- `supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift`
3535+- `supacode/Infrastructure/Ghostty/GhosttyRuntime.swift`
3636+- `supacode/Features/Canvas/Views/CanvasView.swift`
3737+3838+## Repair History
3939+4040+Known commits that attempted to address this family of issues:
4141+4242+- `161f38a0` Fix blank surface when exiting canvas via toggle shortcut
4343+- `516103e4` fix: invalidate occlusion cache when exiting canvas to prevent blank surfaces
4444+- `11e7d16c` Fix occlusion cache invalidation on surface reattachment
4545+- `7ed53813` fix: defer occlusion apply until surface is attached
4646+- `26273089` fix: simplify canvas exit occlusion handling
4747+- `d4e59155` Add canvas exit terminal diagnostics and occlusion refresh
4848+- `e2a29b2c` test: cover Ghostty attachment occlusion behavior
4949+- `32f51451` Fix canvas exit terminal occlusion recovery
5050+5151+Relevant changelog entries:
5252+5353+- `2026.4.2`: occlusion restored whenever a surface is reattached
5454+- `2026.4.5`: surface state refreshed immediately on Canvas exit
5555+- current unreleased `main`: occlusion recovery also resumes from `updateSurfaceSize()`
5656+5757+## Current Working Theory
5858+5959+The highest-probability root cause is a stale native terminal surface state after reparenting, likely amplified by long app lifetime and sleep/wake transitions.
6060+6161+Current best hypothesis:
6262+6363+- Exiting Canvas causes `GhosttySurfaceView` to be reattached into the normal terminal hierarchy.
6464+- Reparenting invalidates the occlusion-applied cache.
6565+- In some sessions, especially after sleep/wake, the expected "surface is ready again, now reapply visible occlusion" chain does not complete reliably.
6666+- SwiftUI has already switched back to the normal terminal view, but Ghostty's renderer remains effectively paused or not fully resumed for that surface.
6767+- Switching tabs forces another round of visibility/focus activity, which recovers the surface.
6868+6969+What is less likely at this point:
7070+7171+- Wrong worktree selection
7272+- Wrong selected tab restoration
7373+- Basic reducer ordering bug in `toggleCanvas`
7474+7575+Those paths have been observed as correct in diagnostic logs while the surface still remained blank.
7676+7777+## Logs Added For Ongoing Investigation
7878+7979+Two log markers should be collected together:
8080+8181+- `[CanvasExit]`
8282+- `[TerminalWake]`
8383+8484+### `[CanvasExit]`
8585+8686+Existing and previous diagnostics already cover:
8787+8888+- `WorktreeTerminalTabsView.onAppear`
8989+- surface attachment changes
9090+- deferred occlusion
9191+- reapply occlusion
9292+- selected worktree transition when leaving Canvas
9393+9494+### `[TerminalWake]`
9595+9696+Added on 2026-04-11 to correlate future failures with sleep/wake and long-lived surface state:
9797+9898+- `GhosttyRuntime`
9999+ - `workspaceWillSleep`
100100+ - `workspaceDidWake`
101101+ - `screensDidSleep`
102102+ - `screensDidWake`
103103+ - runtime surface count
104104+- `GhosttySurfaceView`
105105+ - per-surface state snapshot on workspace sleep/wake
106106+ - per-surface state snapshot on `viewDidMoveToWindow`
107107+ - per-surface state snapshot on `viewDidMoveToSuperview`
108108+- `WindowFocusObserverView`
109109+ - window activity changes (`key`, `visible`, `force`, `windowNumber`)
110110+111111+Per-surface wake logs include:
112112+113113+- `surface`
114114+- `hasSurface`
115115+- `attached`
116116+- `window`
117117+- `desired`
118118+- `focused`
119119+- `firstResponder`
120120+- `bounds`
121121+- `backing`
122122+- `windowVisible`
123123+- `windowKey`
124124+125125+## How To Collect Logs
126126+127127+Use `make log-stream`, then reproduce the issue and save the section covering:
128128+129129+- the last successful Canvas exit before the bug starts
130130+- the first failed Canvas exit after the bug starts
131131+- any sleep/wake events before that failure
132132+133133+If filtering manually, focus on lines containing:
134134+135135+- `[CanvasExit]`
136136+- `[TerminalWake]`
137137+138138+If using `log stream` directly, a useful predicate is:
139139+140140+```bash
141141+log stream --style compact \
142142+ --predicate 'subsystem == "com.onevcat.prowl" && (eventMessage CONTAINS[c] "[CanvasExit]" || eventMessage CONTAINS[c] "[TerminalWake]")'
143143+```
144144+145145+## What To Compare Next Time
146146+147147+When the bug reproduces again, compare a healthy exit and a broken exit for:
148148+149149+- whether `workspaceDidWake` or `screensDidWake` happened shortly before failures started
150150+- whether the affected surface reports `desired=Optional(true)` but never logs `reapplyOcclusion`
151151+- whether `WindowFocusObserverView` still reports the window as visible/key when the blank terminal is shown
152152+- whether the affected surface is attached to a superview and window but still does not recover
153153+- whether `bounds` and `backing` stop changing for the affected surface while the view is visibly present
154154+155155+## Open Questions
156156+157157+- Is sleep/wake the true trigger, or just the most common way to enter the stale state?
158158+- Is the bad state owned by `GhosttySurfaceView`, underlying `ghostty_surface_t`, or AppKit/Metal attachment?
159159+- Does the failure always affect the same surface instance for a worktree, or any surface after the session becomes "poisoned"?
160160+- Would an explicit post-wake surface refresh solve the actual root cause, or only mask a lower-level Ghostty/AppKit lifecycle issue?
161161+162162+## Next Step Candidates
163163+164164+Do not do these preemptively unless new logs support them:
165165+166166+- add explicit post-wake repair for all active surfaces
167167+- force-resend occlusion and size after wake
168168+- force content-scale/display-id refresh after wake
169169+- invalidate more cached state after wake, not only after attachment changes
170170+171171+## Notes
172172+173173+This document should be updated every time:
174174+175175+- a new hypothesis is formed
176176+- a new instrumentation point is added
177177+- a repro pattern changes
178178+- a candidate fix is attempted
179179+- a failed fix is ruled out