Files
Chris Tate f8c14c59c7 Terminal: pty effects, libghostty-vt, and a replayable terminal example (#191)
* Add the POSIX pty primitive: openpty, controlling-terminal fork/exec, resize, reap

- One transport for terminal children: parent-built argv/envp (clean env plus TERM), async-signal-safe child path via login_tty/execve, group kill and waitpid decode
- macOS and Linux-with-libc report supported; every other target compiles to loud PtyUnsupported stubs
- Live tests prove the controlling terminal (test -t 0), exit codes, signal decode, and the env policy

* Pin libghostty-vt and stage the terminal example skeleton

- examples/terminal owns its build: ghostty pinned at 7aa9591 (the first upstream commit that builds under Zig 0.16.0; v1.3.1 still targets 0.15) with simd off so the vt module stays pure Zig
- The pin is proven in-tree: the example's test round-trips VT parsing through the pinned module
- zig-pkg/ (the project-local package store) joins .gitignore

* Add the pty effect vocabulary: spawn, coalesced output, write, resize, kill, replayable exits

- fx.ptySpawn/ptyWrite/ptyResize/ptyKill ride the keyed families' seams: one key space, the spawn argv budgets and env policy, exactly one exit per spawn, rejections staged loud and regenerating
- Output coalesces through a per-pty staging ring paced by the drain (lossless back-pressure: a full ring parks the reader, never drops), delivered as bounded batches journaled via the content-addressed blob store; replay never spawns a process
- The fake pty scripts the whole vocabulary headless (request/write/resize/kill mirrors plus output/exit feeds), and live POSIX tests pin coalescing, losslessness, controlling-terminal wiring, and teardown convergence

* Render the terminal example: libghostty-vt grid on the canvas, keyboard-first, replayable

- grid.zig wraps one emulator session (cell state, damage, scrollback, keyboard selection) and paints the viewport as real text: per-row background/text runs, theme-mapped ANSI-16 with exact 256-color and truecolor, wide CJK cells, a scrollback thumb
- main.zig drives it on the pty vocabulary: typing rides the IME-correct text channel and the emulator key encoder, selection/scroll/copy chords, a variable-length chrome prefix for the grid, and the frame pump resizes the grid and pty together
- UiApp gains on_text (the target-less committed-text seam) and a variable_prefix chrome mode; the gpu-surface layer routes unclaimed text_input to the app, the key_down fallback's twin
- Tests pin the emulator round trip, CJK width, scrollback, line/block selection, palette honesty, and a fake-pty session that replays byte-identical offline

* Document the terminal recipe, platform matrix, and replay story

- docs/terminal: the pty vocabulary, coalesced output, the exit taxonomy, grid rendering, and the byte-identical offline replay story, with the honest macOS/Linux/Windows-staged/null matrix
- Registered in the Core Concepts nav beside Dynamic Images
- Changelog fragments for the pty vocabulary, the terminal example, on_text, and the TS Cmd family

* Harden the pty transport: exec-failure detection, teardown, and write accounting

- pty spawn distinguishes a failed exec from a real 127 exit via a close-on-exec self-pipe (a bad shebang interpreter now reports spawn_failed, not exited), and rejects argv entries with embedded NULs instead of silently truncating them at the C boundary
- The io loop observes shutdown so a reader parked on a full staging ring exits promptly at teardown instead of forcing the abandon path
- dropped_writes now counts outbound bytes lost to a write failure or a child that exits with input still staged; a kill racing a natural exit rewrites the terminal to cancelled with the -1 sentinel code
- Bounded per-drain output: a delivered batch re-stamps the remaining backlog past the pass boundary, so a continuously writing child delivers one batch per drain instead of starving other events

* Harden pty fd hygiene, the kill/reap races, replay provenance, and grid fidelity

- pty spawn aborts if CLOEXEC on the exec self-pipe cannot be set (a leaked write end would hang the parent), and the wake pipe is now CLOEXEC so no descriptor leaks into the child; empty PATH components resolve as the current directory (POSIX)
- ptyKill no longer signals a reaped pid (the OS may have reused it) and the io loop winds down on kill even when an escaped descendant holds the pty open, so the exit always delivers; dropped_writes counts each lost payload, not one lumped event
- Platform-unsupported rejections are executor truth and feed under replay (a journal recorded where ptys are unsupported replays its rejection verbatim); the regenerating marker rides the record's truncated bit so a pty exit reason stays honest, with a replay damage gate for the pairing
- feedPtyOutput refuses an over-bound batch instead of truncating, and every fake pty (not just replay parks) refuses a second feed after its exit is queued
- Example: inverse-video cells paint text in the background color (not foreground-on-foreground), the grid self-limits to its chrome command budget so a pathological screen degrades to fewer rows, and the Linux default shell is /bin/sh (present on every install, unlike /bin/bash)

* Wire the pty command family into the TypeScript tier

- Cmd.ptySpawn/ptyWrite/ptyResize/ptyKill (wire opcodes 0x19-0x1C, additive over the current cmd format) expose the pty vocabulary to transpiled cores, with an event arm matched by field name (key/kind/bytes/code/reason/signal/droppedWrites)
- The rt kernel encodes the four ops; the emitter lowers and validates them (argv and grid bounds) with teaching diagnostics; the ts-core host decodes them into the runtime Effects pty calls and dispatches events back to the core's event arm
- Posting stays native-only (transpiled cores are single-threaded): the TS tier spawns, writes, resizes, kills, and receives. Covered by packages/core effects/conformance tests and the ts_core_host bridge tests, plus a cmdview decoder arm so the evals harness names the new commands

* Harden pty close-on-exec fds, the pid-reuse guard, exact write accounting, and NUL-safe TERM

- The pty parent end/child end (and the exec self-pipe read end) are close-on-exec, so a concurrent process or pty spawn on another thread cannot inherit another session's descriptors and hold its pty open; pipePair now aborts if either non-blocking fcntl fails instead of returning a pipe that could hang the UI thread in nudge
- Closed the pid-reuse window: the io thread publishes a reaping flag under the mutex before waitpid, and ptyKill and teardown decline to signal once it (or exit_staged/io_done) is set, so a reaped child's reused pid is never signalled
- dropped_writes now tracks per-payload boundaries: a fully-sent write pops as its bytes flush, so a discard counts only the writes that never reached the child, not already-delivered ones
- ptySpawn rejects argv or TERM containing an embedded NUL (validated at the effect boundary so the fake executor and replay agree), never a silently truncated value

* Bound the pty exec probe and reap, make kill signalling atomic, and cap the grid command budget

- The exec-status probe polls with a timeout instead of a blocking read: a real exec failure writes its byte instantly (always detected), success is EOF, and the timeout is the net for the residual CLOEXEC window so a concurrent fork inheriting the pipe writer can never hang the spawn. Pipes are created close-on-exec atomically on Linux (pipe2); Darwin keeps the immediate fcntl behind the same timeout net
- reapEnding replaces the unbounded reapBlocking after the stream ends: the normal case (child already exited) returns at once with no signal, and a child that closed its terminal but kept running is hung up and escalated to SIGKILL within a bounded window, so the exit always arrives and no kill is stranded
- ptyKill and teardown signal the child UNDER the shared mutex, atomic against the io thread's pre-waitpid reaping publish, so a reaped pid's reuse is never signalled
- dropped_writes stays exact: the outbound record ring is an admission bound (a write past it is refused and counted once, like a full byte buffer), so every accepted write owns a length record and none is folded
- Example: the grid's per-row command reserve accounts for background + text + underline per cell (3/column) so a pathological screen truncates safely under the chrome budget; a resize allocation failure leaves the model dimensions uncommitted so the emulator and pty never disagree and the frame pump retries

* Open the pty pair with an atomically close-on-exec child end, reset reused header flags

- Replace openpty with posix_openpt + grantpt + unlockpt and open the child end O_CLOEXEC atomically: the child end is the descriptor whose inheritance across a concurrent fork's exec would hold the pty open and starve the exit event, and an atomic open closes that window on both platforms (the parent end's Darwin sub-syscall window is benign — an inherited parent end does not hold the pty open)
- Reset every reused PtyShared flag on a new spawn (reaping, the outbound write records) so a prior session's state never steers the next: a stale reaping would make ptyKill skip its immediate SIGKILL
- Declare ioctl variadic to match the C ABI — a fixed-arg declaration mis-passed the winsize pointer on arm64, silently dropping the initial grid and every resize; the resize test now verifies the applied size, not just the call

