Files
ClaudeDo/docs/superpowers/specs/2026-07-23-conpty-interactive-sessions-design.md
T

142 lines
6.2 KiB
Markdown

# ConPTY Interactive Sessions — Design
Date: 2026-07-23
Status: Approved (design), implementation not started
## Problem
The current in-app interactive session path streams `stream-json` from a
`claude` process spawned **in the Worker** and renders it as a chat log with a
composer. It does not surface permission requests, AskUser questions, and other
TUI-native interactions well — the rendering is a partial reimplementation of
what the real Claude Code TUI already does. We want full fidelity for
interactive work without rebuilding the TUI.
## Decision
**Hybrid execution model:**
- **Autonomous queue tasks** (`Status=Queued`, picked by the queue): unchanged.
Headless `stream-json`, full orchestration (status flow, diff, review, merge).
- **Interactive sessions**: an **embedded ConPTY terminal** running the real
`claude` CLI, rendered in the **UI process**. Full TUI fidelity (permission
prompts, questions, colors, everything the standalone CLI does). Detached from
the review/merge/status machinery — these are a manual cockpit.
This is the third direction for interactive (external `wt` terminal → streaming
chat → embedded ConPTY). The streaming interactive stack is **removed**, not run
in parallel — accepted as discarded work in exchange for one interactive path
and full fidelity.
## Architecture
### Process location
ConPTY terminal controls render in-process and spawn their child (`claude`) as a
child of the host process. Therefore interactive sessions move **out of the
Worker and into the UI process**. They no longer flow over SignalR. This mirrors
the existing `ResumeTaskInTerminal` behavior (launch real `claude`), but embedded
instead of via external `wt.exe`.
### Worktree preparation (task-based sessions)
Interactive task sessions get the **same worktree preparation as autonomous
runs**: session-skills seeding, agent files, MCP config, environment. The Worker
performs the prep and returns a launch spec to the UI:
```
LaunchSpec {
cwd: string // worktree path
exe: string // resolved claude executable / shell
args: string[] // e.g. --resume <sessionId>
env: Dictionary<string,string>
}
```
The command construction reuses the existing logic in
`WindowsTerminalLauncher.BuildResumeCommand`. Guards mirror
`ResumeTaskInTerminal`: task not Running/Queued, a persisted SessionId exists,
worktree exists and is Active/Kept.
### Free / ad-hoc sessions
In addition to task-based sessions, the user can open an ad-hoc terminal in a
chosen directory (no task). These also get MCP config + env set up so the
`claudedo` tools are available, but no per-level session-skills seeding tied to a
task.
### Terminal host control — RESOLVED by spike (2026-07-23)
**Library: `Iciclecreek.Avalonia.Terminal` 2.0.3** (namespace `Iciclecreek.Terminal`).
It needs Avalonia >= 12.0.2; the repo is on 12.0.4 → compatible, no bump.
`SvcSystems.UI.Terminal` (latest) needs Avalonia 12.1+ → rejected.
Visual pass (user, 2026-07-23): the real `claude` TUI renders correctly inside
the embedded control — Claude Code opened and was usable.
**Binding approach — drive Porta.Pty ourselves, reuse Iciclecreek's VT engine.**
The convenience `TerminalControl.LaunchProcess()` spawns the child itself and
never sets `PtyOptions.Environment`, and there is no settable pty/env seam on
`TerminalControl`/`TerminalView` (env-building lives in the non-virtual
`TerminalView.LaunchProcess()` using private fields). But the VT parser/renderer
is the public `XTerm.Terminal` object exposed via `TerminalControl.Terminal`, so
we reuse it without forking:
- `Porta.Pty.PtyProvider.SpawnAsync(new PtyOptions { Name, App, Cwd, CommandLine,
Environment = <fully populated dict> }, ct)` → `IPtyConnection`.
- Do **not** call `TerminalControl.LaunchProcess()` (leaves its `_ptyConnection`
null).
- Get `TerminalControl.Terminal` (public, `XTerm.Terminal`) after template apply.
- Pump `IPtyConnection.ReaderStream` → `Terminal.Write(text)`; subscribe
`Terminal.DataReceived` → `IPtyConnection.WriterStream`; mirror
`Terminal.Resized` ↔ `IPtyConnection.Resize(cols, rows)`.
- Implement our **own** exit-code / kill / wait-for-exit against our
`IPtyConnection` — do NOT touch `TerminalControl.ExitCode`/`Pid`/`Kill()`/
`WaitForExit()`, they NRE on the null `_ptyConnection`.
Note on environment: `Porta.Pty.SpawnAsync` already seeds the child from the
current (ClaudeDo) process environment (PATH/APPDATA/etc.) and merges
`PtyOptions.Environment` on top — so we only need to ADD our custom vars (MCP
config path, `MAX_THINKING_TOKENS`, …), not rebuild the whole environment. (The
earlier spike's "child gets zero env vars" claim was wrong.)
### Command Center layout
- `MonitorPaneView` keeps the streamed log for autonomous tasks.
- Interactive panes host the terminal control instead of the log+composer.
- Layout is **toggleable**: focus mode (tabs, one session large) ↔ overview mode
(grid, several sessions at once).
## Removals
Worker:
- `StreamingClaudeSession`, `InteractiveSessionService`
- `WorkerHub` interactive methods: `OpenInteractiveTerminal`,
`SendInteractiveMessage`, `RemoveQueuedInteractiveMessage`,
`StopInteractiveSession`, `InterruptInteractiveSession`
- Broadcast events: `InteractiveSessionStarted/Ended`, `InteractiveQueueChanged`,
`InteractiveMessageSent`
- `IdleSessionReaper` and `LiveSessionRegistry` **iff** unused elsewhere
(verify during implementation — do not delete blindly).
UI:
- Composer on `TaskMonitorViewModel`: `ComposerDraft`, `SubmitComposerCommand`,
`InterruptInteractiveCommand`, `StopInteractiveCommand`, `QueuedMessages`,
`IsInteractiveLive`.
- The composer + queued-messages portion of `SessionTerminalView` (the log
portion stays for autonomous panes).
- `IWorkerClient` interactive methods.
## Open items (implementation time)
- Whether `LiveSessionRegistry` is referenced outside the interactive path.
- Session-id availability for a never-run task (no `--resume` → start fresh).
Resolved: env approach (see Terminal host control — add custom vars on top of the
inherited process env via `PtyOptions.Environment`); library + binding seam.
## Non-goals
- No screen-scraping of terminal output back into task status/diff/review.
- No change to the autonomous queue execution path.