Corta — Feature Conformance
Documentation index · Project overview
What Corta must implement, in what order, and how completeness is measured. Priorities:
- P0 — without it the terminal is unusable;
vim/tmux/htopvisibly break, or programs hang. - P1 — expected by any modern terminal; needed to be a daily driver.
- P2 — enhancement, deferrable, or explicitly declined.
A note on "features". Coloured
gitoutput, SSH and plaintmuxare not features. They are consequences of a correct parser and a correct grid. Nothing in this repository is written "for git". Build the primitives; the applications light up on their own.
1. VT Parsing and Screen Model
1.1 Rendering correctness
| Capability | Tier | Notes |
|---|---|---|
| UTF-8 byte stream decoding | P0 | The foundation |
SGR: ANSI / 256 / 24-bit true colour (semicolon and ITU T.416 colon forms), bold, underline (single, double 21 / 4:2, curly 4:3), reverse | P0 | |
| Cursor movement, absolute positioning, save/restore | P0 | |
| CHA / HPA / HPR / VPA / VPR — absolute and relative position | P0 | A TUI that lays a line out in segments jumps between columns rather than writing spaces; without these the whole screen collapses |
| Erase display / line, insert / delete lines and columns | P0 | |
Alternate screen (?1049) | P0 | vim/less/htop depend on it entirely |
Scroll region (DECSTBM) | P0 | tmux and vim status lines depend on it |
| Character width: CJK wide, emoji, combining, zero-width | P0 | Wrong widths mean misaligned CJK text; wide pairs draw scaled into their two-cell box (M3.5) |
| Scrollback | P0 | Ring buffer, variable-length rows |
| Soft-wrap flag per line | P0 | Required by reflow, selection and search — see DECISIONS.md D03 |
| Reflow on resize | P1 | Must be incremental; live window drag fires continuously |
Synchronized output (?2026) | P1 | Neovim, tmux ≥ 3.4 and fzf use it; absence causes visible tearing |
Cursor style (DECSCUSR) | P1 | |
Cursor visibility (?25, DECTCEM) | P0 | vim, htop, fzf and Ink programs (Claude Code) hide the cursor while they draw; terminal-wide, kept across the alternate screen, shown again by RIS and DECSTR |
Autowrap (?7, DECAWM) and origin mode (?6, DECOM) | P1 | Without autowrap the last column is overwritten and a wide character that would cross the margin is not written; under DECOM, CUP/HVP/VPA and the position reports count from the scroll region's top, and DECSC saves the mode |
| OSC 0 / 2 — set window and tab title | P1 | Set only. Query is never implemented, see SECURITY.md §2.2 |
| OSC 7 — report working directory | P1 | Prerequisite for new tabs and splits inheriting the cwd |
| OSC 4 / 104 — indexed palette query, set, reset (B06) | P1 | 256-entry palette, seeded from the theme (0–15) and xterm's cube/greyscale (16–255); an override now repaints, not just answers a query — docs/DESIGN.md §7 |
| OSC 5 / 105 — special colours query, set, reset (B06) | P2 | Five fixed slots (bold/underline/blink/reverse/italic), no themed default — see SpecialColors; docs/DESIGN.md §7 |
| CSI s / u (SCOSC/SCORC) — cursor save/restore alias (B06) | P1 | Corta has no DECLRMM, so these are unconditional aliases for DECSC/DECRC, matching xterm without left/right margins |
?45 — reverse-wraparound mode (B06) | P2 | BS/CUB cross onto the row above when it auto-wrapped into this one; off by default, matching xterm. Not DECBKM, which is the separate ?67 backarrow-key mode |
Bracketed paste (?2004) | P0 | A safety feature, not a convenience — see SECURITY.md §2.3 |
Mouse reporting (?1000/?1002/?1003, SGR ?1006) | P1 | Press/release, wheel, cell-coalesced drag and motion; configurable local-selection override |
Focus reporting (?1004) | P2 | Implemented; Neovim autoread and tmux focus events |
Alternate scroll (?1007) | P1 | The wheel sends arrow keys on the alternate screen when mouse reporting is off, so less, man and git log scroll; on by default, as in Terminal.app and iTerm2 (xterm's is off) |
| OSC 8 — hyperlinks | P2 | Implemented; display text and target may differ — see SECURITY.md §2.4 |
OSC 1, title push/pop (CSI 22/23 ; 0/1/2 t) | P1 | Icon name retained separately; independent stacks keep the newest ten entries; empty pops do nothing. Title reporting remains refused |
| OSC 10–12 / 110–112 | P1 | Set/query dynamic foreground, background and cursor; resets restore the live theme. Overrides survive theme changes until reset |
| OSC 17/19, 117/119 | P1 | Set/query/reset selection background and real selection-text colour. Without OSC 19, selected glyphs keep their own colours; without OSC 17, the theme's translucent highlight applies; with it, the program's colour is drawn at the same translucency, so box glyphs and underlines under the selection stay visible. Queries return opaque RGB, with theme selection / live foreground as defaults |
| OSC 13–16, 18 | P2 | Accepted and ignored: pointer and Tektronix colours have no corresponding surface |
| SGR 58 / 59 | P1 | Indexed/RGB underline colour and reset, colon or semicolon forms; independent of glyph and strikeout colour. Stored in a bounded side table without enlarging Cell |
Colour scheme (?2031, CSI ? 996 n) | P1 | CSI ? 997 ; 1/2 n reports dark/light from the live theme; a subscriber receives one report per actual variant change. DECRQM reports subscription state |
XTGETTCAP (DCS + q) | P1 | Fixed table: TN/name, Co/colors, RGB, Ss/Se, Smulx, Setulc, cursor, keypad, backtab, editing, navigation and F1–F12 keys. Unsupported/malformed names get DCS 0 + r ST, with no echo. Each supported name gets its own fixed reply |
DECRQSS SGR (DCS $ q m ST) | P1 | Reports numeric pen rendition, including underline style/colour, so Neovim can detect curly underlines. Other status strings receive a constant negative reply |
| DCS and rare CSI sequences | P2 | The long tail |
OSC verdict table
This is the complete deliberate OSC dispatcher surface. OSCCoverageTests
compares numeric codes in both directions and tests that removing a row is
caught. Unknown codes and unknown sub-forms are silently dropped. “Refused”
means no state change and no reply for that sub-form. Security section numbers
refer to SECURITY.md.
| Code | Sub-form | Verdict | Reason | Record |
|---|---|---|---|---|
OSC 0 | title setter | implemented | Separate icon/window names; no title reporting | §2.2; #407 |
OSC 1 | title setter | implemented | Separate icon/window names; no title reporting | §2.2; #407 |
OSC 2 | title setter | implemented | Separate icon/window names; no title reporting | §2.2; #407 |
OSC 3 | all | ignored | X properties do not exist on macOS | #411 |
OSC 4 | set/query | implemented | Indexed palette / five special slots | B06 |
OSC 5 | set/query | implemented | Indexed palette / five special slots | B06 |
OSC 6 | all | ignored | xterm colour-mode switches have no effect | #411 |
OSC 7 | file URL | implemented | Host-aware local cwd or informational remote context | §2.8 |
OSC 8 | hyperlink | implemented | Interned URI; user opens it | §2.4 |
OSC 9 | notification; 4 progress; 12 prompt | implemented | Bounded notice / progress; 12 aliases OSC 133 A | #409; #411 |
OSC 9 | 6 macro; 7 exec | refused | No stream-selected execution | §2.9; #409 |
OSC 9 | 9 cwd; other defined numeric subcommands (1–12) | ignored | No host; cannot distinguish a remote path | §2.8; #411 |
OSC 10 | set/query | implemented | Foreground/background/cursor/selection state | #407 |
OSC 11 | set/query | implemented | Foreground/background/cursor/selection state | #407 |
OSC 12 | set/query | implemented | Foreground/background/cursor/selection state | #407 |
OSC 13 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 14 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 15 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 16 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 17 | set/query | implemented | Foreground/background/cursor/selection state | #407 |
OSC 18 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 19 | set/query | implemented | Foreground/background/cursor/selection state | #407 |
OSC 21 | fixed keys / palette 0–255 | implemented | Set/query/reset shared colours; unknown keys dropped; cursor_text drawn | #411 |
OSC 22 | set, push, pop, query | implemented | Fixed CSS/X11 vocabulary; 16 entries per screen; closest public NSCursor | #411 |
OSC 46 | all | refused | Would open a stream-named log file | §2.10; #411 |
OSC 50 | all | refused | Fonts come from verified configuration (D10/D12) | §2.10; #411 |
OSC 51 | all | ignored | Emacs-specific, no consumer | #411 |
OSC 52 | write | implemented | Sanitised text; allow-clipboard-write gate | §2.6 |
OSC 52 | read/clear | refused | No clipboard disclosure or silent clearing | §2.6 |
OSC 66 | text sizing | not yet | Multi-cell glyph scaling needs a renderer decision (D05) | #411 |
OSC 99 | notice / capability query | implemented | Bounded metadata and fixed capabilities | #409 |
OSC 104 | all / named resets | implemented | Indexed palette / special colour resets | B06 |
OSC 105 | all / named resets | implemented | Indexed palette / special colour resets | B06 |
OSC 106 | all | ignored | xterm colour-mode switches have no effect | #411 |
OSC 110 | reset | implemented | Restore live theme / selection defaults | #407 |
OSC 111 | reset | implemented | Restore live theme / selection defaults | #407 |
OSC 112 | reset | implemented | Restore live theme / selection defaults | #407 |
OSC 113 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 114 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 115 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 116 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 117 | reset | implemented | Restore live theme / selection defaults | #407 |
OSC 118 | set/reset | ignored | Pointer/Tektronix colours have no surface | #407 |
OSC 119 | reset | implemented | Restore live theme / selection defaults | #407 |
OSC 133 | A/B/C/D | implemented | Command boundaries and exit status | §2.1 |
OSC 134 | tokened directory completion | implemented | Pane-owned token required | §2.7 |
OSC 633 | A/B/C/D; P;Cwd | implemented | Same command records and host-aware cwd as native hooks | #411 |
OSC 633 | E command/nonce; other P | ignored | Grid owns command text; no VS Code trust model | #411 |
OSC 777 | notify | implemented | Sanitised desktop notification | #409 |
OSC 1337 | CurrentDir; RemoteHost; SetMark; ClearScrollback | implemented | Host-aware metadata, mark that ⌘↑/⌘↓ stop at, ED 3 semantics | #411 |
OSC 1337 | CursorShape; RequestAttention; ReportCellSize; Copy | implemented | Temporary cursor; background Dock attention; numeric metrics; gated clipboard | #411 |
OSC 1337 | File; MultipartFile; FilePart; FileEnd | implemented | inline=1 only; PNG/GIF first frame in sandbox; shared placements/budgets | #411 |
OSC 1337 | SetUserVar; ShellIntegrationVersion; HighlightCursorLine; UnicodeVersion; SetKeyLabel; PushKeyLabels; PopKeyLabels; ClearCapturedOutput | ignored | No consumer or Touch Bar; Unicode rules stay fixed | #411 |
OSC 1337 | StealFocus; SetProfile; SetBadgeFormat; ReportVariable; Custom; inline=0 | refused | No focus theft, config mutation, persistent badges, value echo, scripts or downloads | §2.10; D09/D10 |
OSC 3008 | all | ignored | systemd context hierarchy, no consumer | #411 |
OSC 5113 | all | refused | File transfer writes stream-named files | §2.10; #411 |
OSC 5522 | text/plain write/wdata | implemented | Bounded sanitised text transaction, gated like OSC 52; partial protocol. Replies a fixed status=DONE, EPERM (writes not allowed) or EIO (pasteboard failed) | #411 |
OSC 5522 | read, MIME aliases, non-text, primary selection | refused | No clipboard disclosure or extra clipboard surfaces | §2.6; §2.10 |
OSC 7501 | status / probe | implemented | Bounded task state; constant probe reply | #409 |
OSC 21337 | tab metadata | implemented | Bounded self-reported metadata | #409 |
OSC 21 accepts the same RGB hex specifications as OSC 4/10–12; an empty value
or a bare key resets. The protocol's unknown-key echo is deliberately omitted.
OSC 22 returns boolean lists or fixed-table names for __current__,
__default__, __grabbed__; unknown setters choose default. macOS lacks
public wait/progress cursors, so these use the arrow fallback. Link hover takes precedence.
OSC 5522 supports only text writes, under OSC 52's 3072 decoded-byte cap.
It does not claim full kitty extended clipboard conformance (64 MiB minimum,
MIME aliases, primary selection or paste events).
A successful permitted system write receives only the fixed type=write:status=DONE acknowledgement.
Reads and unsupported packets are silent, and a bad packet invalidates the
whole transaction. allow-clipboard-write is checked by the app before any
pending copy reaches the system pasteboard.
Protocol references: iTerm2 extensions, iTerm2 images, kitty colours, kitty pointers, kitty clipboard.
1.2 Query / response sequences
These are P0 and are the most commonly missed requirement. They are not about rendering: a program that asks a question and receives no answer either waits for a timeout or misdetects the terminal's capabilities.
| Sequence | Asked by | Consequence if unanswered |
|---|---|---|
DA1 (ESC [ c) | vim / Neovim at startup | Startup stalls until timeout |
DA2 (ESC [ > c) | Capability detection | Feature misdetection |
DSR-CPR (ESC [ 6 n) | zsh, starship and similar prompts | Prompt corrupts or hangs |
DECRQM (ESC [ Ps $ p) | tmux, Neovim capability probes | Conservative fallback behaviour |
XTVERSION (ESC [ > 0 q) | Modern TUIs | Feature misdetection |
| Dynamic colour (OSC 10/11/12 query form) | Themes and TUI capability probes | Probe times out |
Kitty keyboard (ESC [ ? u) | Editors that bind Ctrl+I and Tab apart | Both keys arrive as 0x09 |
All four shipped in M6 (M6.5, M6.6, M6.9), plus DSR operating status
(ESC [ 5 n → ESC [ 0 n) and DECXCPR (ESC [ ? 6 n), both of which
went unanswered until M8. A status query is the one sequence where silence
is read as "the terminal is dead", and the programs that wait for it
without a timeout wait forever.
DECRQM answers 0 — "not recognised" — for the modes Corta does not track, which is the honest reply and the one that unblocks the probe; the failure mode this table is about is silence, not a negative answer.
A mode reported as supported has to behave. DECRQM's four-state answer is a contract: a program that is told a mode is set lays its next screen out against that behaviour, so answering 1 or 2 for a bit nothing acts on is worse than answering 0. The four modifiable ANSI modes therefore split:
| Mode | Answer | Why |
|---|---|---|
| IRM (4) | 1 / 2 | Implemented (Grid.insertMode) — a print inserts and shifts the row right |
| LNM (20) | 1 / 2 | Implemented (PerformerState.newLineModeEnabled) — LF/VT/FF also return the carriage, and Return sends CR LF |
| KAM (2) | 4 | Deliberately not implemented: a terminal that stops accepting input on a byte from the child is one a runaway program can wedge, with no way for the user to tell it from a hang |
| SRM (12) | 4 | Deliberately not implemented: Corta never echoes keystrokes itself, so there is no local echo to switch off |
The same rule covers the private modes: DECOM (6), DECAWM (7) and DECTCEM
(25) report their live state. Until they were implemented, 7 and 25
answered 3, "permanently set", while ?7l still wrapped and ?25l still
drew the cursor — the contract above, broken.
4 is "permanently reset", which is a stronger answer than 0 — a program learns not to ask again.
Colour-space specifications (P2). OSC 10/11/12 accept #RGB through
#RRRRGGGGBBBB and rgb:R/G/B. rgbi:, CIELab:, CIEuvY:, CIExyY:,
CIEXYZ: and TekHVC: are refused: the sequence is parsed, no colour
is changed, and nothing is written back. Each of those is a colour-space
conversion needing a white point and a gamma curve — the answer depends on
the display profile, so an implementation is either colour-managed
properly or it is a wrong number dressed as a right one. Refusing is also
the safe direction: a program that sets a colour and sees no change keeps
legible text, whereas a mis-converted CIELab black-on-black is a
terminal you cannot read. The corresponding esctest cases are recorded as
expected failures for this reason (0.1 roadmap), and
Performer+Query.parseColorSpecification carries the policy in full.
DECSCL gates DECRQM. A program that announced VT200 with
ESC [ 62 ; 0 " p has asked to be talked to as an older terminal, and
DECRQM arrived at VT300. Answering anyway is the terminal ignoring what
it was told.
Every response is written to the child's stdin. Responses must therefore
be fixed-format and never echo attacker-controlled text — see
SECURITY.md §2.2.
2. Input, Rendering, Windows
2.1 Input
| Capability | Tier | Notes |
|---|---|---|
| Keyboard → PTY, including control and function keys | P0 | |
CJK IME (NSTextInputClient) | P0 | Harder than it looks — DESIGN.md §7.1. Composition, candidate window and commit verified in the launched app (§4.4) |
| Copy / paste with bracketed paste | P0 | Copy joins soft-wrapped lines into one and trims trailing blanks; ⌘C / Edit ▸ Copy |
| Keyboard and mouse text selection | P0 | Drag, double-click word, triple-click logical line, ⇧-click extend; document-anchored — DESIGN.md §3.1 |
| Configurable key bindings | P1 | bind.<command> in the config file, one table for menus, palette and file — CONFIGURATION.md §5 |
| Click-to-position, drag-to-select | P1 | Drag selects; a TUI that owns the mouse is overridden with mouse-override-modifier |
| ⌘-click to open a URL | P1 | Scheme allowlist required — SECURITY.md §2.4 |
| Kitty keyboard protocol | P2 | Implemented; disambiguate re-encodes the colliding keys only — Ctrl+I/M/H/[, Escape (CSI 27 u), ⌥ with Return, Tab or Backspace, and ⌥-as-Meta text keys — not the whole keyboard; progressive enhancement flags and protocol stack, one per screen (the alternate screen's is dropped when it closes), and the main screen's cleared when a command finishes (OSC 133 D) |
| Tab / Shift-Tab through a candidate UI | P0 | A completion menu or IME that resolves Tab as a command sends insertTab(_:) / insertBacktab(_:) to doCommand(by:); both are forwarded to the child (B02) — DESIGN.md §7.1, TerminalViewIMETests.doCommandForwardsTabAndBacktab |
The candidate-UI row is the one whose evidence is incomplete. The code gap was real and is closed, but a live confirmation that Claude Code's slash-command menu accepts Tab inside a built Corta, compared side by side with a reference terminal under an English input source, a CJK source with no marked text and an active composition, has not been collected: the original report's environment cannot be recovered (CHANGELOG 1.0.0). The 2026-09-17 record's A1 items are the closest evidence since.
2.2 Rendering
| Capability | Tier | Notes |
|---|---|---|
| GPU glyph atlas + instanced quads | P0 | One draw call per screen |
| Foreground / background, bold, italic, underline | P0 | Real faces where the family has them; synthetic oblique and stroked weight where it does not |
| Missing glyph is visible, not blank | P0 | Hollow box for a scalar no font in the cascade covers |
| Every glyph clipped to its cell box | P0 | Overwide ink is scaled to fit rather than painted into the next column |
| Cursor: block / bar / underline, blink | P0 | Configurable shapes and blinking; DECSCUSR overrides (see below) |
| Selection highlight | P0 | Document-anchored quads; follows its text as output scrolls |
| Retina / HiDPI scaling | P0 | Grid laid out in pixels, not points — one of §4.4's shipped bugs |
| Font fallback for CJK and emoji | P0 | Core Text cascade list; the shaped run's font rasterises the glyph (M3.5) |
| Atlas eviction (LRU or multi-page) | P0 | Full-page reset on exhaustion + generation rebuild (M3, DESIGN.md §7.4) |
| Glyph weight under grayscale antialiasing | P1 | Otherwise light-on-dark text looks too thin; a coverage curve over sRGB blending — DESIGN.md §7.5 |
| Runtime font scaling (⌘+ / ⌘−) | P1 | Per-window, temporary; never writes font-size |
| Ligatures | P2 | Conflicts with the cell grid — DESIGN.md §7.3 |
| Background transparency, blur, padding | P2 |
Runtime font scaling landed at M3 (⌘= bigger, ⌘- smaller, ⌘0 reset).
The glyph atlas is rasterised for one size and scale, so a change
rebuilds the renderer and re-fits the window around the unchanged
grid. Measured on a 2x display (Menlo): 14pt → 9x17pt cell, 1100x554pt
content (120x30 grid); 15pt → 10x18pt cell, 1220x584pt content; the
13pt step was re-measured live (980x524pt) when fixing the ⌘- shadowing
below. The stock storyboard's Format menu (font panel, rich-text
traits) was removed at the same time: its Font ▸ Smaller item claimed
⌘- first in menu order and routed it to modifyFont:, which a terminal
never implements — the shortcut arrived dead. MenuShortcutTests pins
the invariant: no keystroke is claimed by two menu items. The
cursor uses the configured block, bar or underline shape, with optional
blinking (off by default). DECSCUSR can temporarily override shape and blink;
parameter 0 and terminal resets return to the configured defaults. Only the
visible, focused live pane blinks; once its cursor is idle the blink is
handed to a Core Animation opacity animation, so it draws no Metal frame
every half second (DESIGN.md §7.10). Inactive, occluded and
scrolled-back panes do not blink.
2.3 Windows and sessions
| Capability | Tier | Notes |
|---|---|---|
| Single window, single terminal | P0 | M1 |
| Multiple windows (⌘N), each its own session | P1 | Landed at M3; composition, not new mechanism |
PTY lifecycle: spawn, read/write, TIOCSWINSZ, SIGCHLD | P0 | Resize must be reported or remote vim and htop desynchronise |
| Resize debouncing | P1 | A live window drag otherwise hammers the child |
| Tabs | P1 | Native window tabs; a restored group keeps its order and selection (§4.4 item 6) |
| Split panes (layout tree + focus routing) | P1 | Renderer and input are multi-viewport from M1 |
| Search scrollback (⌘F) | P1 | Must match across soft-wrapped lines |
| Scrolling (wheel, ⌘↑↓, page) | P0 | ⌘↑↓ jump between commands once shell integration reports them; ⇧Page/Home/End scroll |
| Bell (audible / visual / mute) | P1 | bell in the config file |
| Settings page | P1 | Native page in the menu bar next to Edit/Shell, backed by one text file needing no third-party parser — M6.1 |
| Multiplexing | — | Not doing; use tmux |
2.4 Shrinking the screen
Reducing the row count moves rows off the top, into scrollback, until the cursor fits. Only rows below the cursor — which are blank — come off the bottom. Truncating from the bottom instead destroys the newest output and does not preserve it in history, so making a window smaller silently ate the last commands that ran.
3. The Daily-Driver Checklist
The operational definition of success from DESIGN.md §8. If all ten
hold, Corta has replaced Terminal.app for this repository's own use.
Each row names the record that last verified it by hand; an automated
suite is listed only where one pins the behaviour afterwards. A row
whose record is a roadmap tick has not been re-run since it landed.
| # | Holds when | Last verified by hand | Pinned by |
|---|---|---|---|
| 1 | Opens to a working shell with correct colour output | 2026-09-17 pass, H31 and G29 (record) | §4.4's launched-app check |
| 2 | vim / less / htop render without artifacts | 2026-09-17 pass: less and git log clean (G29); tmux + htop left residue after a window shrink (G25) — ECH was unimplemented, fixed the same day (§4.4.2) | EditingTests, golden fixtures |
| 3 | Chinese input works, displays, and never drifts out of alignment | 2026-09-17 pass, A1 on a physical keyboard, both panes of a split | TerminalViewIMETests, WideGlyphRenderTests |
| 4 | Copy and paste work; pasting multi-line code does not auto-execute | 2026-09-17 pass, G28 (the multi-line warning appeared for a REPL without bracketed paste) and 2026-09-19 (Option-drag selection to the clipboard) | PasteTests, MouseReportingTests |
| 5 | Scrollback holds a long training run and scrolls smoothly | 2026-09-17 pass, G27: 120 s of continuous output, the other window still responsive; the top of the run was past the 10,000-line default cap, as documented | corta-bench write-backpressure |
| 6 | ⌘F searches scrollback | M4.4 roadmap tick (record); not re-run by hand since | SearchTests, SearchAndTabUITests |
| 7 | New tab and split pane | 2026-09-17 pass, C11: a restored two-pane window and a three-tab group | SplitPaneUITests, SearchAndTabUITests |
| 8 | ⌘+ / ⌘− resize the font | 2026-09-17 pass, C12 and H31 | FontSizeZoomTests |
| 9 | Resizing the window resizes the program inside it | 2026-09-17 pass, D16: vim through full screen and back, 120×30 → 207×62 → 120×30 | PTYWindowSizeTests, ResizeDebouncerTests |
| 10 | ⌘-clicking a localhost: URL opens the browser | M4.6 roadmap tick (record); not re-run by hand since | LinkDetectionTests |
4. Measuring Completeness
Built during M1. Fixing the long tail without these is guesswork.
4.1 Golden-file grid tests
The primary harness. Feed a recorded byte stream to a TerminalSession,
serialise the resulting grid to plain text (characters plus attributes),
and diff against a checked-in expectation.
This is how a change to one CSI handler is prevented from silently
breaking vim. Written once, useful for the life of the project.
4.2 External suites
| Suite | What it covers | Use |
|---|---|---|
esctest | xterm's own conformance suite | The objective completeness number |
vttest | Classic VT100/VT220 behaviour | Manual sanity pass at each milestone |
Record the pass rate at each milestone. "Conformance improved" is only meaningful against a previous number.
How the esctest run is done. esctest2 (ThomasDickey/esctest2), run as the child of a live Corta window — it drives the terminal through its own tty, so it has to be the shell:
# A wrapper passed only in the environment of the launch you control.
# Never via `launchctl setenv` (§4.5).
cat > /tmp/corta-esctest.sh <<'EOF'
#!/bin/bash
export TERM=xterm-256color
cd /path/to/esctest2/esctest
python3 esctest.py --expected-terminal xterm --max-vt-level 3 \
--logfile /tmp/esctest-run.log
exec /bin/zsh -l
EOF
chmod +x /tmp/corta-esctest.sh
SHELL=/tmp/corta-esctest.sh Corta.app/Contents/MacOS/Corta
Results, one row per run. "xterm-compatibility" is passes plus
"known bugs" — the number esctest reports and the one comparable across
terminals; it is not a pass rate, and every row states all three
counts. The failing names of each run are kept in esctest/
so the next run is a diff rather than a re-reading.
| Run | Build | Passed | Known bugs | Failed | Total | xterm-compat. | Against the previous row |
|---|---|---|---|---|---|---|---|
| M2 (2026-09-02) | M2 closeout | 50 | 334 | 184 | 568 | 67.6% | Baseline |
| M6 (2026-09-03) | M6 closeout | 106 | 335 | 127 | 568 | 77.6% | 57 failures fixed, none introduced: programmable tabs, CNL/CPL/CHT/CBT, IND/NEL/RI, RIS and DECSTR, including the soft-reset isolation esctest itself relies on |
| 0.1.1 (2026-09-08) | v0.1.1 | 112 | 335 | 121 | 568 | 78.7% | Six more passing, none regressed; classified by application impact in the quality plan Q01; names in esctest/0.1.1-results.txt. 45 of the 121 were OSC 4/5 palette set and query, then unimplemented |
| 2026-09-17 | main, esctest2 2798f12 | 126 | 334 | 107 | 567 | 81.1% | 14 tests moved to pass — B06's OSC 4/104 and 5/105 set/query/reset, SCORC, and the multi-column reverse-wraparound case — none regressed; the suite itself lost one test and one known bug, so the totals are not identical columns. Names in esctest/2026-09-17-results.txt; XtermWinops (28) is still the largest class and still deliberate |
| 1.1.0 (2026-10-03) | main at ef11a93, esctest2 2798f12 | 126 | 334 | 107 | 567 | 81.1% | Identical failing list, name for name: nothing regressed through the 1.1.0 milestone and nothing moved — its terminal-side changes (Kitty o=z, emoji drawing, bounded glyph clusters) have no esctest case. Names in esctest/2026-10-03-results.txt |
| 1.1.5 (2026-10-07) | main at c9432a9, esctest2 2798f12 | 131 | 334 | 102 | 567 | 82.0% | Six moved to pass: #244 made autowrap off (CSI ? 7 l) overwrite the last column instead of wrapping, origin mode address from the region's top, and DECRQM report DECAWM, DECOM and DECTCEM as they are. One moved to fail without regressing: XtermSave_SaveSetState had passed only because ASCII text wrapped with autowrap off — XTSAVE/XTRESTORE are still not implemented. Names in esctest/2026-10-07-results.txt |
1.1.1 (2026-10-03): the empty-Return patch adds real-shell PTY regression coverage and was confirmed in the launched development app after updating the installed integration. Full core and Unit suites passed; esctest2 was not re-run because this patch changes shell hooks rather than terminal escape-sequence handling. Evidence: 1.1.1 release checks.
1.1.5 (2026-10-07): esctest2 re-run on the release tree: 131 passed, 334 known bugs, 102 failed (82.0%); the row above has the per-test difference. The scroll-region-on-resize (#256) and reflow-cursor (#257) fixes have no esctest case; their own core tests and a replay of Claude Code's recorded output cover them. Evidence: 1.1.5 release checks.
1.1.6 (2026-10-07): draws the rounded box corners (╭ ╮ ╰ ╯) as geometry
joined to the grid-drawn borders (#264). A renderer change with no escape-
sequence handling in it, so esctest2 was not re-run; render tests, the
launched app and a Release frame-CPU A/B cover it. Evidence:
1.1.6 release checks.
4.3 Fuzzing
The parser consumes untrusted bytes and must never crash, hang, or
allocate without bound. The harness is the corta-fuzz target in the
CortaTerminal package (M6.11); it asserts the SECURITY.md §3 caps on
every input rather than waiting for a sanitizer to notice — grid
dimensions, the scrollback ring, cursor bounds, row width, the interned
grapheme and hyperlink tables, and the size of the response one input can
queue.
libFuzzer is not available on macOS with the current toolchain.
Xcode 26 ships no libclang_rt.fuzzer_osx.a, and
swiftc -sanitize=fuzzer is rejected outright for
arm64-apple-macosx — verified, not assumed. What runs instead is a
deterministic mutation driver over a checked-in corpus:
swift build --package-path CortaTerminal -c release --product corta-fuzz
CortaTerminal/.build/release/corta-fuzz --fuzz 500000 --seed 1 \
CortaTerminal/Tests/Fuzz/corpus
It has no coverage feedback, so it explores far less per iteration than
libFuzzer would. What it does have is a fixed seed, so a failure is
reproducible from the command line that found it. Recorded at M6:
2.5M mutated inputs across seeds 1–5, clean. A separate 100,000-input
run under AddressSanitizer (seed 3735928559) was also clean.
The corpus itself replays inside the test suite (FuzzCorpusTests), so a
crash found by a long run is fixed by adding its input to
CortaTerminal/Tests/Fuzz/corpus — from then on it is a regression
test.
Targets beyond the feed (2026-10-08). --target points the same
driver at the other inputs a hostile peer controls, each with its own
corpus under CortaTerminal/Tests/Fuzz: osc frames every input as one
OSC sequence whose code the first byte picks from the ones Corta handles
(titles, OSC 4/5/10–12 colours, 7, 8, 52, 104/105, 133, 134); kitty
frames it as one APC graphics command, two thirds of them as an o=z
transmission whose zlib stream is the input, so the inflate ceiling,
header and Adler-32 checks are what it reaches; sftp decodes it as one
frame body (SFTPCodec.decodeFrame, the statvfs reply and the length
check) and requires every frame that decodes to decode to the same message
after encodeFrame. The terminal caps above hold for osc and kitty.
SFTPFuzzCorpusTests replays the SFTP corpus in the test suite. PNG
decoding is not a target: it is ImageIO behind the app's signature and
dimension checks, and the SwiftPM harness cannot reach the app layer.
4.4 App-layer verification requires a launched app
Offscreen render tests assert pixel coverage: that a cell with a known background produces that colour, that a glyph produces non-background pixels inside its cell. Every one of the following passed those tests and still shipped a blank or unusable window:
- a view whose frame did not track its superview, so most of the grid was drawn outside the visible area,
- glyphs rasterised upside down,
- the grid laid out in points against a pixel coordinate space, so everything rendered at half size on a Retina display,
- a transient window size at startup that the shell laid its early output out against,
- a transient winsize delivered at startup: the first layout ran at
the content-rect height (window frame minus titlebar, before
.fullSizeContentViewtook effect) and shrank the session 30 → 28 rows, then grew it back — stranding two blank rows under the prompt whilestty sizeand the render rect both reported the correct 30 (M2 closeout, D.1; first fixed by gating session resizes on the view filling its window's frame, now by creating the window at its final size —DECISIONS.md, "What replaced D15 and D16").
These are properties of the live view hierarchy, of orientation, and of
startup ordering — none of which a texture readback can see. Any change
under Corta/ is therefore verified by launching the app and checking:
- the window's content size matches columns x rows x point metrics,
stty sizein the child agrees with it,- a screenshot shows text upright, full size, and filling the window,
- output longer than the screen scrolls and uses every row,
- gestures and menu actions reach the pane — the terminal view is first
responder, so
keyDownfires and First-Responder menu items (⌘V, ⌘=) are not dead, - a restored tabbed window (three tabs, one of them split and not
selected, quit, relaunch) comes back as one window, tabs in saved
order, the saved tab selected, and the background tab's split and
stacked icon there before it is first shown — the arrangement whose
layout only
AppDelegate.restoredWindowsandapplyPendingRestoredrive (D30), - in a non-English locale (
-AppleLanguages '(zh-Hans)'on the launch), the menu bar is translated, not only the menus beneath it.
The app that gets launched is the development build — the Corta (Dev)
scheme, CortaDev.app, bundle identifier dev.noahqin.Corta.dev (D22).
It can run beside an installed Corta, so identify the one under test by
bundle identifier or by PID; "the frontmost Corta" is ambiguous by
design, and a check that reads the wrong window is a check that proves
nothing. To run these against a Release build instead, stage it with
CORTA_STAGE_DIR.
Secure Keyboard Entry is checked by hand. Shell ▸ Secure Keyboard Entry shows the titlebar lock only while a terminal window is key, and the lock clears when another app comes forward. Test the engaged state without recording input from another application. Historical system-entry-point checks are retained in the dated test-results records below.
Remote workflows (B13/B14) have a launched-app check of their own,
CortaUITests/RemoteWorkflowUITests, which is the shape every
remote-path change is verified in. CORTA_STAGE_DIR (AppPaths) moves
the config file and Application Support under a throwaway directory —
$HOME moves neither on macOS — SHELL names a script called ssh
standing in for the remote shell (banner, a remote OSC 7, then
/bin/sh -i), and CORTA_SFTP_SSH points the SFTP channel at a script
that execs the real /usr/libexec/sftp-server. The UI-test runner is
sandboxed (it reads all of /, writes only its container, which the app
cannot read), so the app builds the stage itself: the test's first
launch types stage-remote-ui.sh into an ordinary terminal. The walk:
Remote: badge → keystrokes reach the pane → Browse Remote Files… (asked
first, host prefilled, nothing spawned before Connect) → a listing
answered by OpenSSH's own server → Edit → the "editor" rewrites the
managed copy → the upload prompt → the remote file changed with no
partial left → the fake shell exiting → badge gone → Reconnect → badge
back, keyboard live → the stage removed through the same terminal.
Nothing on the machine is changed and no network is used. Its first run
found six defects the unit suites had passed over (a spawn that never
returned, a write size the server rejected, a SIGPIPE that killed the
app, a consent step that auto-connected, an untitled window, a badge
that outlived its connection) — which is the argument for running it.
What it cannot cover, and a human still judges against a real host:
authentication and host keys (there is no ssh in the loop), and how
the panes look — they are Metal surfaces the accessibility tree cannot
read. SFTPRealServerTests in the CortaSFTP package target drives the same server
without the app. The test selects the ABC keyboard layout for its own
duration and restores the previous input source afterwards — a CJK
input method would otherwise compose every typed line into candidates
instead of delivering it to the terminal.
4.4.1 IME verification
IME behaviour additionally requires driving a real input method in the
launched app; no offscreen or in-suite test can do it (DESIGN.md §7.1 —
synthetic events with baked characters bypass composition). Procedure used
at M3:
- Launch the app with a wrapper
$SHELLthat recordsstty sizeand ends withexec cat > capture-file(a backgrounded reader is stopped bySIGTTIN; the capture must own the terminal). - Select an enabled Chinese input source for the session via TIS
(
TISSelectInputSource, e.g.com.apple.inputmethod.SCIM.ITABC) and restore the previous source afterwards. - Inject key events at HID level (
CGEvent.post(tap: .cghidEventTap)) with key codes only — setting a unicode string on the event makes the IME treat it as plain text. Requires the injecting process to be accessibility-trusted; without that grant this is a manual test. - Verify: typing pinyin opens a composition (marked text overlay at the cursor, underlined; nothing reaches the child), the candidate window appears under the cursor cell, selecting a candidate writes it to the child as UTF-8, and ⌃C / ⌃D / ⌃Z and the arrows behave identically with and without the IME selected.
4.4.2 Real-program verification (P0)
Render tests and golden-file grid tests both assert that a byte stream
produces a grid. Neither can tell you that vim is usable. The five P0
areas below are behavioural, and every one of them has a failure mode
that a passing grid test is compatible with:
| Area | Verified with | What to look for | Result |
|---|---|---|---|
| Cursor movement, shape, blink, mode switches | vim, nvim, htop, less | The cursor sits where the program thinks it does after a mode change; DECSCUSR shapes take effect; no ghost cursor in an unfocused pane | Pass |
| Scroll regions and the viewport | tmux with several panes, less on a long file | A region scroll does not disturb rows outside it; scrollback follows the bottom; scrolling back and returning lands where it started | Pass; see the note below |
| Left/right margins and wide characters | vim with a CJK file, tmux split narrow | A wide glyph never straddles the right margin; a resize rewraps without stranding rows | Pass |
| Insert and delete (ICH/IL/DCH/DL) | vim editing mid-line, readline with IRM | The redraw range matches the edit; nothing is left behind to the right of an insert | Pass |
| Erase (ED/EL/ECH) | clear, htop redraw, tmux window switch, tmux window shrink | No residue from the previous screen, and the cursor ends where the sequence says | Pass for ED/EL. ECH was not implemented at all until 2026-09-17 — this row said Pass on the strength of clear/htop, which never send it; tmux's status line (left part, CSI n X, right part) kept the previous screen's cells in the gap after a window shrink. Now implemented and covered by EditingTests. |
Run interactively on the release build (vim ~/.zshrc; tmux split
%/" and window switch n/p; htop refresh; less on
/var/log/system.log with /search, n, g/G; a CJK file in vim
and in a narrow tmux split; a mid-line edit in vim; clear), judged
by eye — not something a test target can assert. esctest (§4.2) covers
the sequences; this covers the programs.
An earlier run of this table recorded less's search-match highlight
(reverse video on a / hit) as never rendering, with the fault not
isolated. The 2026-09-17 pass (G29) could not reproduce it: the highlight
was visible on the same command. It is not tracked as open; a fresh
reproduction with a grid dump of what less sends would reopen it.
4.5 Test fixtures must not outlive the test
A fixture shell was wired in with launchctl setenv SHELL, which applies
to every GUI application the user launches afterwards. Corta then started
under zsh -f — no rc files, so no PATH — and a demo banner printed on
every launch. Both looked like defects in the terminal.
Pass fixtures in the environment of a launch you control:
SHELL=/path/to/fixture.zsh Corta.app/Contents/MacOS/Corta
Never launchctl setenv, never the user's rc files, never defaults write. Delete the fixture when finished.
4.6 Manual scenario pass
Run at every milestone, because these are the actual workload:
tmuxwith a split runninghtop, resize the window- Neovim editing a UTF-8 file with mixed CJK and emoji
sshto a remote host, runvim, resize the window- A long training run producing continuous output for minutes
- A Python REPL, paste a multi-line function
git log --graph --colorthrough a pager
B01 additions — the real-workflow matrix. The issue asked for zsh, fish, ssh, tmux, Neovim, fzf, CJK input, long output, sleep/wake, restoration and AI CLI applications. zsh/tmux/Neovim/ssh/long-output are items 1–4 above; fzf and fish were covered by the 0.1.1 audit's real-program harness (U10 in the quality plan, which records what it proved and what it could not); it was a one-time verification and was removed in 1.1.0. The remainder, added here rather than to the scriptable harness because none of them are drivable through a PTY alone:
- CJK input — an actual input source composing (not just CJK text,
which item 2 already covers) through a live candidate window. Manual only
—
DESIGN.md§7.1's verification caveat: synthetic key events carry baked characters, so composition never opens for them. - Sleep/wake — the machine actually sleeping and waking with Corta running. Manual only, deliberately not scripted: sleep/wake and low-power mode change machine-wide state other processes depend on, and need a dedicated session on an idle machine, not a CI-style script (D13).
- Restoration — force-quit or crash Corta with a multi-pane layout and
scrollback, relaunch, confirm the arrangement and directories return (U07).
Terminal text and running child processes must not return.
Partly scriptable: a staged
CORTA_STAGE_DIRwithrestore-windows = trueand a writtenstate.jsondrives the mechanism without touching global machine state — but a human still has to judge whether what came back looks right. - An AI CLI application — Claude Code (or a similar TUI-driven AI CLI) run as the child, covering ordinary interactive use including tab-completion and its own keyboard handling. Manual only, and the origin of B02's reported regression — see that issue's entry under §2.1 above; cross-referenced rather than duplicated here.
Items 7, 8 and 10 above have no automated substitute and are recorded with an explicit not judged state when a pass has not actually run one (precedent: the 0.1.1 manual verification's VoiceOver read-through, marked "not judged" rather than silently skipped when the tester could not follow spoken English closely enough to have an opinion). "Not judged" means exactly that — no automated check stands in for it, and no claim of success is made in its place.
4.7 The records
Up to 1.2.0, every pass of §4.4, §4.4.2 and §4.6 was written up as a dated
file under
test-results/
— what was checked, what passed, what failed and what was not judged —
and those records are preserved at commit f632d59, listed below. Since
then a pass is recorded in the pull request or issue it verifies, in the
same terms; the tree carries no dated records.
| Record | What it covers |
|---|---|
| 2026-09-02 — M2 closeout | §4.6 items 1 and 3: tmux + htop under resize; no reachable ssh host |
| 2026-09-03 — M6 closeout | Tabs, the settings page and theme switching live; focus reporting end to end; the esctest re-run; the gesture items left to a human |
| 2026-09-12 — B10 pass | The text-selection API decision, the accessibility tree audit, the translation audit, and what stayed not judged |
| 2026-09-16/17 — interactive pass | The first full sweep of §4.6 since 0.1.1, by tool and then by hand |
| 2026-09-18 — release checks | What the 09-17 pass changed; the six human and hardware items for 1.0.0, including the Quick Terminal probe |
| 2026-09-18/19 — issues #88–#90 | Mouse tracking, shared coordinates and the toolchain decision |
| 2026-09-21 — 1.0.1 release checks | §4.4 points 1–5 for the patch release, the themed cursor on screen, what was not re-run and why |
| 2026-10-01 — 1.1.0 audit closeout | The 27-finding review accounted for by #189–#194, with its automated and launched-app evidence |
| 2026-10-02 — audit follow-up | Search, SFTP lifecycle, local storage and reflow follow-ups, with the A/B measurements |
| 2026-10-03 — 1.1.0 release checks | §4.4 points 1–5 and 7 on the merged tree, the esctest re-run, what was not judged |
| 2026-10-03 — 1.1.1 release checks | Real-shell empty-Return regression tests, full core/Unit suites and user-confirmed launched-app check |
| 2026-10-07 — 1.1.5 release checks | Full core/Unit suites, fuzz, esctest re-run, core benchmark and Release frame CPU, and the user-confirmed resize check |
| 2026-10-07 — 1.1.6 release checks | Rounded box corners: render tests failing on 1.1.5, the launched app, and a Release frame-CPU A/B |
| 2026-10-09 — Mac audit closeout | §4.4 points 1–5, native session lifecycle, Quick Terminal and paste checks; explicit UI, VoiceOver, Shortcuts and remote limits |
Program status and desktop notifications
Claude Code is currently the only CLI with a dedicated task-state integration. General protocol parsing and process-name detection do not constitute task-state integration for other CLIs. No Codex, Gemini, pi, Kimi, Grok Build, Qwen, OpenCode or Aider state adapter is shipped.
OSC 7501 replies to probes with the constant OSC 7501 ; ? ST and accepts
working/blocked/done/idle/error and clear, per optional bounded id (one main slot plus 32 subtask slots). done
is a completed turn; idle is a separate state (including startup and stop).
Prompt redraws and ordinary notifications do not change reported task state.
Corta's internal unknown is used for process-only detection and OSC 21337
metadata, which do not establish an agent's task state. It is not an extra
OSC 7501 wire state. OSC 21337
accepts escaped indicator/status/status-color metadata in a separate bounded
slot. It decorates explicit state instead of competing with it; status text
fills an empty task title. Native text uses system colours; status-color is
retained, not rendered. OSC 9 distinguishes
numeric ConEmu subcommands from notification text. OSC 9;4 supplies progress;
a bare OSC 9;12 supplies a shell prompt mark. Neither establishes an agent's
typed task state. OSC 9;6/9;7 never execute; OSC 9;9 directory reports and
other unimplemented ConEmu commands are inert. OSC 777 accepts only notify.
OSC 99 supports title/body/close, bounded chunking and base64, occasion, urgency metadata, focus, expiry and silent/system sounds. Urgency never bypasses the OS's notification policy. The capability reply echoes only a charset-checked query id, as required by the kitty specification. The default kitty occasion is always; other protocols default to hidden panes. Corta coalesces all program notices to one mutable notice per pane. Up to eight unfinished chunked messages are kept, a ninth replacing the oldest; a close is applied to its own id only and never drops another id's notice. An explicit notice delayed beyond coalescing does not re-alert a pane whose current blocked episode was already announced; leaving blocked resets that suppression. BEL and kind changes use the same episode suppression. Fixed attention is cancelled when its wait ends; unrelated program messages are preserved. Visibility/occasion is rechecked before submission, including after OS authorization. OS delivery can occur after that final check.
Unsupported: a=report, c=1 event reports, p=alive, icons (n, g,
p=icon) and buttons. These generate no stdin reports. Close requests remove
only the pane's matching delivered/pending id; closing one cannot discard a
newer pending id or reset a blocked episode. Program-chosen ids never enter click
routing; clicks use Corta's pane id, window number and session generation.
ProgramSignalsTests covers byte-split sequences, bad fields/base64, entry
and chunk exhaustion/recovery, sanitisation, refused ConEmu actions, progress,
capability reply allowlisting and tab-status escaping. Launched-app acceptance
also needs a real Claude startup/permission/completion, notification click
routing and each explicit channel (iterm2, ghostty, kitty). Process fallback
uses process changes, prompt return and exit; an active process alone means
Unknown, a clean session end means Idle, and a known failed exit means Failed.
OSC notifications and BEL never imply blocked, done or working; no idle timer
is added. Accepted OSC 133 command boundaries clear stale children, waiting,
progress and tab metadata, even if the CLI never sent clear. A command
started on main may end while a crashed CLI leaves the alternate screen
active: lifecycle metadata finishes against the parked main grid without
switching or marking the alternate canvas. On the alternate screen a D
takes effect only when the shell's next prompt A follows it, as every
integration script sends them; a stray D printed by a live full-screen
program (a log in less -R) leaves its paste and keyboard modes alone. A clean boundary
preserves explicit done/error; otherwise it means Idle. Status 130 or a direct session terminated by SIGINT means
interrupted/Idle, other known nonzero statuses mean Failed. The next command
clears this summary. No shell boundary means no inferred CLI exit. Flood delivery is bounded to 0.2 notices/second/pane after coalescing.
RealClaudeStatusTests is opt-in: set CORTA_REAL_CLAUDE to the installed
executable's absolute path and run the core test of that name. It starts the
real program under TerminalSession in a fresh temporary configuration and
checks that Claude accepts the constant probe reply. It submits no model
prompt. It isolates file configuration and removes inherited API/OAuth tokens;
it is not an assertion about the CLI's internal keychain access. It does not replace the
permission, completion or Notification Center checks above.
CLI signal compatibility
Development and acceptance version: Claude Code 2.1.310 (Claude Code),
native macOS arm64. Dedicated task-state integration: Claude Code only.
The adapter was developed against this installed version and exercised with
real interactive sessions identifying the terminal as Corta
(TERM=xterm-256color). The checked-in capture records the executable's
SHA-256 alongside its version. This is a verified reference version, not a
promise of compatibility with every Claude Code release.
| Support category | What Corta supports | Boundary |
|---|---|---|
| Dedicated Claude task state | OSC 7501 working, blocked, done, idle, error and clear; permission/question/auth reasons; one main slot and 32 subtask slots | Only explicitly reported state; output, silence, BEL and notification prose do not establish an outcome. |
| Verified real 2.1.310 flows | Probe, normal completion, permission waiting/cancellation, invalid-model failure, isolated invalid-token auth waiting, Agent child id/clear and abrupt exit to an integrated shell | These concrete flows do not cover every failure cause or auth recovery. Question kind and capacity limits also have parser/policy coverage. |
| Verified native notifications | Default auto plus fixed 7501 attention; explicit iterm2/ghostty/kitty text, notification clicks and waiting/completed Dock badge screenshots | Auto does not select a Claude text channel for Corta; OS authorization and delivery policy still apply. |
| Generic protocols only | OSC 9/777/99 notifications, OSC 9;4 progress and OSC 21337 metadata | Claude 2.1.310 does not enable native progress for Corta's normal identity; its OSC 21337 sender is disabled. Generic support is not evidence of Claude emission. |
| Not implemented | Other CLIs' dedicated state adapters; OSC 99 activation/close/alive reports, icons and buttons; ConEmu execution commands | Other recognised tools have process detection and generic protocols only. No hooks or tool configuration are installed. |
| Not guaranteed | Other Claude versions; 7501 through tmux, Claude background sessions or disabled terminal-title reporting | The upstream minimum 2.1.295 is not a Corta-tested version range. Live regroup/restart click permutations and auth recovery need separate acceptance; routing policy has unit coverage. |
The native OSC 7501 vocabulary above was checked against that
installed version, including a captured idle → working → done turn.
The opt-in core test verifies the probe; app fixtures verify state, layout
and notification routing. The separate opt-in real-Claude UI acceptance
checks background Bash permission, Dock tile screenshots, notification
clicks, completion and the three explicit notification channels. See
running the acceptance test.
That UI test does not establish failure, cancellation or auth-required
sessions. RealClaudeLifecycleTests separately exercises real permission
cancellation, invalid-model failure, isolated invalid-token auth waiting,
an actual Agent subtask id/clear, and abrupt exit back to an integrated shell
with the alternate screen still active. These checks are opt-in and billable;
their existence is not proof of a pass, and auth recovery remains separate.
Newer versions must be checked independently. The vocabulary here is based
on the installed CLI and captured output; it is not a claim of an officially
documented shared standard or a guarantee for all Claude versions. The following upstream-source and
interface references describe candidate integrations, not runtime-tested
version ranges or adapters already shipped by Corta. Record exact CLI
versions, source revisions and acceptance results in a change's PR when an
adapter is added; an untested update is not a verified compatible version.
The official terminal guide and official changelog explicitly describe OSC 7501 support since 2.1.295. That minimum is not a Corta-tested version range. Full wire details below come from the installed 2.1.310 distribution; undocumented fields are not evidence of a defect.
| Channel | Evidence and actual 2.1.310 sending conditions | Corta contract |
|---|---|---|
| OSC 7501 | Official feature; distribution probes 7501;? and accepts same-code replies beginning with ?. Interactive sessions only; disabled by CLAUDE_CODE_DISABLE_TERMINAL_TITLE, background sessions and terminals known to print unknown sequences. In tmux the probe targets tmux, not passthrough. | Constant 7501;? ST reply; typed states, optional reason, bounded id/base64 title/message and progress. |
| done/idle/error/blocked | Distribution maps completed to done; startup/stopped to idle; ordinary failures to error; selected unauthenticated failures to blocked/auth. A blocked dialog may omit kind. | Done means completed turn, not a claim that every tool succeeded. Clear removes one id or all 7501 entries; it is not an outcome. |
| OSC 9 / 777 / 99 | Distribution selects explicit iterm2 / ghostty / kitty; auto recognises those terminals, not Corta. No OSC 99 capability query is used to select a notification channel. | Generic bounded notification parsers; no typed task-state inference. |
| BEL | terminal_bell, or added by iterm2_with_bell. notifications_disabled disables Claude's message channel only. | Existing bell policy; attention uses the same blocked-episode deduplication. |
| OSC 9;4 | Distribution requires terminalProgressBarEnabled plus recognised progress support: attachment capability, ConEmu, Ghostty ≥1.2.0 or iTerm2 ≥3.6.6; excludes its Windows Terminal branch. Normal Corta identity is not selected. | Generic progress only; no claim of native Claude-to-Corta progress emission. |
| OSC 21337 | Distribution has an encoder but its enablement function returns false. | Generic metadata only; no claim that Claude enables it. |
Claude's notification timing
is independent of status: permission/elicitation usually waits about six
seconds without typing; idle reminders usually wait about 60 seconds, with
no running background agent. A notification is not proof of completion.
The distribution normally emits BEL-terminated OSC; identification as kitty
selects ST. Tests replay both. Its kitty body packet omits d=0, completes
the accumulated title/body, and is followed by an empty focus packet; tests
assert exactly one resulting notice.
ProgramSignalsTests's distribution-derived wire fixtures are not captured
lifecycle sessions. The checked-in, text-redacted
claude-2.1.310-signals.json replays actual PTY captures for cancellation,
failure, auth waiting, a subtask and abrupt exit, plus actual OSC 9/777/99
idle notices. It retains field order, ids and terminators. Mocked transitions
also exercise stop/failure/auth parsing,
not Claude's actual execution of those flows. The separate opt-in
RealClaudeLifecycleTests runs selected real flows; it does not infer their
results from mock transitions. ProgramNotificationTests
injects authorization/submission/removal to test cancellation, visibility,
retry and late submissions without touching system permissions. The opt-in
UI test checks a later idle notification's real text separately from fixed
7501 permission attention, so a fixed notice cannot stand in for a channel.
Dock screenshots still require visual review; taking them is not an assertion
that the badge or progress is correct. Foreground acceptance and live model
lifecycles remain separate checks, not implied by a background unit pass.
| Tool / reference | Signals in the checked interface | Current Corta integration scope |
|---|---|---|
| Claude Code 2.1.310 | OSC 7501 working, blocked, done, idle, error, clear; blocked kind question, permission, auth | Explicit task state; clear removes state |
| Codex terminal backend | OSC 9 message or BEL; neither has a typed task state | Attention only; no completion/permission inference |
| Gemini terminal notifications | OSC 9/777 text or BEL for attention/session-complete events | Attention only; the event type is not retained in the text protocol |
| pi extensions | agent_start, agent_end, agent_before_settle, agent_settled | Extension events need explicit bridging; agent_end may be followed by retries/queued work, so notification-only work belongs at agent_settled |
| Kimi hooks | UserPromptSubmit, Stop, StopFailure, Notification | Hook events need explicit bridging; a stop hook can cause continuation |
| Grok Build hooks | UserPromptSubmit, Stop, StopFailure, Notification | Hook events need explicit bridging |
| Qwen hooks | UserPromptSubmit, Stop, StopFailure, Notification | Hook events need explicit bridging |
| OpenCode attention API | Terminal notification title/message; semantic sounds include done/error/question/permission | Notification text is not a typed task state |
| Aider notifications | OS notification command, or BEL on return to input | No typed task state in the checked notification path |
A StopFailure or tool-failure hook must not be confused with a normal
completion; a failed individual tool can also be recovered within a turn.
Hooks, RPC events and application-internal events are not automatically
visible in an ordinary terminal PTY. No tool configuration is installed or
changed automatically, and Corta never classifies notification prose as
success, failure or permission. Gemini's AfterAgent
and Kimi/Qwen's Stop can request retries; observing either hook alone is not proof that automatic
processing has stopped.