* Handle already-reaped children, low child end fds, post-exit writes, and pre-output input

- reap reports ECHILD (a child reaped by an embedder's own SIGCHLD handling) as a gone child, never as still-running, so the escalation path cannot signal a reused pid
- Relocate a child end that lands on fd 0/1/2 to a high descriptor before login_tty: a same-fd dup2 does not clear close-on-exec, which would make the child's stdio vanish at execve when the host started with standard descriptors closed
- ptyWrite drops a write once the reaper has staged the exit: there is no io thread left to send it, and mutating the finalized exit's dropped_writes would race the drain
- The terminal example accepts input from spawn onward, not only after the first output batch — a shell with an empty prompt and no banner never flips to live, and gating input on it would strand every keystroke

* Relocate low pty fds, wind down orphaned ptys, commit-only text, IME preedit, guarded restart

- Relocate the child end AND the exec self-pipe write end above the standard descriptors before the fork: login_tty dup2's the child end onto 0/1/2, and a write end left on fd 1/2 would be clobbered, making an exec failure masquerade as a normal exit when the host began with stdio closed
- Bind-time services-snapshot failure now winds down already-running ptys (bounded), not just channels, so their exit still delivers instead of stranding with no wake services
- on_text delivers COMMITTED text only: an IME preedit (set_composition) or cancel routed through a focused non-text widget no longer types provisional bytes into a terminal
- The targetless text path tracks the IME preedit and delivers it on commit — hosts emit an empty commit for unchanged marked text, so the composed bytes come from the buffered preedit; only committed UTF-8 reaches on_text
- The terminal example restarts only a genuinely ended or failed session, never during starting/live, so a quiet shell is not duplicated onto its own occupied key

* Non-blocking pty parent end, detach at retire, UTF-8-safe preedit truncation

- The pty parent end is opened non-blocking and read/write surface WouldBlock: the sole io thread paces both directions through poll and never blocks inside a write when the child stops reading stdin while producing output, so stdout keeps draining and neither side deadlocks (ptyFlushOutbound writes at most one chunk per POLLOUT, leaving the rest staged)
- Retirement detaches the io thread instead of joining it: retirement runs at exit delivery, inside the very dispatch a synchronous-marshal wake hook may be waiting on, and the thread's last act is that host wake — joining would recreate the deadlock ChannelWake forbids. By retirement the thread has published io_done and is past all transport/staging/pipe access, so detaching and reclaiming its fds is safe (conforming enqueue-only wakes have already returned; a violating one lingers against the process-lived header)
- A truncated targetless IME preedit now cuts on a UTF-8 boundary, so an empty commit never forwards a split code point as committed text

* Quiesce every pty wake at teardown, route IME to editable widgets only, chunk input

- Teardown now quiesces the wake header of EVERY pty slot, idle included: the header is process-lifetime and reused, so a retired session's slow wake_fn can still be in flight when the services snapshot and platform are freed. A call still executing at the deadline is abandoned, counted, and the platform signalled to outlive it — the abandoned io-thread path accounts for its quiesce failure the same way, no longer silently
- Text and IME route to a focused widget only when it is an editable text-entry widget; a focused non-text widget (a button, a list row) lets committed text fall through to the target-less on_text seam instead of dropping it
- The targetless IME preedit buffer holds a full phrase-sized composition and the child's exec-failure report retries EINTR, so an interrupted one-byte report is never mistaken for a successful exec
- The terminal example chunks committed text into per-write-bound pieces, so a long paste or IME commit types every byte instead of being refused whole

* Free the pty header on clean teardown, honor emulator colors, bound the grid text store

- A cleanly quiesced pty header is now freed at teardown: unlike a channel header (which an app thread may post to forever), the pty io thread is the sole toucher and is gone once quiesce confirms no in-flight wake, so repeatedly creating and destroying runtimes that used a pty no longer leaks ~64 KiB each; an abandoned header still leaks deliberately
- The grid reads the emulator's resolved foreground, background, and cursor: the theme colors are pushed into the emulator's DEFAULTS so ghostty composes OSC 10/11/12 overrides and DECSCNM reverse-video itself, and the renderer honors an application's requested colors instead of forcing the theme
- The grid degrades by whole rows when the display-list text store nears its budget (an emoji-dense screen), never blanking cells mid-frame with a swallowed allocation failure
- Documented that an .exited pty terminal carries -1 only when the child was reaped outside the toolkit (an embedder installing SA_NOCLDWAIT / ignoring SIGCHLD / running its own reaper) — an already-broken parent/child contract where the real code is unrecoverable

* Keep the pty header process-lived, serialize ptsname, honor OSC 4 by mask

- Revert the teardown free of PtyShared: quiescing proves in_flight is zero, not that the io thread has RETURNED (it publishes its exit before calling requestHostWake, and a detached mid-life thread's late or violating wake still locks the header's mutex), so freeing risked a use-after-free. The header is now permanently retained like ChannelShared — a bounded per-slot leak that never grows during a runtime's life
- ptsname's shared static buffer is resolved and copied under a process-wide spinlock, so concurrent pty spawns from independent runtimes on different threads never open each other's child end
- The grid decides an ANSI slot is untouched by the emulator's override MASK, not RGB equality: a program that OSC-4-sets a slot to exactly the default RGB is honored instead of being replaced by the theme color

* Stop macOS Option double-input and stage heavy graphemes whole

- On macOS, Option is a compose key: Option+F commits the composed character through the text channel, so the key encoder no longer also treats Alt+printable as a chord there (which sent both the composed character and an Alt-escape to the child). Elsewhere Alt stays Meta
- The grid's run staging buffer holds a full row plus a heavy grapheme cluster (8 KiB), so a cell with hundreds of combining marks stages whole; the run-break splits long runs across draw commands rather than overflowing, and any residual cut lands between code points (valid UTF-8)

* Block for the exec verdict on Linux, refuse empty output records, make grid rows atomic

- The exec-status probe blocks for a reliable verdict on Linux, where the report pipe is atomically close-on-exec (pipe2): a slow execve (an interpreter on a sluggish filesystem) is now awaited and correctly reported as spawn_failed rather than assumed successful. Darwin keeps the bounded poll — it has no atomic-CLOEXEC pipe, so blocking could hang on the fork-inheritance window (the documented residual every macOS terminal shares)
- Replay refuses a .output pty record with a zero-length blob: the recorder journals output only for a non-empty batch, so a zero-length one is damage that would otherwise replay a synthetic empty event and diverge the fingerprint
- The grid measures each row's exact text bytes and skips the row WHOLE when the display-list text store cannot hold them, and breaks a run before a cell whose grapheme would overflow the run scratch — so a heavy-grapheme row is never torn mid-way and a large cluster landing near the buffer's end keeps all its marks
- The terminal-response staging buffer holds a large pipelined query burst (16 KiB); an overflowing reply is still dropped whole (never cut) and counted

* Zero the signal on a cancelled pty, chunk query replies, grow the IME preedit to fit

- A cancelled pty exit reports signal 0: the toolkit's own SIGKILL is what felled it, and the contract is "signal is nonzero only for a .signaled end", so the raw SIGKILL value no longer leaks into a cancelled terminal
- Terminal query replies (DSR/DA/XTVERSION) flush through the chunking write path, so a batch that pipelined more than one write's worth of replies delivers every byte instead of being refused whole and leaving the child waiting
- The targetless IME preedit buffer grows to fit the composition instead of truncating at a fixed size: each set_composition replaces it with the host's full string, reallocated only when a larger one arrives and freed at runtime deinit, so an unchanged commit of any length forwards the whole composition

* Ignore empty pty output, clear stale preedit on OOM, reset on restart, map function keys

- feedPtyOutput ignores an empty batch: the live drain only delivers non-empty output, so journaling a zero-length one would write a blob record replay refuses as damage
- A failed IME preedit grow clears the buffer instead of leaving the superseded composition, so a later empty commit never inserts stale text the user has replaced
- Restarting the terminal hard-resets the emulator (RIS + a fresh parser), so a shell that exited mid-escape or in a non-default mode does not corrupt the next session's key encoding or first output
- The key map covers Insert and F1-F12, which carry no committed text and would otherwise be dropped before reaching the child

* Bound the exec probe on both platforms, gate impossible exit records, clear selection on restart

- The exec-status probe blocks up to a generous bound on every platform rather than forever on Linux: O_CLOEXEC closes an inherited pipe copy on the concurrent forker's exec, not its fork, so a forker slow to exec would delay EOF and an unbounded wait could hang the spawn thread. The bound caps that pathological wait; a timeout means our own child exec'd (an inherited writer merely delayed EOF) and is success. A slow-failing exec under the bound is still awaited and reported; only one that both blocks and fails past it misclassifies (an extreme filesystem pathology) — a self-pipe cannot distinguish that from a delayed EOF without the bound
- Replay refuses a pty exit record whose code/signal contradict the delivered contract (signal nonzero iff .signaled; code -1 for every reason but .exited), so a hand-edited .cancelled with code 0 or signal 9 cannot dispatch a contract-violating event
- Restarting the terminal leaves selection mode, so a restart while a selection caret is armed does not reject the new shell's typed input

* Name the two ends of the pty pair parent and child

- The controlling side the toolkit keeps is the parent end; the process side the spawned program adopts as its controlling terminal is the child end. Rename the transport field, the spawn locals, and the poll helper to parent/child, and rephrase every comment to match.
- POSIX's own API names (posix_openpt, ptsname, grantpt/unlockpt) keep their C spellings at the call site; only the words the toolkit owns change.

* Carry the session key on pty events, normalize fake exits, keep query replies lossless, and shrink the pty leak

- The transpiled pty event arm now carries the app's own session key: two sessions routing one event arm were previously indistinguishable, so the arm gains a `key` field the host fills from the wire key (emitter, host bridge, and SDK type updated together).
- `feedPtyExit` clamps its tuple to the event contract the live io loop guarantees — a signal only after a signaled end, a code only after an exited one — so a fake or replay can never stage an exit the recorder journals but replay's damage gate then refuses.
- The terminal example feeds output in sub-slices and drains the emulator's query answers between them, so a burst of pipelined replies cannot outrun the write-back buffer and a child blocked on a DSR answer never hangs; a residual overflow surfaces on the status line instead of vanishing.
- A pty session's ~64 KiB outbound ring and its per-write length ring move into a heap block freed at retire, so only a small header joins the process-lifetime leak (the channel header invariant), not the buffers; a teardown that abandons a stuck io thread still leaks the block with the thread that can reach it.

* Make the pty rejection key self-contained and enforce the signaled-exit contract

- A staged spawn-rejection Msg is delivered a frame after it is issued, past a frame-arena reset, so it can no longer copy the wire key into that arena: it carries the empty key (the self-contained-Msg rule the bytes field already follows), leaving the durable engine-key routing for live events untouched.
- feedPtyExit now enforces the full exit contract as a biconditional — a signaled end REQUIRES a nonzero signal, not just a signal only on a signaled end — so a fake or replay feeding .signaled with signal 0 is refused loudly instead of journaling a record replay's damage gate would later reject.

* Give replayed pty output the -1 code, keep the rejection key, and bound the exec probe by deadline

- A replay-fed output batch now carries the -1 code sentinel the live drain delivers (the Entry default was 0), so an app that folds the event code into its model no longer diverges between a live run and its replay.
- A staged spawn-rejection keeps the app's requested key: it is copied into a process-static ring (durable across the frame reset the staged Msg outlives) and referenced directly, so a duplicate-key or table-full refusal is correlated with the command that caused it instead of arriving keyless.
- The exec-status probe bounds itself by a monotonic DEADLINE rather than a per-poll timeout, so signals interrupting poll (EINTR) can no longer each restart the full wait and defer the timeout indefinitely — the synchronous spawn stays bounded under a signal storm.

* Make staged rejection keys grow with the batch, pace pty input to the FIFO, and reserve grid widget text

- Staged spawn-rejection keys move from a fixed host ring to a growable engine store (stageLoopKey): each key gets its own stable heap buffer reclaimed when the staged stage next empties, so a Cmd.batch staging more rejections than any fixed ring holds can no longer overwrite a key still awaiting delivery and mis-correlate an exit.
- The terminal example buffers typed and pasted input and drains it against the pty stdin FIFO's free space (new ptyOutboundFree query), so a paste larger than the 64 KiB FIFO no longer loses its tail when the child is not reading; it paces out as the child reads, with a per-frame flush covering a child that reads without echoing. Query replies stay direct and the flush reserves FIFO headroom for them, so bulk input never starves an answer the child blocks on.
- Grid painting reserves display-list text bytes for the header and status widgets that share the per-view text store, so a grapheme-heavy viewport degrades to a few fewer rows instead of pushing the combined frame past the runtime limit and failing the whole update.
- Any residual dropped input surfaces on the status line beside the reply-drop count.

* Report pty write acceptance so the terminal never loses input or query replies

- ptyWrite now returns whether the whole payload was accepted, since it alone knows the byte-FIFO and 256-record admission limits; a byte-capacity query could report room the record ring would still refuse, misleading a caller into treating a rejected write as sent. The misleading ptyOutboundFree query is removed.
- The terminal example drains one stream-ordered outbound ring holding typed keys, pastes, AND emulator query replies, retrying any chunk ptyWrite refuses instead of dropping it. A paste larger than the FIFO paces out as the child reads; a reply refused by a full FIFO stays queued rather than being cleared, so a child blocking on a DSR answer is never stranded. A per-frame flush covers a child that reads without echoing, and a full-ring overflow is counted and shown, never silent.

* Journal pty write verdicts for replay and free pty buffers through their allocating seam

- A ptyWrite admission verdict is executor truth — whether the outbound FIFO and record ring had room depends on how fast the child was reading — so each one now journals (a .pty/.write record) and a replayed write returns the RECORDED verdict instead of recomputing optimistically against a fake with no child: an app that retains refused bytes takes the identical retain/remove path in both runs, pinned by a recorded session whose refused and accepted writes replay fingerprint-identical. The journal format fingerprint moves with the new record kind, refusing pre-verdict journals honestly.
- Pty staging rings and outbound blocks now free through the channel_storage_allocator seam that allocated them, so a swapped-in tracking or arena allocator sees its own frees instead of a mismatched destroy against the default backing.

* Scope target-less IME preedit per surface and gate command chords out of target-less text

- The target-less preedit buffer now tracks its originating surface (window + view label), matching how a focused widget's editor scopes composition to the widget: only the owning surface's commit consumes the buffered bytes and only its events clear them, so a second surface's empty commit can never insert a composition typed into the first.
- Target-less text_input now applies the same command-chord gate focused text widgets use (primary/command/control means shortcut, not typing; Alt stays out — Option and AltGr compose text), so Ctrl+C delivers the encoded chord alone and never a stray literal character; pinned at the runtime layer and against the terminal example's pty input.

* Deliver key-carried typing target-less, count post-exit write refusals, and complete the terminal key map

- Committed text carried on a key_down (hosts whose plain typing rides the key event, with no separate text event) now reaches the target-less on_text seam under the same command-chord gate the focused-widget path applies — ordinary typing was silently lost there, while specials carry no text on any host so nothing doubles with the key fallback.
- A ptyWrite refused in the staged-exit window now counts into dropped_writes (the exit reads the count under the mutex at drain delivery, which has not happened yet), closing the one silent-refusal gap; only a write after the exit delivers finds no slot, the documented cancel race.
- Every desktop platform now reports delete/home/end/pageup/pagedown/insert and f1-f12 as named keys (previously private-use strings or nothing), shortcuts and menu accelerators accept them, and the terminal example maps chorded punctuation and supplies the pressed character to the emulator's encoder — Ctrl+\ reaches the child as its C0 byte, Ctrl+[ as the encoder's fixterms CSI-u form, F1 as ESC O P.

* Reset OSC colors on shell restart and honor widget precedence in target-less text

- Session.reset now clears the palette and dynamic color overrides (OSC 4/10/11/12): the emulator's full reset leaves its color state alone, so a shell that tinted the palette and exited would otherwise color the next session; overrides drop while theme defaults stay.
- The target-less committed-text fallback now honors the widget-precedence contract's structural-claim step: a key the focused widget answers as a control intent (Space pressing a focused button) no longer ALSO types its literal character through on_text, on either committed-text carrier, while unclaimed characters keep flowing to a terminal that happens to share focus with a button.

* Wire the new named keys through every platform's menu accelerators

- GTK translates them to accelerator names (Page_Up, F1, ...) so gtk_application_set_accels_for_action actually binds them; AppKit converts named keys to their NSMenuItem key-equivalent characters (function-key unichars, control characters) instead of passing the canonical string through, which also repairs the pre-existing arrow/escape menu bindings.
- The Windows keydown path now dispatches menu-item accelerators alongside registered shortcuts — a menu item's key+modifiers emits its menu command exactly as if clicked; AppKit and GTK get this from their menu systems, and on Win32 the message loop is that system.

* Settle replay feeds at the journal's end and make pty record accounting exact

- Replay now verifies at the journal's end record that every journaled ptyWrite verdict was consumed and no write ran past them (the new .settle replay control): a write-count divergence changes no state when the caller is fire-and-forget, so fingerprint checkpoints alone could pass a diverged run — the settle check fails it loudly in both directions.
- Output records journal the canonical scalar shape from BOTH drain sites (-1 code sentinel, no signal, no drops) and the replay damage gate refuses records claiming anything else — previously replay silently delivered defaults that differed from a damaged record's journaled fields.
- A fed exit's dropped_writes now adds the refusals the fake itself counted (an oversized write against a scripted pty), so the counted-refusal contract holds without every script re-deriving the tally; under replay the slot count is always zero and the journaled count delivers verbatim.

* Keep an IME sequence with the consumer it started over

- A composition buffered by the target-less consumer continues target-less even when a text widget takes focus mid-sequence: routing its empty commit to the newly focused editor would resolve a composition that editor never saw and lose the composed text. Ownership ends with the sequence — the next composition belongs to the focused editor as usual.

* Admit outbound terminal payloads whole or not at all

- The pending-outbound ring's admission is now all-or-nothing: a payload it cannot hold whole is dropped whole and counted, never cut at the ring edge — a torn query reply or encoded key would feed the child a malformed escape sequence, which is worse than a counted loss. Reaching the drop at all means the child ignored 256 KiB of pending input, and the count stays on the status line.

* Key the replay write verdicts by pty and grow the queue with the recording

- The verdict queue now spills to the heap past its inline window (the pending-stage growth story): every verdict a dispatch recorded feeds before that dispatch replays, so a fixed bound would reject a valid recording whose one update wrote more times than the bound — a giant paste drained in per-write chunks is exactly that.
- Each verdict carries the pty key it was recorded against, and a replayed write consumes only a matching-key verdict: the same call count and acceptance results against a DIFFERENT session is still divergent input, which a global boolean sequence would silently pass; a mismatch counts as divergence, strands its verdict, and fails the end-of-journal settle on both signals.

* Canonicalize menu key case, teach the Chromium host the named keys, and state the reaping contract

- Menu-item keys canonicalize to lowercase at each host's storage boundary, matching the shortcut stores that already did: key names validate case-insensitively, so a mixed-case spelling must still translate to a valid GTK accelerator name and a correct AppKit key equivalent (single characters lowercase there too — uppercase implies Shift, which the modifier mask already expresses).
- The macOS Chromium host's key normalizer gains pageup/pagedown/insert and f1-f12, so shortcuts on those keys fire under that host exactly as under AppKit.
- The kill fence's ownership premise is now stated at the signal site and in the recipe: the toolkit forks each pty child, is its sole reaper, and never touches SIGCHLD disposition — an embedder must not either, because POSIX offers no portable way to signal a pid once a third party (including a kernel auto-reap) can free it first; inside the contract the reaping fence is exact.

* Settle the clock feed too, and retain query replies a full ring refuses

- settleReplayFeeds now holds the clock feed to the write-verdict rule: a wallMs read past the journaled values (answered 0 optimistically) or a journaled value never consumed is divergence no checkpoint need see, and either fails the end-of-journal settle loudly.
- A query reply the pending ring cannot take right now stays IN the emulator's buffer — uncleared, uncounted — and retries on the next output, resize, or frame nudge, so a child blocked on a DSR answer behind a full ring is never stranded by a discarded reply. Transient payloads (typed text, encoded keys) keep the counted-drop disposal since their bytes cannot outlive the dispatch; only a payload larger than the whole ring is impossible and counts immediately.

* Align start-failure write verdicts, grow the reply buffer, and strip the primary alias from encoder chords

- A write against a spawn whose transport failed synchronously now journals its refused verdict: the staged executor-truth terminal keeps the key occupied on the live side exactly as the parked slot does under replay, so the two verdict streams stay aligned where they previously diverged (live refused silently, replay consumed a verdict that was never recorded).
- The emulator's query-reply buffer is heap-grown to fit (up to the outbound ring's own capacity): replies retained behind a full ring keep accumulating while further output feeds, instead of overflowing a fixed buffer into counted drops that could strand a blocked child; past the ceiling a reply still drops whole and counted, never cut.
- The terminal encoder strips the runtime's primary-into-super fold when Ctrl raises it, so a bare Ctrl chord reaches the emulator clean and Ctrl+C delivers ETX — a stray super demoted it to a CSI-u chord no foreground shell treats as an interrupt. The GUI+Ctrl double chord encodes as plain Ctrl, the convention terminals follow.

