From badf0652fd9c4c970a62eb4f0cf65a1eae885399 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 15 Jul 2026 04:33:00 +0000 Subject: [PATCH] feat(devtools)!: reuse the built-in Terminals dock via ctx.terminals Retire Nuxt DevTools' bespoke @xterm terminal UI + RPC and surface terminal sessions through the Vite DevTools terminals host (ctx.terminals), so they render in the built-in Terminals dock. Existing modules keep working: the devtools:terminal:* hooks are bridged onto ctx.terminals as read-only registered sessions (module still owns the process and streams output). A new opt-in interactive PTY path is added via a devtools:terminal:spawn hook and the startDevToolsTerminal() helper. Exit-driven cleanup for the internal module-install / analyze-build / npm-update flows is preserved by broadcasting terminal exits from their startChildProcess producers. Created with the help of an agent. --- docs/content/2.module/1.utils-kit.md | 29 +- packages/devtools-kit/src/_types/hooks.ts | 21 +- packages/devtools-kit/src/_types/rpc.ts | 12 +- packages/devtools-kit/src/_types/terminals.ts | 34 +++ packages/devtools-kit/src/index.ts | 26 +- .../devtools/client/components/ModuleItem.vue | 9 +- .../client/components/NpmVersionCheck.vue | 12 - .../client/components/TerminalPage.vue | 64 ----- .../client/components/TerminalView.vue | 68 ----- .../client/composables/state-routes.ts | 5 - packages/devtools/client/composables/state.ts | 4 - packages/devtools/client/nuxt.config.ts | 3 - .../client/pages/modules/analyze-build.vue | 21 +- .../devtools/client/pages/modules/modules.vue | 14 +- .../client/pages/modules/terminals.vue | 24 -- packages/devtools/client/setup/client-rpc.ts | 4 - packages/devtools/package.json | 2 - .../devtools/src/server-rpc/analyze-build.ts | 5 +- packages/devtools/src/server-rpc/index.ts | 11 +- packages/devtools/src/server-rpc/npm.ts | 10 +- packages/devtools/src/server-rpc/terminals.ts | 247 ++++++++++++------ .../module-starter/playground/nuxt.config.ts | 2 +- pnpm-lock.yaml | 12 - pnpm-workspace.yaml | 2 - 24 files changed, 304 insertions(+), 337 deletions(-) delete mode 100644 packages/devtools/client/components/TerminalPage.vue delete mode 100644 packages/devtools/client/components/TerminalView.vue delete mode 100644 packages/devtools/client/pages/modules/terminals.vue diff --git a/docs/content/2.module/1.utils-kit.md b/docs/content/2.module/1.utils-kit.md index b7902c9f68..56de4c4612 100644 --- a/docs/content/2.module/1.utils-kit.md +++ b/docs/content/2.module/1.utils-kit.md @@ -82,7 +82,8 @@ of the Vite DevTools terminals host the [migration guide](/module/migration-v4#ndt_dep_0004). :: -Start a sub process using `tinyexec` and create a terminal tab in DevTools. +Start a sub process using `tinyexec` and surface it as a read-only session in +the built-in **Terminals** dock. ```ts import { startSubprocess } from '@nuxt/devtools-kit' @@ -110,6 +111,32 @@ subprocess.restart() subprocess.terminate() ``` +### `startDevToolsTerminal()` + +Spawn a terminal that is **owned by the Vite DevTools terminals host** and +surfaced in the built-in **Terminals** dock. Unlike `startSubprocess()` (where +your module owns the process and only streams output to a read-only session), +DevTools spawns and owns the process here. + +Set `interactive: true` for a fully interactive PTY (stdin + resize, +TUI-capable) — powered by `zigpty` with a graceful pipe fallback where the +native bindings can't load. + +```ts +import { startDevToolsTerminal } from '@nuxt/devtools-kit' + +startDevToolsTerminal({ + id: 'my-module:shell', + name: 'Shell', + command: 'bash', + interactive: true, +}) +``` + +Under the hood this calls the `devtools:terminal:spawn` Nuxt hook, which the +DevTools bridge maps to `ctx.terminals.startPtySession(...)` (interactive) or +`ctx.terminals.startChildProcess(...)` (output-only). + ### `extendServerRpc()` ::warning diff --git a/packages/devtools-kit/src/_types/hooks.ts b/packages/devtools-kit/src/_types/hooks.ts index a348a1feb5..3ad0d9129b 100644 --- a/packages/devtools-kit/src/_types/hooks.ts +++ b/packages/devtools-kit/src/_types/hooks.ts @@ -1,7 +1,7 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { ModuleCustomTab } from './custom-tabs' import type { NuxtDevtoolsInfo } from './server-ctx' -import type { TerminalState } from './terminals' +import type { SpawnTerminalOptions, TerminalState } from './terminals' declare module '@nuxt/schema' { interface NuxtHooks { @@ -37,7 +37,11 @@ declare module '@nuxt/schema' { 'devtools:customTabs:refresh': () => void /** - * Register a terminal. + * Register a terminal whose process is owned by the caller (module). + * + * The registered session is surfaced **read-only** in the built-in Vite + * DevTools **Terminals** dock; stream output into it via + * `devtools:terminal:write`. */ 'devtools:terminal:register': (terminal: TerminalState) => void @@ -59,18 +63,13 @@ declare module '@nuxt/schema' { * Mark a terminal as terminated. */ 'devtools:terminal:exit': (_: { id: string, code?: number }) => void - } -} -declare module '@nuxt/schema' { - /** - * Runtime Hooks - */ - interface RuntimeNuxtHooks { /** - * On terminal data. + * Spawn a terminal whose process is owned by the Vite DevTools terminals + * host (surfaced in the built-in **Terminals** dock). Set + * `interactive: true` for a fully interactive PTY (stdin + resize). */ - 'devtools:terminal:data': (payload: { id: string, data: string }) => void + 'devtools:terminal:spawn': (options: SpawnTerminalOptions) => void | Promise } } diff --git a/packages/devtools-kit/src/_types/rpc.ts b/packages/devtools-kit/src/_types/rpc.ts index e52759a275..54dd254bae 100644 --- a/packages/devtools-kit/src/_types/rpc.ts +++ b/packages/devtools-kit/src/_types/rpc.ts @@ -7,7 +7,6 @@ import type { ModuleCustomTab } from './custom-tabs' import type { AssetEntry, AssetInfo, AutoImportsWithMetadata, ComponentRelationship, HookInfo, ImageMeta, NpmCommandOptions, NpmCommandType, PackageUpdateInfo, ScannedNitroTasks, ServerRouteInfo } from './integrations' import type { ModuleOptions, NuxtDevToolsOptions } from './options' import type { InstallModuleReturn, ServerDebugContext } from './server-ctx' -import type { TerminalAction, TerminalInfo } from './terminals' export interface ServerFunctions { // Static RPCs (can be provide on production build in the future) @@ -38,11 +37,6 @@ export interface ServerFunctions { getNpmCommand: (command: NpmCommandType, packageName: string, options?: NpmCommandOptions) => Promise runNpmCommand: (command: NpmCommandType, packageName: string, options?: NpmCommandOptions) => Promise<{ processId: string } | undefined> - // Terminal - getTerminals: () => TerminalInfo[] - getTerminalDetail: (id: string) => Promise - runTerminalAction: (id: string, action: TerminalAction) => Promise - // Storage getStorageMounts: () => Promise getStorageKeys: (base?: string) => Promise @@ -83,7 +77,11 @@ export interface ClientFunctions { callHook: (hook: string, ...args: any[]) => Promise navigateTo: (path: string) => void - onTerminalData: (_: { id: string, data: string }) => void + /** + * Server→client signal that a terminal session has exited. Kept so the + * client can clear transient subprocess UI state (installing-modules / + * analyze-build / npm updates) when the underlying process finishes. + */ onTerminalExit: (_: { id: string, code?: number }) => void } diff --git a/packages/devtools-kit/src/_types/terminals.ts b/packages/devtools-kit/src/_types/terminals.ts index 46f6ff0914..33d9535212 100644 --- a/packages/devtools-kit/src/_types/terminals.ts +++ b/packages/devtools-kit/src/_types/terminals.ts @@ -49,3 +49,37 @@ export interface TerminalState extends TerminalInfo { */ onActionTerminate?: () => Promise | void } + +/** + * Options for spawning a DevTools terminal that is owned by the Vite DevTools + * terminals host (surfaced in the built-in **Terminals** dock). + * + * Unlike the `devtools:terminal:register`/`:write` hooks (where the *module* + * owns the process and merely streams output), the process is spawned and + * owned by DevTools. Set {@link interactive} to `true` to get a fully + * interactive PTY (stdin + resize, TUI-capable) instead of an output-only + * child process. + */ +export interface SpawnTerminalOptions extends TerminalBase { + /** Command to execute. */ + command: string + /** Command arguments. */ + args?: string[] + /** Working directory. Defaults to the process cwd. */ + cwd?: string + /** Extra environment variables. */ + env?: Record + /** + * Spawn a fully interactive pseudo-terminal (PTY) instead of an output-only + * child process. Interactive sessions accept keystrokes and resizing in the + * built-in Terminals dock. Powered by `zigpty`; where its native bindings + * can't load, it gracefully degrades to pipe-based emulation. + * + * @default false + */ + interactive?: boolean + /** Initial column count (PTY only). Default: 80. */ + cols?: number + /** Initial row count (PTY only). Default: 24. */ + rows?: number +} diff --git a/packages/devtools-kit/src/index.ts b/packages/devtools-kit/src/index.ts index d7f54c62dc..49d4876c55 100644 --- a/packages/devtools-kit/src/index.ts +++ b/packages/devtools-kit/src/index.ts @@ -2,7 +2,7 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { BirpcGroup } from 'birpc' import type { ChildProcess } from 'node:child_process' import type { Result } from 'tinyexec' -import type { ModuleCustomTab, NuxtDevtoolsInfo, NuxtDevtoolsServerContext, SubprocessOptions, TerminalState } from './types' +import type { ModuleCustomTab, NuxtDevtoolsInfo, NuxtDevtoolsServerContext, SpawnTerminalOptions, SubprocessOptions, TerminalState } from './types' import { useNuxt } from '@nuxt/kit' import { x } from 'tinyexec' import { deprecate } from './diagnostics' @@ -195,6 +195,30 @@ export function onDevToolsInitialized(fn: (info: NuxtDevtoolsInfo) => void, nuxt nuxt.hook('devtools:initialized', fn) } +/** + * Spawn a terminal owned by the Vite DevTools terminals host, surfaced in the + * built-in **Terminals** dock. + * + * Unlike {@link startSubprocess} (where the module owns the process and only + * streams output to a read-only session), DevTools spawns and owns the process + * here. Set `interactive: true` for a fully interactive PTY (stdin + resize, + * TUI-capable) — powered by `zigpty` with a graceful pipe fallback where the + * native bindings can't load. + * + * @example + * ```ts + * startDevToolsTerminal({ + * id: 'my-module:shell', + * name: 'Shell', + * command: 'bash', + * interactive: true, + * }) + * ``` + */ +export function startDevToolsTerminal(options: SpawnTerminalOptions, nuxt = useNuxt()): Promise { + return Promise.resolve(nuxt.callHook('devtools:terminal:spawn', options)) +} + /** * Run a callback once the Vite DevTools kit has connected, receiving the * connected `ViteDevToolsNodeContext`. diff --git a/packages/devtools/client/components/ModuleItem.vue b/packages/devtools/client/components/ModuleItem.vue index fdfe0b43bf..08eab60c37 100644 --- a/packages/devtools/client/components/ModuleItem.vue +++ b/packages/devtools/client/components/ModuleItem.vue @@ -1,7 +1,6 @@