From 9d1e4d78054829e843105ae96704f79c9efa9d4e Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sat, 30 May 2026 19:57:52 +0800 Subject: [PATCH] feat(runtime)!: replace useExit() with Ink-aligned useAppContext() (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ink's `useApp()` returns `{ exit, waitUntilRenderFlush }`. vue-tui previously exposed only `exit()` via `useExit()` and kept `waitUntilRenderFlush` on the `TuiApp` handle alone. Align the public surface with Ink: add `useAppContext()` returning the same pair, and remove `useExit()`. - thread `waitUntilRenderFlush` into the injected `AppContext` via a hoisted impl shared by the `TuiApp` handle and the composable, so both resolve identically - add `useAppContext()`; delete `useExit()`; migrate all call sites, PTY fixtures, examples, READMEs and the public-API surface test - port Ink's two "useApp waitUntilRenderFlush" tests; Ink's third relies on React concurrent mode (N/A in Vue) - rewrite the ink-divergences entry: this is now a *naming* divergence (`useAppContext` vs `useApp`, mirroring `createApp` vs `render`), not a surface one — and fix the prior wrong claim that Ink's `useApp` returns stdin/stdout/stderr The name is qualified (`useAppContext`, not `useApp`) so it doesn't read as the Vue application instance (`createApp`/`app.mount`) — the same Vue-native naming choice vue-tui already makes with `createApp()` vs Ink's `render()`. BREAKING CHANGE: `useExit()` is removed. Replace `const exit = useExit()` with `const { exit } = useAppContext()`. Co-authored-by: Claude Opus 4.8 --- .agents/docs/ink-divergences.md | 15 +-- README.md | 12 +-- examples/coding-agent/src/App.vue | 4 +- examples/flappy-bird/src/App.vue | 4 +- .../accessibility/screen-reader.test.tsx | 1 + .../lifecycle/error-handling.test.tsx | 8 +- .../integration/lifecycle/exit.test.tsx | 49 +++++----- .../integration/lifecycle/wait-flush.test.tsx | 95 +++++++++++++++++-- ...exit-on-exit-with-error-value-property.tsx | 4 +- .../pty/fixtures/exit-on-exit-with-error.tsx | 4 +- .../pty/fixtures/exit-on-exit-with-result.tsx | 4 +- .../exit-on-exit-with-value-object.tsx | 4 +- .../integration/pty/fixtures/exit-on-exit.tsx | 4 +- .../fixtures/exit-raw-on-exit-with-error.tsx | 4 +- .../pty/fixtures/exit-raw-on-exit.tsx | 4 +- .../pty/fixtures/exit-with-static.tsx | 4 +- .../fixtures/fullscreen-no-extra-newline.tsx | 4 +- .../pty/fixtures/issue-442-full-height.tsx | 4 +- .../fixtures/issue-450-fixture-helpers.tsx | 6 +- .../use-animation-interactive-false-exit.tsx | 4 +- .../use-animation-non-interactive-exit.tsx | 4 +- .../pty/fixtures/use-input-ctrl-c.tsx | 4 +- .../fixtures/use-input-discrete-priority.tsx | 4 +- .../pty/fixtures/use-input-kitty.tsx | 4 +- .../pty/fixtures/use-input-many.tsx | 4 +- .../pty/fixtures/use-input-multiple.tsx | 4 +- .../integration/pty/fixtures/use-input.tsx | 4 +- .../integration/pty/fixtures/use-paste.tsx | 6 +- .../integration/pty/fixtures/use-stdout.tsx | 4 +- .../integration/public-api.test.ts | 2 +- .../integration/render-to-string.test.tsx | 6 +- packages/runtime/README.md | 2 +- .../runtime/src/composables/useAppContext.ts | 27 ++++++ packages/runtime/src/composables/useExit.ts | 14 --- packages/runtime/src/context.ts | 1 + packages/runtime/src/index.ts | 2 +- packages/runtime/src/render-to-string.ts | 5 +- packages/runtime/src/render.ts | 13 ++- 38 files changed, 226 insertions(+), 122 deletions(-) create mode 100644 packages/runtime/src/composables/useAppContext.ts delete mode 100644 packages/runtime/src/composables/useExit.ts diff --git a/.agents/docs/ink-divergences.md b/.agents/docs/ink-divergences.md index fb01f5c..d7a9042 100644 --- a/.agents/docs/ink-divergences.md +++ b/.agents/docs/ink-divergences.md @@ -31,14 +31,15 @@ deliberate. Divergences fall into a few kinds: - **Why:** mirrors Vue's own `createApp` mental model — a Vue developer expects an app object they mount, not a one-shot render call. -### App composable — `useExit()` instead of `useApp()` +### App composable — `useAppContext()` instead of `useApp()` -- **Ink:** `useApp()` returns the full AppContext (`exit`, `waitUntilRenderFlush`, - stdin/stdout/stderr, …). -- **vue-tui:** `useExit()` returns only the `exit` function; the rest is reached through - dedicated composables (`useStdin`, `useStdout`, `useStderr`, …). -- **Why:** intentionally minimal, single-purpose composables. `waitUntilRenderFlush` is - deliberately **not** exposed. +- **Ink:** `useApp()` returns `{ exit, waitUntilRenderFlush }` — stdin/stdout/stderr are + separate hooks (`useStdin`/`useStdout`/`useStderr`), not part of it. +- **vue-tui:** `useAppContext()` returns the same `{ exit, waitUntilRenderFlush }`, with + streams on those same peer composables. +- **Why:** only the name differs — `useApp` reads as "the Vue application instance" + (`createApp`, `app.mount`), so vue-tui qualifies it as `useAppContext`. The shape is + identical to Ink; a naming divergence, not a surface one. ### Named type / prop re-exports diff --git a/README.md b/README.md index cbcf545..b727872 100644 --- a/README.md +++ b/README.md @@ -83,11 +83,11 @@ useInput((input) => { ## Packages -| Package | Description | -| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [`@vue-tui/runtime`](https://www.npmjs.com/package/@vue-tui/runtime) | The core framework — Vue 3 renderer for the terminal with components (`Box`, `Text`, `Static`, etc.), composables (`useInput`, `useFocus`, `useExit`, etc.), and yoga-based flexbox layout | -| [`@vue-tui/cli`](https://www.npmjs.com/package/@vue-tui/cli) | Development tool — `vue-tui dev` starts your app with Vite-powered HMR | -| [`@vue-tui/testing`](https://www.npmjs.com/package/@vue-tui/testing) | Test harness — render in an isolated fake terminal, simulate input, assert output frame by frame | +| Package | Description | +| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`@vue-tui/runtime`](https://www.npmjs.com/package/@vue-tui/runtime) | The core framework — Vue 3 renderer for the terminal with components (`Box`, `Text`, `Static`, etc.), composables (`useInput`, `useFocus`, `useAppContext`, etc.), and yoga-based flexbox layout | +| [`@vue-tui/cli`](https://www.npmjs.com/package/@vue-tui/cli) | Development tool — `vue-tui dev` starts your app with Vite-powered HMR | +| [`@vue-tui/testing`](https://www.npmjs.com/package/@vue-tui/testing) | Test harness — render in an isolated fake terminal, simulate input, assert output frame by frame | ## Examples @@ -116,7 +116,7 @@ useInput((input) => { | `useInput(handler, opts?)` | Handle keyboard input — receives `(input, key)` with modifier and arrow key detection | | `useFocus(opts?)` | Component-level focus — returns `{ isFocused, focus }` | | `useFocusManager()` | App-level focus control — `focusNext()`, `focusPrevious()`, `focus(id)` | -| `useExit()` | Programmatic app exit — returns `exit(error?)` | +| `useAppContext()` | App context — `{ exit(error?), waitUntilRenderFlush() }` | | `useTerminalSize()` | Reactive terminal dimensions — `{ columns, rows }` | | `useStdin()` | Access stdin stream and raw mode control | | `useStdout()` | Write directly to stdout | diff --git a/examples/coding-agent/src/App.vue b/examples/coding-agent/src/App.vue index ad6d98c..b4a7c76 100644 --- a/examples/coding-agent/src/App.vue +++ b/examples/coding-agent/src/App.vue @@ -1,6 +1,6 @@