* Hold stdin order across a retained reply

- A query reply retained behind a full outbound ring is older than any later keystroke, so transient input now gives the reply its retry first and refuses to jump the queue while it remains stuck (dropped counted, never reordered) — the child's stdin keeps the order a real terminal delivers: the answer it is parsing toward, then the typing.
- The output arm retries retained replies right after its flush frees ring room, so the oldest bytes take that room before anything a later dispatch could enqueue.

* Refuse and count fake writes after the staged exit, folding drops at delivery

- A fake pty now mirrors the live admission path's staged-exit rule: a write after feedPtyExit staged the terminal refuses and counts as dropped, never a silent acceptance into a session that is already over.
- The dropped tally folds the slot's count into the delivered exit AT DELIVERY (both the fed-exit queue drain and the start-failure stage), the same read-at-delivery rule the live staged-exit drain follows — so refusals landing between the feed and the drain still reach the delivered count; under replay the slot count is provably zero and the journaled count delivers verbatim.

* Land start-failure write refusals in the staged exit's tally

- A write refused in the synchronous start-failure window now counts onto the staged terminal entry itself: that terminal's slot was already released, so no slot tally could carry the drop to delivery, and the exit reported zero despite the refusal. The counting walk is the occupies-key predicate with the bump folded in, so the verdict journal and the delivered tally move together — the exit reports every refusal, never silence.

