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.
172 lines
8.1 KiB
Markdown
172 lines
8.1 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 `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.
|