453 Commits

Author SHA1 Message Date
Yunfei He dee060bb76 feat: Static style prop, identity tracking, and rerender semantics
- 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>
2026-05-26 15:47:43 +08:00
Yunfei He a280b4f684 feat: add interactive mode detection and output stream coordination
- Auto-detect interactive mode via is-in-ci + stdout.isTTY
- writeToStdout/writeToStderr with frame clear/restore coordination
- Non-interactive mode: static immediate, dynamic at unmount
- Cursor position tracking for Phase 5 useCursor integration
- useStdout/useStderr route through coordinated context methods

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:34:41 +08:00
Yunfei He 1a888a8eb1 feat: add usePaste composable with bracketed paste mode
- 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>
2026-05-26 15:25:07 +08:00
Yunfei He f51079a5ea feat: rewrite stdin pipeline with input-parser, exitOnCtrlC via \x03
- 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>
2026-05-26 14:04:39 +08:00
Yunfei He 283444e87b feat: port parse-keypress (semantic mapper) from Ink
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>
2026-05-26 13:55:30 +08:00
Yunfei He ab2444fcb0 feat: port input-parser (chunk splitter) from Ink
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>
2026-05-26 13:52:01 +08:00
Yunfei He 1106be867b feat: add error boundary with onErrorCaptured to catch descendant errors
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>
2026-05-26 13:48:23 +08:00
Yunfei He 272f1f0c32 refactor!: tree validation throws on invalid nesting instead of warn+skip
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>
2026-05-26 13:37:39 +08:00
Yunfei He 57887eefe1 feat: port ANSI sanitizer and integrate into paint pipeline
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>
2026-05-26 13:29:33 +08:00
Yunfei He f6bd888444 feat: port ANSI tokenizer from Ink
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>
2026-05-26 13:27:29 +08:00
Yunfei He d0df28850c fix: prepend transform in paintNode to match Ink inner-first nesting order
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>
2026-05-26 13:23:03 +08:00
Yunfei He 957d30f43a feat: rewrite Output class with StyledChar[][] grid for correct emoji/wide char handling
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>
2026-05-26 13:19:50 +08:00
Yunfei He 8b834c2b7f chore: add @alcalzone/ansi-tokenize dependency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:10:22 +08:00
Yunfei He 37c4905bbc fix: warn and skip insertion when Box is nested inside Text
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>
2026-05-26 01:04:16 +08:00
Yunfei He 45b9ed3d9d fix: use EDGE_START/EDGE_END for marginLeft/marginRight matching Ink
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>
2026-05-26 01:02:49 +08:00
Yunfei He d86abe0fb1 fix: Newline renders as yoga carrier (text node) when standalone
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>
2026-05-26 01:02:02 +08:00
Yunfei He e79c3660f5 test: verify error boundary routes post-mount errors through exit()
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>
2026-05-26 01:00:56 +08:00
Yunfei He 42c69d72f2 feat: hide cursor on mount, show on unmount (matching Ink)
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>
2026-05-26 01:00:17 +08:00
Yunfei He 8585598d14 fix: make Transform a yoga carrier matching Ink's ink-text behavior
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>
2026-05-26 00:59:03 +08:00
Yunfei He 8565f41827 feat: add time-based render throttle (~30fps) matching Ink
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>
2026-05-26 00:55:02 +08:00
Yunfei He b8778c1847 fix: Text node defaults to flexDirection='row' matching Ink's ink-text
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>
2026-05-26 00:52:37 +08:00
Yunfei He 2e1493b785 test: verify text wraps correctly within border/padding content area
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>
2026-05-26 00:51:27 +08:00
Yunfei He c4161c99a7 fix: Box defaults to flexDirection='row', flexShrink=1, flexWrap='nowrap', flexGrow=0
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>
2026-05-26 00:49:27 +08:00
Yunfei He 8351712ea2 chore: gitignore local yoga bugs doc
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:16:02 +08:00
Yunfei He 9d0c59c641 feat: expand key parser with home, end, ctrl+arrow, meta+arrow
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>
2026-05-26 00:12:05 +08:00
Yunfei He bcfdaddec7 feat: exit() accepts result value, waitUntilExit resolves with it
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>
2026-05-26 00:10:52 +08:00
Yunfei He 97ee43ea14 feat: support Transform inside Text for inline text transformation
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>
2026-05-26 00:08:44 +08:00
Yunfei He bbaa77bce1 feat: support per-axis overflowX/overflowY clipping
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>
2026-05-26 00:04:57 +08:00
Yunfei He fa60d12f5a fix: handle null/undefined Text children without crashing yoga
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>
2026-05-26 00:01:32 +08:00
Yunfei He 9e0bdfcb97 fix: use Object.hasOwn in isYogaProp to avoid Object.prototype false positives
"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>
2026-05-25 23:58:04 +08:00
Yunfei He 3565c5dfb5 fix: clean up ChildProcess type workaround in CLI
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>
2026-05-25 23:34:49 +08:00
Yunfei He 5e54e5742d refactor: replace ref() with shallowRef() in all test files
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>
2026-05-25 23:28:06 +08:00
Yunfei He 6e01954cf9 fix: resolve vpr ready failures
- 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>
2026-05-25 23:19:55 +08:00
Yunfei He 07cfe5d6fb fix: align wrap mode with Ink (trim: false)
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>
2026-05-25 23:14:17 +08:00
Yunfei He 630ee8488a chore: pin vite-plus version, add strip-ansi to catalog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:06:59 +08:00
Yunfei He 664b568536 fix: align overflow and type behavior with Ink
- 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>
2026-05-25 23:05:14 +08:00
Yunfei He 771b8ef180 fix: emit empty frames so components rendering null produce output
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>
2026-05-25 22:55:21 +08:00
Yunfei He 04707e82d9 fix: intersect all clip rects in stack for nested overflow:hidden
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>
2026-05-25 22:53:41 +08:00
Yunfei He da7c843d44 feat: implement overflow clipping in paint pipeline
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>
2026-05-25 22:47:10 +08:00
Yunfei He 07a851ce2a fix: add flexShrink=1 default to Text and sub-1px measure guard
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>
2026-05-25 22:44:26 +08:00
Yunfei He 5e59a3a28e fix: add explicit flexDirection="row" to align-self multi-text tests
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>
2026-05-25 22:40:55 +08:00
Yunfei He d990f0f1ee fix: correct space-around expected value to match Ink and yoga output
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>
2026-05-25 22:37:29 +08:00
Yunfei He d14c11e2d2 fix: correct flex basis column test expectations to match Ink output
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>
2026-05-25 22:37:10 +08:00
Yunfei He 5a1f2eba2d fix: trim trailing whitespace per line in paint output to match Ink behavior
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>
2026-05-25 22:36:48 +08:00
Yunfei He d1c9945cc4 test: port Ink lifecycle, error, and resize tests (15 tests)
Covers exit lifecycle, error handling, and terminal resize
reactivity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:14:35 +08:00
Yunfei He 1354cb1b7c test: port Ink input and navigation tests (17 tests)
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>
2026-05-25 22:09:04 +08:00
Yunfei He 3ff03a1895 test: port Ink focus tests (22 tests)
Covers tab/shift-tab cycling, auto-focus, per-item disable,
enable/disable focus globally, unmount behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:06:06 +08:00
Yunfei He 562b07b60e test: port Ink component tests (38 tests)
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>
2026-05-25 22:01:10 +08:00
Yunfei He 8c4a3b2c9d test: port Ink background and border-background tests (29 tests)
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>
2026-05-25 21:48:50 +08:00
Yunfei He ce9ed4c399 test: port Ink border tests (48 tests)
Covers border styles, edge hiding, colors, dim, per-side dim,
nested boxes, wide chars, emojis, and reactive updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:43:08 +08:00