Yunfei He 16e49024da test(runtime-tests): guard the shipped examples launch+paint in CI (#212) (#220)
* fix(examples): restore flexDirection column on basic-template

A past restyle (#137) replaced the root Box's `flexDirection="column"
:paddingX="1"` with `backgroundColor/borderStyle/width="20"` and dropped
the column direction. With the Ink-aligned default (row) and a fixed
`width="20"`, the six children pack into ~3-char columns and the title
interleaves illegibly. Restore `flexDirection="column"` so the example
renders as the intended bordered card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(runtime-tests): PTY smoke suite guarding the examples launch+paint (#212)

#212's `Calling \`require\` for "node:module"` crash came from the old
@vue-tui/cli bundledDev step. The @vue-tui/vite plugin (#215) removed it,
but nothing exercised the shipped examples end to end, so a future
regression could break "the examples run" silently.

Add a node-pty smoke suite that launches basic-template and basic-jsx
through both the dev server (`vite`) and the production build
(`node dist/main.js`) under a real TTY and waits for the frame to paint;
coding-agent (needs an LLM key to run) gets a key-free build guard. A
static `CJS_REQUIRE_SHIM` assertion on the built bundle locks the #212
invariant directly, and `[vue-tui] failed to launch` / process-exit
signals fail a broken launch fast instead of burning the render timeout.
Wired in as the `ci:test:examples` branch of the CI graph.

Verified RED->GREEN: injecting a bare `require()` into an entry
reproduces #212 exactly and the guard catches it (statically and at
runtime); a non-module throw is caught via the dev launch-failure signal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(runtime-tests): simplify the examples smoke harness

Cleanup pass on the new #212 smoke suite (no behavior change):
- use the `strip-ansi` package instead of a hand-rolled CSI regex (already a
  devDep here and used across the suite; strips OSC too, drops the eslint
  no-control-regex suppression);
- drop the onExitWaiters wake-up set — the 100ms poll already observes the exit
  flag, so onExit only needs to record the code;
- fold the three reject sites into one `fail()` helper;
- give the #212 bundle-shim invariant a single home: move CJS_REQUIRE_SHIM next
  to the builder and factor `buildAndExpectNoCjsRequire`, shared by the runnable
  apps and the coding-agent build guard;
- drop dead exports (repoRoot, CRASH_SIGNATURE).

Re-verified RED->GREEN (bundled require still caught) and all 5 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(runtime-tests): serialize the examples suite files (match #222)

Adversarial review flagged the examples config's parallel-safety comment as
imprecise: PTY-process isolation does NOT isolate on-disk state. Each launched
example writes its optimizeDeps cache (examples/<name>/node_modules/.vite) and
bundle (examples/<name>/dist), so two test files launching the SAME example at
once would race that shared dir — the exact #222 failure the sibling
@vue-tui/vite suite just fixed with fileParallelism:false.

It can't happen today (one serial file, per-example caches), but set
fileParallelism:false to match #222 and keep it safe as the suite grows, and
correct the comment to state the real guarantee.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 03:01:10 +08:00

vue-tui

Public beta — the @vue-tui/runtime API is stabilizing toward 1.0; dev-mode HMR is still experimental. Bug reports welcome.

The Vue framework for terminal UIs. Build with components, develop with HMR, test with confidence.

@vue-tui/runtime · @vue-tui/vite · @vue-tui/testing

  • Vue SFC & JSX — write terminal interfaces with <template>, TSX, or both
  • Flexbox layout — powered by Yoga, the same engine behind React Native
  • Dev toolkit (experimental)HMR in the terminal via the @vue-tui/vite plugin (npm run dev)
  • Input & focus — keyboard handling, focus management, Tab navigation, Kitty keyboard protocol
  • Testing harness — out-of-the-box component-level terminal testing — render, simulate input, assert frames

Flappy Bird — one of the examples included in the repo

Flappy Bird built with vue-tui

Quick Start

npx tiged vuejs-ai/vue-tui-starter my-app
cd my-app
npm install
npm run dev      # vite + @vue-tui/vite plugin, in-process terminal HMR

Edit app.vue and watch the terminal update instantly.

Add to an existing project

npm install @vue-tui/runtime

Example

// src/main.ts
import { createApp } from "@vue-tui/runtime";
import App from "./app.vue";

createApp(App).mount();
<!-- src/app.vue -->
<script setup lang="ts">
import { shallowRef } from "vue";
import { Box, Text, useInput } from "@vue-tui/runtime";

const count = shallowRef(0);

useInput((input) => {
  if (input === "+") count.value++;
  if (input === "-") count.value--;
});
</script>

<template>
  <Box>
    <Text>Count: </Text>
    <Text bold color="green">{{ count }}</Text>
    <Text dimColor> (+/- to change)</Text>
  </Box>
</template>

For non-interactive output — snapshots, CI logs, piped commands — renderToString(App) renders a single frame to a string instead of mounting.

Table of Contents

Packages

Package Description
@vue-tui/runtime The core framework — Vue 3 renderer for the terminal with components (Box, Text, Static, etc.), composables (useInput, useFocus, useApp, etc.), and yoga-based flexbox layout. API stabilizing.
@vue-tui/vite Vite plugin — add vueTui() to vite.config.ts for an in-process terminal dev server with HMR (npm run dev) plus a production build (vite build). Experimental; may change.
@vue-tui/testing Test harness — render in an isolated fake terminal, simulate input, assert output frame by frame

Examples

Example Description
basic-template Vue SFC with <template> syntax
basic-jsx Same app in TSX
coding-agent AI coding agent with LLM streaming and interactive UI
flappy-bird Physics-based terminal game with reactive state and borders

Components

Component Description
<Box> Flexbox container — direction, wrap, align, justify, gap, padding, margin, borders, background
<Text> Styled text — color, bold, italic, underline, strikethrough, dimColor, wrap/truncate modes
<Spacer> Expands to fill available space (flex-grow: 1)
<Newline> Inserts line breaks (configurable count)
<Static> Renders a list of items once, above the redrawn region
<Transform> Applies a string transform function to each rendered line

Composables (Hooks)

Composable Description
useInput(handler, opts?) Handle keyboard input — receives (input, key) with modifier and arrow key detection
usePaste(handler, opts?) Handle bracketed paste — receives the pasted text as a single event
useFocus(opts?) Component-level focus — returns { isFocused, focus }
useFocusManager() App-level focus control — focusNext(), focusPrevious(), focus(id)
useApp() App lifecycle — { exit(error?), waitUntilRenderFlush() }
useWindowSize() Reactive terminal dimensions — { columns, rows }
useStdin() Access stdin stream and raw mode control
useStdout() Write directly to stdout
useStderr() Write directly to stderr
useBoxMetrics(ref) Measure a <Box> via a template ref — reactive { width, height, left, top, hasMeasured } (or measureElement(el) for a one-off { width, height } read)
useCursor() Control the terminal cursor — setCursorPosition(pos) in output coordinates
useIsScreenReaderEnabled() Whether a screen reader is active — returns a boolean for adapting accessible output
useAnimation(opts?) Frame-based animation driver — reactive { frame, time, delta } + reset()

Testing

The @vue-tui/testing package renders components in an isolated environment and lets you simulate input and assert visual output:

npm install -D @vue-tui/testing
import { defineComponent, shallowRef } from "vue";
import { expect, test } from "vitest";
import { render } from "@vue-tui/testing";
import { Box, Text, useInput } from "@vue-tui/runtime";

test("counter responds to + and - keys", async () => {
  const Counter = defineComponent(() => {
    const count = shallowRef(0);
    useInput((input) => {
      if (input === "+") count.value++;
      if (input === "-") count.value--;
    });
    return () => (
      <Box>
        <Text>Count: {count.value}</Text>
      </Box>
    );
  });

  const { lastFrame, stdin } = await render(Counter);
  expect(lastFrame()).toContain("Count: 0");

  await stdin.write("+");
  expect(lastFrame()).toContain("Count: 1");

  await stdin.write("-");
  expect(lastFrame()).toContain("Count: 0");
});

Development

Requires pnpm and Node.js 22+.

pnpm install          # install dependencies
vp run ready          # lint, typecheck, test, and build (the full check)
vp run -r test        # run tests across all packages
vp run -r build       # build all packages

To run an example with terminal HMR, use vanilla vite@8 (the recommended setup): cd examples/basic-template && npm run dev. See that example's README.md for the in-monorepo caveat.

Contributing

Contributions welcome! vue-tui is evolving fast — please open an issue before starting large changes. If you use AI tools, disclose it in your PR and make sure you've reviewed and tested everything before submitting.

Credits

vue-tui is built on the ideas pioneered by Ink — component model, yoga-based layout, focus system, and rendering pipeline — adapted to Vue's philosophy. Thanks to Vadim Demedes, Sindre Sorhus, and the Ink contributors.

License

MIT

S
Description
Vue 3 terminal UI framework — 备份
Readme 3 MiB
Languages
TypeScript 99.2%
Vue 0.7%