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.
This commit is contained in:
@@ -53,10 +53,19 @@ LaunchSpec {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -74,31 +83,38 @@ It needs Avalonia >= 12.0.2; the repo is on 12.0.4 → compatible, no bump.
|
||||
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:
|
||||
**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`):
|
||||
|
||||
- `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`.
|
||||
- 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).
|
||||
|
||||
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.)
|
||||
**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
|
||||
|
||||
@@ -139,3 +155,17 @@ inherited process env via `PtyOptions.Environment`); library + binding seam.
|
||||
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user