Replace the external wt.exe 'Run interactively' launch with an in-app streaming chat (persistent claude --input-format stream-json), rendered in the shared SessionTerminalView in task detail and Mission Control. Autonomous task execution is untouched. Mid-turn interrupt+redirect verified against CLI 2.1.191 via spike.
9.1 KiB
In-App Interactive Sessions — Design
Date: 2026-06-26 Status: Proposed (awaiting approval)
Goal
Replace the external Windows-Terminal "Run interactively" session with an in-app
streaming chat, rendered in the existing SessionTerminalView in both task detail and
Mission Control. Keep everything inside the app — no wt.exe pop-out. Autonomous task
execution is untouched (stays one-shot, non-interactive).
Decisions (brainstorm)
- Engine: persistent streaming session. One
claudeprocess kept alive with--input-format stream-json; user messages pushed over stdin. - Scope: interactive sessions only. The autonomous
TaskRunner/ClaudeProcessrun loop, review, queue, and worktree machinery are NOT changed. - Placement: shared
SessionTerminalView— the in-app session + composer appear in the task-detail session surface and in the Mission Control monitor pane. - Full replace. "Run interactively" now opens the in-app session; the
WindowsTerminalLauncher.LaunchInteractiveAsyncpath is removed. Planning sessions keep usingwt(untouched). - Send semantics: interrupt + redirect mid-turn (control protocol), with automatic queue-for-next-turn fallback if interrupt is unavailable.
What an interactive session is (unchanged semantics, new transport)
Today (PlanningSessionManager.OpenInteractiveAsync + WindowsTerminalLauncher):
claude --model <PlanningAlias> --permission-mode auto "<task title+description>" in the
list's working dir, env MAX_THINKING_TOKENS=20000, full default toolset, relies on the
globally-registered claudedo MCP. Ephemeral — no worktree, no task_run record, no
status change, no review.
We keep all of that. Only the transport changes: instead of a wt window, the same
claude invocation runs as a persistent stream-json process owned by the worker, its output
streamed into the app and its stdin fed from an in-app composer.
Honest tradeoff: the
wtterminal gave the full Claude Code TUI (slash-command UX, interactive prompts). An in-app stream-json chat is plainer — type messages, watch streamed output.--permission-mode automeans no blocking permission prompts (so headless works), but it is a simpler surface than the real TUI. Accepted per the "full replace" decision.
The streaming engine
Flags: --model <PlanningAlias> --permission-mode auto --input-format stream-json --output-format stream-json --verbose --replay-user-messages in the list working dir, env
MAX_THINKING_TOKENS=20000. No --mcp-config/--allowedTools (interactive uses the global
MCP + default tools, exactly as today).
- First stdin message = the seeded interactive prompt:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"…"}]},"parent_tool_use_id":null}\n(stdin stays open). - A stdout read task forwards each NDJSON line to a callback (→ broadcast + the session's log)
and detects
resultevents (turn boundary; the process then idles for the next message). SendUserMessageAsync(text)writes a user-message JSON line; if a turn is in flight, alsoInterruptAsync()(control-protocol interrupt) so Claude pivots immediately. If interrupt is unavailable, the message lands when the current turn ends → automatic queue fallback.- Interrupt is verified working (spike, 2026-06-26, CLI 2.1.191). Exact shape:
{"type":"control_request","request_id":"<id>","request":{"subtype":"interrupt"}}— noinitializehandshake needed;control_response {"subtype":"success"}confirms synchronously; the same process then accepts the redirect and runs a fresh turn with context intact. - Interrupt artifact: the aborted turn emits a
resultwithis_error=true, subtype="error_during_execution". The session must treat an interrupt-induced result as "turn aborted, continue" (drain the queued redirect), not as a session failure. Tolerate the incidentalsystem:init/system:status/rate_limit_event/hook events that also appear in the stream. --replay-user-messagesechoes each sent message back on stdout as auserevent, so it rides the existing stream pipeline into the timeline (ordered + confirmed) with no extra broadcast surface.- The session ends only when the user stops it (kill the process tree) — an interactive session has no auto-finalize and never enters review. No queue slot is involved (it is launched directly, not via the autonomous picker).
Surface changes
Worker
Runner/StreamingClaudeSession.cs(new) — persistent process + send/interrupt/stop; reuse theProcessStartInfoshape +MCP_TOOL_TIMEOUTfromClaudeProcess; streams via a line callback;IsTurnInFlight. Cancellation kills the tree.Runner/LiveSessionRegistry.cs(new, singleton) —taskId → StreamingClaudeSession(Register/TryGet/Unregister/Stop), mirrorsPendingQuestionRegistry.Planning/InteractiveSessionService.cs(new) — owns interactive lifecycle:StartAsync( taskId)resolves the list working dir + seeded prompt (reuseOpenInteractiveAsync's body), spawns the session, registers it, wires output toHubBroadcaster.TaskMessage, broadcastsInteractiveSessionStarted;SendAsync(taskId, text);StopAsync(taskId)→InteractiveSessionEnded.Planning/WindowsTerminalLauncher.cs+Planning/Interfaces/ITerminalLauncher.cs— removeLaunchInteractiveAsync(+InteractiveLaunchContext). Planning start/resume stay.Hub/WorkerHub.cs—OpenInteractiveTerminalAsyncre-pointed toInteractiveSessionService.StartAsync(no terminal); addSendInteractiveMessage(taskId, text),StopInteractiveSession(taskId)(+ optionalInterruptInteractiveSession).Hub/HubBroadcaster.cs—InteractiveSessionStarted(taskId),InteractiveSessionEnded(taskId). Log lines reuse the existingTaskMessage(taskId, line).Program.cs— registerLiveSessionRegistry+InteractiveSessionService.
UI
Views/Islands/SessionTerminalView.axaml(.cs)— add an optional composer (styled properties:IsComposerVisible,ComposerText,SubmitCommand,ComposerPlaceholder). Both hosts (task detail + Mission Control) get it by binding their VM's composer state.StreamLineFormatter— rendertype:"user"NDJSON events as aLogKind.Userbubble.- A small shared composer concept on
TaskMonitorViewModelandDetailsIslandViewModel(factor a helper to avoid duplication):ComposerDraft,SubmitComposerCommand,IsInteractiveLive(set byInteractiveSessionStarted/Ended). Submit →SendInteractiveMessageAsync; clear draft. (If a pending AskUser question exists, the same composer answers it — keep the existing answer route.) MissionControlViewModel—EnsureMonitor(taskId)onInteractiveSessionStartedso the session appears as a monitor; mark it interactive.Services/Interfaces/IWorkerClient.cs+WorkerClient.cs—SendInteractiveMessageAsync,StopInteractiveSessionAsync(+ optional interrupt); eventsInteractiveSessionStartedEvent/InteractiveSessionEndedEvent.OpenInteractiveTerminalAsynckeeps its name/signature (now starts the in-app session). Update hand-rolled fakes in both test projects (iworkerclient_fakes_sync).TasksIslandViewModel.RunInteractivelyAsync— unchanged call site; now opens/focuses the in-app session surface instead of a terminal.- Localization
interactive.*/missionControl.chat.*(en/de, parity enforced).
Tests
StreamingClaudeSessionTests(fake process stream, no real Claude): first message streams;resultidles; a sent message starts another turn; mid-turn send callsInterruptAsyncthen delivers; interrupt-failure degrades to queue; stop kills.LiveSessionRegistryTests— register/get/unregister/stop.InteractiveSessionServiceTests— start resolves working dir + seeds prompt + registers + broadcasts started; send routes to the session; stop broadcasts ended (fake session + broadcaster).TaskMonitorViewModelTests/DetailsIslandViewModelTests— composer enabled while interactive-live; submit invokes client + clears;userline renders; question route still answers.
Risks / open questions
- Interrupt protocol shape — RESOLVED (spike 2026-06-26, see "The streaming engine"). Mid-turn interrupt works on CLI 2.1.191 with the documented shape; the queue fallback is a genuine fallback now, not the expected path. Re-verify if the CLI version changes.
- Plainer than the TUI — slash-command/interactive-prompt UX differs (accepted).
- Auto-mode editing the list working dir directly (no worktree) — this is the existing interactive behavior, unchanged here.
- No real-Claude tests (project rule) — the live loop is covered only by the fake stream; real interrupt/redirect is a manual verification gap to flag.
Non-goals
- Changing autonomous task execution / review / queue / worktrees.
- Interactive sessions producing run records, worktrees, or review (stays ephemeral).
- Worktree isolation for interactive edits; image/attachment messages in the composer.
- Removing planning's
wtterminal launch.