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>
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>
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>
* 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>
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.
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>