Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/docs/plans/plan_editor_backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# MiniPty Editor Backend Plan (Use Case 4)

The "separate plan" deferred from [plan_minipty_next.md](plan_minipty_next.md) (Deferred: Editor Terminal Backend). Goal: make MiniPty usable as the backend PTY (node-pty–equivalent role) for xterm.js and editor terminals such as VS Code.

**Status: implemented.** Contracts live in [specs/terminal.md](../specs/terminal.md) and [specs/core_session.md](../specs/core_session.md); this document records scope decisions and the gap analysis that drove them.

## Gap analysis (node-pty / xterm.js / VS Code)

Research inputs: the node-pty `IPty` API, the xterm.js flow-control guide (ACK watermark protocol), and the VS Code `Pseudoterminal` extension API.

| Needed by frontends | node-pty shape | MiniPty before this plan | Resolution |
|---|---|---|---|
| Push output / exit-after-drain | `onData` / `onExit` | pull (`ReadOutputAsync` / `WaitForExitAsync`) | **MiniPty.Terminal** `PtyTerminal` (handler + `Completion`) |
| Flow control | `pause()` / `resume()` | strict handoff (consumer-driven) | `PtyTerminal.Pause/Resume` — pump gate; no core change needed |
| WebSocket bridging + ACK watermarks | external server code | none | `PtyWebSocketBridge` (binary=data, text=JSON control) |
| Exit signal reporting | `onExit {exitCode, signal}` | exit code only | core `PtyExitStatus` / `WaitForExitStatusAsync` / `ExitStatus` |
| Signal kill | `kill(signal)` | SIGKILL only | core `Kill(PtySignal)` (Windows: advisory, terminates — node-pty parity) |
| ConPTY `clear()` | conpty.dll signal pipe | — | **rejected**: not reachable via public Win32 API; node-pty ships its own conpty.dll |
| Pixel-size resize, uid/gid, `openpty` w/o spawn | supported | — | deferred (no frontend demand yet) |
| Process title tracking (`process`) | supported | — | deferred; needs per-OS foreground-process lookup (tcgetpgrp + /proc, libproc) |

## Scope decisions

- **Single new package `MiniPty.Terminal`** (facade + transport-agnostic bridge; depends on MiniPty only). One package because the bridge is thin and both layers target the same audience; a split (`.Xterm`) would multiply packaging for no dependency win.
- **Core additions limited to exit-status/signal parity** (`PtyExitStatus`, `WaitForExitStatusAsync`, `Kill(PtySignal)`). Flow control did not need core changes: strict handoff already makes "stop consuming" equal "backpressure the child", so pause/resume is a facade-level gate.
- **Sample = browser E2E** ([samples/WebTerminal.cs](../../../samples/WebTerminal.cs)): `HttpListener` + embedded xterm.js page + `--smoke` self-check wired into the CI NativeAOT sample loop. A VS Code extension is documented as a bridging pattern in [terminal.md](../specs/terminal.md), not shipped.

## Implementation record (2026-07-09)

| Stage | Delivered |
|---|---|
| Core parity | `PtySignal`, `PtyExitStatus`, `PtySession.ExitStatus` / `WaitForExitStatusAsync` / `Kill(PtySignal)`; `IPtyBackend.ExitSignal` / `Kill(signal)`; Unix `_termSignal` capture in `TryRefreshExitState`; portable `WIFSIGNALED` fix (see lessons). No native shim change — waitpid decoding was already managed and the exited/signaled status layout is identical on Linux/macOS/FreeBSD. |
| Facade | `PtyTerminal` + `PtyTerminalOptions` (required output handler, `Completion`, `Pause`/`Resume` gate). |
| Bridge | `PtyWebSocketBridge` + `PtyBridgeOptions`; `Internal/BridgeFlowControl` (locked watermark state, `Disable` for teardown), `Internal/BridgeJson` (source-generated `System.Text.Json`). |
| Sample/CI | `samples/WebTerminal.cs` added to the `build.yaml` NativeAOT sample loop; `scripts/pack-with-native.sh` packs the new package; verified interactively in a real browser (input, output, resize, exit banner, NormalClosure). |
| Tests | `PtyTests` exit-status/kill-signal additions; `PtyTerminalTests`; `PtyWebSocketBridgeTests` over `WebSocket.CreateFromStream` on an in-memory duplex stream pair (framing, flow-control stall/resume, exit ordering, close semantics, policy violation). |