* Settle undelivered pty feeds, serialize the spawn's descriptor window, and state the process-group kill limit

- settleReplayFeeds now refuses fed pty results still queued at the journal's end: every result was journaled at its live delivery, inside an event that follows it in the stream, so a leftover means the journal was truncated past its consuming event — succeeding would silently omit a recorded delivery. Regenerating staged rejections stay exempt (the live run could end with one staged too).
- The pty spawn's whole descriptor-opening window — posix_openpt through fork — now runs under one process-wide lock, and the parent end's close-on-exec fcntl is verified: Darwin ignores O_CLOEXEC on posix_openpt, so a concurrent toolkit fork landing inside another spawn's flag gap would gift its exec'd child a copy of that pty's parent end for the child's whole life. Our own forks can no longer land there; an embedder fork keeps the documented residual window the exec probe's timeout nets, and the probe still polls outside the lock.
- The kill contract now states its real reach at every site: SIGKILL lands on the child's process group (the whole foreground job), and a descendant that re-grouped itself escapes — POSIX has no kill-whole-session primitive, the spawn family's documented limit.

* Cover every descriptor window with the spawn lock, bound the surrendered reap, and carry an unresolved exec probe to the reap

- Every toolkit fork and every Darwin flag gap now shares one lock: the wake pipe's creation and the job-spawn family's process start hold the pty spawn lock, so no toolkit child can inherit another spawn's not-yet-CLOEXEC parent end, pipe writer, or wake pipe across its exec — only embedder forks on threads the toolkit does not own keep the documented residual window.
- reapEnding's final wait is bounded: a SIGKILL'd child the kernel holds in uninterruptible I/O cannot be waited out by any signal, so past a further deadline the reap is surrendered and the kill reported as the ending — the exit always reaches the app, and the eventual zombie is the bounded, documented cost that beats an io thread wedged forever.
- An exec probe that times out no longer guesses success: the status pipe rides the transport (non-blocking) and is read at reap time, when a failing exec's byte — written before its _exit — is present by construction, so an executable that blocks past the bound and then fails still delivers spawn_failed, never a masqueraded normal exit.

