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
No Branch/Tag specified
main
j470b-server-mesh-rejoin
mesh-compat-382
j444b-d381-key-wins-flood
j461b-macos-utun-reader-thread
j475b-remove-host-motion
fix-reconnect-new-link
j481-d380
dev
j479-mesh-soak
fix/j476-host-facts
j475-motion-pill-split
j470-mac-node-join
root-upgrade-d376
j474-review-merge
latency-targets-120hz
nightly-suite-shards
gotatun-pin-j469
macos-utun-reader-thread
fix-flood-echo-stall
wip/j416
motion-fast-ease-out
mesh-addhost-j259
takeover-close-terminals-d368
kbd-instant-followups
fix-trust-key-double-click
nightly-suite-partial-report
appwin-icons-j207
rusqlite-slice1
swipe-content-first-d374
mesh-mtu-381
addhost-instant-open
glass-contrast-d365-mock
glass-contrast-d365
signout-followups-442
fix-startup-toast-red
research-gotatun-matrix-j317
themes-sync-j304
fix-load-flakes
fix-nix-zig-patch
fix-compute01-browser-baseline
ratchet-size-baseline-419
predict-auto-d130
revoke-coverage-d367
nightly-suite-queue-timeout
nightly-browser-suite
fix-rustsec-2026-0459
fonts-ioskeley-obel
log-retention-d375
fix-kbd-no-anim-427
fix-dev-red-specs-2
takeover-proof-305-j205
wip/j410-server-pump
ui-consistency-d372
fix-osc52-clipboard
libghostty-vt-1.0
forget-retired-host-d369
j404-merge-426
fix-setup-lock-race
fix-touch-select-426
revoke-access-report-ui
fix-predictive-echo-specs
fix-selinux-agent-exec
wip/j395-checkpoint2
wip/j395
j391-review-scratch
ui-consistency-j195-deferred-rebased
fix-rustc-paths-421
decisions-wording-341
fix-install-suite-red
fix-dev-rtt-specs
fix-pending-removal-align
fix-privileged-skip
fix-install-suite-404
review-j368
mesh-dns-j254
revoke-access-report-d356b
revoke-privilege-round5
fix-removal-confirm
review-j360
gotatun-rotation-367
deps-refresh-340
review-j354
review-J351
review-j350
addhost-takeover-d359
revoke-report-items-347
revoke-end-sessions-d362
review-j348
size-mesh-ssh-346
size-trim-337
revoke-access-diff-d356
j259-pre-rebase-b06c5a6
j305-ended-at-start-spec
addhost-sudo-consent
fix-layout-real-specs-411
fix-shell-open-411
fix-console-hosts-373
fix-shell-exit
fuzz-host-name-353
fix-host-details-410
macos-filter-j197
research-gotatun-tricks
revoke-p3-j216l
stale-state-pill
revoke-followups-j216k
revoke-progress-j216e
backup/macos01-20261106/lume/j197
backup/macos01-20261106/lume/appwin-icons-j207-stash0
backup/macos01-20261106/claude/stash-0
backup/macos01-20261106/claude/stash-1
backup/macos01-20261106/claude/J80-macos-s1
backup/macos01-20261106/claude/J80
backup/macos01-20261106/claude/J80-slice1-20261102
backup/macos01-20261106/claude/J16b-appwin-on-dev
backup/macos01-20261106/claude/appwin-launch-idle-146
backup/macos01-20261106/claude/J80-s1-run
wip/j259
trust-285-p3c
runtime-lock-j216f
mesh-ssh-p3-j175
tun-proof-p3b
trust-285-p3
terminal-polish-j271
tun-proof-p3
ssh-install-flakes-j216i
fuzz-fixes-j264
trust-285-a
trust-285-b
restart-oversized-fixture-j198
backup/macos01-20261106/lume/appwin-icons-j207
restart-bound-j216h
up-preflight-p3-j198
backup/macos01-20261106/lume/j251
up-preflight-j198
wip/up-preflight-j198-j245-p2
wip/up-preflight-j198-j245-p1
agent-fsync-offloop-j196
wip/up-preflight-j198-j245
settings-offscreen-j231
wip/up-preflight-j198-j226-inspection
wip/j103f
themes-j206
wip/up-preflight-j198-j226
flakes-j216c
backup/macos01-20261106/claude/j197
flakes-j216b
backup/macos01-20261106/claude/j197-red
wip/up-preflight-j198-review
flakes-j216
backup/macos01-20261106/claude/appwin-icons-j207
engineering-j208b
appwin-mock-j209
toast-stack-j165
engineering-j208
decisions-d351
decisions-d350
decisions-d349
appearance-presets-mock-j129
decisions-d348
docs-audit-spec
doc-coverage-baseline-raise
decisions-d347
decisions-d346b
decisions-d346
j88h-profile
ui-consistency-j195-deferred
ui-consistency-j195
engineering-practices-j194
preferred-login-ui-j166
signin-pools-263-j174
macos-node-265-s1
decisions-d345
decisions-d344
proxy-cgroup-j164
verify-quick-fuzz-329
proxy-start-io-j168
timing-flakes-j163
up-263-s1
mcp-events-j147
verify-quick-fuzz-j158
proxy-p3-j159
agent-resource-j157-detect
wip/j103d-final
remote-cancel-hang-j155
proxy-traefik-281-j62d
j149-verify-quick
proxy-traefik-281-j62b
j150-drop-unnamed
wip/j79d-red-r4
wip/j103d-d332
rootless-toast-j142
wip/j103d-head
wip/j103d
j125c-mcp
macos-build-j136
backup/macos01-20261106/claude/j136
wip/j79d-red-cli
wip/j79d-red-server
j121-link-liveness
j118c-tracer-failures
wip/j79c-head
release-desktop-j131
j124c-signed-macos
wip/j79c-red-p3
wip/j79c-red-p23
wip/j79c-red-p22
merge-j128-check
perf-deferred-j128
sidebar-toggle-260
j127-shortcut-changed-dot
j125-mcp
j118-tracer-failures
j126-account-summary
settings-sections-mock-j92
j120-pinch-lock
sidebar-292
backup/macos01-20261106/claude/j120
taste-2026-11-03
row-actions-242-215
backup/macos01-20261106/claude/stash-0-untracked
backup/macos01-20261106/claude/J80-265-s1-run
backup/macos01-20261106/claude/J80-macos-s1-20261102
j112-canvas-commands
ui-no-overshoot-scroll
fix-recovery-restart
backup/macos01-20261106/claude/stash-1-untracked
taste-2026-11-02
size-baseline-ratchet
machine-cards-structure-j108
j109-boot-copy-mockups
fix-dev-red
wip/up-263-s1
j95b-settings-rows
proxy-traefik-281
j95-option-rows
docs-human-j91
docs-keys-palette
fix-flood-1013-270-j38d
mesh-ssh-s1
mesh-ssh-s0
addhost-d291-tunnel
test-stability-load
mesh-relay-220
repro-296
shim-thp-off-24
mesh-pins-258-v2
mesh-dns
signin-d311-merged
docs-pages
ui-size-283
fix-ui-batch-specs
wip/j12b-edge-update
gate-on-arm
fix-rename-sync-293
trust-285-j60b
fix-focus-ring-clip
integ-ui-1101
edge-update-v2
signin-d311
taste-2026-11-01
layout-per-tab-250
ui-approved-splitopt-224
ui-approved-splitopt-224-on-dev
icon-text-centre-282
ui-approved-189-212-216
research-reverse-proxy
transfer-budget-320
review-mesh-pins-258-r2
sidebar-pill-259
review-mesh-relay-220
wip/j12-edge-update-v2
nix-package
status-crash-loop-237
fix-dev-test-failures
fix-fixture-socket-path
wip/mesh-relay-220-pre-dev-advance-rebase
fix-canvas-side-clip
research-head-to-head
frosted-real-256
review-mesh-pins-258
wip/mesh-relay-220-pre-final-rebase
review-layout-per-tab-250-r2
broad-suite-triage
nix-devshell
review-status-crash-loop-237-r3
wip/nix-devshell-pre-p3
taste-frosted-real
wip/code-serve-demo-2026-10-30
wip/code-serve-glass-2026-10-30
wip/code-serve-clusters-2026-10-30
wip/code-merge-release94-dev-2026-10-30
wip/rproxy-delta-scratch-2026-10-30
wip/stale-note-257-staged-2026-10-30
wip/d241-size-staged-2026-10-30
review-broad-suite-207
wip/mesh-pins-258-pre-split
wip/mesh-relay-220-pre-rebase
mesh-measure-220
plan-211-update
review-edge-autoupdate-r3
perf24-reconnect
review-stale-note-r2
release-prod-live
d241-shell-marker
review-mesh-pins-server-258
review-revocation-r3
review-setup-token-r2
review-sync-journal-r3
wip/review-mesh-pins-258-prune-review-1995fee46
frosted-scroll-256
sync-journal-r3
revocation-per-account-wip
review-tokio-asyncfd-0386
review-sync-p3s
wip/stash-8by64-stash0-2026-10-30
mesh-drag-r2
setup-token-data-wip
release-9-4
auth-busy-1013
mesh-view-overlap
sync-p3s
review-auth-busy-wake
review-readd-r8-wip
mesh-drag-bounds
audit-auth-93
app-windows-stream-start
review-authority-cap-r3
app-windows-r7-measure
readd-minimal-r6
readd-minimal-r5
app-windows-r7-soak
readd-minimal-r4
app-windows-r7
app-windows-r6
readd-minimal-r3
readd-minimal-r2
readd-minimal
merge-release8-dev
review-busy-page-liveness-r2
review-busy-page-liveness
mesh-view-trim
review-authority-read-retry-r2
review-remove-uninstall-r2
release-retry-reattach
release-retry
review-remove-uninstall
review-authority-read-retry
app-windows-ship
mesh-autoswitch-p2
mesh-ssh
mesh-relay
mesh-macos-node
screen-and-control
session-labels
review-real-logins-r5
review-real-logins-r4
review-real-logins-r3
shim-memory-r2
review-real-logins-r2
shim-memory
shadow-addhost-instant
shadow-real-logins
shadow-same-session-flood
shadow-prod-perf-fix
hosts-from-server
app-windows-real
app-windows-transport
release-prod-6w
mesh-policy-verify
quiet-close-toast
dev-merge-r4
review-mesh-agent-transport
review-app-windows-transport
settings-roundtrip
review-release-pipeline-r4
mesh-agent-transport
ended-strip-fix
review-audit-ca-update-d244
settings-rows
phone-width
signout-clear
fix-restart-tests
dev-merge-rp
signer-hardening
review-audit-ca-update
terminal-fidelity
review-audit-ca-update-wip
mesh-integrate
review-mesh-agent-transport-515d3712
review-mesh-dataplane-feec1988
mesh-dataplane
signout
research-canvas-node
wip/review-renderer-cost
wip/review-terminal-fidelity
padlock-glyph
review-release-pipeline-2e95b689
phone-polish
sidebar-polish
inbox-generic
review-mesh-dataplane-378d4055
prod-batch
ui-inbox
ui-ca-unlock
ui-machine-sections
review-mesh-dataplane-a5f744fb
tls-session-prod
session-bounds-prod
fix-dead-session
session-bounds
tls-session
palette-polish
shortcuts-d222
research-appmeta
phone-gutter
ui-centre-button
wip/ui-centre-button
wip/tls-session
review-addhost-breakit
ui-platform
renderer-cost
remote-agents
mesh-api
ssh-bootstrap
prod-hotfix2
prod-fixes
review-remote-agents
review-mesh-dataplane
session-life
research-browser
review-mesh-api
research-capture
review-mesh-api-round3
review-mesh-api-round1
gate-stability
review-gate-stability
review-relay-direct
review-mesh-api-findings
proto-sessions
font-picker
proto-addhost
proto-windows
mesh-core
latency-gate
research-bsd
research-discovery
keytray-mods
mock-core
ui-tweaks
fix-motion-sel
fix-motion
integrate2
fix-robust
t-web
t-server
vt-ghostty
fix-snap-echo
t-web-auth
review-vt2
t-agent
lane-protocol
lane-shim
lane-auth
s1-base
s1-contracts
s1-workspace
No results found.
No labels
launch-qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.
Starting in worktree branch fix-term-theme from dev at
760490ce4e. Issue history reviewed: no prior comments.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.
Production-build terminal screenshots from the theme regression test: 10 palettes × 2 viewports (20 PNGs, 1,529,938 bytes), generated on the
fix-term-themeworktree based on760490ce4e134b4a7434530b89a8e2fdd5339044.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.
Updated production-build terminal screenshots from the final theme implementation: 10 palettes × 2 viewports (20 PNGs, 1,530,405 bytes).
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.
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.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.
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.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.Finished on branch fix-term-theme at head
414025a3a5. Two atomic commits:c4d54d5rebuilds Ghostty with a replayed theme;414025aadds 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:
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.
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.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.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 / 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).
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):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: 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: 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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: active-session theme integration committed as
bbdc694.bun run checkpassed ($ 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.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/.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/.
Deployed dev
9e09924to 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.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.Deployed dev
7eca507to 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.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, then7eca507after the cursor regression was fixed bybd12725fe); all verified in dev and prod and checked on the production build. Reopen if anything is still missing.kayg referenced this issue2026-10-28 17:53:54 +00:00