Bug: themes don't apply to the terminal in production #75

Closed
opened 2026-10-07 09:46:44 +00:00 by kayg · 40 comments
Owner

Owner (2026-10-07): themes do not apply to the terminal in production; it looks extremely weird. The core screen calls client.setTheme(terminalTheme(...)) (core-screen.ts ~216/529, terminal.ts setTheme ~389 with a bounded-row repaint), but ghostty-web resolves default colours into its WASM cells at open() and largely ignores later theme changes (the mockup hit the same thing: 'changing themes is cursed'; mock-core main.ts rebuilt the terminal, with the product plan to ask the shim for a Snapshot). Fix so that every theme (and the initial theme at load) applies fully to the terminal: background, foreground, cursor, selection and the 16 ANSI colours, including already-printed text. Verify with screenshots of all themes.

Owner (2026-10-07): themes do not apply to the terminal in production; it looks extremely weird. The core screen calls client.setTheme(terminalTheme(...)) (core-screen.ts ~216/529, terminal.ts setTheme ~389 with a bounded-row repaint), but ghostty-web resolves default colours into its WASM cells at open() and largely ignores later theme changes (the mockup hit the same thing: 'changing themes is cursed'; mock-core main.ts rebuilt the terminal, with the product plan to ask the shim for a Snapshot). Fix so that every theme (and the initial theme at load) applies fully to the terminal: background, foreground, cursor, selection and the 16 ANSI colours, including already-printed text. Verify with screenshots of all themes.
Author
Owner

Starting in worktree branch fix-term-theme from dev at 760490ce4e. Issue history reviewed: no prior comments.

Starting in worktree branch fix-term-theme from dev at 760490ce4e134b4a7434530b89a8e2fdd5339044. Issue history reviewed: no prior comments.
Author
Owner

