# agterm parity tracker What [umputun/agterm](https://github.com/umputun/agterm) has that **agwinterm** and **agliteterm** do not, and what we have closed. - Compared against **agterm v0.26.0** (2026-09-02), releases 0.22.0 → 0.26.0. - **The running order lives in [plans/2026-09-03-parity-batches.md](plans/2026-09-03-parity-batches.md)** — this file is what is missing, that file is which batch builds it and when. - Ours: agwinterm **0.17.13** released (main is ahead), agliteterm **0.17.15** released. - Update this file in the PR that closes an item. A line that says "missing" long after it shipped is worse than no tracker. - **This supersedes the earlier gap docs in this directory** (`agterm-gap-analysis*.md`, `agterm-roadmap.md`), which compare against agterm v0.10.2 and the July waves. Those describe a version of both products that no longer exists; read them as history. agterm is macOS + libghostty; we are Win32 + ConPTY. Some items do not port at all, and those are named as such rather than left to look like a backlog nobody is working on. --- ## Closed | Item | agterm | Closed by | | --- | --- | --- | | `session.text --lines N` reaching scrollback | 0.25.0 | agwinterm #213 | | `session.type` refuses control bytes (NUL truncation) | 0.25.0 | agwinterm #213, lite #15 *(open)* | | An escape hatch that works: `--allow-control` | — | agwinterm #214 *(open)*, lite #15 *(open)* | | `session.overlay` stops widening a pane id to its session | — | agwinterm #213 | | `session.write` (display injection) in lite | — | lite #15 *(open)* | | Per-session split panes (lite matched agwinterm) | — | lite #13 | | `session.new --workspace` in lite | — | lite #13 | | `surface.cursor` — the caret column | 0.24.0 | agwinterm #221 *(P1)*, lite: P8 | | `statusChangedAt` on the tree's session node | 0.25.0 | agwinterm #221 *(P1)*, lite: P8 | | `agwintermctl version` | 0.25.0 | agwinterm #221 *(P1)*, lite: P8 | | `--stdin` on `session type` / `session write`, rejecting invalid UTF-8 | 0.26.0 | agwinterm #226 *(P2)*, lite: P2-lite | | `session.overlay.open` / `resize` validate `--size-percent` as 1–100 | 0.26.0 | agwinterm #226 *(P2)*, lite: P2-lite | | `session.restore` reports which pane received the content (+ `restoreCommands` in `tree`) | 0.26.0 | agwinterm #226 *(P2)* | | `sidebar.width` — the width in effect, out of range refused; unknown `sidebar` ops refused | 0.26.0 | agwinterm #226 *(P2)* | | `session.new` refuses an unknown workspace; a bare `session new` lands in the caller's workspace | — | agwinterm #226 *(P2)*, lite: P2-lite *(caller half)* | | `session.context` — one line per session, shown in the title bar, the row and the palette, in `tree` as `context`, restored after a restart | 0.26.0 | agwinterm #233 *(P3)*, lite: P3-lite | | `restore.capture` — fill the captured-command slots on demand, per-pane reply, `capturedCommands` in `tree` | 0.26.0 | agwinterm #233 *(P3)*, lite: P3-lite | | `session.split` replies with the **pane id** (the split pane's on `on`, also when already split; the survivor's on `off`) | — | agwinterm *(P4, #238)*; lite had it (#13) | | Horizontal splits — `--axis vertical\|horizontal` on `session.split` (agterm's words: vertical = left/right, horizontal = top/bottom), per session, re-orients live, survives restore, `axis` in `tree`; `session.focus` takes `primary\|split\|left\|right\|top\|bottom\|other`, `session.resize` gains `--grow-top` / `--grow-bottom` | 0.23.0 | agwinterm *(P4, #238)*, lite: P4-lite | | `session.split.close` — closes **either** pane, the survivor's id as the reply; a one-pane session refused | 0.23.0 | agwinterm *(P4, #238)*, lite: P4-lite | | `session.swap` — the panes exchanged; axis, ratio sequence, focus's pane, the session-wide overlay, status and **every id** kept (a pane overlay travels with its pane since P5) | 0.26.0 | agwinterm *(P4, #238)*, lite: P4-lite *(agliteterm #30 — one flag read in its hidden-session model; see [lite-parity.md](lite-parity.md))* | | Pane-scoped overlays and reading an overlay — `--pane left\|right` on `session.overlay` `open` / `close` / `result` (the flag omitted = the session-wide slot, unchanged byte for byte; `left` = pane 0 and `right` = pane 1 whatever the axis; a pane overlay is that pane's **surface** while open, moves with its pane on a swap and dies with it), `session.overlay.copy`, `session.overlay.text [--all\|--lines N]`, `session.text --all`, `paneOverlays` in `tree` | 0.24.0 (`copy` / `text`), pane scoping 2026-08-01 | agwinterm *(P5, #250)*, lite *(P5-lite, agliteterm #40)* | | `session.rename` replies `{session, name}` — the session the name landed on and the name in effect; a pane id names the session that pane belongs to, a blank name and an unknown target are refused in their own words | — | agwinterm #287, and agliteterm's mirror of it | `session.rename`'s row is the third in the family P2 opened: `session.restore` stopped answering the constant "pinned" and named the pane, P3's `session.context` shipped `{session, context}`, and #287 found `rename` still answering "renamed" for every outcome — so a caller that targeted a PANE could not see which session took the name. Widening to the owner is kept, deliberately and now visibly: a pane has no label of its own, and `AGWINTERM_SESSION_ID` IS a pane id, so refusing would refuse the verb's commonest call. That is the same rule the `session.overlay` note below records from the other side: a pane id is refused where the verb acts on a PANE and a `--pane` flag can name one, and lands on the session where the verb acts on a SESSION — what is never allowed is doing either one silently. The two `session.overlay` rows are the two halves of one item: #213 the *honesty* half (a pane id is refused rather than silently widened — still the rule for the verbs **without** `--pane`), P5 the *capability* half (`--pane` names one pane; the refusal for a bare pane id on `--target` stays, and now says `--pane` is how a pane is named). The P5 row carries **two deliberate divergences** from agterm, recorded here rather than left to be discovered: - **The session-wide `session overlay result` stays window-wide and `ok`.** agterm's `result` is per overlay. Ours (no `--pane`) keeps answering the **last** session-wide overlay exit in the window (`ok:true "no overlay"` when none) — shipped, tested (`tests/integration/win32-control.ps1`), and the P2 leftovers (#227 / #228) were closed on that shape; changing it is a contract break for no caller. The **pane** arm is agterm's: per slot, `exit N`, refused `overlay still running` while its program is up and `no overlay result` when nothing has run in that slot since the window opened — reached by `result --pane left|right`, or by `result --target ` while the overlay is up (the rule's "the id names that overlay's slot"; once it closed the id resolves nowhere and the bare form is window-wide again). A pane overlay's exit never writes the window-wide value. - **`--pane` with `--size-percent`, and `resize --pane`, are refused at both ends** — the CLI exits 2 with "Nothing sent", and a raw client is refused by the server with the same words; agterm calls both a usage error. A pane overlay is always its pane's full box; the session-wide floating panel (`--size-percent`) is unchanged. Also recorded, not a divergence: agterm's `overlay not realized` (a `copy` / `text` between `open` and the terminal being up) is kept as the phrase for a slot with no terminal to read, but the moment does not exist here — `open` builds the terminal before it replies, so the buffer is readable the moment the id comes back. The P4 rows carry **two deliberate divergences** from agterm, recorded here rather than left to be discovered: - **`session split off` destroys the split shell.** agterm's `off` hides it and keeps the process. Pre-existing and unchanged by P4; `session split close` is the same destruction with a pane target, and what it adds is that the target may be **either** side (`off` can only keep pane 0). - **A swap moves panes, never ids.** agterm addresses a session's panes by role (`primary|split`), so its swap moves the session's public identity to the other shell. Ours are addressed by **id**, and an agent holding a pane id must keep reaching the same shell after a swap. So a swap reverses the pane order, keeps the axis and the ratio sequence (the left/top box keeps its size; the contents change places), moves the focus with its pane and leaves every id where it was — the session id keeps naming the pane it always named, now on the other side. The invariant "the first pane carries the session id" is therefore "at most one pane carries the session id" — a swap may put it on either side, and closing that pane by any path (`split close`, Ctrl+Shift+W, `split off` after a swap, a shell exiting) leaves none, after which a session-id target resolves to the focused pane like a name; the resolver's exact-pane-first order is what keeps the session id naming the carrier while one exists. What that costs on the way **down** (checked once, #238's final task): the released 0.17.12 restores pane 0 under the session id, so a file saved with a *swapped* session loads there with **both panes carrying the session id** and the split shell's id gone — no `.bad`, no crash, two panes side by side; an unswapped file loses only the axis. A P4 build loads what 0.17.12 writes back and re-mints the duplicate. The P1 rows were items **1, 2 and 8** of the open list before P1 closed them, the P2 rows were item **6**, the `sidebar.width` half of item **5** and two sub-items of item **11**, the P3 rows were item **1** and the `restore.capture` half of item **5** (which closes that item), the P4 rows were items **2 and 3** (`session.swap`, and the three split gaps), and the P5 row was item **1** (pane-scoped overlays, `overlay copy` / `text`); the list below is renumbered after each, so read those plans' "closes items ..." against this note rather than against the current numbering. P3 is the first batch to touch the per-window restore file, and it set the format rule the later batches inherit: additive keys only, no version field, every loaded value validated (`plans/completed/2026-09-05-p3-persistence.md`); P4 is the second, and follows it with one additive key (`Axis`, written only for a split horizontal session, validated on load). `--stdin` is `session type` / `session write` here — `quick type` has no separate verb because the quick terminal is a pane with an id that `session type --target` reaches. The agliteterm mirrors are the per-batch `P-lite` plans, tracked in [lite-parity.md](lite-parity.md) — agwinterm-first, so the contract is fixed before it is copied. --- ## Open — agent-facing control API Nothing left here: the control-API items closed with P1–P5. What remains of the overlay verbs is agterm's `--cwd`, `--follow` and `--background-color` on `open`, deliberately left out of P5 (tracked as #139 / #88). P13 adds `session.hud` in agwinterm (see below). --- ## Open — UI ### 1. Native picker — P16 `pick.open/result/cancel` and `agwintermctl pick` provide bounded native controls, label filtering, custom results, exact-ID waiting and window-owned lifetime. See [native-picker.md](native-picker.md). Lite mirror/conformance remain P17. ### 2. `session.hud` and `--position` — implemented in P13 **agterm 0.22.0 / 0.24.0.** A transient overlay for status an agent wants seen without printing into the terminal, anchored to one of nine positions. Agwinterm now draws a passive native HUD with detail, spinners, colors and bounded width; it leaves terminal input and geometry unchanged. See [session-hud.md](session-hud.md) for targeting, replacement and validation. The agliteterm mirror remains owed in P17; the shared conformance floor is unchanged. ### 3. Quick terminal — shipped in P14 (#266) Detached app-wide shell, monitor work-area percentage and opt-in global hotkey. See [quick-terminal.md](quick-terminal.md) for focus, lifetime and refusal policy; UIA per-window provider parity remains #267. Lite mirror is P17. ### 4. Workspace navigation and keymap alternatives — P15 Merged in #269: wrapped `workspace.go`, collapse action, alternative normal/leader map chords. See [navigation.md](navigation.md); QA records delivery gates. ### 5. Smaller things from 0.26 P15 covers existing cursor shape/blink controls with live acceptance, adds truncated-name tooltips and conservative Windows foreground-shell hints (not prompt safety). (The other two that were here — `session.restore` reporting the pane, and `--size-percent` validated rather than clamped — closed in P2.) --- ## Not chasing, with reasons ### Live and remote sessions (zmx) **agterm 0.26.0.** Processes survive a restart through a bundled zmx multiplexer; sessions on another Mac attach over SSH. Requires zsh as the login shell and macOS. **We are partly there by a different route, and it is worth knowing where the line is.** Our pty-host outlives the UI by design, and lite *adopts* live shells on restart rather than relaunching them — so "quit the app, come back to running processes" already works here. What we do not have is surviving a **reboot**, or attaching to another machine's sessions. agwinterm's `restore-commands` re-runs a captured command line, which is a much weaker thing: the agent's conversation is gone, only the invocation comes back. ### GPU buffer release for hidden panes **agterm 0.26.0.** Hidden panes release their Metal swap chain buffers; their measurement was 1.1 GB → 199 MB with 11 hidden panes. Our renderer is Direct2D with a different allocation shape — measure before assuming it applies, then decide. ### macOS-only Voice dictation and the accessibility tree (0.22.0), hardened-runtime and TCC entitlements, Apple notarisation. agwinterm has its own UIA path. --- ## Where we are ahead Not a one-way list. Ours have, and agterm's control surface does not appear to: `session.readonly`, shell `profiles`, `omp` theme control, `claude.adopt` / `claude.yolo` / `claude.update`, `app.update`, `session.metrics` (live cell and pixel geometry) and `image.frameshm` (shared-memory frame delivery, which ConPTY makes necessary) — the last two on `main` since **#220**. ## agliteterm, against agwinterm Tracked separately, in **[lite-parity.md](lite-parity.md)** — the goal there is that the two products expose the same features, so it is a list with an end, not a survey. Short version as of 2026-09-02: agliteterm answers **41 of agwinterm's 85** control verbs, the sharpest gap being `selection.*` (it can read a selection but not make one). It is also *ahead* in three places, which should move the other way. --- *The 0.22–0.25 survey this builds on was done by another session on 2026-09-01 and lives at `C:\Users\boris\AI\docs\agterm-parity.md`; this file is the maintained copy, extended to 0.26.0 and corrected where we have since closed items.*