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.
6.6 KiB
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: persistentclaudeprocess. Ctor takes resolved args, working dir, seeded first prompt, a line callback,WorkerConfig. Reuse theProcessStartInfoshape +MCP_TOOL_TIMEOUT="200000"fromClaudeProcess.- Keeps stdin open; sends the first prompt as a user-message JSON line (escape via
JsonSerializer). - stdout/stderr read tasks → line callback; parse
resultevents to trackIsTurnInFlight. SendUserMessageAsync(text, ct)— enqueue/write a user-message JSON line; ifIsTurnInFlight, alsoInterruptAsync.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
claudebinary.
- Keeps stdin open; sends the first prompt as a user-message JSON line (escape via
- Test:
StreamingClaudeSessionTests(fake stream) — first message emitted;resultflipsIsTurnInFlightoff; 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: injectIDbContextFactory,WorkerConfig,ClaudeArgsBuilder(or build args inline),HubBroadcaster,LiveSessionRegistry.StartAsync(taskId, ct): resolve list working dir + seeded prompt (reuse the body ofPlanningSessionManager.OpenInteractiveAsync+BuildInteractivePrompt); build interactive args (--model PlanningAlias --permission-mode auto+ streaming flags); spawn the session with a callback that doesHubBroadcaster.TaskMessage(taskId, "[stdout] " + line); register; broadcastInteractiveSessionStarted. Reject if one is already live for the task.SendAsync(taskId, text, ct)→ registryTryGet→SendUserMessageAsync.StopAsync(taskId, ct)→ registry stop +InteractiveSessionEnded.
- Move
OpenInteractiveAsync/BuildInteractivePromptout ofPlanningSessionManagerif it reads cleaner (or call into it). Remove theInteractiveLaunchContextterminal 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: deleteLaunchInteractiveAsync; removeInteractiveLaunchContextfromPlanningSessionContext.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-pointOpenInteractiveTerminalAsynctoInteractiveSessionService.StartAsync(drop_launcher.LaunchInteractiveAsync); addTask SendInteractiveMessage(taskId, text),Task StopInteractiveSession(taskId)(+ optionalInterruptInteractiveSession).Hub/HubBroadcaster.cs:InteractiveSessionStarted(taskId),InteractiveSessionEnded(taskId).Program.cs: registerLiveSessionRegistry+InteractiveSessionServicesingletons.- Test:
WorkerHubsend 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); eventsAction<string>? InteractiveSessionStartedEvent,InteractiveSessionEndedEventwithOn<...>handlers.OpenInteractiveTerminalAsynckeeps name/signature.- Update hand-rolled
IWorkerClientfakes in both Ui.Tests and Worker.Tests.
Task 7 — StreamLineFormatter user bubble (ui)
- Render
type:"user"NDJSON events asLogKind.User(add the kind if missing). - Test: a
userevent yields aLogKind.UserLogLineViewModelwith the text.
Task 8 — Shared composer state on the session VMs (ui, hot files)
- Add to
TaskMonitorViewModelandDetailsIslandViewModel(factor a shared helper —InteractiveComposer— to avoid duplication):ComposerDraft,IsInteractiveLive(toggled byInteractiveSessionStarted/Endedfor the subscribed task),SubmitComposerCommand(CanExecute: non-empty draft && (HasPendingQuestion||IsInteractiveLive)). Route: pending question → existingAnswerTaskQuestionAsync; else →SendInteractiveMessageAsync. Clear draft on submit; clearIsInteractiveLiveon ended. MissionControlViewModel:EnsureMonitor(taskId)onInteractiveSessionStarted.- 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 propsIsComposerVisible,ComposerText,SubmitCommand,ComposerPlaceholder); TextBox (Enter submits) + Send button. Reuse existing tokens (no inline values).- Bind it in
MonitorPaneView.axamlandDetailsIslandView.axamlto 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.