# ConPTY interactive sessions & launch specs > **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative. > 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. Covers `InteractiveLaunchSpecService` and the four kinds of embedded ConPTY session the UI process hosts (real `claude` TUI in a Mission Control tile). Autonomous queue tasks are **not** covered here — they stay on the stream-json path via `TaskRunner`. See [worker-task-pipeline.md](worker-task-pipeline.md). ## The four session kinds | Kind | Hub spec method | Notes | |---|---|---| | Task session | `GetInteractiveLaunchSpec` | Effort from the task/list model preset | | Ad-hoc | `GetAdHocLaunchSpec` | Effort from the global default | | Planning | (planning start/resume) | Effort from `PlanningAlias`; uses `--permission-mode default`, **not** `plan` | | List handler | `GetMergeHelperLaunchSpec` | Effort from list config; `--permission-mode auto` (unattended) | ## ⚠️ Gotcha: never pass task free-text as a CLI argument **No ConPTY path ever passes task free-text (title / description / brief) as a CLI argument.** Every one of them writes it to a file first and hands `claude` a single-line kickoff pointing at that file, exposed via `--add-dir`. Two independent reasons: 1. The ConPTY host flattens `Args` into **one command line** to spawn the process, and `claude` re-splits that line on whitespace. Any token starting with `-` in real task text (e.g. `->`, `--abort`) is then misread as an unknown option. 2. A raw multi-line positional prompt truncates at its **first newline** regardless. A fresh task session's brief lives at `~/.todo-app/task-sessions//brief.md` (`InteractiveLaunchSpecService.BuildFreshTaskArgsAsync`). A task with neither title nor description skips the file **and** the positional arg entirely. ## Argument ordering Every spec passes `--effort ` from the relevant model's preset. It **leads** the args — except for a fresh task session with a brief, where `--add-dir ` must come first so `--effort` (a single-value flag) can sit directly before the positional kickoff. `--model` is deliberately **NOT** forced on an interactive session — the user can still switch models in the TUI. ## List handler ("Let Claude handle it") `BuildForMergeHelperAsync` uses `--permission-mode auto` so it runs unattended. The `--allowedTools` allowlist is the security boundary: `mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill`. `MCP_TOOL_TIMEOUT` is 200 s here — `TaskWaitMcpTools` clamps its own timeout to 170 s to stay comfortably under it (see [external-mcp.md](external-mcp.md)). ### The host task and its commit range The handler run **owns a real ClaudeDo task**, created by `CreateMergeHelperTask` (hub) → `InteractiveLaunchSpecService.CreateMergeHelperTaskAsync`, called by the UI *before* it opens the tile: - One new task per run in that list, `Idle` + `IsManual=true` (never queued). - Title/description localized via `missionControl.mergeHelperTaskTitle` / `mergeHelperTaskDescriptionHeader`. - `TaskEntity.HandlerBaseCommit` stamped to the list repo's current HEAD. The host task **never gets a worktree of its own** — the handler commits straight into the list's working dir and merges the tasks it handles itself. Consequences: - `SubmitTaskForReview` branches on whether the task has a `WorktreeEntity`: with one, it commits the worktree; without one, it stamps `HandlerHeadCommit` to the list repo's current HEAD. Both paths then flip the task `Idle`/`Failed` → `WaitingForReview`. - `GetTaskDiff` and the UI's `DetailsIslandViewModel` / `MergeSectionViewModel` fall back to the `HandlerBaseCommit`..`HandlerHeadCommit` range whenever `Worktree` is null. ### UI flow `MergeHelperSelectionModalViewModel` — checkbox picker over one list's non-terminal, non-manual tasks, pre-ticking the actionable ones. **List-scoped only** (`Configure(listId, listName)`, no global scope). Opened from the list row's context menu, which is hidden when the list has no working dir. On confirm: `ListsIslandViewModel` raises `LetClaudeHandleRequested` → shell → `MissionControlViewModel.OpenMergeHelperConPtySessionAsync`, which calls `CreateMergeHelperTaskAsync` and then opens a **task-based** tile (deduped by `TaskId` like `OpenConPtySessionAsync`, **not** `CreateAdHoc`) running the five-phase handler prompt. ## Tile lifecycle `ConPtyPaneViewModel` resolves its own launch spec — the ctor takes a descriptor **factory**, the host wires handlers and then calls `Start()`. So the tile appears **immediately** with its 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`. ## Focus / key handling `InteractiveTerminalView` lives in `MissionControlWindow`, so the `FocusClearing` Escape handler (scoped to `MainWindow` via `AddClassHandler`) never runs there — **Escape always reaches the PTY**. See the note in `src/ClaudeDo.Ui/CLAUDE.md`. `TaskRowViewModel.HasInteractiveSession` shows an accent "Interactive" chip instead of "Parked"; tapping it jumps to that Mission Control pane. `TasksIslandViewModel.SyncInteractiveSessions` mirrors Mission Control's open panes onto the rows. ## System-prompt matrix (autonomous vs. interactive) Autonomous and interactive sessions do **not** share a system prompt. Per start path: | Start path | Entry point | System prompt | |---|---|---| | Autonomous run/continue/retry | `TaskRunner.ResolveConfigAsync` → `ClaudeArgsBuilder.Build` | `--append-system-prompt `, recomputed and re-sent on **every** invocation including a `--resume` continue (`PromptKind.System` + improvement/list/task overrides) | | Interactive task session, fresh | `InteractiveLaunchSpecService.BuildForTaskAsync` → `BuildFreshTaskArgsAsync` | none — no `--append-system-prompt(-file)` at all | | Interactive task session, resume | `InteractiveLaunchSpecService.BuildForTaskAsync` → `WindowsTerminalLauncher.BuildResumeArgs` | none — only `--resume ` (+ `--effort`) | | Ad-hoc directory session | `InteractiveLaunchSpecService.BuildForDirectoryAsync` | none | | Planning session start | `InteractiveLaunchSpecService.BuildPlanningStart` → `WindowsTerminalLauncher.BuildPlanningStartArgs` | `--append-system-prompt-file ` (`PromptKind.Planning`) | | Planning session resume | `InteractiveLaunchSpecService.BuildPlanningResume` → `WindowsTerminalLauncher.BuildPlanningResumeArgs` | none — only `--permission-mode default --allowedTools --resume ` | | List handler ("Let Claude handle it") | `InteractiveLaunchSpecService.BuildForMergeHelperAsync` | `--append-system-prompt-file ` (`PromptKind.MergeHelper`), always fresh — this path never resumes | So every interactive resume (task session and planning) drops the system prompt entirely — it's not that they inherit the autonomous one, it's that **no** `claude` process on any resume path ever passes `--append-system-prompt(-file)`. ### Does `--resume` bring back a prior `--append-system-prompt`? No. Checked by reading real session transcripts (`~/.claude/projects//.jsonl`) for several autonomous ClaudeDo task runs, including ones with multiple invocations (initial run + `ContinueAsync`/retry on the same session id, confirmed via that project's `task_runs` history). Grepped for the `PromptKind.System` default text ("You are completing one well-defined task autonomously...") and for any `"type":"system"` entry or `message.role == "system"` anywhere in those files: the prompt text only ever showed up as ordinary tool-result content (e.g. a task that happened to read `PromptFiles.cs`'s own source), never as a persisted system/config entry. No session transcript — autonomous or interactive — carries a system-role message or a per-session record of the CLI flags it was launched with; there is no sidecar file next to the `.jsonl` either. The system prompt is purely a per-process request parameter the CLI builds fresh from that invocation's own flags, never replayed from a resumed session's history. This matches why `TaskRunner.ContinueAsync` (autonomous) explicitly re-resolves and re-passes `--append-system-prompt` on every continue instead of relying on `--resume` to carry it — if inheritance worked, that re-resolution would be redundant. **Conclusion: no leak.** An interactive resume (task or planning) does not pick up the autonomous run's `--append-system-prompt` text — including the "commit your work" / `CLAUDEDO_BLOCKED` instructions from `PromptKind.System`. It simply runs with the `claude` CLI's own baseline system prompt, same as every other path in this table that passes no system-prompt flag. No code change needed here. ## Related hub methods `GetInteractiveLaunchSpec`, `GetAdHocLaunchSpec`, `GetMergeHelperLaunchSpec`, `CreateMergeHelperTask`, `SubmitTaskForReview`. Planning sessions: `StartPlanningSession`, `ResumePlanningSession`, `DiscardPlanningSession`, `FinalizePlanningSession`, `QueuePlanningSubtasks`, `GetPendingDraftCount`, `GetPlanningAggregate`, `BuildPlanningIntegrationBranch`.