* Intern staged rejection keys for the model's life, bound failure-path reaps, and free the spawn lock from job starts

- Staged rejection keys are now INTERNED and instance-lived: the TS commit walker shares non-frame pointers into the committed model rather than copying them, so a reducer that stored a rejection's key held memory the old reclaim could free mid-run. Interning bounds the storage by the app's distinct key vocabulary (repeat rejections of one key cost nothing) and the buffers outlive every model that references them.
- The pty thread-start and wake-snapshot failure paths use the escalate-then-surrender reap: a child wedged inside an exec on a stalled mount cannot be waited out, and an unbounded waitpid there froze the UI loop instead of delivering spawn_failed.
- The job-spawn process start no longer holds the pty spawn lock — it blocks on its own exec-status pipe, so a stalled execve would have wedged the lock and frozen the UI the moment a pty needed it. A job fork landing in a pty flag gap joins the documented bounded residual (inherited copies die at the job child's exec; a delayed exec-pipe EOF resolves through the carried reap-time verdict, never a guess).

* Pin compositions to their starting editor, shorten the exec probe, and re-poll surrendered reaps

- An IME sequence now routes its continuations to the text entry it STARTED in: a per-view owner is pinned when a set_composition routes and released when its commit, cancel, or a direct insertion closes the sequence — so a focus move mid-composition no longer strands the starting editor's marked text or leaks the commit through the target-less fallback, the mirror of the target-less ownership rule.
- The exec probe's deadline drops to half a second: the unresolved verdict carries to the reap where a late failure still reports spawn_failed exactly, so the synchronous wait buys only the instant-failure nicety and no longer holds the loop for seconds; the PATH walk's access() probes remain the documented stalled-mount residual of the spawn-position verdict the replay contract requires.
- A surrendered reap now parks its pid on a process-wide list re-polled (WNOHANG) at later spawns and at teardown, so a child that dies after its device or mount recovers is reaped then instead of zombieing for the process's life.

* Decide the text claim before app rebuilds, swallow orphaned compositions, and state the Darwin pipe residual

- The target-less committed-text claim is decided against the tree the input actually routed through, BEFORE the app dispatches that may rebuild it: a Space that pressed a focused button whose command removes that button still counts as claimed, so one physical keystroke can never double into a command and a literal space.
- A widget-owned composition whose editor vanished mid-sequence resolves nowhere: its commit or cancel is swallowed (the newly focused editor never saw the composition, and the target-less consumer never composed it) and the stale pin clears, so a fresh composition reopens cleanly at the focused editor.
- The Darwin pipe-then-fcntl window is stated fully at its site: pty forks are locked out, and the residual — a job or embedder fork landing inside it — is bounded on every axis (the copy dies at that child's exec, a wedged exec costs at most the half-second probe with the carried verdict keeping the outcome correct, a held wake pipe merely outlives its session's close until that exec).

* Route converted commits to the composing editor, latch orphaned sequences whole, and keep EINTR'd surrendered pids

- Every host encodes a converted commit (the composed result differs from the marked text) as cancel-then-text_input, so a cancel now holds the owner pin one event and arms a one-shot grace: the trailing text_input lands in the editor that composed it, never the newly focused one — while a plain cancel's own key_down disarms the grace before ordinary typing could inherit it.
- An orphaned sequence swallows EVERY continuation: the pin holds through preedit updates (the sequence is still open, and the focused editor must not inherit them) and releases only at its commit or cancel, with an orphaned cancel arming the swallow grace so the converted commit's trailing text vanishes with the sequence instead of typing into a stranger.
- reapSurrendered keeps a pid whose non-blocking poll was interrupted, rather than abandoning a live child to zombie unreaped; only a reap or ECHILD settles the entry.

* Give target-less compositions the converted-commit grace too

- A target-less composition's cancel now arms the same one-shot grace its widget twin got: the hosts encode a converted commit as cancel-then-text_input, so the trailing text still belongs to the surface that composed it — delivered target-less on that surface, never inserted into whichever text widget took focus mid-sequence. A plain cancel's own key_down disarms the grace before ordinary typing could inherit it.

* Retry the late-failure read, orphan-check the commit grace, and die with the view

- lateExecFailure retries an interrupted read instead of mistaking EINTR for success: the failure byte may be sitting right there, and reading past the signal is what keeps a delayed exec failure reporting spawn_failed.
- The route-to-owner grace re-checks its owner at consumption: the cancel's own app dispatch can rebuild the tree away from the composing editor, and a dead owner converts the grace to a swallow — the converted commit's text resolves nowhere, never in whichever editor holds focus by then.
- A target-less composition dies with its view: removing a surface clears any sequence it owned, so a later view reusing the same window and label never mistakes its first composition for a stale continuation that would bypass its own focused editor.

* Disarm IME graces on blur and view removal, and re-anchor selection across a resize

- A view losing focus disarms its cancel-to-commit grace (and the target-less twin) and releases the owner pin: hosts emit a standalone cancel when a composing view blurs, and after refocus an IM-consumed keystroke's text_input arrives before its key_down echo — a stale grace would route that fresh commit to the old editor or swallow it.
- Removing a view clears the target-less grace even when the preedit length is already zero (cancellation zeroes it before arming), so a converted commit's trailing text can never leak target-less into a surface recreated under the same label.
- Resizing the terminal re-anchors an armed selection at the clamped caret: reflow moves every cell, so coordinates into the old grid are dropped rather than copied, and Shift+Arrow keeps operating inside the new grid.

* Share the blur-side IME hygiene across every focus path, fix the backspace key equivalent, and range-gate exit records

- Every focus-mutation entry point — the pointer-driven focus move, the programmatic focusView, and the window-level clearFocusedView blur — now routes through one shared blur helper that disarms the cancel grace and releases its owner pin, closing the lifecycle class structurally instead of per path; audited all view-focus writers to exactly these three.
- The macOS "backspace" menu key equivalent maps to 0x7f, the byte the physical Delete/backspace key actually emits (and the key-event normalizer maps back to "backspace") — the nominal BS control 0x08 never fired; every other named key in the table was audited against the normalizer and matches.
- Pty exit records are range-gated to what waitpid's status word can produce — codes 0..255 plus the documented -1 externally-reaped sentinel, signals 1..127 — at both the replay damage gate and the feed boundary, refused as damaged rather than replayed into an event no live run can emit; pinned with hand-damaged records on both axes and both directions.

* Honor the requested TERM, AltGr text, IME resolutions past claims, flush-first admission, and a clipped grid

- The pty environment now replaces an inherited TERM with the spawn's requested value (the spawn declared what terminal the child is attached to; a stale TERM=dumb from the host would misdeclare it), pinned live; and the restart resets the previous session's copy feedback.
- AltGr input survives both layers: the text-chord gate exempts Ctrl+Alt together (hosts represent AltGr that way, so a text event carrying both is composed text, not a shortcut), and the terminal's key mapper stops encoding Ctrl+Alt printables as chords for the same reason — the composed character rides the text channel alone. An IME resolution (an owned empty commit, or a converted commit's grace-ridden text) also bypasses the structural-claim gate: it ends a sequence the focused widget never participated in, so a button's Space claim cannot eat it.
- Outbound admission flushes before refusing (stale occupancy must not drop a keystroke the drain would have made room for), with a fake full-FIFO test seam standing in for a child that stopped reading; and the grid paints under a clip to its frame with an exact bottom-row guard, so the one stale pre-resize frame degrades to a cropped grid instead of painting over the status bar and past the right edge.