## Lessons learned

Contract-level lessons live in [specs/terminal.md](../specs/terminal.md), [core_session.md](../specs/core_session.md), and [lifecycle.md](../specs/lifecycle.md). Plan-level:

- The direct C# transcription of glibc's `WIFSIGNALED` macro loses the signed-char cast and misclassifies the stopped marker `0x7f`; unreachable under WNOHANG-only polling, but it became load-bearing the moment the signal turned into public API. Use `(status & 0x7f) != 0 && (status & 0x7f) != 0x7f`.
- SIGUSR1/SIGUSR2 numbering differs per OS (Linux 10/12, macOS/FreeBSD 30/31), which is why `Kill` takes a `PtySignal` enum mapped per platform instead of a raw int, while `PtyExitStatus.Signal` reports the raw OS number (node-pty parity).
- `PtyExitStatus.ExitCode` deliberately keeps MiniPty's `128 + signal` semantics instead of node-pty's 0-on-signal so a killed child never reads as success and `ExitStatus.ExitCode == ExitCode` always holds.

## Deferred / future

- `Attach(PtySession)` facade overload → prerequisite for bridge reconnect/detach options.
- Process title tracking, pixel-size resize, uid/gid, `openpty` without spawn — revisit on demand.
- ConPTY `clear()` — only viable by shipping conpty.dll (third-party dependency; against AGENTS.md) or if Windows exposes a public API.
23 changes: 10 additions & 13 deletions .github/docs/plans/plan_minipty_next.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,7 @@ public readonly record struct PtyExitStatus(
string? Signal = null);
```

Open questions:

- Whether signal reporting is worth adding now or should remain future work.
- Whether `ExitCode` should remain the simple cross-platform contract and `PtyExitStatus` be introduced only when needed.
Resolved (editor backend plan): implemented as `PtyExitStatus(int ExitCode, int? Signal)` with `WaitForExitStatusAsync` / `ExitStatus`. The signal is the raw OS number (node-pty parity), not a string; `ExitCode` keeps the `128 + signal` contract. See [specs/core_session.md](../specs/core_session.md) and [plan_editor_backend.md](plan_editor_backend.md).

### Persistent Session Convenience

Expand Down Expand Up @@ -665,16 +662,16 @@ This sample does not use host raw mode. It demonstrates persistent **transport**
- [ ] Manual smoke on host TTY (vim or minimal TUI) — verify locally on macOS / interactive host.
- [x] README / spec.md updated when implemented.

### Deferred: Editor Terminal Backend (use case 4 — separate plan)
### Editor Terminal Backend (use case 4 — **implemented in a separate plan**)

Not part of Milestone 5. Track in a **separate plan** when VS Code / xterm.js backend work starts.
Implemented as **MiniPty.Terminal** plus core exit-status parity; see [plan_editor_backend.md](plan_editor_backend.md) and [specs/terminal.md](../specs/terminal.md).

Candidate **MiniPty core** parity items (formerly “Milestone 5 node-pty”):
Resolution of the candidate parity items:

- Flow control (`pause` / `resume`, XON/XOFF).
- Windows ConPTY `clear()`.
- `PtyExitStatus` / signal reporting; `Kill(signal)` on Unix.
- Unix pixel winsize; `uid` / `gid`; `openpty` without spawn; ConPTY cursor inheritance.
- Flow control (`pause` / `resume`) — implemented in `PtyTerminal` (facade pump gate; no core change needed under strict handoff).
- `PtyExitStatus` / signal reporting; `Kill(signal)` — implemented in **MiniPty** core.
- Windows ConPTY `clear()` — rejected: requires node-pty's bundled conpty.dll signal pipe; not reachable via public Win32 API.
- Unix pixel winsize; `uid` / `gid`; `openpty` without spawn; ConPTY cursor inheritance — deferred (no frontend demand yet).

**MiniPty.Console** is not used for use case 4.

Expand Down Expand Up @@ -719,9 +716,9 @@ As milestones land, update:
- Should backpressure limits (buffer upper bound, chunk size) become configurable start/read options?
- Is Unix `uid` / `gid` worth supporting in a minimal AOT-friendly library?
- Does Windows ConPTY require a public ready state or only internal write/resize deferral?
- Should flow control be explicit (`Pause` / `Resume`) or expressed through bounded async streams/channels?
- ~~Should flow control be explicit (`Pause` / `Resume`) or expressed through bounded async streams/channels?~~ — resolved: explicit `Pause` / `Resume` on **MiniPty.Terminal** `PtyTerminal`; core keeps strict-handoff backpressure only ([terminal.md](../specs/terminal.md)).
- ~~Should `MiniPty.Console` be a package, a sample, or both at first?~~ — resolved: separate **MiniPty.Console** NuGet; spec in [console.md](../specs/console.md).
- Editor terminal backend (use case 4) parity items deferred to a **separate plan** (not Milestone 5).
- ~~Editor terminal backend (use case 4) parity items deferred to a **separate plan** (not Milestone 5).~~ — resolved: implemented; see [plan_editor_backend.md](plan_editor_backend.md).

## Guiding Principle

Expand Down
2 changes: 2 additions & 0 deletions .github/docs/references/pty_crossplatform.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Library callers use `Pty.Start` → `PtySession`. `PtyCapture.RunAsync` wraps th
| `WaitForExitAsync(CancellationToken)` | Polls the child. Cancellation stops waiting only; the child keeps running (`OperationCanceledException`). |
| `CompleteAsync(PtyCompleteOptions, CancellationToken)` | Optional stdin, wait for exit, drain output, return `PtyResult`. Cancellation kills the child when `KillOnCancellation` is true (default). |
| `Kill()` | `TerminateProcess` (Windows) or `kill(SIGKILL)` (Unix). Does not release handles; call `Dispose` afterward. |
| `Kill(PtySignal)` | **Unix:** `kill(2)` with the per-OS native number (SIGUSR1/2 differ: Linux 10/12, macOS/FreeBSD 30/31). **Windows:** signal is advisory; validates the enum then `TerminateProcess` (node-pty parity). |
| `WaitForExitStatusAsync` / `ExitStatus` | Adds the Unix termination signal (`waitpid` `WTERMSIG`) to the exit code; decoding is fully managed (identical exited/signaled status layout on Linux/macOS/FreeBSD). Windows always reports a null signal. |
| `PtyCompleteOptions.OutputDrainGrace` | Default 1s—post-exit drain before closing transport. |
| `PtyCompleteOptions.OutputReaderCloseTimeout` | Default 5s—wait after transport close for the reader to finish. |
| `Dispose()` | If the child is still running, **kills** it, then closes ConPTY/pipes/process handles. On Unix, `Dispose` also attempts a bounded `waitpid` after `SIGKILL` to avoid leaving a zombie (up to ~1s). |
Expand Down
23 changes: 16 additions & 7 deletions .github/docs/spec.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MiniPty Specification

User-facing specification entry point for **MiniPty**, **MiniPty.Capture**, and **MiniPty.Console** NuGet packages. Detailed contracts are split by behavior area under [specs/](specs/). OS-level implementation notes live in [references/](references/) (for example [pty_crossplatform.md](references/pty_crossplatform.md), [windows_console_input.md](references/windows_console_input.md)).
User-facing specification entry point for **MiniPty**, **MiniPty.Capture**, **MiniPty.Console**, and **MiniPty.Terminal** NuGet packages. Detailed contracts are split by behavior area under [specs/](specs/). OS-level implementation notes live in [references/](references/) (for example [pty_crossplatform.md](references/pty_crossplatform.md), [windows_console_input.md](references/windows_console_input.md)).

## Motivation

Expand All @@ -15,7 +15,7 @@ MiniPty exists as a **standalone, NativeAOT-friendly** library so any .NET progr
| **1** | PTY transport (node-pty–equivalent core) | **MiniPty** | [Core session](specs/core_session.md), [Lifecycle](specs/lifecycle.md) |
| **2** | One-shot stdin + timestamped record | **MiniPty.Capture** | [Capture](specs/capture.md) |
| **3** | Interactive host (vim, etc.) — human types on real terminal | **MiniPty** + **MiniPty.Console** | [Console](specs/console.md) (embedder owns `ReadOutputAsync`) |
| **4** | In-editor terminal backend (xterm.js, etc.) | **MiniPty** only | Core embedder pattern in [Core session](specs/core_session.md); editor parity features are a **separate plan** |
| **4** | In-editor terminal backend (xterm.js, etc.) | **MiniPty** + **MiniPty.Terminal** | [Terminal](specs/terminal.md) (push facade, WebSocket bridge, flow control) |

Use case 3 recording and cast format remain **scenetake** (or other embedder) responsibilities. **MiniPty.Console** does not record output.

Expand All @@ -26,23 +26,27 @@ Use case 3 recording and cast format remain **scenetake** (or other embedder) re
| **MiniPty** | Core PTY transport: spawn, streams, lifecycle | — |
| **MiniPty.Capture** | One-shot run with per-read timestamps (`PtyCapture.RunAsync`) | MiniPty |
| **MiniPty.Console** | Host keyboard → PTY input (`PtyConsoleInput.Attach`) | MiniPty |
| **MiniPty.Terminal** | Frontend terminal backend: push facade (`PtyTerminal`), xterm.js WebSocket bridge (`PtyWebSocketBridge`) | MiniPty |

```mermaid
flowchart TB
M["MiniPty"]
C["MiniPty.Capture"]
O["MiniPty.Console"]
T["MiniPty.Terminal"]
C --> M
O --> M
T --> M
```

| You need… | Packages |
|---|---|
| General PTY I/O, `ReadOutputAsync`, `CompleteAsync`, editor backend | **MiniPty** |
| General PTY I/O, `ReadOutputAsync`, `CompleteAsync` | **MiniPty** |
| One-shot command with timestamped output chunks | **MiniPty** + **MiniPty.Capture** |
| Human types on the host terminal (vim, etc.) | **MiniPty** + **MiniPty.Console** (+ your `ReadOutputAsync` for display/record) |
| Backend PTY for xterm.js / editor terminals (push events, flow control, WebSocket) | **MiniPty** + **MiniPty.Terminal** |

**MiniPty.Capture** and **MiniPty.Console** are optional add-ons. Both depend on core only; Console does not read PTY output.
**MiniPty.Capture**, **MiniPty.Console**, and **MiniPty.Terminal** are optional add-ons. All depend on core only; Console does not read PTY output; Terminal owns its session's output exclusively.

## Implemented Scope

Expand All @@ -57,28 +61,33 @@ flowchart TB
| Understand supported OS targets and public platform guarantees | [Platform support](specs/platform_support.md) |
| Persistent transport sample (`ReadOutputAsync` command loop) | [samples/Interactive.cs](../../samples/Interactive.cs) |
| Attach host terminal input to an existing `PtySession` (use case 3) | [Console](specs/console.md) |
| Exit status with Unix termination signal; `Kill(PtySignal)` | [Core session](specs/core_session.md) |
| Backend PTY for frontend terminals: push facade, flow control, xterm.js WebSocket bridge (use case 4) | [Terminal](specs/terminal.md) |
| Browser terminal sample (xterm.js over WebSocket) | [samples/WebTerminal.cs](../../samples/WebTerminal.cs) |

## Planned Scope

_(No open package milestones. Editor terminal backend parity for use case 4 is tracked in a separate plan.)_
_(No open package milestones.)_

## Out of Scope For The Current Implementation

- Terminal emulation, TUI replay, or faithful screen-buffer rendering
- Cast / asciinema recording (embedder responsibility; scenetake for example)
- In-editor terminal integration and node-pty parity (pause, ConPTY clear, exit signal split) — separate plan for use case 4
- Windows ConPTY `clear()` (requires the conpty.dll signal pipe; not reachable via public Win32 API)
- Terminal session reconnect / detach-reattach ([Terminal](specs/terminal.md) non-goal N5)
- Remote shells (`ssh`)
- Spilling capture to disk when memory is exhausted
- Capture tuning such as max chunk size or chunk timestamp modes

Planning notes for Console implementation and deferred editor parity are in [plans/plan_minipty_next.md](plans/plan_minipty_next.md). Planning documents are not implemented API contracts unless mirrored in [specs/](specs/).
Planning notes for Console implementation are in [plans/plan_minipty_next.md](plans/plan_minipty_next.md); the editor terminal backend (use case 4) decision record is in [plans/plan_editor_backend.md](plans/plan_editor_backend.md). Planning documents are not implemented API contracts unless mirrored in [specs/](specs/).

## Related Documents

- [specs/core_session.md](specs/core_session.md) — `Pty.Start`, `PtySession`, streams, resize, wait, kill, dispose, embedder patterns
- [specs/completion.md](specs/completion.md) — `CompleteAsync`, `PtyCompleteOptions`, `PtyResult`
- [specs/capture.md](specs/capture.md) — `MiniPty.Capture` timestamped observation
- [specs/console.md](specs/console.md) — `MiniPty.Console` host input attach
- [specs/terminal.md](specs/terminal.md) — `MiniPty.Terminal` frontend terminal backend (use case 4)
- [specs/display_text.md](specs/display_text.md) — `PtyOutput.ToDisplayText`
- [specs/lifecycle.md](specs/lifecycle.md) — mental model, session flow, cancellation, EOF, drain, disposal, failure behavior
- [specs/platform_support.md](specs/platform_support.md) — public platform support and verification constraints
Expand Down
4 changes: 2 additions & 2 deletions .github/docs/specs/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ PTY output **display** on the host and **timestamped recording** stay embedder r
| N2 | Writing PTY output to the host display (embedder writes `stdout` after reading the PTY) |
| N3 | Cast / timestamped recording / `MiniPty.Capture` orchestration |
| N4 | Terminal emulation (screen buffer, ANSI parsing, scrollback) |
| N5 | In-editor terminal integration (VS Code / xterm.js backend — separate plan) |
| N5 | In-editor terminal integration (VS Code / xterm.js backend — **MiniPty.Terminal**, [terminal.md](terminal.md)) |
| N6 | Spawning the child (`Pty.Start` remains **MiniPty** core) |
| N7 | Generating or interpreting bracketed-paste sequences |

Expand Down Expand Up @@ -186,7 +186,7 @@ Typical interactive host flow:

`Attach` returns **`PtyConsoleInputHandle`** (`IDisposable`); do not use `await using` unless a future API adds `IAsyncDisposable`.

Use case 2 (one-shot recorded steps) continues to use [Capture](capture.md) only. Use case 4 (editor terminal backend) uses **MiniPty** core without **MiniPty.Console**.
Use case 2 (one-shot recorded steps) continues to use [Capture](capture.md) only. Use case 4 (editor terminal backend) uses **MiniPty.Terminal** ([terminal.md](terminal.md)) without **MiniPty.Console**.

## Failure Behavior

Expand Down
Loading