Files
ClaudeDo/docs/superpowers/specs/2026-07-23-conpty-interactive-sessions-design.md
T
mika kuns 85c7e650c9
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 41s
docs(interactive): update ConPTY spec + open items to final state
Correct the spec's binding approach (library LaunchProcess, not the abandoned
custom-pty bypass), add the monospace-font sizing gotcha, on-demand worktree +
prompt seeding, and the deferred Avalonia 12.1 upgrade. Add a ConPTY manual-
verification entry to open.md.
2026-07-23 16:47:16 +02:00

8.1 KiB

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 WindowsTerminalLauncher.BuildResumeArgs/Resolve. Guards mirror ResumeTaskInTerminal for Running/Queued (rejected). Worktree handling (FINAL):

  • Existing Active/Kept worktree + persisted SessionId → --resume <id>.
  • No usable worktree but the list has a WorkingDir (git repo) → create a worktree on demand via WorktreeManager.CreateAsync (the same path autonomous runs use), then a fresh-start spec. This lets never-run tasks be opened interactively.
  • Fresh (non-resume) session → the task's prompt (title + description) is passed as claude's positional prompt so the session starts on the task.
  • No worktree and no WorkingDir → clear error. Interactive sessions are detached: ClaudeDo does NOT record their claude session id (ConPTY is opaque, no stream-json), so resuming a specific past interactive conversation is only via claude's own --continue/--resume in the worktree dir.

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 — use the library's own LaunchProcess() (FINAL). An initial attempt drove Porta.Pty ourselves (own read loop, key tunneling via GenerateKeyInput/GenerateCharInput, manual resize) to bypass LaunchProcess() and inject a custom PtyOptions.Environment. That was a mistake: it rendered wrong, lagged, and dropped input. The spike had proven the library's own TerminalControl.LaunchProcess() pipeline renders correctly, stays responsive, and handles input/resize/focus. So PtyTerminalSession is a thin wrapper (src/ClaudeDo.Ui/Services/PtyTerminalSession.cs):

  • Set control.Process = descriptor.Exe, control.Args = descriptor.Args, control.StartingDirectory = descriptor.Cwd, then await control.LaunchProcess().
  • Relay the control's own ProcessExited event and Kill().
  • Process="" stays on the AXAML TerminalControl to suppress the library's auto-launch-on-load, so exactly one process starts (our manual launch).

Environment: LaunchProcess() gives no per-launch env dict, but Porta.Pty.SpawnAsync inherits the current process environment. So apply descriptor.Env entries via Environment.SetEnvironmentVariable(key, value) (process scope) before LaunchProcess(). The only var needed is MCP_TOOL_TIMEOUT; session-skills/agent-files/MCP-config are on disk / globally registered, independent of env. (The earlier "child gets zero env vars" claim was wrong — Porta.Pty seeds from the process env.)

Sizing gotcha (critical): the control derives Cols/Rows from arranged-size / character-cell-size. Without an EXPLICIT monospace font the cell metrics are wrong and the child TUI renders into the wrong area. Set FontFamily="Cascadia Mono,Consolas,monospace", FontSize, BufferSize, and HorizontalAlignment/VerticalAlignment=Stretch on the TerminalControl (matching the spike) — this is what made rendering correct in the pane.

Lesson: do not hand-roll pty/input/render around this control — use its LaunchProcess() pipeline.

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.

Status (2026-07-23)

Implemented on main (not pushed) and visually verified by the user (rendering, input, responsiveness correct after switching to LaunchProcess() + setting a monospace font). Done: worker launch-spec (task + ad-hoc, on-demand worktree, prompt seeding), UI terminal host, Command Center hosting (grid↔tabs), streaming stack removed. Permission mode: left to claude's default (not forced), per user.

Deferred: Avalonia 12.1 upgrade — blocked, not adopted. 12.1's XAML source generator needs Roslyn 4.14 (.NET 9.0.3xx SDK); the repo pins .NET 8 in global.json, and bumping the SDK floor risks the Gitea Actions release build. Staying on Avalonia 12.0.x (Iciclecreek 2.0.3 works there). Revisit only with a deliberate SDK-floor decision.