Files
ClaudeDo/docs/superpowers/plans/2026-06-26-in-app-interactive-sessions.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

6.6 KiB

Plan — In-App Interactive Sessions

Spec: docs/superpowers/specs/2026-06-26-in-app-interactive-sessions-design.md

Implement on the shared main tree. Commit explicit paths per task (never git add -A). Build with -c Release (running Worker locks Debug). No real-Claude tests — fake the process stream. Sonnet subagents. Autonomous TaskRunner/ClaudeProcess path stays untouched.

Task 1 — StreamingClaudeSession (worker, new file)

  • Runner/StreamingClaudeSession.cs: persistent claude process. Ctor takes resolved args, working dir, seeded first prompt, a line callback, WorkerConfig. Reuse the ProcessStartInfo shape + MCP_TOOL_TIMEOUT="200000" from ClaudeProcess.
    • Keeps stdin open; sends the first prompt as a user-message JSON line (escape via JsonSerializer).
    • stdout/stderr read tasks → line callback; parse result events to track IsTurnInFlight.
    • SendUserMessageAsync(text, ct) — enqueue/write a user-message JSON line; if IsTurnInFlight, also InterruptAsync.
    • InterruptAsync(ct) — write the control-protocol interrupt line; best-effort (swallow + log on failure → queue fallback applies).
    • StopAsync / DisposeAsync — close stdin, kill the tree, await exit.
    • Injectable stream seam so a fake can drive it without a real claude binary.
  • Test: StreamingClaudeSessionTests (fake stream) — first message emitted; result flips IsTurnInFlight off; a sent message produces a second turn; mid-turn send calls interrupt then delivers; interrupt throw → delivered at natural turn end; stop kills.

Task 2 — LiveSessionRegistry (worker, new file)

  • Runner/LiveSessionRegistry.cs: singleton; Register(taskId, StreamingClaudeSession), bool TryGet(taskId, out session), Unregister(taskId), Task StopAsync(taskId).
  • Test: register→get; unregister; second register stops+replaces; missing get returns false.

Task 3 — InteractiveSessionService (worker, new file)

  • Planning/InteractiveSessionService.cs: inject IDbContextFactory, WorkerConfig, ClaudeArgsBuilder (or build args inline), HubBroadcaster, LiveSessionRegistry.
    • StartAsync(taskId, ct): resolve list working dir + seeded prompt (reuse the body of PlanningSessionManager.OpenInteractiveAsync + BuildInteractivePrompt); build interactive args (--model PlanningAlias --permission-mode auto + streaming flags); spawn the session with a callback that does HubBroadcaster.TaskMessage(taskId, "[stdout] " + line); register; broadcast InteractiveSessionStarted. Reject if one is already live for the task.
    • SendAsync(taskId, text, ct) → registry TryGetSendUserMessageAsync.
    • StopAsync(taskId, ct) → registry stop + InteractiveSessionEnded.
  • Move OpenInteractiveAsync/BuildInteractivePrompt out of PlanningSessionManager if it reads cleaner (or call into it). Remove the InteractiveLaunchContext terminal coupling.
  • Test: InteractiveSessionServiceTests (fake session factory + fake broadcaster) — start resolves dir, seeds prompt, registers, broadcasts started; missing working dir throws; send routes; stop broadcasts ended.

Task 4 — Remove terminal interactive path (worker)

  • Planning/Interfaces/ITerminalLauncher.cs + WindowsTerminalLauncher.cs: delete LaunchInteractiveAsync; remove InteractiveLaunchContext from PlanningSessionContext.cs. Keep planning start/resume launches.
  • Fix any references; ensure the planning launcher tests still build.

Task 5 — Hub + Broadcaster + DI (worker)

  • Hub/WorkerHub.cs: re-point OpenInteractiveTerminalAsync to InteractiveSessionService.StartAsync (drop _launcher.LaunchInteractiveAsync); add Task SendInteractiveMessage(taskId, text), Task StopInteractiveSession(taskId) (+ optional InterruptInteractiveSession).
  • Hub/HubBroadcaster.cs: InteractiveSessionStarted(taskId), InteractiveSessionEnded(taskId).
  • Program.cs: register LiveSessionRegistry + InteractiveSessionService singletons.
  • Test: WorkerHub send routes to a fake service; start invokes the service.

Task 6 — UI client + fakes (ui)

  • Services/Interfaces/IWorkerClient.cs + WorkerClient.cs: SendInteractiveMessageAsync( taskId, text), StopInteractiveSessionAsync(taskId) (+ optional interrupt); events Action<string>? InteractiveSessionStartedEvent, InteractiveSessionEndedEvent with On<...> handlers. OpenInteractiveTerminalAsync keeps name/signature.
  • Update hand-rolled IWorkerClient fakes in both Ui.Tests and Worker.Tests.

Task 7 — StreamLineFormatter user bubble (ui)

  • Render type:"user" NDJSON events as LogKind.User (add the kind if missing).
  • Test: a user event yields a LogKind.User LogLineViewModel with the text.

Task 8 — Shared composer state on the session VMs (ui, hot files)

  • Add to TaskMonitorViewModel and DetailsIslandViewModel (factor a shared helper — InteractiveComposer — to avoid duplication): ComposerDraft, IsInteractiveLive (toggled by InteractiveSessionStarted/Ended for the subscribed task), SubmitComposerCommand (CanExecute: non-empty draft && (HasPendingQuestion || IsInteractiveLive)). Route: pending question → existing AnswerTaskQuestionAsync; else → SendInteractiveMessageAsync. Clear draft on submit; clear IsInteractiveLive on ended.
  • MissionControlViewModel: EnsureMonitor(taskId) on InteractiveSessionStarted.
  • Test: composer enabled while interactive-live; submit routes (chat vs answer) + clears; ended clears live state.

Task 9 — SessionTerminalView composer (ui)

  • Views/Islands/SessionTerminalView.axaml(.cs): optional composer docked bottom (styled props IsComposerVisible, ComposerText, SubmitCommand, ComposerPlaceholder); TextBox (Enter submits) + Send button. Reuse existing tokens (no inline values).
  • Bind it in MonitorPaneView.axaml and DetailsIslandView.axaml to each VM's composer state. Fold the existing AskUser banner into the composer's "answering" state if it reads cleaner; otherwise leave the banner and add the composer below.

Task 10 — Localization

  • en.json + de.json: interactive.composer.placeholder, .send, .stop, plus any "session ended" notice. Keep parity (Localization.Tests).

Task 11 — Build + test + verify

  • Build App + Worker -c Release; run Worker.Tests, Ui.Tests, Localization.Tests.
  • Self-review diffs. Manual smoke (real CLI) — flag to Mika: (a) Run interactively opens an in-app chat (no terminal) and streams; (b) sending a message mid-turn interrupts + redirects; (c) stop kills the process; (d) session shows in both task detail and Mission Control. Do not push.