* Deadline the reap graces, carry claims across split events, and finish the exit accounting

- reapEnding's grace windows are monotonic deadlines: usleep returns early on EINTR, so iteration counting let a signal-heavy embedder collapse the 500 ms hangup grace into an almost-immediate SIGKILL and shrink the surrender window; the surrendered list is also re-polled at every session end, so a recovered child is reaped at the next pty activity of any kind.
- The committed-text claim carries across the event split: hosts that deliver a claimed key_down and its committed character as separate events (unlike the key-with-text shape) would recompute the claim against a tree the activation already rebuilt — a one-shot per-view carry keeps one physical keystroke from doubling into a command and a literal character. View blur now clears the composition owner unconditionally too: an active sequence's pin surviving a blur would redirect post-refocus typing to the stale editor.
- The example's AltGr exemption narrows to the host that actually represents AltGr as Ctrl+Alt — elsewhere that combination is a genuine chord that must encode — and session end is accounted honestly: cleared outbound bytes count as dropped, retained replies drop with the dead key instead of retrying against it every frame, transport write refusals reach the status tally, and a signaled or cancelled end says so instead of rendering as exited (-1).

* Never strand a pre-bind pty exit, and die the claim carry at blur

- A failed bind-site snapshot publication now stages a live pty's cancelled terminal from the loop past the wind-down deadline, so the exit delivers instead of waiting on a wake that can never fire
- The split-event claim carry clears at blur with the IME grace: a claimed activation that moves focus no longer swallows the refocused surface's first commit
- A restarted shell starts its refused-write tally at zero instead of inheriting the dead session's drops

