-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathtodos.json
More file actions
494 lines (478 loc) · 35 KB
/
Copy pathtodos.json
File metadata and controls
494 lines (478 loc) · 35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
{
"project": "vue-termui",
"summary": "Ground-up rewrite of vue-termui: build terminal apps with Vue 3, rendered by @opentui/core (Zig native renderer over Node FFI). The previous implementation (custom renderer + yoga + hand-rolled ANSI) lives in old/ and is mined/dropped progressively.",
"goal": "Re-create a Vue 3 custom renderer on top of @opentui/core, then migrate the old component/composable API (or drop pieces OpenTUI now handles). Keep @opentui/core a hidden implementation detail of vue-termui (never re-export it from the public API).",
"architecture": {
"decision": "vue-termui = a Vue custom renderer (Vue's createRenderer + custom nodeOps) whose host elements map to OpenTUI Renderables, mounted into the OpenTUI renderer's `root`. This mirrors the OLD design but swaps the DOMElement+yoga+manual-ANSI backend for OpenTUI's Renderable tree (OpenTUI does layout, ANSI, input, focus, mouse, resize, screen modes natively).",
"publicApiBoundary": "Apps depend ONLY on vue-termui. @opentui/core is a normal dependency of the vue-termui package (root package.json) and must NOT be re-exported. Wrap what users need behind vue-termui's own API.",
"vueRuntime": "@vue/runtime-core (NOT @vue/runtime-dom). The old core re-exported * from @vue/runtime-core; decide whether to keep that convenience or curate exports."
},
"runtime": {
"node": ">=26.3.0 with --experimental-ffi (creating an OpenTUI renderer loads native code via FFI; plain imports of @opentui/core do not need FFI)",
"packageManager": "pnpm 11 (corepack). Settings live in pnpm-workspace.yaml (allowBuilds, etc.), NOT package.json#pnpm.",
"knownGotcha": "On dev machines using volta, the corepack tool can be pinned to an old node (e.g. 24), so pnpm-run scripts get the wrong node. Fix is on the machine (re-pin corepack to node 26), not the repo. The repo pins via engines + volta.node + CI node-version-file: package.json.",
"buildGotchas": "tsdown uses isolatedDeclarations (explicit types on all exports). @opentui/core and vue stay EXTERNAL (deps/peerDeps are auto-externalized; vue is in tsdown neverBundle). Run scripts non-interactively in CI/agents with CI=true (pnpm purges modules / confirms without a TTY otherwise)."
},
"conventions": [
"Conventional Commits (feat|fix|refactor|build|ci|chore|docs|style|perf|test). Commit in small steps.",
"TDD when it fits: write failing tests BEFORE implementation. Tests co-located as *.spec.ts; type tests as *.test-d.ts.",
"Testing: prefer OpenTUI's test renderer from `@opentui/core/testing` (see opentui skill /docs/core-concepts/testing) + vitest. The old mocks/stdmock.ts is obsolete.",
"oxc toolchain: `pnpm lint` (oxlint), `pnpm fmt` (oxfmt). `pnpm test` = build + coverage + typecheck. `pnpm build` = tsdown. `pnpm play` / `nr play` runs the playground.",
"Keep AGENTS.md updated when commands/structure/tooling change.",
"Use `node` to run TS directly (node 26 strips types); never tsx. ESM only.",
"Reuse code from old/ by reading it for intent, not copy-paste — most backend code is replaced by OpenTUI."
],
"openTui": {
"skill": "Use the `opentui` skill. Entry docs: /docs/getting-started, /docs/core-concepts/renderer, /docs/core-concepts/layout, /docs/core-concepts/keyboard, /docs/core-concepts/testing, /docs/components/text, /docs/components/input.",
"providesNatively_dropOldEquivalents": [
"Rendering + ANSI + render loop (createCliRenderer) -> drop old renderer/Output, LogUpdate, render, renderNodeToOutput, renderBorders, text, textColor",
"Flexbox layout (yoga internal) -> drop old renderer/styles + yoga-layout-prebuilt; map style props to OpenTUI props",
"Keyboard (incl. kitty protocol) + mouse parsing -> drop old input/inputSequences, keyEvents, handling, debug",
"Focus management (autoFocus, currentFocusedRenderable, focused_renderable event) -> drop old focus/FocusManager + Focusable internals; keep a thin composable",
"Exit/signals (exitOnCtrlC, exitSignals) -> drop old deps/signal-exit",
"Screen modes, resize event, terminal title, cursor, clipboard, theme detection, console/debug overlays"
],
"keyExports": {
"renderer": "createCliRenderer(config) -> renderer with `root` (RootRenderable), events (resize, frame, focused_renderable, ...), keyInput (KeyHandler), width/height, idle(), destroy()",
"renderableClasses": "BoxRenderable, TextRenderable, TextNodeRenderable, RootTextNodeRenderable, InputRenderable, SelectRenderable, ScrollBoxRenderable, etc. Plus isRenderable, maybeMakeRenderable, isTextNodeRenderable.",
"factories": "Box(props, ...children), Text(props), Input(props), Select(props) — convenience wrappers over the classes.",
"color": "RGBA, parseColor"
},
"investigateFirst": "Exact Renderable constructor signature (they take a renderContext from the renderer) and how to add/remove/reorder children + set props imperatively — this is what the Vue nodeOps must call. Check @opentui/core types and the testing docs."
},
"oldReferences": {
"note": "All paths under old/packages/. Read for API shape and intent.",
"corePublicApi": "old/packages/core/src/index.ts (full list of exported components, composables, types)",
"vueCustomRendererPattern": "old/packages/core/src/renderer/index.ts (createRenderer wiring) + renderer/nodeOpts.ts (insert/remove/createElement/createText/patchProp/setText/parentNode/nextSibling)",
"appLifecycle": "old/packages/core/src/app/createApp.ts + app/types.ts (mount({renderOnce, exitOnCtrlC}), waitUntilExit, unmount, exitApp)",
"components": "old/packages/core/src/components/* (TuiBox, TuiText, TuiNewline, TuiLink, TuiTextTransform, TuiInput (Input.vue), TuiProgressBar, TuiApp). Box/Text show the props surface.",
"styleSyntax": "old/packages/core/src/style-syntax/* (class string -> style props, with aliases) — nice DX, optional port",
"composables": "old/packages/core/src/composables/* (keyboard onKeyData, mouse onMouseData, input onInputData, screen onResize/useStdoutDimensions/useTitle, utils useInterval/useTimeout, writeStreams useStdout)",
"inputTypes": "old/packages/core/src/input/types.ts (KeyDataEvent/MouseDataEvent shapes & type guards) — keep the public event types worth preserving",
"focus": "old/packages/core/src/focus/* (useFocus, useFocusManager API surface)",
"vitePlugin": "old/packages/vite-plugin-vue-termui/src/index.ts (SFC via @vitejs/plugin-vue, auto-import of vue-termui, component auto-resolver, externals)"
},
"completed": [
"Branch rewrite-opentui created; old project moved into old/.",
"Core scaffolded from template-lib-ts (tsdown + oxc + vitest + isolatedDeclarations), renamed to vue-termui.",
"playground/ workspace package added; OpenTUI hello world renders over Node FFI (Box+Text, Ctrl+C to exit).",
"Migrated tooling to pnpm 11 via official codemod; engines.node >=26.3.0 set; CI reads node-version-file: package.json.",
"@opentui/core moved to vue-termui's dependencies; opentui re-exports REMOVED from public API (kept as implementation detail). Playground temporarily imports @opentui/core directly until the renderer API exists.",
"PHASE 1 DONE: Vue custom renderer foundation. src/renderer/nodeOps.ts (createNodeOps(ctx) -> RendererOptions; host tags 'box'->BoxRenderable, 'text'->TextRenderable; lone-string text child uses setElementText/.content fast path, array text uses TextNodeRenderable.fromString; comments = invisible BoxRenderable anchors; ids auto-generated by OpenTUI). src/renderer/index.ts: async createApp(rootComponent, props?, rendererConfig?) -> awaits createCliRenderer, createRenderer(createNodeOps(renderer)), mounts into renderer.root. Added @vue/runtime-core dep; vue-termui RE-EXPORTS @vue/runtime-core (single runtime-core instance — avoids 'two Vues'). Playground now imports ONLY from vue-termui (dropped @opentui/core + vue deps). Tests: src/renderer/nodeOps.spec.ts via @opentui/core/testing createTestRenderer (// @vitest-environment node; NODE_OPTIONS=--experimental-ffi baked into test:cov/dev scripts since vitest poolOptions.forks.execArgv did not propagate)."
],
"phases": [
{
"id": 1,
"title": "Vue custom renderer foundation (box + text only)",
"status": "done",
"goal": "Stand up a Vue createRenderer with custom nodeOps that map host elements to OpenTUI Renderables, mounted into an OpenTUI renderer's root. Support just two host elements to start.",
"steps": [
"Investigate OpenTUI Renderable construction (renderContext, add/remove/insertBefore child ops, prop setters, text nodes via TextNodeRenderable).",
"Create src/renderer/nodeOps.ts: createElement(tag) -> new BoxRenderable/TextRenderable; createText/createComment -> text node; insert(child, parent, anchor); remove; patchProp(el, key, prev, next) -> set renderable prop; parentNode; nextSibling; setElementText/setText.",
"Create src/renderer/index.ts: const { createApp: baseCreateApp, render } = createRenderer(nodeOps) from @vue/runtime-core.",
"Decide host element tag names (e.g. 'box', 'text') and how text content maps to TextNodeRenderable.",
"Write a minimal createApp wiring (full lifecycle is phase 3) that creates the OpenTUI renderer and mounts the Vue root into renderer.root."
],
"oldRefs": [
"renderer/index.ts",
"renderer/nodeOpts.ts",
"renderer/dom.ts (for the tree-ops mental model only)"
],
"openTuiApis": [
"createCliRenderer",
"renderer.root",
"BoxRenderable",
"TextRenderable",
"TextNodeRenderable",
"RootRenderable"
],
"newFiles": ["src/renderer/nodeOps.ts", "src/renderer/index.ts"],
"acceptance": [
"A Vue render-function component using the host tags renders a box with text in the playground.",
"Reactive update (changing a ref) re-renders correctly.",
"Unit test with @opentui/core/testing asserts the rendered frame/tree (test written first)."
],
"notes": "Drop DOMElement + yoga entirely; OpenTUI owns layout. The nodeOps just translate Vue tree mutations into Renderable tree mutations."
},
{
"id": 2,
"title": "Core components: Box, Text, Newline",
"status": "done",
"goal": "Ship Vue components that render the host elements with a clean prop surface mapped to OpenTUI props.",
"steps": [
"Implement Box (flex container: layout, padding/margin, border, gap, bg) and Text (fg/bg + bold/italic/underline/dim/strikethrough/inverse) and Newline.",
"Map old props (which used chalk + yoga style names) to OpenTUI prop names/colors (RGBA/parseColor). Drop chalk.",
"Decide export naming: keep Tui-prefixed (TuiBox) or rename to Box/Text now that there's no DOM clash. Pick one and document in AGENTS.md.",
"Optionally port style-syntax (class string -> props) as a later sub-step."
],
"oldRefs": [
"components/Box.ts",
"components/Text.ts",
"components/Newline.ts",
"components/index.ts",
"style-syntax/*"
],
"openTuiApis": ["Box props", "Text props", "RGBA", "parseColor"],
"acceptance": [
"Components render with reactive props; spec tests first; playground demo updated to use the components via createApp."
],
"notes": "TextTransform/Link/ProgressBar/Input come later (phase 7)."
},
{
"id": 3,
"title": "App lifecycle & exit",
"status": "done",
"notesDone": "waitUntilExit()/exit()/useExit() landed on rewrite-opentui. exitOnCtrlC/exitSignals stay native CliRendererConfig (passed via createApp's rendererConfig). renderOnce is EXPERIMENTAL and parked on branch explore/render-once (real-terminal persistence unverified).",
"goal": "Full createApp API backed by the OpenTUI renderer.",
"steps": [
"createApp(rootComponent, options): create renderer, provide injection symbols, mount({ renderOnce, exitOnCtrlC }).",
"Implement waitUntilExit(), unmount() (destroy renderer, restore terminal), and exitApp().",
"Use OpenTUI's exitOnCtrlC/exitSignals + renderer.destroy(); drop old deps/signal-exit and manual raw-mode/screen-buffer code.",
"Port the injection symbols the components need (logSymbol, rootNodeSymbol, scheduleUpdate, etc.) — keep only what's still relevant."
],
"oldRefs": ["app/createApp.ts", "app/types.ts", "injectionSymbols.ts", "errors/TuiError.ts"],
"openTuiApis": [
"createCliRenderer({exitOnCtrlC, exitSignals})",
"renderer.destroy()",
"renderer.on('destroy')",
"renderer screen modes"
],
"acceptance": [
"App mounts, Ctrl+C exits cleanly restoring the terminal, waitUntilExit resolves; tested."
],
"notes": "renderOnce mode maps to rendering a frame then destroying (see renderer.idle())."
},
{
"id": 4,
"title": "Input composables (keyboard / mouse)",
"status": "done",
"notesDone": "onKeyDown/onKeyUp over renderer.keyInput (keypress/keyrelease); public KeyEvent type defined locally (OpenTUI's KeyEvent is NOT exported from the package root). Cleanup via onScopeDispose. Mouse has NO global stream — per-element onMouse*/onMouseDown props are forwarded natively by Box, so no onMouseData composable. onInputData (raw stdin) dropped (OpenTUI parses input).",
"goal": "onKeyData / onMouseData / onInputData composables backed by OpenTUI input.",
"steps": [
"Wire onKeyData/onMouseData onto renderer.keyInput (KeyHandler) and renderer input events; expose RemoveListener cleanup tied to component scope.",
"Drop hand-rolled parsing (input/inputSequences, keyEvents, handling, debug). Keep/port only the public event TYPES worth preserving (input/types.ts).",
"Reconcile the event shape with OpenTUI's parsed key events (modifiers, key codes)."
],
"oldRefs": [
"composables/keyboard.ts",
"composables/mouse.ts",
"composables/input.ts",
"input/types.ts",
"input/debug.ts"
],
"openTuiApis": [
"renderer.keyInput / KeyHandler",
"addInputHandler / prependInputHandler",
"useKittyKeyboard config"
],
"acceptance": [
"A playground key handler reacts to keypresses; mouse optional; tested with simulated input via the test renderer."
],
"notes": "See opentui skill /docs/core-concepts/keyboard."
},
{
"id": 5,
"title": "Screen & timing composables",
"status": "done",
"notesDone": "onResize/useTerminalSize over renderer RESIZE (dims read live off renderer.width/height). useTitle via renderer.setTerminalTitle (resets on unmount). useInterval/useTimeout are pure timers with onScopeDispose cleanup. writeStreams/useStdout dropped (OpenTUI owns stdout).",
"goal": "onResize, useStdoutDimensions, useTitle, useInterval, useTimeout.",
"steps": [
"onResize -> renderer.on('resize'); useStdoutDimensions -> renderer.width/height (reactive, updated on resize); useTitle -> renderer.setTerminalTitle.",
"Port useInterval/useTimeout (framework-agnostic; consider tying animations to renderer.requestLive/dropLive).",
"Decide fate of useStdout/writeStreams: OpenTUI owns stdout. Either drop, or adapt to writeToScrollback (split-footer)."
],
"oldRefs": ["composables/screen.ts", "composables/utils.ts", "composables/writeStreams.ts"],
"openTuiApis": [
"renderer.on('resize')",
"renderer.width/height",
"renderer.setTerminalTitle",
"requestLive/dropLive",
"writeToScrollback"
],
"acceptance": [
"Resize updates dimensions reactively; title sets; interval/timeout cleaned up on unmount; tested."
]
},
{
"id": 6,
"title": "Focus",
"status": "done",
"notesDone": "useFocus({autoFocus}) -> {ref, focused, focus, blur}: bind ref to a host element; sets focusable=true and tracks renderer.currentFocusedRenderable via FOCUSED_RENDERABLE. useFocusManager() -> {focused (reactive), focus(renderable), blur()}. OpenTUI has NO global Tab cycling/ordering — apps manage their own list (per OpenTUI docs).",
"goal": "useFocus / useFocusManager over OpenTUI focus.",
"steps": [
"Expose composables that read/drive OpenTUI focus (currentFocusedRenderable, focused_renderable event, autoFocus, Tab/Shift-Tab navigation).",
"Drop the hand-rolled FocusManager/Focusable internals; keep a compatible public API where sensible."
],
"oldRefs": [
"focus/Focusable.ts",
"focus/FocusManager.ts",
"focus/types.ts",
"focus/FocusManager.spec.ts"
],
"openTuiApis": [
"renderer.currentFocusedRenderable",
"renderer.on('focused_renderable')",
"autoFocus",
"focusable renderables"
],
"acceptance": [
"Tab cycles focus across focusable components; useFocus reports focused state; tested."
]
},
{
"id": 7,
"title": "Interactive & rich components",
"status": "partial",
"notesDone": "DONE: Input (host 'input'->InputRenderable) + Select (host 'select'->SelectRenderable) with v-model; ProgressBar as a Box+Text composite (OpenTUI has no native progress renderable). Host tags 'input'/'select' added to nodeOps + vite HOST_TAGS. GOTCHA: never forward `undefined` host props — they clobber renderable defaults (Input maxLength defaults to 1000; undefined -> substring(0,NaN) swallows all typed text). DEFERRED: Link (OSC8 hyperlink) and TextTransform need TextNode-with-link/transform support threaded through the renderer (nodeOps text-node children don't carry per-node link/style yet) — a separate effort; low priority, not required by acceptance.",
"goal": "Input, Select, ProgressBar, Link, TextTransform.",
"steps": [
"Map Input/Select onto InputRenderable/SelectRenderable. Port ProgressBar, Link (OSC 8 hyperlinks), TextTransform.",
"v-model integration for Input/Select."
],
"oldRefs": [
"components/Input.vue",
"components/ProgressBar.ts",
"components/Link.ts",
"components/TextTransform.ts"
],
"openTuiApis": ["Input/InputRenderable", "Select/SelectRenderable", "ScrollBoxRenderable"],
"acceptance": ["Text input editing + select navigation work with v-model; tested."]
},
{
"id": 8,
"title": "SFC + DX tooling (RECONSIDER / DEFER)",
"status": "done",
"notesDone": "REVIEW COMPLETE. SFC authoring works via vue-termui/vite (src/vite.ts: plugin-vue client-compile + host-tag custom elements + module-runner HMR rerender/reload + autoLaunch). Render-function (.ts h()) authoring needs no build. DECISION: NO auto-import / component resolver — explicit imports from 'vue-termui' are clearer, fully typed, and avoid maintaining unplugin against the runnable-ssr module runner. New host tags must be added in BOTH nodeOps.createElement and vite HOST_TAGS (done for input/select).",
"goal": "Decide how .vue SFCs and auto-import are supported, if at all.",
"steps": [
"Node can't run .vue directly. Options: (a) author components as .ts with h()/defineComponent (no build) — simplest, default for now; (b) a vite/tsdown plugin using @vue/compiler-sfc for SFCs; (c) an OpenTUI runtime-plugin.",
"If keeping a vite plugin, modernize old vite-plugin-vue-termui (SFC + auto-import of vue-termui exports + component resolver). Drop node14 target/externals cruft.",
"Decide whether auto-import/component-resolver DX is worth maintaining."
],
"oldRefs": [
"vite-plugin-vue-termui/src/index.ts",
"playground SFC usage in old/packages/playground"
],
"acceptance": [
"A documented, working way to author components (render fns at minimum); SFC support only if chosen."
],
"notes": "Keep simple: ship render-function components first; SFC tooling is optional and later."
},
{
"id": 9,
"title": "Docs (DEFER)",
"status": "todo",
"goal": "Refresh/migrate docs once the public API stabilizes.",
"oldRefs": ["old/packages/docs (vitepress)"],
"acceptance": ["Docs reflect the OpenTUI-based API."],
"notes": "Do not start until phases 1-7 settle the API."
}
],
"drop": {
"packages": [
"cli / @vue-termui/cli — user decided NO CLI",
"create-vue-termui — scaffolder; drop for now, maybe revisit",
"domino — experimental DOM-ish app/example; drop",
"xterm-playground — drop",
"vite-plugin-vue-termui — only if SFC/auto-import DX is abandoned (see phase 8)"
],
"coreModules": [
"renderer/Output.ts, LogUpdate.ts, render.ts, renderNodeToOutput.ts, renderBorders.ts, text.ts, textColor.ts, styles.ts, dom.ts — replaced by OpenTUI",
"input/inputSequences.ts, keyEvents.ts, handling.ts, debug.ts — OpenTUI parses input",
"deps/signal-exit/* — OpenTUI handles exit signals",
"hmr/* — drop custom WS HMR; rely on vite/runtime-plugin later if needed",
"mocks/stdmock.ts — replaced by @opentui/core/testing",
"yoga-layout-prebuilt + chalk + ansi-* deps — no longer needed",
"globals.d.ts (__DEV__ etc.) / shims-vue.d.ts — revisit only if SFC tooling returns"
]
},
"openQuestions": [
"RESOLVED (Phase 2): public components are UNPREFIXED (Box/Text/Newline). Host tags stay internal lowercase 'box'/'text' and still work in templates. Components authored as explicitly-typed FunctionalComponents (not defineComponent) to satisfy isolatedDeclarations; runtime .props give Boolean coercion. Text folds bold/italic/etc into OpenTUI's `attributes` bitmask.",
"RESOLVED (Phase 1): keep `export * from '@vue/runtime-core'`. Required, not just convenience — apps must get h/defineComponent/ref from the same runtime-core instance the renderer uses, else vnode/instance interop breaks. Revisit curation later if surface is too broad.",
"RESOLVED (Phase 8): both supported — SFCs via vue-termui/vite, render fns (.ts) with no build. No auto-import/component-resolver (explicit imports).",
"RESOLVED: placeholder src/useHello.ts + tests removed now that real composables exist."
],
"backlog": {
"note": "Post-phase-7 refinement + investigation items (from review 2026-06-23). Not sequential phases; grouped by theme. `challenge:` records pushback on the original ask where the repo already does it or a different approach is better.",
"apiErgonomics": [
{
"id": "be1",
"title": "useRendererEvent(name, handler) generic composable",
"status": "todo",
"detail": "Create a single composable that wraps `renderer.on/off` with scope-tied cleanup (onScopeDispose) and returns a RemoveListener. Accept a plain string union of event names instead of forcing users to import the CliRenderEvents enum. DRY: onResize, useFocus, useFocusManager all hand-roll this exact on/off+dispose dance today.",
"challenge": "It does NOT exist yet — the original ask said 'rename', but nothing defines useRendererEvent. This is a create, and it should be the shared primitive the other renderer composables are refactored onto.",
"location": "src/composables/renderer.ts (new) — also a natural home for useRenderer re-export.",
"openTuiRefs": ["CliRenderEvents", "renderer.on/off"]
},
{
"id": "be2",
"title": "useFocusedElement(): ShallowRef<Renderable | null>",
"status": "todo",
"detail": "Thin composable exposing just the reactive currently-focused renderable.",
"challenge": "Largely already exists as useFocusManager().focused (focus.ts:115, shallowRef synced via FOCUSED_RENDERABLE). Decide: (a) extract a standalone useFocusedElement() that useFocusManager reuses, or (b) skip and document `const { focused } = useFocusManager()`. Lean (a) for discoverability, but it must share one listener, not add a second.",
"location": "src/composables/focus.ts"
},
{
"id": "be3",
"title": "Shared TS utils module (RemoveListener, etc.)",
"status": "todo",
"detail": "Move RemoveListener out of keyboard.ts into a shared src/utils/types.ts (or composables/renderer.ts). Collect other cross-cutting helper types as they appear.",
"challenge": "RemoveListener currently lives in keyboard.ts and is imported by screen.ts — odd home. Keep the public re-export path stable (still exported from index). Don't over-build a utils grab-bag; add types only when ≥2 modules need them.",
"location": "src/composables/keyboard.ts -> shared module"
},
{
"id": "be4",
"title": "Reconsider reimplementing vueuse/core composables",
"status": "todo",
"detail": "Audit useInterval/useTimeout (timing.ts) and any future helper against @vueuse/core / @vueuse/shared (which are DOM-free and runtime-core compatible).",
"challenge": "Decision rule from the ask: do NOT ship our own version of something vueuse already does well. If we only need it internally, depend on it but DON'T re-export it (keep public surface minimal). Only expose/own a composable when terminal semantics differ from the web (e.g. timers tied to renderer.requestLive/dropLive). Verify @vueuse/shared truly has no DOM coupling before adding the dep.",
"location": "src/composables/timing.ts"
},
{
"id": "be5",
"title": "createApp renderer-options naming",
"status": "todo",
"detail": "createApp(root, props?, rendererConfig?: CliRendererConfig) already forwards options to createCliRenderer (renderer/index.ts:210).",
"challenge": "The ask ('pass createCliRenderer options as rendererOptions') is ALREADY DONE — only the name differs (rendererConfig vs rendererOptions). Reduce to a naming decision; rename to rendererOptions only if we prefer it, then update docs/cookbook. Low effort, low priority."
}
],
"typesAndDocs": [
{
"id": "be6",
"title": "Audit BoxProps against OpenTUI layout docs",
"status": "todo",
"detail": "Cross-check every BoxProps field + value union against https://opentui.com/docs/core-concepts/layout/ (flex, sizing, spacing, position). Fill gaps, fix value unions, drop anything OpenTUI doesn't accept.",
"location": "src/components/types.ts:38 (BoxProps)",
"challenge": "BoxProps is already fairly complete; this is a verification pass, not a rewrite. Add a type-level test (*.test-d.ts) so drift is caught."
},
{
"id": "be7",
"title": "Co-locate component prop types with components",
"status": "todo",
"detail": "Move BoxProps -> Box.ts, TextProps -> Text.ts, SelectOption -> Select.ts. Keep only shared primitives (ColorInput, Dimension, FlexDirection, Align, Justify, Position, Overflow, BorderStyle) in types.ts.",
"challenge": "InputProps already lives in Input.ts — so this just makes Box/Text/Select consistent. Re-export from components/index + root index so the public path is unchanged.",
"location": "src/components/types.ts"
},
{
"id": "be8",
"title": "Complete TextProps docs + JSDoc style sweep",
"status": "todo",
"detail": "Document every TextProps field (bold/dim/italic/underline/blink/inverse/strikethrough have no descriptions today). Apply house JSDoc style everywhere: always multiline block comments, and a blank line after each documented property.",
"location": "src/components/types.ts:114 (TextProps)"
},
{
"id": "be9",
"title": "Type-safe KeyEvent.name (and related fields)",
"status": "todo",
"detail": "KeyEvent.name is `string` today (keyboard.ts:13). Find OpenTUI's canonical key-name list and type name as a union (with a `(string & {})` escape hatch for forward-compat). Same treatment for eventType ('press'|'release'|'repeat' already known).",
"location": "src/composables/keyboard.ts:12",
"challenge": "Investigate whether OpenTUI exports the name list; if not, maintain our own union and add a test that flags when OpenTUI emits a name we don't cover."
}
],
"focusSystem": [
{
"id": "be10",
"title": "Rethink Input `focus` prop",
"status": "todo",
"detail": "Input exposes `focus?: boolean` meaning 'focus on mount' (Input.ts:22,79). Docs (https://opentui.com/docs/components/input/) talk about id/delegate, but those belong to the construct API we deliberately don't use.",
"challenge": "`focus: true` reads as a weird boolean. Options: rename to `autoFocus` (matches useFocus({autoFocus})), or drop in favor of useFocus + template ref. Decide one and apply across all focusable components consistently. Do NOT adopt the construct/delegate API — imperative only.",
"location": "src/components/Input.ts"
},
{
"id": "be11",
"title": "Design a real focus system (registration-based)",
"status": "todo",
"detail": "Investigate a focus manager where components register themselves as focusable elements into an ordered set, enabling app-managed traversal. OpenTUI has NO global Tab cycling/ordering (it leaves it to the app — see phase 6 notes).",
"challenge": "User isn't sure registration is the right model ('I might be wrong'). Weigh registration-set vs. DOM-order traversal vs. explicit app-owned list. Prototype before committing; document the tradeoff. This underpins be10/be12.",
"oldRefs": ["focus/FocusManager.ts", "focus/Focusable.ts"]
},
{
"id": "be12",
"title": "Advanced keyboard navigation (browser-like)",
"status": "todo",
"detail": "Go beyond the basic Tab example at https://opentui.com/docs/components/input/: Tab/Shift-Tab order, focus traps (modals/popups), Esc to dismiss/blur, arrow-key navigation within composite widgets (lists, radio groups). Mirror browser/AT semantics where sensible.",
"challenge": "Depends on be11's model. Keep it composable (a useFocusTrap, arrow-nav helper) rather than baked into every component."
}
],
"components": [
{
"id": "be13",
"title": "Fix Select rendering",
"status": "todo",
"detail": "Select 'looks weird' / doesn't render properly. Reproduce in playground, capture the bad frame in a spec (TDD: failing test first), then fix the mapping to SelectRenderable.",
"location": "src/components/Select.ts, Select.spec.ts",
"challenge": "Need a concrete repro before changing code — is it option layout, height, highlight, or scroll? Pin it down with the test renderer."
},
{
"id": "be14",
"title": "ProgressBar colors derive from Text colors",
"status": "todo",
"detail": "Replace hardcoded default colors: filled portion uses the inherited/text fg, background uses a dimmed variant — not magic literals.",
"location": "src/components/ProgressBar.ts"
},
{
"id": "be15",
"title": "Full prop/option forwarding per component (split me)",
"status": "todo",
"detail": "Every component should expose typed props that actually forward the underlying renderable's options — today many options aren't passed through. Split into one task per component (Box, Text, Input, Select, ProgressBar, ...).",
"challenge": "This is a meta-task: don't implement as one PR. Each component already has SOME typed props; the gap is coverage + correct undefined-omission (see the Input maxLength gotcha — never forward undefined onto a renderable default)."
},
{
"id": "be16",
"title": "Design composition for FrameBuffer & line-numbers",
"status": "todo",
"detail": "These OpenTUI features have imperative/composition patterns that don't map cleanly to declarative components — needs design discussion before any code.",
"challenge": "Explicitly deferred for deeper discussion; do NOT lump with be15. Capture the API question first."
},
{
"id": "be17",
"title": "Rich text: does `t` tagged template work? Link/TextTransform",
"status": "todo",
"detail": "Investigate OpenTUI's `t` template literal for rich/styled inline text (https://opentui.com/docs/components/text/). If viable, it likely unblocks the deferred Link (OSC8) + TextTransform from phase 7.",
"challenge": "Phase 7 deferred Link/TextTransform because text-node children don't carry per-node link/style through nodeOps. `t` may be the missing primitive — confirm before designing the components.",
"oldRefs": ["components/Link.ts", "components/TextTransform.ts"]
}
],
"errorDX": [
{
"id": "be18",
"title": "Validate common authoring mistakes + dev error overlay",
"status": "todo",
"detail": "Two parts: (1) nodeOps-level guards for invalid trees (e.g. <Text> nested in <Text>, text where a box is required) with clear messages; (2) a built-in dev-only error component/overlay that shows the error + full stack trace on screen (errors are currently hard to debug).",
"challenge": "Check what OpenTUI's console/debug overlay already gives us before building our own surface — wrap it rather than duplicate. Gate everything behind a dev flag so prod TUIs stay clean. Split into validation vs. overlay if it grows.",
"location": "src/renderer/nodeOps.ts, src/renderer/errors.spec.ts"
}
],
"exploration": [
{
"id": "be19",
"title": "Explore id registry + getElementById()",
"status": "todo",
"detail": "Keep a map of renderables by id so code can look one up and call e.g. el.focus() across components.",
"challenge": "el.focus() already works via template refs locally — the only real win is CROSS-component access by id. Validate that need with a concrete use case before building a registry (memory/teardown cost). OpenTUI auto-generates ids; we'd need a stable user-supplied id prop. Low priority until something needs it.",
"priority": "low"
},
{
"id": "be20",
"title": "Investigate keymap package for high-level widgets",
"status": "todo",
"detail": "Evaluate https://opentui.com/docs/keymap/overview/ for command palettes, key combos/chords — likely distinct from renderer.keyInput we wired in phase 4.",
"challenge": "Confirm it's additive to keyInput (not a replacement) and whether it belongs in core or a future higher-level package."
},
{
"id": "be21",
"title": "tree-sitter caching strategy",
"status": "todo",
"detail": "https://opentui.com/docs/reference/tree-sitter/ downloads grammars at runtime with no obvious cache. If we ship a CLI we need a cache location. Check how OpenCode (OpenTUI-based) handles it.",
"priority": "low"
},
{
"id": "be22",
"title": "Bun standalone-executable bundling",
"status": "todo",
"detail": "We support Node well; also support bundling to a single binary via Bun (https://opentui.com/docs/reference/standalone-executables/). Verify FFI/native renderer survives the Bun bundle.",
"challenge": "Keep Node the primary target; Bun bundling is an additive distribution path, not a runtime switch. Confirm @opentui/core's native FFI is compatible with `bun build --compile`."
}
]
}
}