Commit Graph

254 Commits

Author SHA1 Message Date
Yunfei He eaf333a5ea fix(runtime): unmount written <Static> items to match Ink (G01) (#34)
* fix(runtime): unmount written <Static> items to match Ink (G01)

Ink's <Static> renders `items.slice(index)` and advances `index` to
`items.length` in a post-commit `useLayoutEffect`, so once an item has been
painted it is removed from the tree and its component unmounts. vue-tui kept
every Static item mounted forever: the component always mapped the full
`props.items`, and write-once was enforced only at flush time via a positional
`writtenCount` slice — the item components never tore down.

Now the <Static> component owns a `cursor` (Ink's `index`) and renders only
`items.slice(cursor)`. The renderer advances the cursor AFTER a commit has
painted the fresh items, via an `onWritten` callback registered on the host
static node — the vue-tui analogue of Ink's post-commit layout effect. This
ordering guarantees items are written before they are sliced out and unmounted,
so no item is ever lost or re-painted.

Write-once bookkeeping moved from a positional `writtenCount` to a
`writtenNodes` Set keyed by host-node identity. A single logical item expands to
several host nodes (the <Text>/<Box> plus empty text-leaf fragment anchors Vue
inserts), so a positional count mis-sliced once the cursor advanced; identity
tracking is anchor-agnostic. The shared `paintStaticNode` helper paints children
not yet in the set, records them, prunes unmounted entries, then fires
`onWritten`; render.ts, render-to-string.ts and flushStatic all use it.

Make the cursor mirror Ink fully so it can DECREASE, not just increase.
`onWritten` now SETS the cursor to items.length (was max-with-current), and a
length watch lowers it on shrink — needed because a shrink that leaves the
already-sliced children empty produces no host mutation, hence no commit/
onWritten to re-sync. Without this, [A,B] (cursor→2) → [A] → [A,C] sliced(2)=[]
and silently dropped C. paintStaticNode now always prunes and calls onWritten
(even on empty commits), painting only when there are fresh children.

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

* chore(parity): ledger — G01 pr-open, reconcile G12 merged

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 01:01:08 +08:00
Yunfei He 144db33d0b fix(runtime): use terminal-size fallback when stdout reports 0 cols/rows (Ink parity, G12) (#33)
Replace three `stdout.columns ?? 80` / `stdout.rows ?? 24` spots in render.ts with
`resolveSize(stdout).columns/rows`. The `??` guard only falls back on null/undefined,
not on 0 — so non-TTY environments where stdout reports 0 columns would collapse Yoga
layout to width 0. Ink's `getWindowSize` (utils.ts:8-23) uses a truthy guard
(`if (columns && rows)`) and a fallback chain through terminal-size → 80/24 defaults.
`resolveSize()` in useTerminalSize.ts already implements this chain; now exported and
used by the renderer. The non-TTY viewportRows → 24 branch is preserved (Ink-aligned).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 00:24:02 +08:00
Yunfei He 78ec54977e chore(parity): refute G06 (false positive) + reconcile G08 merged (#32)
G06 re-verification: the audit claimed <Transform>'s fn gets a hardcoded
index 0 "instead of the childNode index". Ink's index (output.ts:230-239)
is the LINE index, applied per output line — not a child index; the audit
misread it. vue-tui already produces correct per-line line indices for
multi-line transforms (existing tests "transform with multiple lines" and
transform-yoga pass unmodified). paint.ts:314's transform(innerText, 0) is
only the inline <Transform>-in-<Text> path (single logical line, 0 matches
Ink). No observable gap — marked refuted, not fixed.

Also reconciles G08 -> merged (landed in #31).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:46:45 +08:00
Yunfei He f9f72f7f68 fix(runtime): make useFocus react to id prop changes (Ink parity, G08) (#31)
Ink derives the focus id via useMemo(() => customId ?? random, [customId])
and keys its add/remove effect on [id], so changing the id prop re-registers
the component under the new id. vue-tui captured `const id = options.id ?? …`
once at setup (and typed id as a plain string), so it never reacted.

- Widen id to MaybeRefOrGetter<string>.
- Track the current registration and re-register (unsubscribe/remove old,
  subscribe/add new, re-apply active state) in a watcher keyed on the resolved
  id, mirroring Ink's [id] effect. isActive handling unchanged.

Test (test-first, verified red before the fix): focus is driven purely by
focus(id) (no Tab, which would focus by position and mask the bug); changing
the id re-registers under the new value and the old id goes dead.

Also reconciles G04 -> merged (landed in #30).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:39:46 +08:00
Yunfei He 0c113a3063 fix(runtime): don't paint Box backgroundColor onto border glyphs (Ink parity, G04) (#30)
Ink's render-border.ts computes each border edge's background from
border<Edge>BackgroundColor ?? borderBackgroundColor only — it never falls
back to the Box's own backgroundColor. vue-tui's colorizeEdge had an extra
`?? bgColor` fallback, so a Box with backgroundColor but no explicit border
background painted its background onto the border glyphs too.

Drop the fallback. Background still fills the inner content area; border
glyphs are now uncolored unless an explicit border background is set.

Tests rewritten to match Ink (per maintainer's align-to-Ink policy; see
.agents/docs/parity-ledger.md Decisions log):
- add failing-first repro "Box backgroundColor does not bleed onto border
  glyphs (Ink parity)"
- "wrapped text preserves backgroundColor on every content line": assert
  inner rows carry bg, border rows don't (height 4->5 so text fits)
- "Box background with border fills content area": snapshot updated so
  border rows have no bg

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:14:25 +08:00
Yunfei He 1a38474cbe chore(parity): record audit sweep #1 — 14 confirmed gaps (#29)
Ink-parity audit against v7.0.4 (commit 40b3a75): 10 areas reviewed in
parallel, candidates adversarially verified by 26 agents total.

16 candidates verified → 14 confirmed gaps (3 medium, 11 low), 2 refuted:
- exit() second-wins: vue-tui is already guarded (not last-wins).
- kitty key-release printable-text suppression: Ink behaves the same.

Each gap recorded in the ledger with Ink/vue-tui evidence and a fix sketch;
fixes ship as individual PRs per the loop in ink-parity-loop.md.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 22:51:17 +08:00
Yunfei He 7af3ff3e00 docs(agents): add .agents/docs context-engineering home + Ink-parity loop (#28)
Establishes the committed .agents/docs/ convention (distinct from the
uncommitted docs/ working-notes folder) and seeds the Ink-parity
verification loop:

- ink-parity-loop.md: design spec + reusable /loop prompt (audit →
  test-first fix → codex review → PR → CI → auto-merge, hard codex gate).
- ink-parity.md: pinned Ink reference (v7.0.4, commit 40b3a75) + the
  intentional-divergence allowlist the audit skips.
- parity-ledger.md: working ledger of audit sweeps and confirmed gaps.
- AGENTS.md: "Context Engineering" section documenting the convention.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 22:35:14 +08:00
Yunfei He 6d856f8913 build(ci): wire @vue-tui/cli test branch into the ci graph
Add a "test" script (vp test --passWithNoTests) to @vue-tui/cli and a
ci:test:cli branch (dependsOn ci:build) to the run.tasks graph. No CLI tests
exist yet, but wiring the branch now means future CLI tests are covered
automatically rather than silently skipped. #26 (item 3).
2026-05-29 20:00:36 +08:00
Yunfei He 051e9f128e test(runtime-tests): move beforeExit listener assertion into sequential file
The test asserting process.listenerCount("beforeExit") lived in the
parallel wait-flush.test.tsx, conflicting with the branch's rule that
process-global-state tests live in *.sequential.test.* files. Move it into
leak.sequential.test.tsx (already the home for process exit/SIGINT listener-
count assertions) and extend that file's header comment. #26 (item 2).
2026-05-29 20:00:36 +08:00
Yunfei He f3bb5f268a fix(runtime): cancel pending throttled commit on resize
The resize handler painted synchronously via commit() but left the commit
scheduler's pending trailing throttle timer armed. If an update was sitting
in that timer, it fired a second doCommit() right after the resize paint —
and because shouldClearTerminalForFrame clears whenever the previous frame
overflowed the viewport, the second commit emitted a duplicate clearTerminal.

Cancel the pending trailing commit before the synchronous paint; the paint
already reflects the current tree, so the pending commit is redundant.

Regression test (test-first) in throttle.sequential.test.tsx reproduces the
double-clear (2 clears) and verifies the fix (1 clear). Closes #26 (item 1).
2026-05-29 20:00:36 +08:00
Yunfei He d3480dc285 fix: align ready order and docs with lint-needs-build dependency
Review follow-ups on PR #25:

- package.json `ready`: run build BEFORE check:lint, matching the CI graph
  where ci:lint dependsOn ci:build. Type-aware lint rules need the built
  @vue-tui/runtime types; with lint before build, `vp run ready` on a fresh
  checkout (dist removed) could misreport lint. Verified `CI=true vp run ready`
  on a clean checkout now passes with 0 lint warnings.
- vite.config.ts + ci.yml comments: corrected the stale claim that "fmt and
  lint run immediately alongside build" — only fmt has no build dependency;
  lint now waits on build too. Dropped the outdated "~40s vs ~60s" figure.
2026-05-29 16:54:13 +08:00
Yunfei He 06ede6fded docs(agents): correct the testing guidance to match the shipped CI
The concurrency notes described `sequence.concurrent: true`, but that was
rolled back — it starved timing-sensitive render tests on the 4-core CI runner.
Rewrite to reflect what actually ships:

- File-level parallelism (fileParallelism: true), tests within a file serial;
  explain WHY in-file concurrency is deliberately avoided (the local-vs-CI
  core-count trap) and that PTY needs pool: forks.
- *.sequential.test.* files group process-global-state tests (fake timers,
  listener/yoga-node counts).
- New rule: tests must not implicitly depend on host env. CI=true flips
  interactive mode off, so both vitest configs force CI:false; inject env
  behavior explicitly and reproduce CI with `CI=true vp run ci` on a fresh
  checkout.
- FORCE_COLOR must also be set in spawned child envs, not just vitest config.
2026-05-29 16:54:13 +08:00
Yunfei He 77459a4e57 refactor(ci): target test suites by package#script instead of a filter glob
Replace the ci:test:misc filter-glob branch with explicit package#script
branches — ci:test:runtime, ci:test:testing, ci:test:integration,
ci:test:pty — matching the package#script form already used for the
runtime-tests suites. Clearer and consistent: each branch names exactly one
suite, no filter syntax. (vp run takes a single package#script per task, so
each normal package gets its own branch.)

Verified `CI=true vp run ci` on a fresh checkout: runtime (309) + testing (7)
+ integration (756) + pty (110), PTY once, exit 0.
2026-05-29 16:54:13 +08:00
Yunfei He 1f73c9ecea refactor(ci): cover all packages without polluting the test contract
The previous fix (b77f42e, reverted in f0cc614) added a test:integration
script to runtime and testing just so `vp run -r test:integration` would match
them — polluting every package's contract to satisfy the CI graph.

Restore the clean contract: normal packages (runtime, testing, cli) have only
a plain `test`; only runtime-tests splits into test:integration + test:pty.
The ci graph adapts instead of the packages:

  ci:test:misc        -> vp run --filter "@vue-tui/*" --filter "!@vue-tui/runtime-tests" test
  ci:test:integration -> vp run @vue-tui/runtime-tests#test:integration
  ci:test:pty         -> vp run @vue-tui/runtime-tests#test:pty

misc runs every normal package's own `test` (the glob auto-covers future
packages); runtime-tests' two suites are separate parallel branches, keeping
the slow PTY suite off the critical path. Verified `CI=true vp run ci` on a
fresh checkout: runtime (309) + testing (7) + integration (756) + pty (110),
PTY runs exactly once, exit 0.
2026-05-29 16:54:13 +08:00
Yunfei He 04f19da97c Revert "fix(ci): include runtime and testing unit tests in vp run ci"
This reverts commit b77f42ecab1cacedd1d792e0eb09d67ab50a054e.
2026-05-29 16:54:13 +08:00
Yunfei He 90dc89272e fix(ci): include runtime and testing unit tests in vp run ci
The ci graph's test:integration task runs `vp run -r test:integration`, but
that script only existed in runtime-tests, so `vp run -r` silently skipped
packages/runtime (11 files, 309 tests) and packages/testing (2 files, 7 tests)
— their unit tests never ran in CI.

Give both packages a test:integration script (= vp test) and make their `test`
compose it, mirroring runtime-tests. `vp run -r test:integration` now covers
all three packages. PTY stays its own branch (no double-run).

Verified: `CI=true vp run ci` on a fresh checkout runs runtime (309) + testing
(7) + runtime-tests (756), exit 0.

Reported-by: @reviewer on PR #25
2026-05-29 16:54:13 +08:00
Yunfei He 1e9d6d0b82 fix(ci): force CI=false in vitest env so interactive render tests pass
Fifth CI run failed on ~15 resize/throttle/flush tests that pass locally. Root
cause: the GitHub runner sets CI=true, and vue-tui computes
`interactive = !isInCi && isTTY`. With CI=true, interactive is false, so the
resize listener, cursor control, and ANSI erases are never wired up — exactly
the behavior those tests assert. Locally CI is unset, so they passed (the
local-vs-CI trap, reproducible with `CI=true vp test run`).

Set env CI:"false" in both vitest configs. The PTY child helpers already force
CI=false per-spawn for this reason; the in-process suite (and the testing
harness's render()) needs the same. Verified: `CI=true vp run ci` on a fresh
checkout (no dist) now exits 0 with all 756 + 110 tests passing.
2026-05-29 16:54:13 +08:00
Yunfei He a4116ba08f fix(ci): drop in-file test concurrency, keep file-level parallelism
The fourth CI run failed broadly: ~15 timing-sensitive tests (throttle, flush,
resize clear-counts, rapid-input ordering) failed under sequence.concurrent on
the 4-core ubuntu runner. These assert render/commit counts driven by the
~32ms commit throttle; in-file concurrency starves them of wall-clock when many
share few cores. It passed locally only because dev machines have more cores —
the classic "works on my 12-core mac" trap.

Remove sequence.concurrent from both the main and PTY configs; keep
fileParallelism (pool: forks), which is the proven, stable win (PTY suite still
~3x faster than serial). The it.sequential/describe.sequential markers and
context-local expect become harmless no-ops under serial in-file execution.

Verified cold (no dist): vp run ci exits 0, 0 lint warnings, all tests pass.
2026-05-29 16:54:13 +08:00
Yunfei He ee286a3246 fix(ci): make lint depend on build in the ci graph
Third CI run failed: `vp lint --deny-warnings` reported 4 no-implied-eval
warnings on PTY fixtures (setTimeout(exit, ...)). The type-aware rule needs
@vue-tui/runtime's built types to prove the callback is a function; on a fresh
checkout dist doesn't exist yet, so it misfires. Locally it was hidden because
dist was always present.

Add dependsOn: ["ci:build"] to ci:lint. build is already on the critical path
(build -> test:pty), so overall wall-clock is unchanged. Verified by removing
all dist and running vp run ci: exit 0, 0 lint warnings, all tests pass.
2026-05-29 16:54:13 +08:00
Yunfei He bdf06ddf1f fix(ci): bump Node to 22.18.0 for native TS config loading
Second CI run got past install but failed loading vite.config.ts: native TS
type-stripping (used by vite/oxlint to read .ts config) requires Node
^20.19.0 || >=22.18.0, and 22.13.0 is below that. 22.18.0 is the true floor —
it satisfies both pnpm (>=22.13) and TS config loading (>=22.18). Match
engines.node.
2026-05-29 16:54:13 +08:00
Yunfei He 1318f86888 fix(ci): bump Node to 22.13.0 (pnpm@11 minimum)
The first CI run failed at `vp install`: pnpm@11.1.2 requires Node >=22.13, but
the workflow pinned 22.12.0 (the old engines floor). Pin 22.13.0 — the real
lowest Node the toolchain supports — and correct engines.node to >=22.13.0 so
the declared engine matches what pnpm actually needs.
2026-05-29 16:54:13 +08:00
Yunfei He b8ad628a6e ci(lint): treat lint warnings as failures via --deny-warnings
check:lint previously exited 0 even with warnings, so warnings accumulated
unnoticed (7 had piled up before being cleared). Add --deny-warnings so any
warning fails check:lint — locally, in `vp run ready`, and in CI alike.

Verified: clean tree still exits 0; a deliberately reintroduced warning makes
check:lint exit 1.
2026-05-29 16:54:13 +08:00
Yunfei He a0ec8d1d33 docs(agents): require a comment when code must deviate from normal style
Record the rule: when code has to be written in a surprising or non-idiomatic
way because the situation requires it (control-char regexes, deliberate
code-point spread, justified lint suppressions), add a comment explaining why,
so the next reader doesn't have to guess whether it's intentional.
2026-05-29 16:54:13 +08:00
Yunfei He f2aa604939 fix(lint): clear 7 false-positive lint warnings
vp run check:lint reported 7 warnings, all false positives for a terminal UI
library:

- no-control-regex (×6): parsing keyboard escape sequences and stripping ANSI
  inherently requires regexes matching control chars (ESC, BEL). Disable the
  rule library-wide in vite.config.ts — it flags every such pattern and we
  already use \x1b/\u escapes (its own suggested form). Also drop the now-
  redundant (and misplaced — it sat one line above the regex) inline disable in
  sanitize-ansi.test.ts.

- no-misused-spread (×1) at parse-keypress.ts:500: `[...s]` on a terminal
  escape sequence (ASCII control chars + digits, never user text), so the
  emoji-splitting concern doesn't apply. Suppressed in place with a comment
  explaining why, keeping the rule active elsewhere.

check:lint now reports 0 warnings, 0 errors.
2026-05-29 16:54:13 +08:00
Yunfei He 5289062414 docs(agents): update for concurrent-by-default main suite
The main suite is now concurrent too, so correct the earlier "not concurrent"
note. Record the convention: snapshot tests stay concurrent via context-local
expect; process-global-state tests (fake timers, listener/node counts) move to
*.sequential.test.* files marked it.sequential / describe.sequential.
2026-05-29 16:54:13 +08:00
Yunfei He ee6004b8be test(runtime-tests): run the main suite concurrently by default
Enable sequence.concurrent: true in vite.config.ts so the non-PTY suite runs
concurrently like the PTY suite. Stress-verified stable (8/8 at maxForks=4);
the suite drops from ~13s to ~4-5s.

Three test patterns were incompatible with concurrency; handled per cause:

- Inline snapshots (background-color, borders): the module-level `expect`
  loses snapshot test context under concurrency. Fixed in place by using the
  context-local `expect` (async ({ expect }) => ...), so they stay concurrent.

- Process-global state (throttle/animation-scheduler use fake timers; leak
  asserts on process exit/SIGINT listener counts and live yoga nodes): a
  concurrent sibling clobbers the shared global mid-test. These genuinely
  require serial execution, so they move to *.sequential.test.* files with
  it.sequential / describe.sequential and a header explaining why.

`vp run ready` passes.
2026-05-29 16:54:13 +08:00
Yunfei He fa626dc90f docs(agents): record PTY-concurrent testing and concurrency constraints
Document that the PTY suite runs concurrently (and the wall-clock-assertion
pitfall there), plus the two patterns that force the main suite to stay
sequential, with the root cause and fix for each:

- Inline snapshots lose test context under concurrency — fixable via the
  context-local `expect` (test.concurrent("...", ({ expect }) => ...)).
- Fake timers mutate process-global timer functions, so concurrent tests
  clobber each other's mocked timer state — not fixable with context; must
  stay sequential.

Investigated empirically: enabling sequence.concurrent on the main suite fails
deterministically (not flaky) in exactly the snapshot files (background-color,
borders) and fake-timer files (throttle, animation-scheduler).
2026-05-29 16:54:13 +08:00
Yunfei He a07fc889c4 test(runtime-tests): run PTY tests concurrently by default
With resize rendering now synchronous, no PTY test depends on wall-clock
timing, so concurrent execution is safe. Enable sequence.concurrent: true.
Each test already spawns its own isolated PTY subprocess (or in-process app)
with no shared state.

Verified stable: 12/12 runs green under sequence.concurrent with forks capped
to 4 (mimicking a 4-core CI runner) — the configuration that reliably flaked
before the resize fix. No it.sequential opt-outs are needed.
2026-05-29 16:54:13 +08:00
Yunfei He bc61d57a11 fix(runtime): render synchronously on resize, matching Ink
The resize handler routed through scheduler.schedule(), deferring the repaint
through the ~32ms commit throttle. Ink's resized() calls onRender() directly,
and a resize is a discrete viewport change that should repaint immediately —
deferring it can leave stale/overlapping content on screen for a frame.

It also made the clearTerminal-on-overflow behavior depend on wall-clock
timing: the #450 "shrink into overflow" test passed only because the throttled
resize emitted ZERO clears (its trailing timer never fired within the test's
nextTicks) and the single clear came entirely from unmount. The test asserted
the right number for the wrong reason, and the dependency on real elapsed time
made it flaky under CPU contention.

Change the resize handler to commit() directly. Now the resize itself emits the
overflow clear deterministically. Update the test to assert the clear happens
ON the resize (clearsAfterResize - clearsBeforeResize === 1) after a single
nextTick — no longer dependent on throttle timing.
2026-05-29 16:54:13 +08:00
Yunfei He 02f9cfa98a docs(runtime-tests): correct stale "no parallelism" PTY comment
The PTY suite now runs file-parallel (see vitest.pty.config.ts), so the comment
claiming "no parallelism" was wrong. The real constraint is node-pty's forks
pool, not serial execution.
2026-05-29 16:54:13 +08:00
Yunfei He 2fef2f3a3b perf(runtime-tests): run PTY tests file-parallel across forked workers
The PTY suite was the CI wall-clock bottleneck, run serially via
fileParallelism:false. The original reason for serializing was a node-pty
constraint — it needs child_process.fork(), not worker_threads — but that only
dictates the pool TYPE, not single-file execution. Each test already spawns its
own isolated PTY subprocess (helpers/term.ts, run.ts: no shared ports, temp
files, or mutable globals; cwd is the read-only fixtures dir), so files
parallelize safely.

Set pool:"forks" explicitly (the real node-pty requirement) and
fileParallelism:true. Measured: the PTY suite drops ~37s -> ~13s (~3x), and the
full `vp run ci` graph drops ~41s -> ~20s cold (no task cache, no prebuilt
dist). Verified stable across 9 isolated PTY runs (incl. maxForks capped to 4
to mimic a 4-core CI runner) and 3 cold full-graph runs — 110 PTY tests pass
every time, zero flakes. testTimeout stays 15s (slowest test ~2s) to absorb CPU
contention on smaller runners.
2026-05-29 16:54:13 +08:00
Yunfei He bdadea0d02 perf(ci): parallelize verification via a vp run.tasks graph
Replace ci.yml's five serial steps with a single `vp run ci` whose graph lives
in vite.config.ts (run.tasks). The vp task runner fans out independent branches
concurrently: fmt and lint start immediately while check:type and the test
suites wait on build (their consumers resolve @vue-tui/runtime from the built
dist/*.d.mts). The wall-clock critical path becomes build -> test:pty instead
of the sum of every check.

Measured cold (no task cache, no prebuilt dist — the real CI condition): the
graph completes in ~41s vs ~60s serial, ~32% faster, with build correctly
fanned out before the type and test branches. `vp run --last-details` (and the
run summary) still pinpoints which sub-task failed.

Also set run.cache=false so neither local nor CI verification depends on any
task-cache replay. The serial `ready` script in package.json is kept for simple
local use.
2026-05-29 16:54:13 +08:00
Yunfei He 2f60278450 ci: add GitHub Actions workflow mirroring vp run ready
Single job on ubuntu-latest that runs the same checks as `vp run ready`, as
individual labeled steps so the run shows exactly which concern failed: format,
lint, build, type-check, test. Build precedes type-check and test because
@vue-tui/runtime has no "types" export — testing/runtime-tests resolve its
types and runtime from the built dist/*.d.mts, and same-job steps share the
filesystem so the dist persists.

Uses voidzero-dev/setup-vp@v1 (per the Vite+ CI docs), which installs Node, the
vp CLI, and runs vp install automatically (run-install defaults true), so no
separate install step. node-version pins the exact engines.node floor 22.12.0
so CI fails if code relies on a newer Node API; cache enables the pnpm store
cache (off by default). Triggers on pull_request, push to main, and manual
dispatch; concurrency cancels superseded PR runs but never a push-to-main run;
permissions are read-only.

Verified: actionlint reports no issues; both action refs confirmed via
git ls-remote.
2026-05-29 16:54:13 +08:00
Yunfei He 4335cbbc64 refactor(scripts): fold check:fixtures into check:type
A single top-level `vp run check:type` should fan out (-r) and fully type-check
every package, fixtures included — no separate top-level check:fixtures step.
runtime-tests' check:type now runs its main tsc then its fixtures tsc
(check:fixtures stays as a runnable sub-script). Drop the root check:fixtures
entry and its standalone step in ready.
2026-05-29 16:54:13 +08:00
Yunfei He 40d89bdf47 refactor: move type-checking from vp lint to tsc-based check:type
Set lint.options.typeCheck=false so vp lint/check no longer runs the
tsgolint full type-check (typeAware stays on, keeping type-aware lint rules).
Type-checking is now owned by check:type, which runs the real tsc and honors
each tsconfig's project semantics — unlike tsgolint, which ignored tsconfig
exclude/nested configs.

Wire check:type into ready after build (it needs the built dist for
cross-package type resolution): fmt, lint, build, check:type, check:fixtures,
test.

Verified: with typeCheck off, vp lint no longer reports a TS2322 type error
(but keeps its type-aware warnings); check:type catches it via tsc.
2026-05-29 16:54:13 +08:00
Yunfei He 1d6c47ca88 feat(scripts): add check:type running real tsc per package
Each package gets check:type = `tsc --noEmit` (under its own tsconfig); root
check:type = `vp run -r check:type` fans out across the workspace. Unlike vp's
tsgolint-based type-aware path, this is the standard TypeScript compiler, so it
honors each tsconfig's real project semantics.

Additive only here — vp lint still carries typeCheck; the switch-over and ready
rewiring land in the next commit. Note: testing and runtime-tests resolve
@vue-tui/runtime types from its built dist, so check:type requires a prior
build.
2026-05-29 16:54:13 +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 b6e5313611 style(runtime): reformat write-synchronized.ts to satisfy oxfmt
The committed file had unformatted line wrapping that vp fmt --check flags,
which would fail CI the moment check:fmt runs. Reflow to the formatter's
output; no behavior change.
2026-05-29 16:54:13 +08:00
Yunfei He 6ca8990270 refactor(scripts): restructure workspace scripts into check:* / test:* families
Replace the opaque `vp check` bundling and the ad-hoc `ready` chain (which
shelled out via `cd ... && pnpm ...`) with explicit, composable named scripts.

Root delegates; category splits live in the package that owns them:

  root: check:fmt / check:lint / check:fixtures / test / build / ready
  runtime-tests: test = test:integration + test:pty; check:fixtures

`ready` now composes the named scripts in dependency order (fmt, lint, build,
fixtures, test) — build runs before check:fixtures and test:pty, which need
the built dist. Drop the broken root `dev` (referenced a non-existent website
package) and the redundant per-package `check` scripts (fmt/lint already run
workspace-wide from root). Renames: pty-test -> test:pty, typecheck:fixtures
-> check:fixtures.
2026-05-29 16:54:13 +08:00
Yunfei He ddb45a4869 chore: add granular check:fmt / check:lint scripts at root
`vp check` bundles format, lint, and typecheck, which hides which concern
failed and offers no way to run a single slice. Add explicit per-concern
entry points at the workspace root:

  check       -> vp check            (umbrella, unchanged behavior)
  check:fmt   -> vp fmt --check      (format only)
  check:lint  -> vp lint             (lint + typecheck)

No separate check:type: with typeAware/typeCheck enabled in the root config,
vp lint already runs the tsc typecheck, so check:lint covers it.
2026-05-29 16:54:13 +08:00
Yunfei He fb1f6d33d4 refactor(config): drop per-package lint/fmt blocks redundant with root
The root vite.config.ts lint/fmt settings cascade into every workspace
package, and a package-level lint block merges with — rather than replaces —
the root options. So the `lint.options { typeAware, typeCheck }` and the empty
`fmt: {}` repeated in runtime, testing, and runtime-tests only restated what
each package already inherits.

Remove them. runtime-tests keeps just its `ignorePatterns` (the one genuine
per-package override); typeAware/typeCheck now come from root via the merge.
Verified with `vp check`: no lint or typecheck regression, and the PTY
fixtures stay excluded from lint.
2026-05-29 16:54:13 +08:00
Yunfei He 3ef28ca3a1 test(runtime-tests): pin JSX children typing under the automatic runtime
The WithChildren shim is only exercised under jsx:"react-jsx", which lives
solely in integration/pty/fixtures/tsconfig.json. Nothing in `ready` ran tsc
against that config (vp check uses jsx:"preserve" and excludes the fixtures;
pty-test only transpiles them), so a regression in the shim — children
silently rejected, or declared props silently widened away — would pass
verification unnoticed.

Add a type-only regression fixture (not a runnable PTY program; not a
*.test.tsx, so vitest never collects it) that pins both directions of the
contract: children are accepted on Box/Text/Static/Transform, and declared
props stay validated via @ts-expect-error (invalid value, wrong type, unknown
prop, and missing required props on Transform/Static).

Wire `tsc -p integration/pty/fixtures/tsconfig.json --noEmit` into `ready` via
a typecheck:fixtures script, run after build (so @vue-tui/runtime resolves
against fresh dist types) and before pty-test, so the react-jsx path is
actually enforced rather than only manually checkable.
2026-05-28 23:13:03 +08:00
Yunfei He fa0708672c docs(runtime): clarify WithChildren adds a children prop to $props 2026-05-28 23:13:03 +08:00
Yunfei He ab3ae78ba2 fix(runtime-tests): use ignorePatterns for lint, not the invalid exclude
`lint.exclude` is not a property of OxlintConfig (the correct key is
`ignorePatterns`). The invalid property failed defineConfig overload
resolution, which surfaced as a TS2769 plus a TS2321 excessive-stack-depth
error against the recursive vitest-augmented UserConfig. Using
`ignorePatterns` excludes the PTY fixtures from lint as intended and clears
both config type errors.
2026-05-28 23:13:03 +08:00
Yunfei He 304fdf2e21 fix(runtime): accept JSX children on components under automatic runtime
Text/Box/Static/Transform read children via slots but never surfaced
`children` on their JSX `$props`. Under the automatic JSX runtime
(jsx: react-jsx + jsxImportSource: vue), children are passed as a
`children` prop, so `<Text>x</Text>` failed to type-check. Add a
type-only WithChildren cast that declares optional `children` on $props;
Vue routes that prop to the default slot at runtime, so there is no
runtime change.
2026-05-28 23:13:03 +08:00
Yunfei He a5a96fed31 chore: hoist text-measure import in yoga.ts; declare node>=22 engine
Addresses PR #23 review: move the mid-file text-measure import to the
top-level import block, and declare engines.node>=22 to match the
upgraded text stack (cli-truncate@6, slice-ansi@9 require node>=22).
2026-05-28 17:57:52 +08:00
Yunfei He 7b8ed05ef9 test: pin narrow-truncate re-measure parity with Ink 2026-05-28 17:57:52 +08:00
Yunfei He 4696b49313 test: pin absolute-non-edge ZWJ parity with Ink (closes #21 final class) 2026-05-28 17:57:52 +08:00
Yunfei He 1550ab9ad1 fix: measure text naturally like Ink, wrap only when constrained (closes #21 height/wrap classes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:57:52 +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