Skip to content

Commit 1887f84

Browse files
Fix starship rendering regressions with clean diff and PTY debug runbook (#225)
* Fix starship rendering regressions and add PTY debug runbook * Fix starship template theme to use packed rgb colors * Address PR feedback and fix lint/native diff telemetry audit * Fix native vendor integrity check for uninitialized submodules * Fix CI color-style tests and native payload short-read audit
1 parent 115410b commit 1887f84

File tree

21 files changed

+835
-319
lines changed

21 files changed

+835
-319
lines changed

AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,25 @@ node scripts/run-tests.mjs --filter "widget"
170170
2. If changing runtime, layout, or renderer code, also run integration tests.
171171
3. Run the full suite before committing.
172172

173+
### Mandatory Live PTY Validation for UI Regressions
174+
175+
For rendering/layout/theme regressions, do not stop at unit snapshots. Run the
176+
app in a real PTY and collect frame audit evidence yourself before asking a
177+
human to reproduce.
178+
179+
Canonical runbook:
180+
181+
- [`docs/dev/live-pty-debugging.md`](docs/dev/live-pty-debugging.md)
182+
183+
Minimum required checks for UI regression work:
184+
185+
1. Run target app/template in PTY with deterministic viewport.
186+
2. Exercise relevant routes/keys (for starship: `1..6`, `t`, `q`).
187+
3. Capture `REZI_FRAME_AUDIT` logs and analyze with
188+
`node scripts/frame-audit-report.mjs ... --latest-pid`.
189+
4. Capture app-level debug snapshots (`REZI_STARSHIP_DEBUG=1`) when applicable.
190+
5. Include concrete evidence in your report (hash changes, route summary, key stages).
191+
173192
## Verification Protocol (Two-Agent Verification)
174193

175194
When verifying documentation or code changes, split into two passes:

CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,15 @@ result.toText(); // Render to plain text for snapshots
505505

506506
Test runner: `node:test`. Run all tests with `node scripts/run-tests.mjs`.
507507

508+
For rendering regressions, add a live PTY verification pass and frame-audit
509+
evidence (not just snapshot/unit tests). Use:
510+
511+
- [`docs/dev/live-pty-debugging.md`](docs/dev/live-pty-debugging.md)
512+
513+
This runbook covers deterministic viewport setup, worker-mode PTY execution,
514+
route/theme key driving, and cross-layer log analysis (`REZI_FRAME_AUDIT`,
515+
`REZI_STARSHIP_DEBUG`, `frame-audit-report.mjs`).
516+
508517
## Skills (Repeatable Recipes)
509518

510519
Project-level skills for both Claude Code and Codex:

docs/dev/live-pty-debugging.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Live PTY UI Testing and Frame Audit Runbook
2+
3+
This runbook documents how to validate Rezi UI behavior autonomously in a real
4+
terminal (PTY), capture end-to-end frame telemetry, and pinpoint regressions
5+
across core/node/native layers.
6+
7+
Use this before asking a human for screenshots.
8+
9+
## Why this exists
10+
11+
Headless/unit tests catch many issues, but rendering regressions often involve:
12+
13+
- terminal dimensions and capability negotiation
14+
- worker transport boundaries (core -> node worker -> native)
15+
- partial redraw/damage behavior across many frames
16+
17+
The PTY + frame-audit workflow gives deterministic evidence for all of those.
18+
19+
## Prerequisites
20+
21+
From repo root:
22+
23+
```bash
24+
cd <repo-root>
25+
npx tsc -b packages/core packages/node packages/create-rezi
26+
```
27+
28+
## Canonical interactive run (Starship template)
29+
30+
This enables:
31+
32+
- app-level debug snapshots (`REZI_STARSHIP_DEBUG`)
33+
- cross-layer frame audit (`REZI_FRAME_AUDIT`)
34+
- worker execution path (`REZI_STARSHIP_EXECUTION_MODE=worker`)
35+
36+
```bash
37+
cd <repo-root>
38+
: > /tmp/rezi-frame-audit.ndjson
39+
: > /tmp/starship.log
40+
41+
env -u NO_COLOR \
42+
REZI_STARSHIP_EXECUTION_MODE=worker \
43+
REZI_STARSHIP_DEBUG=1 \
44+
REZI_STARSHIP_DEBUG_LOG=/tmp/starship.log \
45+
REZI_FRAME_AUDIT=1 \
46+
REZI_FRAME_AUDIT_LOG=/tmp/rezi-frame-audit.ndjson \
47+
npx tsx packages/create-rezi/templates/starship/src/main.ts
48+
```
49+
50+
Key controls in template:
51+
52+
- `1..6`: route switch (bridge/engineering/crew/comms/cargo/settings)
53+
- `t`: cycle theme
54+
- `q`: quit
55+
56+
## Deterministic viewport (important)
57+
58+
Many regressions are viewport-threshold dependent. Always test with a known
59+
size before comparing runs.
60+
61+
For an interactive shell/PTY:
62+
63+
```bash
64+
stty rows 68 cols 300
65+
```
66+
67+
Then launch the app in that same PTY.
68+
69+
## Autonomous PTY execution (agent workflow)
70+
71+
When your agent runtime supports PTY stdin/stdout control:
72+
73+
1. Start app in PTY mode (with env above).
74+
2. Send key sequences (`2`, `3`, `t`, `q`) through stdin.
75+
3. Wait between keys to allow frames to settle.
76+
4. Quit and analyze logs.
77+
78+
Do not rely only on static test snapshots for visual regressions.
79+
80+
## Frame audit analysis
81+
82+
Use the built-in analyzer:
83+
84+
```bash
85+
node scripts/frame-audit-report.mjs /tmp/rezi-frame-audit.ndjson --latest-pid
86+
```
87+
88+
What to look for:
89+
90+
- `backend_submitted`, `worker_payload`, `worker_accepted`, `worker_completed`
91+
should stay aligned in worker mode.
92+
- `hash_mismatch_backend_vs_worker` should be `0`.
93+
- `top_opcodes` should reflect expected widget workload.
94+
- `route_summary` should show submissions for every exercised route.
95+
- `native_summary_records`/`native_header_records` confirm native debug pull
96+
from worker path.
97+
98+
If a log contains multiple app runs, always use `--latest-pid` (or `--pid=<n>`)
99+
to avoid mixed-session confusion.
100+
101+
## Useful grep patterns
102+
103+
```bash
104+
rg "runtime.command|runtime.fatal|shell.layout|engineering.layout|engineering.render|crew.render" /tmp/starship.log
105+
rg "\"stage\":\"table.layout\"|\"stage\":\"drawlist.built\"|\"stage\":\"frame.submitted\"|\"stage\":\"frame.completed\"" /tmp/rezi-frame-audit.ndjson
106+
```
107+
108+
## Optional deep capture (drawlist bytes)
109+
110+
Capture raw drawlist payload snapshots for diffing:
111+
112+
```bash
113+
env \
114+
REZI_FRAME_AUDIT=1 \
115+
REZI_FRAME_AUDIT_DUMP_DIR=/tmp/rezi-drawlist-dumps \
116+
REZI_FRAME_AUDIT_DUMP_MAX=20 \
117+
REZI_FRAME_AUDIT_DUMP_ROUTE=crew \
118+
npx tsx packages/create-rezi/templates/starship/src/main.ts
119+
```
120+
121+
This writes paired `.bin` + `.json` files with hashes and metadata.
122+
123+
## Native trace through frame-audit
124+
125+
Native debug records are enabled by frame audit in worker mode. Controls:
126+
127+
- `REZI_FRAME_AUDIT_NATIVE=1|0` (default on when frame audit is enabled)
128+
- `REZI_FRAME_AUDIT_NATIVE_RING=<bytes>` (ring size override)
129+
130+
Look for stages such as:
131+
132+
- `native.debug.header`
133+
- `native.drawlist.summary`
134+
- `native.frame.*`
135+
- `native.perf.*`
136+
137+
## Triage playbook for common regressions
138+
139+
### 1) “Theme only updates animated region”
140+
141+
Check:
142+
143+
1. `runtime.command` contains `cycle-theme`.
144+
2. `drawlist.built` hashes change after theme switch.
145+
3. `frame.submitted`/`frame.completed` continue for that route.
146+
147+
If hashes do not change, bug is likely in view/theme resolution.
148+
If hashes change but screen does not, investigate native diff/damage path.
149+
150+
### 2) “Table looks empty or only one row visible”
151+
152+
Check `table.layout` record:
153+
154+
- `bodyH`
155+
- `visibleRows`
156+
- `startIndex` / `endIndex`
157+
- table rect height
158+
159+
If `bodyH` is too small, inspect parent layout/flex and sibling widgets
160+
(pagination or controls often steal height).
161+
162+
### 3) “Worker mode renders differently from inline”
163+
164+
Run both modes with identical viewport and compare audit summaries:
165+
166+
- worker: `REZI_STARSHIP_EXECUTION_MODE=worker`
167+
- inline: `REZI_STARSHIP_EXECUTION_MODE=inline`
168+
169+
If only worker diverges, focus on backend transport and worker audit stages.
170+
171+
## Guardrails
172+
173+
- Keep all instrumentation opt-in via env vars.
174+
- Never print continuous debug spam to stdout during normal app usage.
175+
- Write logs to files (`/tmp/...`) and inspect post-run.
176+
- Prefer deterministic viewport + scripted route/theme steps when verifying fixes.

docs/dev/testing.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ npm run test:e2e
6666
npm run test:e2e:reduced
6767
```
6868

69+
## Live PTY Rendering Validation (for UI regressions)
70+
71+
For terminal rendering/theme/layout regressions, run a live PTY session with
72+
frame-audit instrumentation in addition to normal tests.
73+
74+
Use the dedicated runbook:
75+
76+
- [Live PTY UI Testing and Frame Audit Runbook](live-pty-debugging.md)
77+
78+
That guide includes deterministic viewport setup, worker-mode run commands,
79+
scripted key driving, and cross-layer telemetry analysis.
80+
6981
## Test Categories
7082

7183
### Unit Tests

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ nav:
250250
- Repo Layout: dev/repo-layout.md
251251
- Build: dev/build.md
252252
- Testing: dev/testing.md
253+
- Live PTY Debugging: dev/live-pty-debugging.md
253254
- Code Standards: dev/code-standards.md
254255
- Ink Compat Debugging: dev/ink-compat-debugging.md
255256
- Perf Regressions: dev/perf-regressions.md

packages/core/src/renderer/renderToDrawlist/widgets/collections.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
truncateWithEllipsis,
77
} from "../../../layout/textMeasure.js";
88
import type { Rect } from "../../../layout/types.js";
9+
import { FRAME_AUDIT_ENABLED, emitFrameAudit } from "../../../perf/frameAudit.js";
910
import type { RuntimeInstance } from "../../../runtime/commit.js";
1011
import type { FocusState } from "../../../runtime/focus.js";
1112
import type {
@@ -605,6 +606,32 @@ export function renderCollectionWidget(
605606
? Math.min(rowCount, startIndex + visibleRows + overscan)
606607
: rowCount;
607608

609+
if (FRAME_AUDIT_ENABLED) {
610+
emitFrameAudit(
611+
"tableWidget",
612+
"table.layout",
613+
Object.freeze({
614+
tableId: props.id,
615+
x: rect.x,
616+
y: rect.y,
617+
w: rect.w,
618+
h: rect.h,
619+
innerW,
620+
innerH,
621+
bodyY,
622+
bodyH,
623+
rowCount,
624+
headerHeight,
625+
rowHeight: safeRowHeight,
626+
virtualized,
627+
startIndex,
628+
endIndex,
629+
visibleRows,
630+
overscan,
631+
}),
632+
);
633+
}
634+
608635
if (tableStore) {
609636
tableStore.set(props.id, { viewportHeight: bodyH, startIndex, endIndex });
610637
}

packages/core/src/widgets/__tests__/pagination.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ describe("pagination ids and vnode", () => {
128128
const zoneNode = children[0];
129129
assert.equal(zoneNode?.kind, "focusZone");
130130
if (zoneNode?.kind !== "focusZone") return;
131-
const ids = zoneNode.children
131+
const controlsRow = zoneNode.children[0];
132+
assert.equal(controlsRow?.kind, "row");
133+
if (controlsRow?.kind !== "row") return;
134+
const ids = controlsRow.children
132135
.filter((child) => child.kind === "button")
133136
.map((child) => (child.kind === "button" ? child.props.id : ""));
134137
assert.equal(ids.includes(getPaginationControlId("pages", "first")), true);

packages/core/src/widgets/pagination.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,16 @@ export function buildPaginationChildren(props: PaginationProps): readonly VNode[
243243
});
244244
}
245245

246+
const controlsRow: VNode = {
247+
kind: "row",
248+
props: {
249+
gap: 0,
250+
wrap: true,
251+
items: "center",
252+
},
253+
children: Object.freeze(controls),
254+
};
255+
246256
const zone: VNode = {
247257
kind: "focusZone",
248258
props: {
@@ -252,7 +262,7 @@ export function buildPaginationChildren(props: PaginationProps): readonly VNode[
252262
columns: 1,
253263
wrapAround: false,
254264
},
255-
children: Object.freeze(controls),
265+
children: Object.freeze([controlsRow]),
256266
};
257267

258268
return Object.freeze([zone]);

packages/create-rezi/templates/starship/src/screens/bridge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ export function renderBridgeScreen(
554554
title: "Bridge Overview",
555555
context,
556556
deps,
557-
body: ui.column({ gap: SPACE.sm, width: "100%" }, [
557+
body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
558558
BridgeCommandDeck({ key: "bridge-command-deck", state, dispatch: deps.dispatch }),
559559
]),
560560
});

packages/create-rezi/templates/starship/src/screens/cargo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function renderCargoScreen(
3030
title: "Cargo Hold",
3131
context,
3232
deps,
33-
body: ui.column({ gap: SPACE.sm, width: "100%" }, [
33+
body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
3434
CargoDeck({
3535
key: "cargo-deck",
3636
state: context.state,

0 commit comments

Comments
 (0)