* Give the input method its keys, and make the grid preflight measure what paints

- A target-less composition now owns its surface's key_downs (candidate navigation, the trailing resolver key on hosts that run the IM filter first), so confirming a candidate with Enter never also sends CR; the buffered preedit and its graces die at the surface's blur
- The split-event claim carry is keyed to the armed activation key's own literal, so a different key's committed text arriving next flows instead of feeding a stale latch
- The grid's row preflight now counts only bytes painting emits (invisible cells suppress, clusters cap at the run scratch), a session exit counts retained reply bytes as loss, and each fix is pinned in both tiers

* Free pty staging only on the io thread's own completion proof

- Retirement defers the staging/outbound frees to the thread's io_done publish (same critical section as its staged exit) and leaks them past an abandon, so a detached thread can never observe freed blocks
- The outbound flush re-checks open/generation after its unlocked write before touching the block or folding drop counts, closing the one ungated deref
- A superseded thread (slot reused after an abandon) goes silent at its next loop head and never publishes reaping/io_done into the successor; pinned with a deadline-miss abandon carrying an in-flight write and a free-counting seam

* Suppress IM-consumed composition keys at the GTK source, and bound the grid by what the renderer can hold

- Replace the one-shot resolved-key grace with host-side suppression: GTK surfaces no key_down for a key its input method consumed while composing, so a mouse-committed candidate never costs the next genuine Enter or arrow
- A styled wide character's spacer tail extends the primary cell's background run, covering both cells for SGR backgrounds and inverse video
- Painting stops row-atomically before crossing a distinct-code-point budget (the glyph-atlas proxy), and the run scratch grows to the full text store so any cluster the emulator holds paints whole; each fix pinned

* Let compositions own their keys before widget routing, and put real cells in the terminal's semantic surface

- A live target-less composition suppresses unchorded key routing ahead of every widget pass (dismissal, focus moves, activation), so a confirming Enter never presses a freshly focused button; chorded shortcuts stay live
- hostRequest's inline pre-flight now rejects keys held by live or staged ptys, matching the shared keyed-effect namespace every other issuer enforces
- The grid's accessibility label is the viewport text, carrying real cell state into the session fingerprint (equal byte counters with different screens no longer verify), and a failed selection re-pin clears the emulator selection instead of leaving a stale copyable range

* Settle every fed family, refuse fed overflow loudly, and keep the semantic screen honest

- The end-of-journal settle now reports ANY fed result left undelivered (every family plus journaled env records), and a fed line against a full queue answers EffectQueueFull for the replay pump's drain-and-retry instead of silently converting a recorded delivery into a drop
- GTK swallows a suppressed composition key's release too (no orphan key_up), and Windows lets AltGr-composed WM_CHAR text through the chord gate it raises Ctrl+Alt for
- The terminal's semantic screen text is heap-exact (never truncated or cut mid-scalar), refreshes when a scroll moves the viewport, clears to unknown on a failed render instead of going stale, and a failed selection serialization surfaces as a failed copy with the selection kept

* Close the key-suppression edges and keep selection, scrollback, and copy coherent

- GTK dedupes suppressed composition keys under autorepeat and clears them at focus loss; Windows discards the WM_CHAR a dispatched shortcut already consumed (AltGr chords type only when no accelerator matched)
- The surrendered-reap table evicts round-robin on overflow, so every wedged child keeps a retry seat instead of all forgetting behind slot zero
- Scrollback chords pause while a keyboard selection is armed (viewport-relative caret vs absolute range), and a copy over a vanished emulator range reports failure instead of quietly keeping stale clipboard content

