- Add style prop to Static (default: position absolute, flexDirection column)
- Apply style props to isolated static paint (padding, flexDirection, etc.)
- Track staticNode/previousStaticNode for identity changes
- Reset fullStaticOutput on Static unmount/remount
- Convert all 6 Static todo tests to active passing tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Ref-counted bracketed paste mode (\x1b[?2004h/l)
- usePaste acquires raw mode + paste mode, listens on paste channel
- Paste falls through to useInput when no paste listeners exist
- isActive option for conditional paste listening
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace data event with readable + input-parser state machine
- Intercept \x03 in input pipeline (not process SIGINT)
- useInput now uses parse-keypress for semantic key mapping
- 20ms pending escape flush timer matching Ink
- Esc focus reset in input pipeline
- Delete old key-parser.ts (replaced by input-parser + parse-keypress)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's parse-keypress.ts to map raw input event strings to semantic
key objects with name, ctrl, shift, meta, and kitty protocol support.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's stateful input parser that splits raw stdin chunks into
discrete input events. Supports CSI/SS3 sequence parsing, double-ESC
prefix, bracketed paste, backspace byte splitting, and pending state
for incomplete escape sequences.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap user root component in an InternalErrorBoundary that catches all
descendant errors (setup, render, lifecycle) via onErrorCaptured and
routes them through exit(error). This prevents yoga WASM state corruption
that occurred when errors propagated uncaught during Vue's render phase.
- Add ErrorOverview component for rendering error frames
- Replace try-catch around mount with onErrorCaptured boundary
- Update testing render() to detect early exit errors
- Convert 3 todo tests to active tests (nested setup, unhandledRejection, post-mount)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change Box-in-Text validation from dev-only warn+skip to unconditional
throw, matching Ink's reconciler behavior. Add text-outside-Text
validation that throws when non-empty text-leaf nodes are inserted into
box/root/static containers outside a Text context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's sanitize-ansi.ts to strip non-SGR/non-OSC ANSI escape
sequences (cursor movement, screen clearing) from text nodes.
Integrated as the final step of renderTextWithInlineStyles() so all
squashed text is sanitized before layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's ansi-tokenizer.ts (509 lines) as a standalone ANSI escape
sequence parser that produces typed tokens. Exports tokenizeAnsi() and
hasAnsiControlCharacters(). Supports CSI, ESC, OSC, DCS, PM, APC, SOS,
C1 control characters, string terminator, and invalid/malformed tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Transforms were appended (`[...transformers, node.transform]`) causing outer
transforms to execute before inner ones. Ink prepends so inner transforms run
first and outer transforms wrap the result. Adds a nesting-order regression test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's output character model using @alcalzone/ansi-tokenize. Each cell is now a
StyledChar with proper grapheme clustering. Fixes emoji alignment, wide char boundaries,
and changes clipping to top-only (matching Ink). Upgraded ansi-tokenize to ^0.3.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detect invalid <Box> inside <Text> nesting at the DOM insert level and
emit a dev warning instead of crashing the WASM yoga engine. The box
insertion is skipped to prevent layout corruption.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink uses Yoga.EDGE_START for marginLeft and Yoga.EDGE_END for
marginRight, which is correct for RTL-aware layout. Changed from
EDGE_LEFT/EDGE_RIGHT to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match Ink's behavior where Newline renders as ink-text (yoga carrier).
When inside a Text parent, Newline still renders as virtual-text for
inline behavior. When standalone, it renders as a text node so it
participates in yoga layout and occupies vertical space.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vue's errorHandler (installed after mount) already catches render-time
errors from nested components and routes them through exit(err),
providing error boundary behavior matching Ink's design.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Write ESC[?25l to stdout after mount and ESC[?25h during teardown,
matching Ink's cursor management. Only active in production mode
(debug: false) to avoid interfering with test streams.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Transform nodes now participate in yoga layout with flexShrink=1,
flexDirection='row', matching Ink's Transform which renders as ink-text.
This fixes multi-line text under Transform not getting proper layout
height. Transform nodes inside Text parents remain inline (excluded
from yoga tree) to preserve renderTextWithInlineStyles behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add leading+trailing throttle to the commit scheduler in production mode.
In debug/test mode (immediate: true), commits fire without delay to
preserve test determinism. Adjusted the non-debug static test to account
for the throttle timer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set flexDirection=row, flexShrink=1, flexGrow=0 on text yoga nodes at
creation time, matching Ink's <ink-text> style defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yoga's measure function already receives the content-area width (minus
parent border/padding), so text wrapping is correct without additional
adjustment in the paint pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match Ink's Box defaults by setting yoga properties at node creation time.
This ensures proper row-based layout regardless of Vue's prop patching order.
Updated affected test snapshots and added explicit flexDirection="column"
where tests relied on the old column default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add parsing for Home (ESC[H, ESC[1~, ESC[7~), End (ESC[F, ESC[4~,
ESC[8~), Ctrl+arrow (CSI 1;5 A/B/C/D), and Meta+arrow (double-ESC
prefix) key sequences. Add home/end fields to the Key interface.
Implement 12 new useInput tests for these key combinations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change exit(error?: Error) to exit(errorOrResult?: unknown). When an
Error is passed, waitUntilExit rejects; otherwise it resolves with the
value. This matches Ink's behavior where exit("hello") resolves the
promise with "hello".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update renderTextWithInlineStyles and flattenLeaves to handle transform
nodes nested inside text elements, matching Ink's squashTextNodes
behavior. Skip transform application on empty text to avoid wrapping
empty strings. Skip writing empty text nodes to avoid applying line
transformers to empty content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add independent horizontal/vertical clip support in the paint pass,
matching Ink's per-axis overflow behavior. Add overflowX/overflowY to
STYLE_PROPS so they are stored in el.props for the paint pass.
Implement 26 new overflow tests translated from Ink.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guard flattenLeaves and renderTextWithInlineStyles to skip comment
nodes (produced by Vue for null/undefined children) and return empty
strings for childless text nodes. Also guard bindTextMeasure to return
zero dimensions for empty text. Expand TuiInlineNode to include
TuiComment since Vue inserts comments into text containers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"constructor" in {} returns true because it hits Object.prototype.
Using Object.hasOwn ensures only actual yoga prop keys are recognized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace ugly `as unknown as EventEmitter` casts with a proper
type alias `Process = ChildProcess & NodeJS.EventEmitter`.
@types/node@25 removed .on() from ChildProcess class declaration
even though it extends EventEmitter at runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AGENTS.md requires shallowRef by default. All test state uses
reassignment (not mutation), so shallowRef is correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix unhandled promise in coding-agent example (void operator)
- Fix unhandled promise in cli/dev.ts (void operator)
- Fix ChildProcess.on() type error in cli/process-manager.ts
(@types/node v25 removed .on() from ChildProcess type)
- Change example build scripts from "vite build" to "vp build"
(vite resolves to vite-plus-core which has no CLI bin)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink's wrap mode uses { trim: false } to preserve leading/trailing
whitespace on wrapped lines. We incorrectly used trim: true which
stripped whitespace. Updates unit test and inline snapshots.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove yoga setOverflow call — Ink only clips visually in paint,
not at the yoga layout level. Calling setOverflow(HIDDEN) prevented
nodes from expanding, diverging from Ink's behavior.
- Fix AlignContent type to exclude baseline (not valid for align-content
in CSS/Yoga, and Ink doesn't allow it).
- Keep border background fallback to backgroundColor — Ink achieves
the same visual result via background fill underneath borders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The frame writer initialized lastFrame to "" and also reset it to ""
on clear(). Since commit() calls clear() then write(), any component
that painted to an empty string (e.g. Transform with no children, or
a component returning null after unmounting children) would be
deduplicated against the cleared state and never emitted.
Use null as the sentinel for "no previous frame" so the first write
after a clear always goes through, even when the frame is empty.
Fixes: Transform with null/undefined children tests
Fixes: Focus unmount crash tests (stale frame persisted)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously Output.get() only used the last clip rect (clips.at(-1)),
which meant an inner overflow:hidden box wider than its outer ancestor
could effectively loosen the outer clip. Now all active clips are
intersected via reduce, and writes are skipped entirely when the
intersection is empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port Ink's clip/unclip approach to vue-tui's paint output. When a box
has overflow="hidden", content painted by its children that falls
outside the box's content area (inside borders) is clipped.
Implementation:
- Add ClipRect, clip(), and unclip() to the Output class
- Maintain a clip stack; active clip is applied to each write op
- Horizontal clipping uses slice-ansi for correct ANSI handling
- Vertical clipping slices the lines array
- Store "overflow" in STYLE_PROPS so it's available in node.props for
the paint pass
Also adds explicit flexDirection="row" to two overflow tests where
multiple child boxes should lay out side-by-side (Ink defaults to row,
vue-tui defaults to yoga's column).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink's <Text> sets flexShrink: 1 by default so text nodes shrink when
they overflow their container in no-wrap flex rows. Without this,
vue-tui's text nodes had flexShrink=0 (yoga default) and would never
shrink, causing overflow tests to fail.
Also ports Ink's measureTextNode guard: when yoga asks if text can fit
in a sub-1px space during shrink calculations, return the natural
dimensions to prevent text from wrapping to infinite height.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink's Box defaults to flexDirection="row", so multiple <Text> children
lay out side-by-side. Vue-tui uses yoga's default (column). Rather than
changing the global default and breaking 20+ existing tests, add
explicit flexDirection="row" to the inner boxes in these two tests so
they match the intended layout behavior being verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The space-around alignment distributes space as 1-row gap / AB / 2-row
gap / CD / 1-row gap for a 6-row container with 2 content rows.
Expected value was "\nAB\n\nCD\n\n" but Ink produces "\nAB\n\n\nCD\n".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
height=6 containers produce 6 lines. Ink's renderToString returns
"A\n\n\nB\n\n" (preserving trailing blank lines). The tests incorrectly
expected "A\n\n\nB" with a comment claiming lastFrame({ trimLines: true })
trims trailing newlines — it does not; it only trims trailing spaces per
line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink's Output.get() calls .trimEnd() on each line of the rendered grid.
Without this, vue-tui's paint grid always fills the full root width with
spaces, causing raw frame output to contain 100-char-wide padding. This
broke tests using { raw: true } and made meaningful trailing-space
assertions impossible with { trimLines: true }.
Also corrects test assertions for 'set width in percent' and 'relative
position' that expected trailing spaces which Ink never produces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers basic character input, arrow keys, return, escape,
backspace, tab, and isActive option.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Distributes across text, static, transform, newline-spacer,
error-handling, and prop-reset test files.
Tests requiring features not yet in vue-tui are marked test.todo:
- <Transform> inside <Text> (paint pass limitation)
- multi-line transform (transform nodes not yoga carriers)
- text/box nesting validation (no runtime validation yet)
- complex Static rerender patterns (Ink-specific API)
Also fixes wrap="hard" to use wordWrap:false so text breaks at exact
character boundaries rather than word boundaries, matching Ink behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers inheritance, override, hex/rgb/ansi256 colors, wide chars,
emojis, fill area, fill with border/padding, border backgrounds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>