Finding: production Playwright at TRACER_TEST_PORT=4681 fails before first paint. The test delays WASM init, selects Ink during startup, then finds Paper pixels (#f6f6f4 bg, #1d1d1f fg and Paper ANSI palette) where Ink expects #0d0e10, #ececee and its 16 colors. The pinned 5f0c6b7 build constructs WASM colors in buildWasmConfig at open/reset; setTheme after open warns it is not fully supported, and existing cells expose resolved RGB only (no palette indices). CanvasRenderer.setTheme changes its palette array, while its text painter consumes WASM RGB. The WASM artifact includes allocator-required warnings for OSC 10/11/104; there is no browser runtime palette-update API. Shim snapshot serialization preserves default/palette overrides and indexed styles (native/snapshot.zig restoreColors/effectiveStyle), so the re-theme path must recreate the WASM terminal and replay session state.

Finding: production Playwright at TRACER_TEST_PORT=4681 fails before first paint. The test delays WASM init, selects Ink during startup, then finds Paper pixels (#f6f6f4 bg, #1d1d1f fg and Paper ANSI palette) where Ink expects #0d0e10, #ececee and its 16 colors. The pinned 5f0c6b7 build constructs WASM colors in buildWasmConfig at open/reset; setTheme after open warns it is not fully supported, and existing cells expose resolved RGB only (no palette indices). CanvasRenderer.setTheme changes its palette array, while its text painter consumes WASM RGB. The WASM artifact includes allocator-required warnings for OSC 10/11/104; there is no browser runtime palette-update API. Shim snapshot serialization preserves default/palette overrides and indexed styles (native/snapshot.zig restoreColors/effectiveStyle), so the re-theme path must recreate the WASM terminal and replay session state.
Author
Owner

Production-build terminal screenshots from the theme regression test: 10 palettes × 2 viewports (20 PNGs, 1,529,938 bytes), generated on the fix-term-theme worktree based on 760490ce4e134b4a7434530b89a8e2fdd5339044.

Theme 1440×900 393×852
Paper PNG PNG
Ink PNG PNG
Catppuccin Latte PNG PNG
Catppuccin Mocha PNG PNG
Rosé Pine Dawn PNG PNG
Rosé Pine PNG PNG
Nord PNG PNG
Tokyo Night PNG PNG
Gruvbox PNG PNG
Dracula PNG PNG
Production-build terminal screenshots from the theme regression test: 10 palettes × 2 viewports (20 PNGs, 1,529,938 bytes), generated on the `fix-term-theme` worktree based on `760490ce4e134b4a7434530b89a8e2fdd5339044`. | Theme | 1440×900 | 393×852 | |---|---|---| | Paper | [PNG](https://git.kayg.org/attachments/725f17a7-80eb-43a5-a985-c89d69a3df87) | [PNG](https://git.kayg.org/attachments/d5e88e07-ef60-49ff-a46c-c49209ebdc89) | | Ink | [PNG](https://git.kayg.org/attachments/e4dd8d8d-c2d7-46c0-a4a0-7517fed7043c) | [PNG](https://git.kayg.org/attachments/f19ed3ec-1039-4bc6-a36e-e3839ef65870) | | Catppuccin Latte | [PNG](https://git.kayg.org/attachments/8d3d552b-f073-4262-acb9-2713e52e1a31) | [PNG](https://git.kayg.org/attachments/2b7ace6c-e827-4d7f-81e8-467fefb5ec0d) | | Catppuccin Mocha | [PNG](https://git.kayg.org/attachments/4d50f4e6-6ddd-4a76-8541-6f52eee917a7) | [PNG](https://git.kayg.org/attachments/41229f18-f67a-403b-a306-618a11ec59a1) | | Rosé Pine Dawn | [PNG](https://git.kayg.org/attachments/2251eff8-15db-4fd4-8781-bb63b9e8df4e) | [PNG](https://git.kayg.org/attachments/e7183566-c708-4bc1-bd34-c692959751bb) | | Rosé Pine | [PNG](https://git.kayg.org/attachments/ddbfb3e2-64cd-4624-a4b5-8687cb308b04) | [PNG](https://git.kayg.org/attachments/dd721610-9aad-4f2d-9984-9687cb308b04) | | Nord | [PNG](https://git.kayg.org/attachments/34d4401b-13dd-4239-a3e2-67bdda3c6bf4) | [PNG](https://git.kayg.org/attachments/bc05cc39-0803-4b80-9474-bdbea8afc72b) | | Tokyo Night | [PNG](https://git.kayg.org/attachments/f6a15764-62f2-46c4-b58d-66bc172fdb32) | [PNG](https://git.kayg.org/attachments/66519f10-a9e4-479e-8583-d025b9cc904c) | | Gruvbox | [PNG](https://git.kayg.org/attachments/48507f4f-9e39-4592-9291-df292d22be69) | [PNG](https://git.kayg.org/attachments/be59502b-cd1f-49a5-bdd1-7d3b58029a0b) | | Dracula | [PNG](https://git.kayg.org/attachments/63475039-5cd9-4315-aec9-a7074e4af1cc) | [PNG](https://git.kayg.org/attachments/08db7669-6efb-4e3b-95df-162f3d42b7a8) |
Author
Owner

Finding: isolated production motion audit passes with 0 theme long tasks and 0 idle rAF frames. The first full Playwright run measured S1 at p99 17.8 ms (500 keys). A later full-suite run under shared host load 20.49/17.82/12.91 (13:24 UTC; concurrent Playwright and Rust/Zig builds in other worktrees) measured p99 110.7 ms and the auth shader produced 17 frames vs the 20-frame minimum. Theme and Snapshot tests passed in that run. I will rerun the probe when shared load clears; current evidence indicates host contention, not a steady-state theme-path regression.

Finding: isolated production motion audit passes with 0 theme long tasks and 0 idle rAF frames. The first full Playwright run measured S1 at p99 17.8 ms (500 keys). A later full-suite run under shared host load 20.49/17.82/12.91 (13:24 UTC; concurrent Playwright and Rust/Zig builds in other worktrees) measured p99 110.7 ms and the auth shader produced 17 frames vs the 20-frame minimum. Theme and Snapshot tests passed in that run. I will rerun the probe when shared load clears; current evidence indicates host contention, not a steady-state theme-path regression.
Author
Owner

Updated production-build terminal screenshots from the final theme implementation: 10 palettes × 2 viewports (20 PNGs, 1,530,405 bytes).

Theme 1440×900 393×852
Paper PNG PNG
Ink PNG PNG
Catppuccin Latte PNG PNG
Catppuccin Mocha PNG PNG
Rosé Pine Dawn PNG PNG
Rosé Pine PNG PNG
Nord PNG PNG
Tokyo Night PNG PNG
Gruvbox PNG PNG
Dracula PNG PNG
Updated production-build terminal screenshots from the final theme implementation: 10 palettes × 2 viewports (20 PNGs, 1,530,405 bytes). | Theme | 1440×900 | 393×852 | |---|---|---| | Paper | [PNG](https://git.kayg.org/attachments/063b9eeb-daf2-406f-a07c-2304c787a391) | [PNG](https://git.kayg.org/attachments/662a5cf3-dbc6-49ca-ba31-633e2c1c042c) | | Ink | [PNG](https://git.kayg.org/attachments/89a7a5da-ea5c-480d-838e-9d37dcd36d31) | [PNG](https://git.kayg.org/attachments/c6435ca8-72a5-463b-bd52-98b91e93ddcf) | | Catppuccin Latte | [PNG](https://git.kayg.org/attachments/317a913f-534a-46fc-ab2e-6fe931724d8d) | [PNG](https://git.kayg.org/attachments/dc914053-df58-4077-ae06-cd3fb7f9c56f) | | Catppuccin Mocha | [PNG](https://git.kayg.org/attachments/0a3c2baa-5b18-48fb-8e93-bfd2039c5a94) | [PNG](https://git.kayg.org/attachments/37f746ac-b8fb-4ac5-83fa-d7efda92172e) | | Rosé Pine Dawn | [PNG](https://git.kayg.org/attachments/ac358dd1-3e20-45bc-bd28-513dece6cf74) | [PNG](https://git.kayg.org/attachments/9f6b15dd-d9da-431c-b351-531695f74fc5) | | Rosé Pine | [PNG](https://git.kayg.org/attachments/4b2db93c-ab25-4f42-a4d6-00ba9a28c0fc) | [PNG](https://git.kayg.org/attachments/34b11831-00fd-46fc-8bf7-ff22120861b0) | | Nord | [PNG](https://git.kayg.org/attachments/6329e379-1458-41c7-84bd-11754c8d2373) | [PNG](https://git.kayg.org/attachments/ba4fac30-5338-47d0-a1bf-f14787a3c86c) | | Tokyo Night | [PNG](https://git.kayg.org/attachments/9f7d23c1-6c03-4693-a107-13d4db6c8d8f) | [PNG](https://git.kayg.org/attachments/cfbb601a-680b-4f23-8b12-20c152a92d00) | | Gruvbox | [PNG](https://git.kayg.org/attachments/48483c8d-3bb9-4604-af42-2901d828ef4d) | [PNG](https://git.kayg.org/attachments/8a32c04b-0873-4f82-8ee2-dd72df529159) | | Dracula | [PNG](https://git.kayg.org/attachments/38a90952-5100-442c-acd4-5cd1e54917a4) | [PNG](https://git.kayg.org/attachments/2cd75442-df22-465b-8e82-fce565c878d5) |
Author
Owner

Performance follow-up: current branch passed an isolated 500-key S1 probe with 500/500 echoes, p50 15.20 ms, p99 18.90 ms, resume 78.10 ms. At 13:29 UTC, load was 17.62/19.14/14.99. Under a later concurrent Zig/Rust build, base p99 was 121.3 ms and current p99 was 72.1 ms; both exceeded 20 ms, and the first base attempt timed out waiting for resume. The current isolated sample is below the 20 ms gate; loaded samples are not comparable perf-host evidence. Isolated motion audit passes with zero theme long tasks and zero idle rAF frames.

Performance follow-up: current branch passed an isolated 500-key S1 probe with 500/500 echoes, p50 15.20 ms, p99 18.90 ms, resume 78.10 ms. At 13:29 UTC, load was 17.62/19.14/14.99. Under a later concurrent Zig/Rust build, base p99 was 121.3 ms and current p99 was 72.1 ms; both exceeded 20 ms, and the first base attempt timed out waiting for resume. The current isolated sample is below the 20 ms gate; loaded samples are not comparable perf-host evidence. Isolated motion audit passes with zero theme long tasks and zero idle rAF frames.
Author
Owner

Step 1 committed as c4d54d5: theme changes now reset Ghostty with the selected palette, replay the session, and hold the painted canvas until replay paints. Focused production Playwright passed (2 tests); motion audit passed with zero theme long tasks and zero idle RAF frames; isolated 500-key probe painted 500/500 with p99 18.90 ms. The full-suite run remains load-contended and is recorded separately.

Step 1 committed as c4d54d5: theme changes now reset Ghostty with the selected palette, replay the session, and hold the painted canvas until replay paints. Focused production Playwright passed (2 tests); motion audit passed with zero theme long tasks and zero idle RAF frames; isolated 500-key probe painted 500/500 with p99 18.90 ms. The full-suite run remains load-contended and is recorded separately.
Author
Owner

Finding: the existing shim Snapshot is bounded to the active screen plus at most 256 trailing scrollback rows (crates/infraternal-shim/src/terminal.rs:195-217). Theme rebuild uses the same ring replay/Snapshot recovery path as reconnect; after ring wrap, scrollback older than that bound cannot be restored by the allowed terminal-client-only change. The test fixture verifies 80 history rows through Snapshot. Exact unbounded scrollback preservation would require expanding the current scope/protocol or retaining/serializing client buffer state.

Finding: the existing shim Snapshot is bounded to the active screen plus at most 256 trailing scrollback rows (crates/infraternal-shim/src/terminal.rs:195-217). Theme rebuild uses the same ring replay/Snapshot recovery path as reconnect; after ring wrap, scrollback older than that bound cannot be restored by the allowed terminal-client-only change. The test fixture verifies 80 history rows through Snapshot. Exact unbounded scrollback preservation would require expanding the current scope/protocol or retaining/serializing client buffer state.
Author
Owner

Per-commit fast gate attempt for c4d54d5: fmt, VT native fmt, clippy, cargo-deny, and static builds passed; test gate failed four timing-sensitive auth tests under load average 31.22/25.68/19.16: review3_backend_reply_has_a_deadline, review3_busy_expiry, review4_ten_second_open_preserves_authority, and clean_control_close_is_not_an_internal_error. 67 server tests passed, 6 ignored. Concurrent Rust/Zig compiles were active. The earlier full-tree fast run passed; I will record the final gate result after the test commit.

Per-commit fast gate attempt for c4d54d5: fmt, VT native fmt, clippy, cargo-deny, and static builds passed; test gate failed four timing-sensitive auth tests under load average 31.22/25.68/19.16: review3_backend_reply_has_a_deadline, review3_busy_expiry, review4_ten_second_open_preserves_authority, and clean_control_close_is_not_an_internal_error. 67 server tests passed, 6 ignored. Concurrent Rust/Zig compiles were active. The earlier full-tree fast run passed; I will record the final gate result after the test commit.
Author
Owner

Step 2 committed as 414025a: production Playwright checks default fg/bg and all 16 ANSI colors for every curated theme at 1440x900 and 393x852, with cursor, viewport, scrollback and delayed Snapshot swap assertions. The 20 screenshot attachments are on this issue (10 themes × 2 sizes, 1,530,405 bytes). Focused theme + Snapshot tests passed 2/2; full suite on port 4681 completed 34 passed, 2 failed: motion shader frame count 17/20 and S1 latency p99 110.7 ms under concurrent load. Isolated final S1 was 500/500, p99 18.90 ms; motion audit had zero theme long tasks and zero idle RAF frames.

Step 2 committed as 414025a: production Playwright checks default fg/bg and all 16 ANSI colors for every curated theme at 1440x900 and 393x852, with cursor, viewport, scrollback and delayed Snapshot swap assertions. The 20 screenshot attachments are on this issue (10 themes × 2 sizes, 1,530,405 bytes). Focused theme + Snapshot tests passed 2/2; full suite on port 4681 completed 34 passed, 2 failed: motion shader frame count 17/20 and S1 latency p99 110.7 ms under concurrent load. Isolated final S1 was 500/500, p99 18.90 ms; motion audit had zero theme long tasks and zero idle RAF frames.
Author
Owner

Finished on branch fix-term-theme at head 414025a3a5. Two atomic commits: c4d54d5 rebuilds Ghostty with a replayed theme; 414025a adds production pixel tests and the Snapshot fixture. The issue has 20 screenshots attached (10 themes × 2 viewports, 1,530,405 bytes). Focused theme/Snapshot tests passed 2/2. Full Playwright on TRACER_TEST_PORT=4681: 34 passed, 2 failed under concurrent machine load (shaderFrames 17/20 and S1 latency p99 110.7 ms). The isolated motion audit reported zero theme long tasks and zero idle RAF frames; isolated S1 painted 500/500 at p99 18.90 ms. bun run check passed.

The first full-tree fast-gate run passed. A repeat during concurrent Rust/Zig builds failed four timing-sensitive auth tests; its gate summary was:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| clippy                    | PASS         |
| test                      | FAIL (101)   |
| cargo-deny                | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+

Failures were review3_backend_reply_has_a_deadline, review3_busy_expiry, review4_ten_second_open_preserves_authority, and clean_control_close_is_not_an_internal_error; load average was 31.22/25.68/19.16. Earlier green output included 7 protocol tests, 34,390 expect() calls, and zero divergences.

Self-review covered theme rebuild races, Snapshot ordering, outstanding writes/credit, canvas hold, cursor/viewport restoration, and latency/idle paths. One existing fidelity limit remains: after ring wrap, the shim Snapshot carries at most 256 trailing scrollback rows; the fixture verifies 80. Exact restoration beyond the current protocol Snapshot bound would need broader server/client-state scope. No push, merge, or deploy.

Finished on branch fix-term-theme at head 414025a3a5ef70c3da3682605bd60bef2d6adc2f. Two atomic commits: c4d54d5 rebuilds Ghostty with a replayed theme; 414025a adds production pixel tests and the Snapshot fixture. The issue has 20 screenshots attached (10 themes × 2 viewports, 1,530,405 bytes). Focused theme/Snapshot tests passed 2/2. Full Playwright on TRACER_TEST_PORT=4681: 34 passed, 2 failed under concurrent machine load (shaderFrames 17/20 and S1 latency p99 110.7 ms). The isolated motion audit reported zero theme long tasks and zero idle RAF frames; isolated S1 painted 500/500 at p99 18.90 ms. bun run check passed. The first full-tree fast-gate run passed. A repeat during concurrent Rust/Zig builds failed four timing-sensitive auth tests; its gate summary was: ```text +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | clippy | PASS | | test | FAIL (101) | | cargo-deny | PASS | | static builds | PASS | +---------------------------+--------------+ ``` Failures were review3_backend_reply_has_a_deadline, review3_busy_expiry, review4_ten_second_open_preserves_authority, and clean_control_close_is_not_an_internal_error; load average was 31.22/25.68/19.16. Earlier green output included 7 protocol tests, 34,390 expect() calls, and zero divergences. Self-review covered theme rebuild races, Snapshot ordering, outstanding writes/credit, canvas hold, cursor/viewport restoration, and latency/idle paths. One existing fidelity limit remains: after ring wrap, the shim Snapshot carries at most 256 trailing scrollback rows; the fixture verifies 80. Exact restoration beyond the current protocol Snapshot bound would need broader server/client-state scope. No push, merge, or deploy.
Author
Owner

Orchestrator review of run 1 (c4d54d5, 414025a): rejected. Theme switching closes the WebSocket and replays the session, which costs about 1 s at 300 ms RTT and loses scrollback beyond the 256-row Snapshot bound on every switch. Run 2 asked to make the switch local, instant and lossless (OSC 4/10/11 written locally into the WASM terminal, or a renderer cache fix; patching the pinned build only as a last resort), with switch-time numbers and a full-scrollback assertion.

Orchestrator review of run 1 (c4d54d5, 414025a): rejected. Theme switching closes the WebSocket and replays the session, which costs about 1 s at 300 ms RTT and loses scrollback beyond the 256-row Snapshot bound on every switch. Run 2 asked to make the switch local, instant and lossless (OSC 4/10/11 written locally into the WASM terminal, or a renderer cache fix; patching the pinned build only as a last resort), with switch-time numbers and a full-scrollback assertion.
Author
Owner

Starting run 2 in worktree fix-term-theme, branch fix-term-theme, at base SHA 414025a3a5. Reviewed all #75 comments and the D109 renderer issue #31 history; the #75 API record exposes no parent/dependency relation.

Starting run 2 in worktree fix-term-theme, branch fix-term-theme, at base SHA 414025a3a5ef70c3da3682605bd60bef2d6adc2f. Reviewed all #75 comments and the D109 renderer issue #31 history; the #75 API record exposes no parent/dependency relation.
Author
Owner

Finding (correcting run 1): the pinned build matches D109 exactly (ghostty-web 5f0c6b7, Ghostty 5714ed0; vendor SHA256SUMS all verify). In pinned source, ghostty/src/terminal/style.zig:43-55 stores cell colors as .palette(u8) or .rgb, and :103-178 resolves palette/default colors against current terminal colors at read/render time; explicit RGB remains literal. ghostty/src/termio/stream_handler.zig:1131-1162 handles OSC 4 and 10/11/12 by mutating palette/default colors and marking palette dirty. The wrapper's getCell view is resolved RGB, which led run 1 to mistake the exposed representation for stored state. Candidate (a) applies: write OSC locally to the existing WASM terminal, update renderer theme, and force one viewport redraw. No pinned-source patch is needed.

Finding (correcting run 1): the pinned build matches D109 exactly (ghostty-web 5f0c6b7, Ghostty 5714ed0; vendor SHA256SUMS all verify). In pinned source, ghostty/src/terminal/style.zig:43-55 stores cell colors as .palette(u8) or .rgb, and :103-178 resolves palette/default colors against current terminal colors at read/render time; explicit RGB remains literal. ghostty/src/termio/stream_handler.zig:1131-1162 handles OSC 4 and 10/11/12 by mutating palette/default colors and marking palette dirty. The wrapper's getCell view is resolved RGB, which led run 1 to mistake the exposed representation for stored state. Candidate (a) applies: write OSC locally to the existing WASM terminal, update renderer theme, and force one viewport redraw. No pinned-source patch is needed.
Author
Owner

Finding / RED evidence: production Playwright at TRACER_TEST_PORT=4275 under CPUWeight=10000 passed all painted palette and truecolor checks and captured 20 screenshots, then failed the no-reconnect assertion. Across 21 theme actions the mock recorded socket opens 1→22, closes 0→21, active sockets stayed 1. The counter regression is in apps/tracer/test/mock-server.ts and test/terminal-theme.spec.ts; run artifacts are apps/tracer/test-results/terminal-theme-every-curat-3627b--and-its-existing-ANSI-text/attachments/ (20 PNGs).

Finding / RED evidence: production Playwright at TRACER_TEST_PORT=4275 under CPUWeight=10000 passed all painted palette and truecolor checks and captured 20 screenshots, then failed the no-reconnect assertion. Across 21 theme actions the mock recorded socket opens 1→22, closes 0→21, active sockets stayed 1. The counter regression is in apps/tracer/test/mock-server.ts and test/terminal-theme.spec.ts; run artifacts are apps/tracer/test-results/terminal-theme-every-curat-3627b--and-its-existing-ANSI-text/attachments/ (20 PNGs).
Author
Owner

Step complete: reverted run 1 reconnect/replay in 808e39708c. Fast gate ran under systemd-run --user --scope -p CPUWeight=10000 with CARGO_TARGET_DIR=/home/kayg/.cache/codex-scratch/fix-term-theme/target. Gate output (verbatim summary):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| clippy                    | PASS         |
| test                      | PASS         |
| cargo-deny                | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
target/x86_64-unknown-linux-musl/release/infraternal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped
PT_INTERP: absent; size: 16121168 bytes
target/aarch64-unknown-linux-musl/release/infraternal: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped
PT_INTERP: absent; size: 13539528 bytes
server subcommand: exit 2, not implemented
agent subcommand: exit 2, not implemented
advisories ok, bans ok, licenses ok, sources ok
stream differential: 134 streams, 11439 schedules, zero divergences
 7 pass
 0 fail
 34390 expect() calls
Ran 7 tests across 2 files. [2.79s]
Step complete: reverted run 1 reconnect/replay in 808e39708c6f1eb8fbeb6d0cb586bdd5165ec2a0. Fast gate ran under systemd-run --user --scope -p CPUWeight=10000 with CARGO_TARGET_DIR=/home/kayg/.cache/codex-scratch/fix-term-theme/target. Gate output (verbatim summary): ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | clippy | PASS | | test | PASS | | cargo-deny | PASS | | static builds | PASS | +---------------------------+--------------+ target/x86_64-unknown-linux-musl/release/infraternal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped PT_INTERP: absent; size: 16121168 bytes target/aarch64-unknown-linux-musl/release/infraternal: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped PT_INTERP: absent; size: 13539528 bytes server subcommand: exit 2, not implemented agent subcommand: exit 2, not implemented advisories ok, bans ok, licenses ok, sources ok stream differential: 134 streams, 11439 schedules, zero divergences 7 pass 0 fail 34390 expect() calls Ran 7 tests across 2 files. [2.79s] ```
Author
Owner

Finding: the pinned Ghostty scrollback setting is bytes, not rows (ghostty/src/terminal/Screen.zig:236 in D109 source). Production-browser fixture: 605,847 bytes, 170 columns, configured 2,000,000 bytes retained 530 rows; after drain receivedSequence=outputSequence=605847. Measured requirement is about 37.7 MB at this width for 10,000 rows, so I’m raising only the mock test cap and measuring the retained row count before the palette assertions.

Finding: the pinned Ghostty scrollback setting is bytes, not rows (ghostty/src/terminal/Screen.zig:236 in D109 source). Production-browser fixture: 605,847 bytes, 170 columns, configured 2,000,000 bytes retained 530 rows; after drain receivedSequence=outputSequence=605847. Measured requirement is about 37.7 MB at this width for 10,000 rows, so I’m raising only the mock test cap and measuring the retained row count before the palette assertions.
Author
Owner

Finding: after all 605847 fixture bytes are painted and 10016 rows are retained, Paper→Ink updates the UI but existing terminal pixels remain Paper: expected bg #0d0e10, fg #ececee and Ink ANSI16; actual bg #f6f6f4, fg #1d1d1f and Paper ANSI16. Explicit RGB stays #18a6c4. This reproduces that CanvasRenderer.setTheme alone does not update libghostty-vt's terminal palette/defaults.

Finding: after all 605847 fixture bytes are painted and 10016 rows are retained, Paper→Ink updates the UI but existing terminal pixels remain Paper: expected bg #0d0e10, fg #ececee and Ink ANSI16; actual bg #f6f6f4, fg #1d1d1f and Paper ANSI16. Explicit RGB stays #18a6c4. This reproduces that CanvasRenderer.setTheme alone does not update libghostty-vt's terminal palette/defaults.
Author
Owner

Finding: the pinned source stores indexed colors, but its WASM C bridge does not currently apply the OSC requests. ghostty-web/patches/ghostty-wasm-api.patch:742-747 binds ResponseHandler.colorOperation as a no-op; lib/ghostty.ts write() enters that handler. The existing stream_readonly colorOperation handles palette/default colors, but the C bridge bypasses it. Browser red test after local OSC: Paper pixels persist and the 10,000-row RGB color probe still returns Paper ANSI values. Before changing the pin/build, I propose a minimal handler patch for OSC 4/10/11/12 that updates Terminal.colors in place; cell storage remains indexed and no terminal reconnect is added.

Finding: the pinned source stores indexed colors, but its WASM C bridge does not currently apply the OSC requests. ghostty-web/patches/ghostty-wasm-api.patch:742-747 binds ResponseHandler.colorOperation as a no-op; lib/ghostty.ts write() enters that handler. The existing stream_readonly colorOperation handles palette/default colors, but the C bridge bypasses it. Browser red test after local OSC: Paper pixels persist and the 10,000-row RGB color probe still returns Paper ANSI values. Before changing the pin/build, I propose a minimal handler patch for OSC 4/10/11/12 that updates Terminal.colors in place; cell storage remains indexed and no terminal reconnect is added.
Author
Owner

Pinned rebuild finding: apps/tracer/vendor/ghostty-web/patches/ghostty-wasm-api.patch declares terminal.zig hunk +1,1123 but contains 1,180 added lines. git apply therefore omits readResponse; Zig 0.15.2 fails at src/terminal/c/main.zig:94 (terminal has no member readResponse). I am correcting the hunk count in the local reproducible patch before rebuilding.

Pinned rebuild finding: apps/tracer/vendor/ghostty-web/patches/ghostty-wasm-api.patch declares terminal.zig hunk +1,1123 but contains 1,180 added lines. git apply therefore omits readResponse; Zig 0.15.2 fails at src/terminal/c/main.zig:94 (terminal has no member readResponse). I am correcting the hunk count in the local reproducible patch before rebuilding.
Author
Owner

Correction to the previous count: terminal.zig's patch hunk needs 1,179 added lines (not 1,180; that first count included the next nested patch header). The corrected local patch applies cleanly, produces a 1,179-line terminal.zig, and includes readResponse at line 1127.

Correction to the previous count: terminal.zig's patch hunk needs 1,179 added lines (not 1,180; that first count included the next nested patch header). The corrected local patch applies cleanly, produces a 1,179-line terminal.zig, and includes readResponse at line 1127.
Author
Owner

Follow-up finding: after applying the color handler, browser metrics showed requested background #0d0e10 while WASM render-state background remained #f6f6f4. The generated WASM contains Ghostty warnings that OSC 4/10/11/12 require an allocator; ResponseStream was initialized with init(), which disables OSC allocations. I am enabling the color parser with a bounded allocator.

Follow-up finding: after applying the color handler, browser metrics showed requested background #0d0e10 while WASM render-state background remained #f6f6f4. The generated WASM contains Ghostty warnings that OSC 4/10/11/12 require an allocator; ResponseStream was initialized with init(), which disables OSC allocations. I am enabling the color parser with a bounded allocator.
Author
Owner

Performance finding: the local color setters are below timer resolution (p50 0.000 ms), while the forced repaint is p50 79.000 ms at 170×52 visible cells (8,840 cells); the 10,000-row fixture retains 10,016 scrollback rows in 35,979,264 WASM bytes. The hot path is CanvasRenderer.renderLine calling fillText once per cell. I am testing a constrained ASCII run path with the existing per-cell renderer as fallback.

Performance finding: the local color setters are below timer resolution (p50 0.000 ms), while the forced repaint is p50 79.000 ms at 170×52 visible cells (8,840 cells); the 10,000-row fixture retains 10,016 scrollback rows in 35,979,264 WASM bytes. The hot path is CanvasRenderer.renderLine calling fillText once per cell. I am testing a constrained ASCII run path with the existing per-cell renderer as fallback.
Author
Owner

Renderer optimization result: the constrained ASCII run path reduced repaint p50 from 79.0 ms to 52.9 ms, but the 50-switch target is still missed (click-to-paint p50 53.3 ms, p99 128.1 ms, 32 tasks over 50 ms). The browser run retained 10,016 rows at 35,? MiB; I am instrumenting Canvas call counts to locate the remaining repaint cost before changing the path again.

Renderer optimization result: the constrained ASCII run path reduced repaint p50 from 79.0 ms to 52.9 ms, but the 50-switch target is still missed (click-to-paint p50 53.3 ms, p99 128.1 ms, 32 tasks over 50 ms). The browser run retained 10,016 rows at 35,? MiB; I am instrumenting Canvas call counts to locate the remaining repaint cost before changing the path again.
Author
Owner

Correction to prior comment: this run retained 10,016 rows using 36,044,800 WASM bytes (34.37 MiB); the “35,? MiB” text was a typing error.

Correction to prior comment: this run retained 10,016 rows using 36,044,800 WASM bytes (34.37 MiB); the “35,? MiB” text was a typing error.
Author
Owner

Root-cause finding: one instrumented repaint issued 8,893 fillRect calls: 8,840 per-cell fills for the 170×52 grid plus row/cursor fills. Ghostty reports default background rgb(246,246,244), while CanvasRenderer skipped fills only for RGB black, so every default-background cell redundantly repainted the row color. The run path cut text calls to 608; repaint p50 remains 49.0 ms.

Root-cause finding: one instrumented repaint issued 8,893 fillRect calls: 8,840 per-cell fills for the 170×52 grid plus row/cursor fills. Ghostty reports default background rgb(246,246,244), while CanvasRenderer skipped fills only for RGB black, so every default-background cell redundantly repainted the row color. The run path cut text calls to 608; repaint p50 remains 49.0 ms.
Author
Owner

Follow-up root cause: after removing redundant default-background fills, calls fell from 8,893 to 53 and repaint p50 fell to 36.6 ms. The remaining 608 text calls include 510 cell-by-cell draws on the three probe rows because each contains a block glyph and the previous fast path rejected the whole line. I am narrowing batching to contiguous safe ASCII runs and keeping each complex cell on the existing renderer path.

Follow-up root cause: after removing redundant default-background fills, calls fell from 8,893 to 53 and repaint p50 fell to 36.6 ms. The remaining 608 text calls include 510 cell-by-cell draws on the three probe rows because each contains a block glyph and the previous fast path rejected the whole line. I am narrowing batching to contiguous safe ASCII runs and keeping each complex cell on the existing renderer path.
Author
Owner

After mixed runs and correct default-background matching, an instrumented repaint is 53 fillRect / 138 fillText calls; repaint p50 is 40.9 ms (p99 95.0 ms, 17 tasks over 50 ms). The remaining full-width ASCII runs include trailing blank terminal cells. I am measuring the effect of omitting only plain trailing spaces, which have no glyph or decoration to paint.

After mixed runs and correct default-background matching, an instrumented repaint is 53 fillRect / 138 fillText calls; repaint p50 is 40.9 ms (p99 95.0 ms, 17 tasks over 50 ms). The remaining full-width ASCII runs include trailing blank terminal cells. I am measuring the effect of omitting only plain trailing spaces, which have no glyph or decoration to paint.
Author
Owner

CPU-profile finding: GhosttyTerminal.getLine() calls getViewport(), reparses all 8,840 cells, then copies one row. CanvasRenderer asks for 52 rows, producing about 459,680 parsed cells per repaint. A sampled browser profile attributed 12.94 ms to parseCellsIntoPool and 13.14 ms to its WASM caller, versus 3.27 ms in renderAsciiRunsAndComplexCells/renderCellText/renderLine. The existing getViewport() API can supply one reusable snapshot for all rows.

CPU-profile finding: GhosttyTerminal.getLine() calls getViewport(), reparses all 8,840 cells, then copies one row. CanvasRenderer asks for 52 rows, producing about 459,680 parsed cells per repaint. A sampled browser profile attributed 12.94 ms to parseCellsIntoPool and 13.14 ms to its WASM caller, versus 3.27 ms in renderAsciiRunsAndComplexCells/renderCellText/renderLine. The existing getViewport() API can supply one reusable snapshot for all rows.
Author
Owner

Using one getViewport() snapshot per repaint reduced the 50-switch median to 6.1 ms (renderer p50 4.2 ms) and long tasks to 0; one outlier reached 34.6 ms, so the p99 gate still fails. I am tracing the 50 sample values and remaining allocations before calling the performance gate complete.

Using one getViewport() snapshot per repaint reduced the 50-switch median to 6.1 ms (renderer p50 4.2 ms) and long tasks to 0; one outlier reached 34.6 ms, so the p99 gate still fails. I am tracing the 50 sample values and remaining allocations before calling the performance gate complete.
Author
Owner

Benchmark-harness finding: each pixel assertion calls getImageData() for the full 1360×780 terminal canvas (4,243,200 bytes at 1× DPR), although it inspects only about 20 cells. Fifty checks allocate about 202 MiB between timed switches, which can trigger GC during a click and explain the single 34.6 ms outlier. I am changing pixel reads to small cell rectangles and will rerun the untouched 50-switch timing.

Benchmark-harness finding: each pixel assertion calls getImageData() for the full 1360×780 terminal canvas (4,243,200 bytes at 1× DPR), although it inspects only about 20 cells. Fifty checks allocate about 202 MiB between timed switches, which can trigger GC during a click and explain the single 34.6 ms outlier. I am changing pixel reads to small cell rectangles and will rerun the untouched 50-switch timing.
Author
Owner

The pixel harness now reads only sampled cell rectangles instead of allocating a full-canvas ImageData. The 50-switch run met the timing gate: p50 3.4 ms, p99 13.4 ms, 0 long tasks. Selection pixel check passed for Paper; clicking the theme picker clears the active selection before Ink repaint (sample was 163 RGB-distance from the expected blend, matching the unselected background), so I am reselecting the probe text per theme for its pixel assertion.

The pixel harness now reads only sampled cell rectangles instead of allocating a full-canvas ImageData. The 50-switch run met the timing gate: p50 3.4 ms, p99 13.4 ms, 0 long tasks. Selection pixel check passed for Paper; clicking the theme picker clears the active selection before Ink repaint (sample was 163 RGB-distance from the expected blend, matching the unselected background), so I am reselecting the probe text per theme for its pixel assertion.
Author
Owner

Step complete: vendored the local terminal color API and single-snapshot repaint in bde024f. Vendor gate: reproduced ghostty-web 5f0c6b7 with Ghostty 5714ed0 (Bun 1.4.2, Zig 0.15.2). Browser measurement with this bundle: 50 switches, 10,016 retained rows, p50 3.1 ms, p99 10.7 ms, 0 tasks >50 ms.

Step complete: vendored the local terminal color API and single-snapshot repaint in bde024f. Vendor gate: reproduced ghostty-web 5f0c6b7 with Ghostty 5714ed0 (Bun 1.4.2, Zig 0.15.2). Browser measurement with this bundle: 50 switches, 10,016 retained rows, p50 3.1 ms, p99 10.7 ms, 0 tasks >50 ms.
Author
Owner

Step complete: active-session theme integration committed as bbdc694. bun run check passed ($ bun x tsc --project tsconfig.json, exit 0). The 50-switch run remained p50 3.1 ms / p99 10.7 ms with 10,016 rows; indexed colors changed and truecolor probes stayed at #18a6c4.

Step complete: active-session theme integration committed as bbdc694. `bun run check` passed (`$ bun x tsc --project tsconfig.json`, exit 0). The 50-switch run remained p50 3.1 ms / p99 10.7 ms with 10,016 rows; indexed colors changed and truecolor probes stayed at #18a6c4.
Author
Owner

Test proof committed as 701804b. Production Chromium with pinned WASM retained 10,016 rows; 50 switches p50 3.1 ms, p99 10.7 ms, max 10.7 ms, and zero tasks over 50 ms. Indexed cells followed each theme, truecolor stayed #18a6c4, viewport and socket stayed stable. Captured 20 desktop/mobile screenshots under apps/tracer/test-results/terminal-themes/.

Test proof committed as 701804b. Production Chromium with pinned WASM retained 10,016 rows; 50 switches p50 3.1 ms, p99 10.7 ms, max 10.7 ms, and zero tasks over 50 ms. Indexed cells followed each theme, truecolor stayed #18a6c4, viewport and socket stayed stable. Captured 20 desktop/mobile screenshots under apps/tracer/test-results/terminal-themes/.
Author
Owner

Finished at 701804b7e8. scripts/verify fast ran under systemd-run --user --scope -p CPUWeight=10000 and exited 0. Gate output:

| Rust gate | Result |
+---------------------------+--------------+
| fmt | PASS |
| VT native fmt | PASS |
| clippy | PASS |
| test | PASS |
| cargo-deny | PASS |
| static builds | PASS |
+---------------------------+--------------+

==> Vendored Ghostty artifact checksums
dist/ghostty-web.js: OK
dist/ghostty-vt.wasm: OK
dist/index.d.ts: OK

==> TypeScript protocol tests
7 pass
0 fail
34390 expect() calls
Ran 7 tests across 2 files. [4.72s]

bun run check exited 0. The production browser check retained 10,016 rows and measured 50 switches: p50 3.1 ms, p99 10.7 ms, max 10.7 ms, zero tasks over 50 ms. 20 screenshots are under apps/tracer/test-results/terminal-themes/.

Finished at 701804b7e85e31c285c5ca366c497ccbadafc6d4. scripts/verify fast ran under systemd-run --user --scope -p CPUWeight=10000 and exited 0. Gate output: | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | clippy | PASS | | test | PASS | | cargo-deny | PASS | | static builds | PASS | +---------------------------+--------------+ ==> Vendored Ghostty artifact checksums dist/ghostty-web.js: OK dist/ghostty-vt.wasm: OK dist/index.d.ts: OK ==> TypeScript protocol tests 7 pass 0 fail 34390 expect() calls Ran 7 tests across 2 files. [4.72s] bun run check exited 0. The production browser check retained 10,016 rows and measured 50 switches: p50 3.1 ms, p99 10.7 ms, max 10.7 ms, zero tasks over 50 ms. 20 screenshots are under apps/tracer/test-results/terminal-themes/.
Author
Owner

Deployed dev 9e09924 to monitor01 (root, 2026-10-07 ~17:20): retire the setup-token socket (#51, verified: socket absent after start with an owner present), local lossless terminal themes (#75), test stabilisation (#65), binary size budget (#57). Gate before deploy: scripts/verify fast exit 0 (6/6 Rust PASS, static size 16,160,816 B within 16,404,816 B, TS 7/7, browser 2/2, RTT smoke PASS). Previous binary kept at /usr/local/bin/infraternal.prev and web at web.prev for rollback.

Deployed dev 9e09924 to monitor01 (root, 2026-10-07 ~17:20): retire the setup-token socket (#51, verified: socket absent after start with an owner present), local lossless terminal themes (#75), test stabilisation (#65), binary size budget (#57). Gate before deploy: scripts/verify fast exit 0 (6/6 Rust PASS, static size 16,160,816 B within 16,404,816 B, TS 7/7, browser 2/2, RTT smoke PASS). Previous binary kept at /usr/local/bin/infraternal.prev and web at web.prev for rollback.
Author
Owner

REGRESSION found by the owner on monitor01 after deploying 9e09924: the cursor sits about 2 cells right of the text, and the drift grows along the line. Cause: local-theme-api.patch draws text runs with fillText at runStart * metrics.width, so glyphs inside a run advance at the font natural width (~8.4 px) while the grid and cursor use the ceil width (9 px). Web bundle rolled back on monitor01 (server binary kept). The fix goes into #84 (fix-cell-metrics, run 2) with pixel tests. dev is not deployable until #84 merges.

REGRESSION found by the owner on monitor01 after deploying 9e09924: the cursor sits about 2 cells right of the text, and the drift grows along the line. Cause: local-theme-api.patch draws text runs with fillText at runStart * metrics.width, so glyphs inside a run advance at the font natural width (~8.4 px) while the grid and cursor use the ceil width (9 px). Web bundle rolled back on monitor01 (server binary kept). The fix goes into #84 (fix-cell-metrics, run 2) with pixel tests. **dev is not deployable** until #84 merges.
Author
Owner

Deployed dev 7eca507 to monitor01 (~19:00): font-height terminal lines and on-grid glyphs (#84), local lossless themes (#75) back in. Before deploying, the orchestrator checked a 86-character line on a local production build at DPR 2: renderer cursorX=86 right after the last glyph, the same relative position as on a 2-character line (no drift). Gate: scripts/verify fast exit 0. Rollback: web.prev and infraternal.prev on the server.

Deployed dev 7eca507 to monitor01 (~19:00): font-height terminal lines and on-grid glyphs (#84), local lossless themes (#75) back in. Before deploying, the orchestrator checked a 86-character line on a local production build at DPR 2: renderer cursorX=86 right after the last glyph, the same relative position as on a 2-character line (no drift). Gate: scripts/verify fast exit 0. Rollback: web.prev and infraternal.prev on the server.
Author
Owner

Closed by the orchestrator during the 2026-10-28 handover (#254), on the owner's request to close finished issues. Evidence: Lossless local terminal themes deployed (9e09924, then 7eca507 after the cursor regression was fixed by bd12725fe); all verified in dev and prod and checked on the production build. Reopen if anything is still missing.

Closed by the orchestrator during the 2026-10-28 handover (#254), on the owner's request to close finished issues. Evidence: Lossless local terminal themes deployed (9e09924, then 7eca507 after the cursor regression was fixed by bd12725fe); all verified in dev and prod and checked on the production build. Reopen if anything is still missing.
kayg closed this issue 2026-10-28 17:52:31 +00:00
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#75
No description provided.