feat(node,native): inline screen mode (Zireael v1.4.0)#416
Conversation
- vendor: bump Zireael to 8745d50 (v1.4.0, engine ABI 1.3.0) in submodule,
build copy, and VENDOR_COMMIT.txt
- native: marshal plat.screenMode + inlineRows through strict config
validation; update FFI struct layouts (plat_config_t, engine configs,
zr_term_state_t) and the layout-verification test
- core: bump requested engine ABI pin to 1.3.0
- node: new NodeBackendConfig.screen option ({ mode: "alt" | "inline",
inlineRows }) folded into the native engine config on both worker and
in-process execution paths; validation via ZrUiError; screen option wins
over raw nativeConfig passthrough keys
- tests: merge/validation matrix + engineCreate forwarding tests on both
execution paths via a config-expectation shim
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new centralized guide: docs/guide/screen-modes.md (usage, sizing, behavior, capability fallback, naming distinction vs executionMode) - mkdocs nav, backend/node.md screen section, protocol/abi.md 1.3.0 pin, README feature visibility, CLAUDE.md examples/map updates - examples/inline-status: runnable inline-mode app (spinner + progress + quit keys), verified end-to-end in a real terminal (tmux): scrollback preserved, live repaints, mid-run resize reflow, final frame left in scrollback with prompt restored below Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR introduces inline screen mode to Rezi, allowing TUI applications to render in a bounded viewport within the terminal's primary screen while preserving scrollback history. The implementation spans documentation, a Zireael native engine upgrade (v1.4.0), Rust FFI bindings, Node.js backend integration, and comprehensive tests and examples. ChangesInline Screen Mode Implementation
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/node/src/backend/nodeBackend/shared.ts (1)
3-25: ⚡ Quick winConsolidated: object-shape declarations should use
interfacein these TS additions.Both new screen-config contracts are object-shape
typealiases; align both with the repository convention by switching tointerfaceandreadonlymembers.As per coding guidelines, "Prefer
interfacefor defining object shapes in TypeScript files".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/node/src/backend/nodeBackend/shared.ts` around lines 3 - 25, Replace the object-shape type alias NodeBackendScreenConfig with an interface following repo convention: change the exported declaration from "export type NodeBackendScreenConfig = Readonly<...>" to "export interface NodeBackendScreenConfig" and make its members readonly (e.g., readonly mode?: "alt" | "inline"; readonly inlineRows?: number;), preserving the existing JSDoc comments and the same optional semantics and validation notes for mode and inlineRows.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/inline-status/src/index.ts`:
- Line 52: The root view currently returns SyncPanel directly in the app.view
handler which violates the root-view layout invariant; modify the app.view
registration (the app.view call) to wrap the returned SyncPanel component with a
root container such as ui.page({ p: 1 }, ...) or ui.appShell(...), so that
SyncPanel is passed as the child to ui.page/ui.appShell (keep SyncPanel(...) as
the inner component) and ensure p: 1 spacing is provided.
In `@packages/native/vendor/zireael/src/core/zr_diff.c`:
- Around line 754-766: Before creating inline blank baseline rows you must also
reset any open OSC 8 hyperlink state in addition to restoring SGR; update the
block that currently calls zr_emit_sgr_absolute(sb, ts,
zr_diff_baseline_style(), caps) and then writes CR/LF so that it also closes the
hyperlink (invoke the hyperlink transition/reset helper used elsewhere, e.g.
zr_diff_emit_link_transition(...) or the OSC8-reset routine) so the emitted
blank rows have link_ref==0 and no live terminal hyperlink; apply the same
change in the other analogous spot that currently only restores SGR.
In `@packages/native/vendor/zireael/src/core/zr_engine.c`:
- Around line 2081-2086: You sync the terminal keyboard with the prospective
profile via zr_engine_sync_kitty_keyboard(e, &prospective_profile) before
calling zr_engine_set_config_apply_viewport; if that apply fails you currently
goto cleanup leaving the terminal in the new keyboard state while
e->cfg_runtime/e->term_profile were not committed. Change the flow so that when
zr_engine_set_config_apply_viewport returns non-ZR_OK you immediately rollback
the kitty keyboard to the original profile (e.g., call
zr_engine_sync_kitty_keyboard(e, &original_profile) or a dedicated revert
helper) before jumping to cleanup; apply the same rollback logic to the other
occurrence around lines 2097-2099 to ensure terminal keyboard state stays in
sync with e->cfg_runtime/e->term_profile on failures.
In `@packages/node/src/__tests__/screen_mode.test.ts`:
- Around line 92-124: The tests only assert config forwarding via shims but do
not exercise the PTY contract (scrollback/final-frame semantics); update or add
tests to use the PTY harness instead of only `@xterm/headless` snapshots with
scrollback: 0 so you can assert scrollback preservation and that the final frame
is present in the scrollback for both execution modes. Specifically, modify the
existing tests named "backend: worker path forwards screen option to
engineCreate" and "backend: inline execution path forwards screen option to
engineCreate" (and/or add new tests) to start createNodeBackendInternal with the
same screen config but drive the actual PTY harness used by the backend (not
just screenExpectNative.js shim), configure a nonzero scrollback in the headless
terminal, capture the full buffer (including scrollback) after backend runs, and
assert that the expected final frame lines appear in the scrollback; keep
references to createNodeBackendInternal, screenExpectNative.js, and the PTY
harness so reviewers can locate the changes.
---
Nitpick comments:
In `@packages/node/src/backend/nodeBackend/shared.ts`:
- Around line 3-25: Replace the object-shape type alias NodeBackendScreenConfig
with an interface following repo convention: change the exported declaration
from "export type NodeBackendScreenConfig = Readonly<...>" to "export interface
NodeBackendScreenConfig" and make its members readonly (e.g., readonly mode?:
"alt" | "inline"; readonly inlineRows?: number;), preserving the existing JSDoc
comments and the same optional semantics and validation notes for mode and
inlineRows.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 494c85db-7265-4a79-abae-f690109b6b02
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (33)
CLAUDE.mdREADME.mddocs/backend/node.mddocs/guide/screen-modes.mddocs/protocol/abi.mdexamples/inline-status/package.jsonexamples/inline-status/src/index.tsexamples/inline-status/tsconfig.jsonmkdocs.ymlpackages/core/src/abi.tspackages/native/src/config.rspackages/native/src/ffi.rspackages/native/src/tests.rspackages/native/vendor/VENDOR_COMMIT.txtpackages/native/vendor/zireael/include/zr/zr_config.hpackages/native/vendor/zireael/include/zr/zr_platform_types.hpackages/native/vendor/zireael/include/zr/zr_version.hpackages/native/vendor/zireael/src/core/zr_config.cpackages/native/vendor/zireael/src/core/zr_diff.cpackages/native/vendor/zireael/src/core/zr_diff.hpackages/native/vendor/zireael/src/core/zr_engine.cpackages/native/vendor/zireael/src/core/zr_engine_present.incpackages/native/vendor/zireael/src/platform/posix/zr_plat_posix.cpackages/native/vendor/zireael/src/platform/win32/zr_plat_win32.cpackages/node/src/__tests__/screen_mode.test.tspackages/node/src/__tests__/worker/testShims/screenExpectNative.tspackages/node/src/backend/backendSharedConfig.tspackages/node/src/backend/nodeBackend.tspackages/node/src/backend/nodeBackend/shared.tspackages/node/src/backend/nodeBackendInline.tspackages/node/src/index.tstsconfig.jsonvendor/zireael
| ]); | ||
| }); | ||
|
|
||
| app.view(() => SyncPanel({})); |
There was a problem hiding this comment.
Wrap the root view with ui.page({ p: 1 }, ...) (or ui.appShell()).
Line 52 returns the panel directly, which violates the root-view layout invariant.
Suggested fix
-app.view(() => SyncPanel({}));
+app.view(() => ui.page({ p: 1 }, [SyncPanel({})]));As per coding guidelines, Root view must use ui.page() or ui.appShell() with at least p: 1 spacing....
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| app.view(() => SyncPanel({})); | |
| app.view(() => ui.page({ p: 1 }, [SyncPanel({})])); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/inline-status/src/index.ts` at line 52, The root view currently
returns SyncPanel directly in the app.view handler which violates the root-view
layout invariant; modify the app.view registration (the app.view call) to wrap
the returned SyncPanel component with a root container such as ui.page({ p: 1 },
...) or ui.appShell(...), so that SyncPanel is passed as the child to
ui.page/ui.appShell (keep SyncPanel(...) as the inner component) and ensure p: 1
spacing is provided.
Source: Coding guidelines
| if (!zr_emit_sgr_absolute(sb, ts, zr_diff_baseline_style(), caps)) { | ||
| return false; | ||
| } | ||
| /* CR before LF claims: LF preserves the column in raw mode. */ | ||
| if (!zr_sb_write_u8(sb, ZR_ASCII_CR)) { | ||
| return false; | ||
| } | ||
| ts->cursor_x = 0u; | ||
| for (uint32_t claim_y = claimed; claim_y <= target_y; claim_y++) { | ||
| if (!zr_sb_write_u8(sb, ZR_ASCII_LF)) { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Reset OSC 8 along with SGR before creating inline blank baseline rows.
Line 754 and Line 1678 only restore the SGR baseline. If the previous cell left a hyperlink open, the subsequent LF/ED-generated blank area is modeled as baseline (link_ref == 0) while the terminal hyperlink state is still live until a later zr_diff_emit_link_transition(). That can leak link state across claimed rows or an inline baseline clear.
Possible fix
+static bool zr_emit_link_baseline(zr_sb_t* sb, zr_term_state_t* ts, const plat_caps_t* caps) {
+ if (!sb || !ts) {
+ return false;
+ }
+ if (caps && caps->supports_hyperlinks != 0u && !zr_emit_osc8_close(sb)) {
+ return false;
+ }
+ ts->style.link_ref = 0u;
+ return true;
+}
+
...
- if (!zr_emit_sgr_absolute(sb, ts, zr_diff_baseline_style(), caps)) {
+ if (!zr_emit_link_baseline(sb, ts, caps) ||
+ !zr_emit_sgr_absolute(sb, ts, zr_diff_baseline_style(), caps)) {
return false;
}
...
- if (!zr_emit_sgr_absolute(&ctx->sb, ts, baseline, ctx->caps)) {
+ if (!zr_emit_link_baseline(&ctx->sb, ts, ctx->caps) ||
+ !zr_emit_sgr_absolute(&ctx->sb, ts, baseline, ctx->caps)) {
return ZR_ERR_LIMIT;
}Also applies to: 1678-1684
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/native/vendor/zireael/src/core/zr_diff.c` around lines 754 - 766,
Before creating inline blank baseline rows you must also reset any open OSC 8
hyperlink state in addition to restoring SGR; update the block that currently
calls zr_emit_sgr_absolute(sb, ts, zr_diff_baseline_style(), caps) and then
writes CR/LF so that it also closes the hyperlink (invoke the hyperlink
transition/reset helper used elsewhere, e.g. zr_diff_emit_link_transition(...)
or the OSC8-reset routine) so the emitted blank rows have link_ref==0 and no
live terminal hyperlink; apply the same change in the other analogous spot that
currently only restores SGR.
| rc = zr_engine_sync_kitty_keyboard(e, &prospective_profile); | ||
| if (rc != ZR_OK) { | ||
| goto cleanup; | ||
| } | ||
| rc = zr_engine_set_config_prepare_arenas(e, cfg, &arena_frame_new, &arena_persistent_new, &want_arena_reinit); | ||
| if (rc != ZR_OK) { | ||
| goto cleanup; | ||
| } | ||
|
|
||
| rc = zr_engine_sync_kitty_keyboard(e, &prospective_profile); | ||
| rc = zr_engine_set_config_apply_viewport(e, viewport_changed, viewport_rows_new); | ||
| if (rc != ZR_OK) { |
There was a problem hiding this comment.
Rollback kitty keyboard state if config apply fails after sync.
At Line 2081 you sync kitty keyboard to the prospective profile before the final fallible step at Line 2085. If viewport resize fails, cleanup returns without commit, leaving terminal keyboard mode potentially out of sync with e->cfg_runtime / e->term_profile.
💡 Proposed fix
+ bool kitty_synced = false;
rc = zr_engine_sync_kitty_keyboard(e, &prospective_profile);
if (rc != ZR_OK) {
goto cleanup;
}
+ kitty_synced = true;
rc = zr_engine_set_config_apply_viewport(e, viewport_changed, viewport_rows_new);
if (rc != ZR_OK) {
goto cleanup;
}
@@
cleanup:
+ if (rc != ZR_OK && kitty_synced) {
+ (void)zr_engine_sync_kitty_keyboard(e, &e->term_profile);
+ }
zr_engine_set_config_release_resources(&res);
return rc;Also applies to: 2097-2099
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/native/vendor/zireael/src/core/zr_engine.c` around lines 2081 -
2086, You sync the terminal keyboard with the prospective profile via
zr_engine_sync_kitty_keyboard(e, &prospective_profile) before calling
zr_engine_set_config_apply_viewport; if that apply fails you currently goto
cleanup leaving the terminal in the new keyboard state while
e->cfg_runtime/e->term_profile were not committed. Change the flow so that when
zr_engine_set_config_apply_viewport returns non-ZR_OK you immediately rollback
the kitty keyboard to the original profile (e.g., call
zr_engine_sync_kitty_keyboard(e, &original_profile) or a dedicated revert
helper) before jumping to cleanup; apply the same rollback logic to the other
occurrence around lines 2097-2099 to ensure terminal keyboard state stays in
sync with e->cfg_runtime/e->term_profile on failures.
| test("backend: worker path forwards screen option to engineCreate", async () => { | ||
| const shim = new URL("./worker/testShims/screenExpectNative.js", import.meta.url).href; | ||
| const backend = createNodeBackendInternal({ | ||
| config: { | ||
| executionMode: "worker", | ||
| fpsCap: 60, | ||
| maxEventBytes: 1024, | ||
| screen: { mode: "inline", inlineRows: 6 }, | ||
| }, | ||
| nativeShimModule: shim, | ||
| }); | ||
|
|
||
| await backend.start(); | ||
| await backend.stop(); | ||
| backend.dispose(); | ||
| }); | ||
|
|
||
| test("backend: inline execution path forwards screen option to engineCreate", async () => { | ||
| const shim = new URL("./worker/testShims/screenExpectNative.js", import.meta.url).href; | ||
| const backend = createNodeBackendInternal({ | ||
| config: { | ||
| executionMode: "inline", | ||
| fpsCap: 60, | ||
| maxEventBytes: 1024, | ||
| screen: { mode: "inline", inlineRows: 6 }, | ||
| }, | ||
| nativeShimModule: shim, | ||
| }); | ||
|
|
||
| await backend.start(); | ||
| await backend.stop(); | ||
| backend.dispose(); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether PTY-oriented evidence/tests exist for inline screen semantics.
# Expected: at least one PTY-backed test or evidence artifact covering scrollback/final-frame behavior.
fd -i 'inline|screen-mode|screen_mode|pty|tmux' packages/node/src/__tests__ docs -t f
rg -n --iglob '**/*.test.ts' 'pty|tmux|scrollback|final frame|inline screen|screen mode' packages/node/src/__tests__Repository: RtlZeroMemory/Rezi
Length of output: 1554
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Identify which PTY tests exist
fd -i 'pty|tmux|screen_mode|screen-mode|inline' packages/node/src/__tests__ -t f
# Search for evidence of scrollback / final frame semantics inside PTY and screen-related tests
rg -n --iglob '**/*.test.ts' \
'scrollback|final frame|finalFrame|in scrollback|last frame|frame.*after burst|end frame|terminate.*scroll|render.*scroll|inline.*screen|screen mode' \
packages/node/src/__tests__
# Also look for PTY harness usage to understand backend-path realism
rg -n --iglob '**/*.test.ts' \
'runPtyScenario|ptyScenario|spawnPty|ptyTarget|tmux|node-pty|ptyEchoTarget|expect.*native|engineCreate|inlineRows|screen:\s*\{.*mode' \
packages/node/src/__tests__/ptyScenario.test.ts \
packages/node/src/__tests__/ptyTargetConfig.test.ts \
packages/node/src/__tests__/worker_integration.test.ts \
packages/node/src/__tests__/screen_mode.test.tsRepository: RtlZeroMemory/Rezi
Length of output: 2826
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect relevant sections with line numbers (small excerpts only)
echo "=== screen_mode.test.ts (full file size check) ==="
wc -l packages/node/src/__tests__/screen_mode.test.ts
echo "=== ptyScenario.test.ts (first 220 lines) ==="
wc -l packages/node/src/__tests__/ptyScenario.test.ts
sed -n '1,220p' packages/node/src/__tests__/ptyScenario.test.ts
echo "=== ptyTargetConfig.test.ts (first 240 lines) ==="
wc -l packages/node/src/__tests__/ptyTargetConfig.test.ts
sed -n '1,240p' packages/node/src/__tests__/ptyTargetConfig.test.ts
echo "=== worker_integration.test.ts (around line ~780-860) ==="
wc -l packages/node/src/__tests__/worker_integration.test.ts
sed -n '760,860p' packages/node/src/__tests__/worker_integration.test.ts
echo "=== testing harness entrypoints for PTY scenarios ==="
rg -n "runPtyScenario|ptyScenario|spawnPty|ptyEchoTarget|ptyTarget" packages/node/src/__tests__/testingHarness.test.ts packages/node/src/__tests__/ptyScenario.test.ts packages/node/src/__tests__/worker_integration.test.ts -SRepository: RtlZeroMemory/Rezi
Length of output: 10256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for scrollback/final-frame semantics across node tests
rg -n --iglob '**/*.test.ts' \
'scrollback|in scrollback|final frame|finalFrame|last frame|end frame|final.*frame|frame.*scroll|scroll.*frame' \
packages/node/src/__tests__
# Search for inline screen mode semantics in PTY scenarios/tests
rg -n --iglob '**/*.test.ts' \
'inlineRows|screen:\s*\{[^}]*mode:\s*"inline"|mode:\s*"inline"|screen-mode' \
packages/node/src/__tests__
# Inspect PTY scenario harness implementation
fd -i 'runPtyScenario' packages/node/src/testing packages/node/src/__tests__ -t f
fd -i 'ptyScenario' packages/node/src/testing -t f
# Print small relevant sections
for f in $(fd -i 'runPtyScenario' packages/node/src -t f); do
echo "=== $f (context) ==="
wc -l "$f"
rg -n 'function runPtyScenario|runPtyScenario\s*\(' "$f"
doneRepository: RtlZeroMemory/Rezi
Length of output: 2191
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show size
wc -l packages/node/src/testing/ptyScenario.ts
# Read key parts: imports + runPtyScenario definition + any assertions about terminal output/scrollback.
sed -n '1,240p' packages/node/src/testing/ptyScenario.ts
sed -n '240,520p' packages/node/src/testing/ptyScenario.tsRepository: RtlZeroMemory/Rezi
Length of output: 14757
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for scrollback/final-frame/in scrollback mentions in the PTY harness implementation
rg -n 'scrollback|in scrollback|final frame|finalFrame|last frame|end frame|page.*frame|frame.*after|after burst' packages/node/src/testing/ptyScenario.ts packages/node/src/testing/ptyHarness.* packages/node/src/testing -S || true
# Find where the harness evaluates results and compares to scenario expectations
rg -n 'evaluateScenarioResult|runResult|mismatches|validateScenarioDefinition|ScenarioRunResult|cursor|latestCursor|renderSeq|visible' packages/node/src/testing/ptyScenario.ts
# Show the tail end where results are computed
tail -n 120 packages/node/src/testing/ptyScenario.ts
# Inspect PTY harness transport: startPtyHarness
fd -i 'ptyHarness' packages/node/src/testing -t f
sed -n '1,220p' $(fd -i 'ptyHarness' packages/node/src/testing -t f | head -n 1)Repository: RtlZeroMemory/Rezi
Length of output: 9047
🏁 Script executed:
#!/bin/bash
set -euo pipefail
wc -l packages/node/src/testing/screen.ts
sed -n '1,220p' packages/node/src/testing/screen.ts
sed -n '220,520p' packages/node/src/testing/screen.ts
rg -n 'scrollback|final|snapshot\(|last|history|buffer' packages/node/src/testing/screen.tsRepository: RtlZeroMemory/Rezi
Length of output: 3775
Add PTY contract coverage for inline screen scrollback/final-frame semantics
screen_mode.test.ts only verifies config forwarding of screen: { mode: "inline" } via shims. The PTY harness snapshots the terminal with @xterm/headless using scrollback: 0 and reads only term.buffer.active lines, so it can’t provide evidence for scrollback preservation or “final frame in scrollback” behavior on the real backend path—matching the guideline requirement for PTY evidence when terminal-visible behavior depends on the backend.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/node/src/__tests__/screen_mode.test.ts` around lines 92 - 124, The
tests only assert config forwarding via shims but do not exercise the PTY
contract (scrollback/final-frame semantics); update or add tests to use the PTY
harness instead of only `@xterm/headless` snapshots with scrollback: 0 so you can
assert scrollback preservation and that the final frame is present in the
scrollback for both execution modes. Specifically, modify the existing tests
named "backend: worker path forwards screen option to engineCreate" and
"backend: inline execution path forwards screen option to engineCreate" (and/or
add new tests) to start createNodeBackendInternal with the same screen config
but drive the actual PTY harness used by the backend (not just
screenExpectNative.js shim), configure a nonzero scrollback in the headless
terminal, capture the full buffer (including scrollback) after backend runs, and
assert that the expected final frame lines appear in the scrollback; keep
references to createNodeBackendInternal, screenExpectNative.js, and the PTY
harness so reviewers can locate the changes.
Sources: Coding guidelines, Learnings
Summary
Adds inline screen mode to Rezi as a first-class app option: render a bounded region on the primary screen with terminal scrollback preserved above it and the final frame left in scrollback on exit — the presentation style of agent CLIs, progress UIs, and REPLs.
What changed
Vendor
8745d50, engine ABI 1.3.0) in the submodule, thepackages/native/vendor/zireaelbuild copy, andVENDOR_COMMIT.txt(integrity check green).Native binding (
@rezi-ui/native)plat_config_t.screen_mode(former pad byte),inline_rowson create/runtime configs, andzr_term_state_t(new engine fields) — the layout-verification test now pins the new sizes/offsets.plat.screenMode+inlineRows(camel/snake) on create and runtime paths.Core
Node backend (
@rezi-ui/node)NodeBackendConfig.screenoption ({ mode: "alt" | "inline", inlineRows }), validated withZrUiErrorand folded into the native config on both worker and in-process execution paths; the high-level option wins over rawnativeConfigpassthrough keys while preserving unrelatedplatkeys. Omittingscreenleaves config byte-for-byte unchanged.NodeBackendScreenConfig;createNodeAppforwardsconfig.screen.executionMode: "inline"(engine-on-main-thread); documented in all relevant places.Example + docs
examples/inline-status: runnable inline app (spinner, progress bar, quit keys).docs/guide/screen-modes.md(+ mkdocs nav), sections indocs/backend/node.md, ABI pin + note indocs/protocol/abi.md, README feature visibility, CLAUDE.md updates.Test evidence
supportsScrollRegion: false), ABI 1.2.0 request rejected (-4), invalid combos rejected (-1), alt mode unchanged, strict unknown-key rejection.Compatibility
screenoption) behavior is unchanged; all pre-existing tests pass unmodified.🤖 Generated with Claude Code