* Track suppressed keys by keycode, disarm graces the suppression starved, and confirm before clearing

- GTK tracks composition-suppressed keys by physical keycode (a lifted Shift cannot rename the release) and a plain cancel whose resolving key_down is suppressed emits a synthetic duplicate cancel, so the runtime's cancel-to-commit grace disarms instead of eating the next ordinary commit
- The Windows shortcut latch holds across the keystroke's whole translated burst (ligature layouts post several WM_CHARs), disarmed by the message sequence at the next key transition
- A terminal selection now anchors at the live cursor (never the last painted snapshot) and survives until the clipboard write CONFIRMS - a failed write keeps it standing for the retry the status promises

* Paint the terminal where the user can see it

- The grid region spacer is a stack, not a panel: the panel's surface chrome (fill, border, shadow, rounded corners) painted OVER the chrome-prefix grid, blanking the whole terminal
- Grid text anchors its BASELINE (origin is not the glyph top), so rows land in their cells instead of one line high with row zero swallowed by the clip
- The session-state indicator is a quiet muted label instead of a pill that read as a half-painted toggle, and a painted-output oracle now renders the retained frame to pixels headless and asserts the prompt's ink and the caret's cell

* Build the terminal example on hosts with only the command-line tools

- Disable ghostty's macOS app and xcframework artifacts in the dependency options: only the vt module is consumed, and their configure step resolves the iOS libc, which aborts without a full Xcode install

* Fall back to FIONBIO when Darwin's pty parent end rejects F_SETFL

- Some macOS releases return ENOTTY from fcntl(F_SETFL, O_NONBLOCK) on the posix_openpt fd while the FIONBIO ioctl succeeds; the shared setNonblock helper tries fcntl first and falls back, so a spawn no longer fails whole on those kernels

* Set the pty parent non-blocking only after the replica opens

- Some Darwin kernels answer ENOTTY to both fcntl(F_SETFL) and FIONBIO on a pty parent whose replica has never been opened, and accept the identical call once it has; the spawn now orders the non-blocking step after the child end opens

* Scope the AltGr chord exemption to Windows and keep an armed selection on its text

- Ctrl+Alt is a genuine command chord on Linux and macOS (AltGr rides its own level-3 shift there), so the text gate exempts the pair on Windows alone - a chorded key mid-composition now reaches the app instead of being swallowed
- Output that scrolls the live screen rebases the keyboard selection from the emulator's absolute pins: the caret follows the selected text, and a range that leaves the viewport clears selection mode instead of desynchronizing copy from the caret

* Arm the cancel-to-commit grace only while a composition owner is pinned

- A duplicate cancel (the hosts' synthetic disarm for a consumed cancelling key) arrives after the grace probe already released the owner; re-arming ownerless converted the next ordinary character into a dead-owner swallow, so the arm now requires a live owner

* Honor negotiated kitty modes for committed text and key releases, and admit one copy at a time

- Committed single-scalar text routes through the emulator's key encoder: byte-identical raw text under legacy modes, CSI-u under a TUI's report-all; multi-scalar IME commits stay raw per the protocol
- Key releases reach the encoder through an opt-in on_key phase (key_release_events), emitting kitty release events when negotiated and nothing under legacy; repeats stay presses, the one event type hosts do not distinguish
- A copy while the clipboard write is in flight is a no-op, so a duplicate-key rejection can never overwrite the first copy's success; the non-Latin Ctrl-chord gap is recorded as an accepted tail pending base-layout key data in the platform vocabulary

* Wheel scrollback, geometric box drawing, and a window that is just the terminal

- Trackpad and wheel scrolls over the grid ride a new on_wheel app channel (the pinch channel's sibling) into whole-row scrollback with fractional accumulation; inert while a selection is armed
- U+2500-259F render as geometry at exact cell bounds - lines, tees, crosses, doubles, quarter-arc rounded corners, diagonals, blocks, shades, and quadrants - with identical-piece runs merged into single bars, so borders join seamlessly where font glyphs showed seams
- The window is a standard titlebar plus the grid: header and status bar removed, size readout gone, keyboard hints moved to the README

* One seamless surface: hidden-inset chrome with the terminal running under the traffic lights

- The window drops its title and titlebar strip (hidden_inset): the theme background fills edge-to-edge including the titlebar band, the grid's text starts below the reported chrome inset, and only the traffic lights float over the terminal

* Shift the video tests' journal byte offsets past the appended pty trailer

- The pty record fields append 33 bytes after the video fields in every effect payload, so the hand-patching damage helpers now offset from the pty trailer

* Stitch the pty conformance fixtures after the video fixtures

- Two fixture-list merge points kept both families' cases in sequence; the video entries close before the pty entries begin
2026-07-24 07:57:38 -05:00

50 lines
1.5 KiB
Plaintext

.DS_Store
.zig-cache/
zig-out/
# Project-local package store for examples with third-party Zig
# dependencies (zig fetches into it beside the build).
zig-pkg/
# CLI-generated build graph for zero-config apps (the examples here) — the
# same entry `native init` writes into a new app's .gitignore.
.native/
# Native binaries in the @native-sdk/cli npm packages (built by CI or locally)
packages/native-sdk/bin/native-sdk-*
packages/native-sdk/npm/*/bin/
# SDK payload mirrored into the npm package at pack time (copy-framework.js)
packages/native-sdk/src/
packages/native-sdk/assets/
packages/native-sdk/skills/
packages/native-sdk/skill-data/
packages/native-sdk/build/
packages/native-sdk/build.zig
packages/native-sdk/build.zig.zon
packages/native-sdk/app.zon
packages/native-sdk/third_party/
packages/native-sdk/packages/
packages/native-sdk/LICENSE
# npm pack output
packages/native-sdk/*.tgz
packages/native-sdk/npm/*/*.tgz
# Downloaded/prepared CEF runtimes are large local artifacts.
third_party/cef/macos/
third_party/cef/windows/
third_party/cef/linux/
# Compiled static libraries
*.a
# TypeScript build info (generated)
docs/tsconfig.tsbuildinfo
.claude/
# Dev-tool dependency trees (installed per package, never committed)
packages/core/node_modules/
# The CLI-materialized editor copy of @native-sdk/core inside the TS example
# apps (node_modules is editor surface, never source — the same entry the
# TS scaffold writes into a new app's .gitignore)
examples/soundboard-ts/node_modules/
examples/system-monitor-ts/node_modules/