Files
ClaudeDo/docs/superpowers/specs/2026-06-26-in-app-interactive-sessions-design.md
Mika Kuns 10342bc562 docs(interactive): spec + plan for in-app interactive sessions
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.
2026-06-26 16:11:52 +02:00

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)

  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 <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 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 <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 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":"<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.csOpenInteractiveTerminalAsync re-pointed to InteractiveSessionService.StartAsync (no terminal); add SendInteractiveMessage(taskId, text), StopInteractiveSession(taskId) (+ optional InterruptInteractiveSession).
  • Hub/HubBroadcaster.csInteractiveSessionStarted(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.)
  • MissionControlViewModelEnsureMonitor(taskId) on InteractiveSessionStarted so the session appears as a monitor; mark it interactive.
  • Services/Interfaces/IWorkerClient.cs + WorkerClient.csSendInteractiveMessageAsync, 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.