Commit Graph

26 Commits

Author SHA1 Message Date
Yunfei He 9fe6d7cc44 refactor(runtime): author public components as template SFCs (+ integrate main)
Rewrites Box/Text/Spacer/Static/Newline from h()/render functions to Vue
<script setup> template SFCs (Transform stays a render fn — it inspects its own
child vnodes), with vue-tsc-verified consumer types (template + JSX fixtures),
provide/inject text context, the always-validate Text divergence (color +
backgroundColor), and three renderer fixes the SFCs surfaced (static anchor skip,
transform line-index Ink-parity, useBoxMetrics subtree drill). Integrates the five
main commits landed after the branch point: #163 public-API audit, generic Static
scoped-slot typing, foreground color validation, useWindowSize/divergence docs.

Squashed from the SFC sub-commits + the two main-integration merges to keep a
linear, rebaseable history. See PR #165 for the full breakdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:49:08 +08:00
Yunfei He e68e36f26b fix(deps): bump is-in-ci catalog ^1.0.0 → ^2.0.0, matching Ink (#108)
is-in-ci feeds the `interactive` default (render.ts:503) and shouldSynchronize
(write-synchronized.ts:9). vue pinned ^1.0.0 vs Ink's ^2.0.0, whose CI-detection
formula differs: v1 scans for any `CI_*`-prefixed var and gates the whole expression
on CI not being falsy; v2 independently checks `CI` and `CONTINUOUS_INTEGRATION` and
drops the prefix scan. The common cases (local CI=false, GitHub CI=true) are identical
on both, so the suite and CI are unaffected; only edge env configs diverge. Bumping to
^2.0.0 makes vue's CI detection byte-identical to Ink's. Locked by a test
(CI=false + CONTINUOUS_INTEGRATION=true → true, which v1 would report false).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:58:25 +08:00
Yunfei He 9b5eac3ba4 feat(runtime): port ErrorOverview to Ink's full error frame (parity) (#81)
ErrorOverview was a one-line `Name: message` stub; Ink v7.0.4 renders a full
ERROR overview. Ports it faithfully:

- white-on-red ` ERROR ` label + message, dim cwd-relative file:line:column origin
- a code excerpt around the throwing line (padded gutter, error line highlighted,
  `Line N` / `Line N, error` aria-labels)
- the parsed stack (`- fn (file:line:col)`, cwd-relative, StackUtils nodeInternals
  filtering, unparsable-line fallback, fs.existsSync guard)

Adds code-excerpt@4.0.0 + stack-utils@2.0.6 (the versions Ink uses). The error
boundary now keeps the raw thrown value for display (Ink stores the raw value;
ErrorOverview renders a stack only when one exists) -- so a non-Error throw no
longer shows a misleading synthetic framework stack. The exit/reject path still
receives a wrapped Error (semantics unchanged). The unparsable-stack fallback
emits a literal backslash-t to match Ink's JSX.

Showing String(value) for a non-Error message (vs Ink's blank) is a documented
additive divergence (ink-divergences.md).

Adds 4 error-overview tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:26:02 +08:00
Yunfei He 216a7021a0 fix(runtime): restore terminal on signal exit via signal-exit (Ink parity, G18, HIGH) (#47)
* fix(runtime): restore terminal on signal exit via signal-exit (Ink parity, G18)

Previously nothing routed a process signal to teardown(): SIGINT-as-signal,
SIGTERM or SIGHUP killed the process with the cursor hidden, the alternate
screen active and raw mode on, leaving the terminal corrupted.

Mirror Ink (ink.tsx:426): register signal-exit's onExit(teardown,
{alwaysLast:false}) at interactive mount, storing the unsubscribe fn, and
call it first thing in teardown() (ink.tsx:765) so the handler is removed on
unmount()/exit() and can't leak or double-run. teardown() stays idempotent
(teardownStarted guard) so a signal-triggered teardown plus a later unmount
won't double-run, and we don't prevent the process from exiting. Only the
live interactive, non-debug mount registers — render-to-string /
non-interactive paths never touch process signal handlers; registration is
guarded against double-registration.

Uses signal-exit v4 (named onExit export; ships ESM + types, so no
@types/signal-exit needed). PTY test sends SIGINT/SIGTERM/SIGHUP to a mounted
alt-screen app and asserts the captured output ends with show-cursor
(\x1b[?25h) + leave-alt-screen (\x1b[?1049l).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Review follow-ups (3 fixes): register signal-exit whenever interactive
(drop the !debug gate so debug-but-interactive apps, which still enter the
alt-screen/hide the cursor, restore on signal — Ink ink.tsx:426); add
!teardownStarted to the registration so a spent app instance does not
re-register on a same-instance remount (the next unmount() returns early at
the teardownStarted guard before it could unsubscribe — a leak); and make
the PTY test prove the SIGNAL drove teardown (fixture never self-unmounts, so
restore bytes can only come from the signal path) with a debug-mode signal
test, an exit-anchored waitForOutput drain, and a bounded retry for the
async-flush race under saturated runners.

Review follow-ups (2 fixes): synchronous restore flush on signal — the
signal-exit teardown path now writes the restore escapes (show-cursor,
leave-alt-screen, disable-kitty) via fs.writeSync to the stdout fd so they
reach the terminal before signal-exit re-raises the signal (a buffered async
stream.write could be lost on abrupt exit); the normal unmount path keeps async
writes. Removed the config-wide retry:3 from vitest.pty.config.ts (it masked
the whole PTY suite) and scoped a retry:2 to the signal-teardown describe only,
for the residual parent-side node-pty onData read-race under a saturated runner.

* chore(parity): ledger — G18 pr-open

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:04:51 +08:00
Yunfei He 0f60da7ec5 build(cli): add tsconfig and typescript dev dep for type-checking
The cli package had no tsconfig, so it was never type-checked under its own
config. Add a tsconfig (matching the other packages, minus JSX which cli does
not use) and the typescript dev dep so `tsc --noEmit` can run here — a
prerequisite for the upcoming check:type script.
2026-05-29 16:54:13 +08:00
Yunfei He 5c60a0af63 fix: upgrade text stack to grapheme-aware slicing/width (closes #21 grapheme classes)
Bump slice-ansi@9, string-width@8, wrap-ansi@10 and add cli-truncate@6
(both the runtime dep and the pnpm catalog entry for string-width). Rewrite
wrapText truncate variants to delegate to cli-truncate, matching Ink's
wrap-text.ts: grapheme clusters (ZWJ emoji, combining marks) stay whole and
newlines are preserved. Adjust the horizontal-clip left-edge compensation in
paint.ts because slice-ansi@9 drops a straddling wide grapheme whole rather
than splitting it, so lineX must advance by the actually-dropped width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:57:52 +08:00
Yunfei He 1ffb847a65 feat: test parity final push — 64 new tests, BSU/ESU, clear() API
Closes remaining test gaps between vue-tui and Ink:

Features:
- Add synchronized output (BSU/ESU) via DEC private mode 2026
- Add clear() API to TuiApp for erasing rendered output

Bug fixes:
- Cancel scheduler trailing timer on teardown (prevents stale commits)
- Guard teardown writes against ended/destroyed streams
- Reorder teardown to cancel timer before final commit

Tests (64 new, 2 skipped for known feature gaps):
- 7 BSU/ESU shouldSynchronize tests
- 5 borderBackgroundColor tests
- 13 component edge cases (empty text, number child, OSC hyperlink
  wrap-width, bare-text-in-Box validation, transform multi-line,
  leading whitespace, link escape closing)
- 13 text-width/CJK tests (alignment, truncation, overlay edge cases)
- 4 throttle + unmount edge cases
- 10 waitUntilRenderFlush write-callback-level tests + 1 clear() test
- 3 exit re-entrance tests
- 5 PTY #450 regression tests + 4 inline #450 tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:18:23 +08:00
Yunfei He 162e022b7f refactor: convert coding-agent example from JSX to SFC templates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:57:53 +08:00
Yunfei He 8befc0d0de fix: rewrite PTY tests to use node-pty directly, upgrade to 1.2.0-beta.13
node-pty 1.1.0's POSIX_SPAWN_CLOEXEC_DEFAULT flag fails on macOS 26 (Tahoe).
Beta.13 fixes this. Rewrote helpers to match Ink's node-pty architecture,
removed python pty-spawn and force-tty workarounds, added check-pty guard
that skips all 82 tests when node-pty is unavailable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 01:08:25 +08:00
Yunfei He 34031f35b6 test: add erase + fullscreen PTY fixtures + 8 tests
- erase.tsx, erase-with-static.tsx, erase-with-state-change.tsx
- fullscreen-no-extra-newline.tsx, issue-442-full-height.tsx
- render.test.ts with clearTerminal/fullscreen assertions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:06:23 +08:00
Yunfei He 7403522c96 feat: port Ink's log-update and replace npm log-update dependency
Port Ink's self-built log-update with standard and incremental rendering
modes, cursor positioning, and sync support. Rewrite frame-writer to use
the new internal log-update instead of the npm package, adding sync,
setCursorPosition, and willRender methods while preserving existing
write/done/clear behavior. Remove the npm log-update dependency (-6
packages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:51:20 +08:00
Yunfei He 4a9d401b0f feat: port cursor-helpers from Ink for cursor positioning escape sequences
Add ansi-escapes to pnpm catalog and runtime dependencies. Port all
cursor helper functions (cursorPositionChanged, buildCursorSuffix,
buildReturnToBottom, buildReturnToBottomPrefix, buildCursorOnlySequence)
with 10 colocated unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:48:03 +08:00
Yunfei He bc4bea93bd chore: add node-pty and tsx deps, PTY test directory structure
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:05:33 +08:00
Yunfei He 222b520678 feat: add useWindowSize alias, terminal-size fallback, update public API + README
- useWindowSize alias for useTerminalSize (Ink API naming parity)
- terminal-size fallback when stdout.columns/rows are 0/missing
- Public API test covers all new exports
- README documents all MountOptions and composables

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:28:46 +08:00
Yunfei He 3ee01dc278 feat: add patchConsole, waitUntilRenderFlush, onRender, and maxFps
Add four behavioral surfaces to the runtime:

- patchConsole (default: true): intercepts console.* methods to route
  output through writeToStdout/writeToStderr, preventing frame corruption.
  Disabled in debug mode. Restored before Vue cleanup on teardown.
- waitUntilRenderFlush(): flushes pending throttled renders and waits
  for the stdout write barrier, exposed on TuiApp and testing RenderResult.
- onRender callback: fires after each commit with { renderTime } in ms.
- maxFps option: overrides the scheduler's default 32ms throttle interval
  with 1000/maxFps. The scheduler now accepts a throttleMs option and
  exposes hasPending() for flush coordination.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:54:18 +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 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 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 0209d5b5aa test: port Ink text tests (13 passing, 4 skipped)
Adds 17 text tests from Ink: color (standard/hex/rgb/ansi256),
background color, dim, bold, inversion, reactive remeasure, and
edge cases. 4 tests skipped due to null-children yoga crash and
constructor text lookup bug.

Also fixes lastFrame() treating empty-string frames as undefined
(Codex review feedback) and adds strip-ansi dev dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:39:10 +08:00
Yunfei He ff75506371 chore: add chalk to workspace catalog, update deps
Adds chalk to pnpm-workspace catalog (runtime-tests referenced
catalog:chalk but it was missing). Also updates vite-plus deps
to fix vitest JSX transform regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:33:24 +08:00
Yunfei He 8e6c4c5c2b fix(paint): backgroundColor now visible with borders and text
Root causes:
- drawBorder wrote plain characters, overwriting fillBackground's ANSI codes
- renderTextWithInlineStyles spread node.props over inherited bg, but
  undefined props from Vue clobbered the inherited backgroundColor
- Child text nodes did not inherit backgroundColor from parent Box

Fixes:
- drawBorder applies backgroundColor (and borderColor) to border chars
- Filter out undefined props before merging in renderTextWithInlineStyles
- Pass inheritedBg through the paint tree to text nodes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:11:54 +08:00
Yunfei He 795251a080 feat(examples): scaffold coding-agent example
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:26:21 +08:00
Yunfei He 020fd0d85a feat: add basic-jsx and basic-template examples
- basic-jsx: Counter + Clock with defineComponent() + JSX
- basic-template: Counter + Clock with .vue SFC + <template>
- Both demonstrate HMR via vue-tui dev
- Fix vite-plugin: strip build.lib and external config for bundledDev,
  add node builtins resolveId to prevent browser-external shimming
- Remove old hmr-demo example (superseded by basic-template)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:21:08 +08:00
Yunfei He 2b7d1250f4 feat(cli): scaffold @vue-tui/cli package with all modules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:55:46 +08:00
Yunfei He def33588e3 chore: monorepo scaffold and shared config
Workspace setup with pnpm, Vite+, and shared TypeScript config.
Includes @vue-tui/utils and AGENTS.md project conventions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:13:22 +08:00