794a3433
docs: the native client plan is rebased onto cells on the wire
a73x 2026-09-04 18:50
Commit message
docs/superpowers/plans/2026-09-04-native-client.md
| Old | New | ||
|---|---|---|---|
| @@ -12,7 +12,9 @@ | |||
| 12 | 12 | ||
| 13 | ## Global Constraints | 13 | ## Global Constraints |
| 14 | 14 | ||
| 15 | - **Prerequisite:** the cells-on-the-wire branch must be on main first. Every type below (`term.grid.Grid`, `Row`, `Cell`, `proto.CellStyle`, `proto.Wide`, `Replica.apply` returning `.painted | .resync`, `Replica.attachArgs`) comes from it. Do not start Task 4 or later on a tree without `src/engine/grid.zig`. | 15 | - **Prerequisite: MET.** Cells-on-the-wire landed on main 2026-09-04 (`f07a56c8` through `406628a2`) and this plan was rebased onto it. Every type it names is present as written: `term.grid.Grid` with `cursor`/`lines`/`row(y)`, `grid.Row` with `cells` and `text: ArrayListUnmanaged(u8)` read through `textOf`, `grid.Cell{style,wide,text_off,text_len}`, `proto.CellStyle`, `proto.Wide`, `Replica.apply` returning the enum `.painted | .resync`, `Replica.attachArgs`, `Replica.state_since_attach`. |
| 16 | - **`apply` has THREE outcomes, not two.** Besides the enum it can fail two ways, and they are not the same failure: `error.BadPayload` left the grid untouched (skip the frame), `error.SnapshotAborted` blanked the grid while `last_seq` claims to be current (end the session). Task 7's `onFrame` carries both arms. `interact.Core.frame` is the wall's copy of the same rule and is the reference if anything reads ambiguously. | ||
| 17 | - **`term` no longer links ghostty-vt.** The merge split the engine out: `term` is the wire, the grid and the replica, and the separate `engine` module is the emulator that imports it. So the `native` row importing `term` pulls in no emulator, and nothing under `src/gui/` should ever import `engine`. | ||
| 16 | - Toolchain is `deps/zig/zig` (0.15.2). System zig will not build this tree. Every command below spells `deps/zig/zig` or `make`. A fresh worktree has no `deps/`: link the main checkout's before anything else (`ln -s ../../../../deps/zig deps/zig; ln -s ../../../../deps/quic deps/quic` from a worktree under `.claude/worktrees/`), and never share `.zig-cache` between worktrees. | 18 | - Toolchain is `deps/zig/zig` (0.15.2). System zig will not build this tree. Every command below spells `deps/zig/zig` or `make`. A fresh worktree has no `deps/`: link the main checkout's before anything else (`ln -s ../../../../deps/zig deps/zig; ln -s ../../../../deps/quic deps/quic` from a worktree under `.claude/worktrees/`), and never share `.zig-cache` between worktrees. |
| 17 | - `make build`, `make check`, `make test` and `make ci` never build or link `src/gui/`. `zig build native`, `zig build native-test` and `zig build native-e2e` are the only steps that touch it; `make native` and `make native-e2e` are their only callers. | 19 | - `make build`, `make check`, `make test` and `make ci` never build or link `src/gui/`. `zig build native`, `zig build native-test` and `zig build native-e2e` are the only steps that touch it; `make native` and `make native-e2e` are their only callers. |
| 18 | - `src/gui/` imports `client` and `term` and nothing else of ours. `src/cli/muxg.zig` (the entry) may also import `cliflags` and `sockpath`. | 20 | - `src/gui/` imports `client` and `term` and nothing else of ours. `src/cli/muxg.zig` (the entry) may also import `cliflags` and `sockpath`. |
| @@ -1007,7 +1009,7 @@ pub fn cursorInstance(x: u16, y: u16, ctx: Ctx) Instance { | |||
| 1007 | - [ ] **Step 4: Run the tests** | 1009 | - [ ] **Step 4: Run the tests** |
| 1008 | 1010 | ||
| 1009 | Run: `deps/zig/zig build native-test 2>&1 | tail -8; echo rc=$?` | 1011 | Run: `deps/zig/zig build native-test 2>&1 | tail -8; echo rc=$?` |
| 1010 | Expected: rc 0. If `grid.Row.text` is not an `ArrayListUnmanaged(u8)` on the landed branch, adapt `rowOf` to the landed type — the production code reads it only through `row.textOf`. | 1012 | Expected: rc 0. `grid.Row.text` is an `ArrayListUnmanaged(u8)` on the landed tree and `rowOf` builds one directly, which is a test-only liberty: the production code reads a row's bytes only through `row.textOf`, so a later change to how a row stores its text cannot reach `quads.zig`. |
| 1011 | 1013 | ||
| 1012 | - [ ] **Step 5: Commit** | 1014 | - [ ] **Step 5: Commit** |
| 1013 | 1015 | ||
| @@ -1835,7 +1837,27 @@ pub const Pump = struct { | |||
| 1835 | var t = try std.time.Timer.start(); | 1837 | var t = try std.time.Timer.start(); |
| 1836 | self.mu.lock(); | 1838 | self.mu.lock(); |
| 1837 | defer self.mu.unlock(); | 1839 | defer self.mu.unlock(); |
| 1838 | const applied = self.replica.apply(frame.type, frame.payload) catch .resync; | 1840 | // The two snapshot failures are DIFFERENT, and telling them |
| 1841 | // apart is the whole point (see `Replica.apply`'s doc comment | ||
| 1842 | // and `interact.Core.frame`, which is the wall's copy of this | ||
| 1843 | // rule). `error.BadPayload` left the grid untouched, so the | ||
| 1844 | // frame is skipped and we paint what we already held. | ||
| 1845 | // `error.SnapshotAborted` did not: the grid is blank while | ||
| 1846 | // `last_seq` claims to be current, so no later delta can fix | ||
| 1847 | // it and a resync's snapshot is what already failed. That one | ||
| 1848 | // ENDS the session rather than paint a replica holding | ||
| 1849 | // nothing. | ||
| 1850 | const applied = self.replica.apply(frame.type, frame.payload) catch |err| switch (err) { | ||
| 1851 | error.BadPayload => { | ||
| 1852 | self.last_apply_us = @intCast(@min(t.read() / std.time.ns_per_us, std.math.maxInt(u32))); | ||
| 1853 | return false; | ||
| 1854 | }, | ||
| 1855 | error.SnapshotAborted => { | ||
| 1856 | self.setPhase(.exited, "the daemon's snapshot could not be decoded"); | ||
| 1857 | return error.SessionOver; | ||
| 1858 | }, | ||
| 1859 | else => .resync, | ||
| 1860 | }; | ||
| 1839 | self.last_apply_us = @intCast(@min(t.read() / std.time.ns_per_us, std.math.maxInt(u32))); | 1861 | self.last_apply_us = @intCast(@min(t.read() / std.time.ns_per_us, std.math.maxInt(u32))); |
| 1840 | switch (applied) { | 1862 | switch (applied) { |
| 1841 | .painted => { | 1863 | .painted => { |
| @@ -1958,7 +1980,7 @@ pub const Pump = struct { | |||
| 1958 | }; | 1980 | }; |
| 1959 | ``` | 1981 | ``` |
| 1960 | 1982 | ||
| 1961 | If `client.openFailure` returns a struct whose text field is not named `message`, read `OpenFailure` at `src/client/client.zig` (around line 914) and use its field. If `Transport.close` followed by re-assignment is not how `wall_pump.redial` does it, mirror `redial` (around `src/tui/wall_pump.zig:340-370`). | 1983 | If `client.openFailure` returns a struct whose text field is not named `message`, read the `OpenFailure` declaration in `src/client/client.zig` and use its field. If `Transport.close` followed by re-assignment is not how the wall reconnects, mirror the `redial` function in `src/tui/wall_pump.zig`. |
| 1962 | 1984 | ||
| 1963 | - [ ] **Step 4: Wire the re-export and run the tests** | 1985 | - [ ] **Step 4: Wire the re-export and run the tests** |
| 1964 | 1986 | ||
| @@ -2719,7 +2741,7 @@ chmod +x test/native.sh | |||
| 2719 | deps/zig/zig build native -Doptimize=ReleaseSafe 2>&1 | tail -3 | 2741 | deps/zig/zig build native -Doptimize=ReleaseSafe 2>&1 | tail -3 |
| 2720 | MUXG_OPT=ReleaseSafe test/native.sh zig-out/bin/mux zig-out/bin/muxg; echo rc=$? | 2742 | MUXG_OPT=ReleaseSafe test/native.sh zig-out/bin/mux zig-out/bin/muxg; echo rc=$? |
| 2721 | ``` | 2743 | ``` |
| 2722 | Expected: six `e2e OK:` lines, `native OK (6 checkpoints)`, rc 0. On the first run, expect to adjust: the `frames_now` grep pattern must match `bench.Ring.report`'s first line exactly (`=== muxg frame timing (N frames) ===`); the status JSON field order is `"cols":N,"rows":N` per `src/cli/muxa.zig:1095`. | 2744 | Expected: six `e2e OK:` lines, `native OK (6 checkpoints)`, rc 0. On the first run, expect to adjust: the `frames_now` grep pattern must match `bench.Ring.report`'s first line exactly (`=== muxg frame timing (N frames) ===`); the status JSON field order is `"cols":N,"rows":N`, which `printStatus` in `src/cli/muxa.zig` spells. |
| 2723 | 2745 | ||
| 2724 | If step 1's `wait_until` fails and `$GLOG` says `SDL_GL_CreateContext`, the offscreen driver has no GL here: run with `MUXG_VIDEODRIVER=x11` under `xvfb-run -a` and record that in the spec's Testing section. | 2746 | If step 1's `wait_until` fails and `$GLOG` says `SDL_GL_CreateContext`, the offscreen driver has no GL here: run with `MUXG_VIDEODRIVER=x11` under `xvfb-run -a` and record that in the spec's Testing section. |
| 2725 | 2747 | ||