# 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) 1. **Engine: persistent streaming session.** One `claude` process kept alive with `--input-format stream-json`; user messages pushed over stdin. 2. **Scope: interactive sessions only.** The autonomous `TaskRunner`/`ClaudeProcess` run loop, review, queue, and worktree machinery are NOT changed. 3. **Placement: shared `SessionTerminalView`** — the in-app session + composer appear in the task-detail session surface and in the Mission Control monitor pane. 4. **Full replace.** "Run interactively" now opens the in-app session; the `WindowsTerminalLauncher.LaunchInteractiveAsync` path is removed. **Planning** sessions keep using `wt` (untouched). 5. **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 --permission-mode auto ""` 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 `wt` terminal 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 auto` means 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 --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 `result` events (turn boundary; the process then idles for the next message). - `SendUserMessageAsync(text)` writes a user-message JSON line; if a turn is in flight, also `InterruptAsync()` (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":"","request":{"subtype":"interrupt"}}` — no `initialize` handshake 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 `result` with `is_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 incidental `system:init`/`system:status`/`rate_limit_event`/hook events that also appear in the stream. - `--replay-user-messages` echoes each sent message back on stdout as a `user` event, 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 the `ProcessStartInfo` shape + `MCP_TOOL_TIMEOUT` from `ClaudeProcess`; streams via a line callback; `IsTurnInFlight`. Cancellation kills the tree. - `Runner/LiveSessionRegistry.cs` (new, singleton) — `taskId → StreamingClaudeSession` (`Register`/`TryGet`/`Unregister`/`Stop`), mirrors `PendingQuestionRegistry`. - `Planning/InteractiveSessionService.cs` (new) — owns interactive lifecycle: `StartAsync( taskId)` resolves the list working dir + seeded prompt (reuse `OpenInteractiveAsync`'s body), spawns the session, registers it, wires output to `HubBroadcaster.TaskMessage`, broadcasts `InteractiveSessionStarted`; `SendAsync(taskId, text)`; `StopAsync(taskId)` → `InteractiveSessionEnded`. - `Planning/WindowsTerminalLauncher.cs` + `Planning/Interfaces/ITerminalLauncher.cs` — remove `LaunchInteractiveAsync` (+ `InteractiveLaunchContext`). Planning start/resume stay. - `Hub/WorkerHub.cs` — `OpenInteractiveTerminalAsync` re-pointed to `InteractiveSessionService.StartAsync` (no terminal); add `SendInteractiveMessage(taskId, text)`, `StopInteractiveSession(taskId)` (+ optional `InterruptInteractiveSession`). - `Hub/HubBroadcaster.cs` — `InteractiveSessionStarted(taskId)`, `InteractiveSessionEnded(taskId)`. Log lines reuse the existing `TaskMessage(taskId, line)`. - `Program.cs` — register `LiveSessionRegistry` + `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` — render `type:"user"` NDJSON events as a `LogKind.User` bubble. - A small shared composer concept on `TaskMonitorViewModel` **and** `DetailsIslandViewModel` (factor a helper to avoid duplication): `ComposerDraft`, `SubmitComposerCommand`, `IsInteractiveLive` (set by `InteractiveSessionStarted/Ended`). Submit → `SendInteractiveMessageAsync`; clear draft. (If a pending AskUser question exists, the same composer answers it — keep the existing answer route.) - `MissionControlViewModel` — `EnsureMonitor(taskId)` on `InteractiveSessionStarted` so the session appears as a monitor; mark it interactive. - `Services/Interfaces/IWorkerClient.cs` + `WorkerClient.cs` — `SendInteractiveMessageAsync`, `StopInteractiveSessionAsync` (+ optional interrupt); events `InteractiveSessionStartedEvent`/`InteractiveSessionEndedEvent`. `OpenInteractiveTerminalAsync` keeps 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; `result` idles; a sent message starts another turn; mid-turn send calls `InterruptAsync` then 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; `user` line 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 `wt` terminal launch.