6.1 KiB
ConPTY interactive sessions & launch specs
Explore-note — verify before trusting. Distilled map of a subsystem, not authoritative. Last verified against commit
f6cb825(2026-08-05). Drift check:git log --oneline f6cb825..HEAD -- src/ClaudeDo.Worker/Planning src/ClaudeDo.Worker/Hub src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/Views/InteractiveTerminalView.axamlStable 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.
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:
- The ConPTY host flattens
Argsinto one command line to spawn the process, andclaudere-splits that line on whitespace. Any token starting with-in real task text (e.g.->,--abort) is then misread as an unknown option. - A raw multi-line positional prompt truncates at its first newline regardless.
A fresh task session's brief lives at ~/.todo-app/task-sessions/<taskId>/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 <level> from the relevant model's preset. It leads the args —
except for a fresh task session with a brief, where --add-dir <sessionDir> 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).
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.HandlerBaseCommitstamped 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:
SubmitTaskForReviewbranches on whether the task has aWorktreeEntity: with one, it commits the worktree; without one, it stampsHandlerHeadCommitto the list repo's current HEAD. Both paths then flip the taskIdle/Failed→WaitingForReview.GetTaskDiffand the UI'sDetailsIslandViewModel/MergeSectionViewModelfall back to theHandlerBaseCommit..HandlerHeadCommitrange wheneverWorktreeis 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.
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<MainWindow>) 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.
Related hub methods
GetInteractiveLaunchSpec, GetAdHocLaunchSpec, GetMergeHelperLaunchSpec,
CreateMergeHelperTask, SubmitTaskForReview.
Planning sessions: StartPlanningSession, ResumePlanningSession, DiscardPlanningSession,
FinalizePlanningSession, QueuePlanningSubtasks, GetPendingDraftCount,
GetPlanningAggregate, BuildPlanningIntegrationBranch.