diff --git a/docs/explore-notes/conpty-sessions.md b/docs/explore-notes/conpty-sessions.md index 8984a383..a6370d06 100644 --- a/docs/explore-notes/conpty-sessions.md +++ b/docs/explore-notes/conpty-sessions.md @@ -1,7 +1,7 @@ # ConPTY interactive sessions & launch specs > **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative. -> Last verified against commit `bdee731` (2026-08-05). +> Last verified against commit `aac84e4` (2026-08-06). > Drift check: `git log --oneline bdee731..HEAD -- src/ClaudeDo.Worker/Planning src/ClaudeDo.Worker/Hub src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml` > Stable structure only (no line numbers). See docs/explore-notes/README.md. @@ -94,6 +94,24 @@ the host wires handlers and then calls `Start()`. So the tile appears **immediat spinner while the worker is still preparing the worktree. A failed launch keeps the tile with an inline error banner instead of the tile never appearing. +### ⚠️ Gotcha: the terminal library kills its child on visual-tree detach + +`Iciclecreek.Avalonia.Terminal`'s `TerminalView.OnDetachedFromLogicalTree` calls +`CleanupProcess()` (kills the PTY child) unless `BeginReparent()` suppressed it — and Mission +Control detaches pane views routinely (`RebuildOverviewGrid` recreates everything on any pane +add/remove/column change; focus-mode tab switches re-present content). Two-part defense (since +`aac84e4`): + +1. `PtyTerminalSession.StartAsync` puts the control in **permanent reparent mode** right after + `LaunchProcess()` — `EndReparent` is deliberately never called. Teardown is explicit only: + `ConPtyPaneViewModel.Dispose` → `Terminal.Kill()` (pane close, VM disposal via DI on exit). +2. `ConPtyPaneHost` (the DataTemplate content for a pane) reparents **one long-lived + `ConPtyPaneView` per pane VM** (`ConditionalWeakTable`, view pins its own `DataContext`) + instead of letting the template instantiate a fresh view — a fresh view would render a dead, + empty terminal because the running session is bound to the original `TerminalControl`. + Hosts only steal the view while `IsEffectivelyVisible`; the layout toggle posts a reclaim + pass (`MissionControlView.ReclaimVisiblePaneHosts`) so the now-visible layout re-steals. + `Ellipse.spinner` (IslandStyles) is the shared indeterminate spinner — used for a starting pane (`InteractiveTerminalViewModel.IsStarting`) and in place of the refine button while `TaskRowViewModel.IsRefining`.