GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Proposal: terminal UI (TUI) target that renders json-render views · Issue #414 · devframes/devframe Open Labels enhancementNew feature or requestNew feature or request DescriptionSummaryAdd a terminal UI (TUI) target for devframe. A devframe author defines a view once with Idea credit: @danielroe. Reference: the interactive terminal UI in nuxt/cli v4 ( What exists todayThe "define once" layer is already in the repo. The TUI is one more renderer of it.
Nothing in the repo mentions a TUI, Ink, or OpenTUI today. What nuxt/cli v4 doesFacts from the source, for reference:
What upstream json-render offers
Proposal1. New opt-in package
|
| devframe component | Ink component | Note |
|---|---|---|
Stack |
Box |
direction maps to flexDirection. |
Card |
Card |
|
Text |
Text, Heading |
variant: heading uses Heading. code uses inverse text. |
Badge |
Badge |
|
Button |
custom | Focusable [ label ]. Enter fires press. Ink has no button. |
Icon |
custom | Name in brackets, or a glyph map for the common ph:* names. |
Divider |
Divider |
|
TextInput |
TextInput |
Two-way $bindState works upstream. |
Switch |
custom | [x] label, space toggles. |
KeyValueTable |
KeyValue |
|
DataTable |
Table |
|
CodeBlock |
custom on Box + Text |
No highlighting in phase 1. |
Progress |
ProgressBar |
|
Tree |
custom on List |
Rendered expanded to a depth limit in phase 1. |
Tabs |
Tabs |
|
Link |
Link |
OSC 8 hyperlink when the terminal supports it. |
Select |
Select |
Unknown component types render a placeholder, as the guide for alternative renderers already requires (docs/content/1.guide/21.build-your-own-json-render-frontend.md).
Attach mode, the primary path:
devframe tui [--port <n>] [--base <path>] [--instances-dir <dir>]
The command lists live instances from the same registry devframe connect uses. One instance attaches at once. Several instances show a Select prompt. The TUI reads __connection.json, opens the RPC client over WebSocket (SSE fallback), subscribes to devframe:json-render:index, and renders the views. It reconnects when the dev server restarts.
In-process mode:
my-app tui
createCac gains a tui subcommand next to dev, build, and mcp. It starts the devframe and the TUI in one process. It reuses the attach code path against the local origin, so there is one renderer and one action bridge.
A hub unites many devframes behind one origin. The TUI must work against a hub from the start, because that is how the framework kits (@devframes/vite/hub, @devframes/nuxt/hub, @devframes/next/hub) run.
When the attached instance is a hub, the TUI reads the devframe:docks shared state (HUB_EVENTS.sharedState.docks) instead of the view index. Dock entries of type json-render render as tabs, in dock order. Other dock kinds (iframe, launcher, action) list their title and a link that opens them in the browser. The devframe:docks:active state follows the selected tab so the browser and the terminal agree.
A single devframe with no hub uses the view index (devframe:json-render:index) as before. The attach code detects which one it talks to from __connection.json.
| Key | Action |
|---|---|
tab, shift-tab |
Move focus |
enter, space |
Press or toggle the focused element |
1..9 |
Switch view or dock tab |
? |
Show all shortcuts |
r |
Reconnect |
q, ctrl-c |
Quit |
Port resolveDevUISupport from nuxt/cli: refuse when stdout or stdin is not a TTY, TERM=dumb, CI, or the terminal is under 40x10. The TUI command has no plain fallback because the TUI is the whole command. It exits with a coded DF diagnostic that names the reason. Respect NO_COLOR. Map semantic variants (success, warning, danger) to a small ANSI palette. The @antfu/design tokens have no terminal form, so this palette is the terminal edition of the same vocabulary.
Question researched: can the devframe TUI open inside the nuxt dev terminal UI with little effort? Facts from the nuxt/cli source:
TerminalHost, on globalThis[Symbol.for('nuxt:terminal-host')] (utils/terminal-host.ts). Its withTerminal(work) closes any overlay, releases raw stdin, erases the pinned panel, awaits work, then repaints (dev/tui/index.ts, lendTerminal). An Ink render() that resolves on waitUntilExit() fits this call with no adapter.nuxt dev forks by default. Nuxt modules, so @devframes/nuxt, run in the child. The parent never loads Nuxt. useTerminalHost() returns undefined in the child.nuxt:internal:dev:* messages (dev/utils.ts, NuxtDevIPCMessage). No message registers a shortcut or an action. There is no public shortcuts API.nuxt dev --no-fork runs Nuxt in the parent. There useTerminalHost() works from a module, but nothing gives the module a key to bind. A module could listen on stdin keypress beside the CLI, but both would answer the same key.Three ways to mount, in order of effort:
| Option | Effort | Works today | Note |
|---|---|---|---|
A. Second terminal: devframe tui --port <nuxt port> --base /__devframes/ |
None beyond phase 1 | Yes | Attach mode over WebSocket. --base exists for this since #403. tmux or a split pane. |
| B. Upstream PR to nuxt/cli: a generic "dev action" | Small, about 50 lines upstream, one IPC send in @devframes/nuxt |
No | The child sends { type: 'nuxt:internal:dev:action', key, label, command } once ready. The parent adds the shortcut to the hint line and, on the key, runs command inside withTerminal with inherited stdio. Generic: any module can offer a terminal action. The devframe TUI stays a separate process, so no React or Ink enters the CLI module graph. |
C. In-process --no-fork hook |
Small on our side, fragile | Partly | @devframes/nuxt calls useTerminalHost()?.withTerminal(() => renderTui()). Needs a trigger the CLI does not offer. Not recommended. |
Recommendation: ship A with phase 1, and open option B upstream as a proposal to nuxt/cli after phase 1 proves the TUI. Option B is the "minimal effort" mount. The devframe side is one process.send call. The design work sits upstream and helps other tools too.
.agents/01-positioning.md).DEVFRAME_EVENTS and HUB_EVENTS, no re-typed literals.DF codes via devframe/utils/nostics, each with a docs page under docs/content/6.errors/.docs/content/8.references/1.terms.md: "a devframe", "node side", "RPC client", "dock entry".terms.md adapter list.Render the examples/json-render dashboard in-process with @json-render/ink and a hand-mapped catalog. Prove: all 17 components render, $state patches update live, a Button press reaches the RPC function. No package yet. Result: a short report on this issue with gaps found.
@devframes/tui, attach mode, single devframe and hub (2 to 3 weeks)devframe tui attach mode with instance discovery, reconnect, and auth.json-render dock entry, link list for other dock kinds, active dock kept in sync.DF diagnostics.examples/custom-hub-vite and examples/custom-hub-next both register a json-render dock the TUI can show (parity rule).Acceptance: pnpm -C examples/json-render dev in one terminal and devframe tui in another shows the dashboard with live counters and working buttons. pnpm -C examples/custom-hub-vite dev in one terminal and devframe tui --base /__devframes/ in another shows one tab per json-render dock and a link list for the iframe docks.
tui subcommand on createCac, reusing the attach path against the local origin.initHub hosts get the same through the devframe bin, no new API.@devframes/nuxt sends the action once the hub is ready. No other devframe change.my-app dev (URLs, status, log fold, shortcuts).@json-render/ink blocks us, we revisit.devframe connect reads a bearer token from DEVFRAME_MCP_AUTH_TOKEN. The TUI can do the same with its own variable, or run the interactive OTP flow from devframe/recipes/interactive-auth in the terminal. Which one first?connectDevframe() in packages/devframe/src/client/index.ts fetches ./__connection.json relative to a page. Attach mode needs a Node-side entry that takes an origin. Is that a new export on devframe/rpc/client or a helper inside @devframes/tui?Button, Switch, Tree, CodeBlock): keep them in @devframes/tui, or propose them upstream to @json-render/ink?NUXT_TERM_THEME override. Do we want the same or a fixed palette in phase 1?nuxt dev docs (interactive terminal UI section): https://github.com/nuxt/cli/blob/main/docs/dev.md@json-render/ink: https://github.com/vercel-labs/json-render/tree/main/packages/inkdocs/content/1.guide/8.json-render.mddocs/content/1.guide/21.build-your-own-json-render-frontend.mdResearch and draft written with the help of an agent.
first, I love to see this issue!
however, I do have a concern with the suggested implementation plan. @json-render/ink would add 8.7 MB to dependencies, including making react a dependency. we opted for a hand-rolled UI in nuxt/cli to make it small and light.
would you make your rendering engine out as a library so we can build on top of it? 👀
(and then we could also contribute back to json-render to add an adapter)