demian-cli 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,468 @@
1
+ # demian TUI Architecture
2
+
3
+ > Status: canonical TUI design
4
+ > Depends on: `nodejs/architecture.md`
5
+ > Scope: terminal UI layer for the integrated `demian` package
6
+
7
+ `demian` has two terminal experiences:
8
+
9
+ - `demian-plain`: plain CLI. It preserves raw Markdown output and uses an interactive startup flow when a TTY is available.
10
+ - `demian` and `demian-cli`: rich terminal UI powered by Ink.
11
+
12
+ The TUI is a presentation layer over the existing `SessionRunner` and `EventBus`. It must not change provider behavior, tool policy, permission ordering, transcript format, or safety rules.
13
+
14
+ ---
15
+
16
+ ## 1. Goals
17
+
18
+ - Provide a high-quality terminal experience for interactive coding-agent sessions.
19
+ - Render model Markdown richly inside the terminal.
20
+ - Keep plain CLI output raw and automation-friendly.
21
+ - Preserve the existing `SessionRunner` and `EventBus` contracts.
22
+ - Make Markdown rendering an independent module so it can be tested and improved without touching the session loop.
23
+ - Keep streaming responsive while still producing high-quality final output.
24
+
25
+ ---
26
+
27
+ ## 2. Command Split
28
+
29
+ | Command | Experience | Output contract |
30
+ |---------|------------|-----------------|
31
+ | `demian` | TUI | Interactive Ink UI |
32
+ | `demian-cli` | TUI alias | Interactive Ink UI |
33
+ | `demian-plain` | Plain CLI | Raw Markdown final answer on stdout |
34
+
35
+ Rationale:
36
+
37
+ - `demian` should be the best human-facing experience.
38
+ - `demian` and `demian-cli` open the TUI first, show the resolved defaults, and collect the first message inside the interface.
39
+ - Prompt arguments may remain as a compatibility shortcut, but the canonical UX is not shell-argument driven.
40
+ - `demian-plain` keeps raw Markdown output, but it also has an interactive settings/message flow when run in a TTY.
41
+ - The command split avoids adding mode ambiguity to the session runtime.
42
+
43
+ ---
44
+
45
+ ## 3. Dependencies
46
+
47
+ TUI dependencies:
48
+
49
+ - `ink`: terminal UI renderer
50
+ - `react`: Ink component runtime
51
+ - `marked`: Markdown parser
52
+ - `highlight.js`: code fence syntax highlighting
53
+ - `chalk`: terminal styling
54
+ - `string-width`, `wrap-ansi`, `slice-ansi`: terminal width and wrapping helpers
55
+
56
+ Dependency policy:
57
+
58
+ - These dependencies belong to the UI layer.
59
+ - Provider, tools, hooks, permissions, and transcript modules must not import Ink or Markdown renderer code.
60
+ - TUI modules may be dynamically imported by the TUI entrypoint so plain CLI and unit tests remain lightweight.
61
+
62
+ ---
63
+
64
+ ## 4. Module Layout
65
+
66
+ ```text
67
+ src/
68
+ cli.ts # existing plain CLI main
69
+ tui.ts # TUI main
70
+ ui/
71
+ settings.ts # shared provider/model selection rules
72
+ plain-renderer.ts # current stderr/stdout renderer extracted later
73
+ plain/
74
+ interactive.ts # plain CLI settings/message startup flow
75
+ tui/
76
+ app.ts # Ink root component
77
+ controller.ts # config/session setup and event bridge
78
+ store.ts # event reducer, settings state, and view model
79
+ input.ts # initial prompt input helpers, extracted as needed
80
+ permission.ts # TUI permission prompt adapter
81
+ theme.ts # colors and semantic styles
82
+ markdown/
83
+ render.ts # Markdown -> render blocks
84
+ component.ts # Ink component for rendered blocks
85
+ highlight.ts # code fence highlighting
86
+ tables.ts # v1 simple table alignment
87
+ wrap.ts # terminal wrapping helpers
88
+ ```
89
+
90
+ Initial implementation may keep `plain-renderer.ts` implicit inside `cli.ts`. TUI modules should still be separate from CLI rendering.
91
+
92
+ ---
93
+
94
+ ## 5. Rendering Model
95
+
96
+ The Markdown renderer is independent from Ink session orchestration.
97
+
98
+ ```text
99
+ markdown string
100
+ -> marked tokens
101
+ -> RenderBlock[]
102
+ -> Ink component rendering
103
+ ```
104
+
105
+ Render blocks:
106
+
107
+ - paragraph
108
+ - heading
109
+ - list
110
+ - blockquote
111
+ - code fence
112
+ - table
113
+ - horizontal rule
114
+ - plain fallback
115
+
116
+ Rendering policy:
117
+
118
+ - Plain CLI never renders Markdown. It prints raw Markdown.
119
+ - TUI renders Markdown for final assistant messages.
120
+ - Streaming uses an incremental fallback render.
121
+ - Once the assistant message is complete, the TUI re-renders with the full Markdown parser.
122
+
123
+ ---
124
+
125
+ ## 6. Streaming Policy
126
+
127
+ During streaming:
128
+
129
+ ```text
130
+ model.text.delta events
131
+ -> append to active assistant buffer
132
+ -> display incremental fallback render
133
+ ```
134
+
135
+ The incremental fallback renderer:
136
+
137
+ - preserves responsiveness
138
+ - does not require balanced Markdown syntax
139
+ - treats unclosed code fences as plain preformatted text
140
+ - avoids expensive full parse on every token
141
+
142
+ After completion:
143
+
144
+ ```text
145
+ model.text event or session.ended
146
+ -> parse full assistant Markdown
147
+ -> replace fallback output with final rich render
148
+ ```
149
+
150
+ Rationale:
151
+
152
+ - Markdown streams often contain temporarily invalid syntax.
153
+ - Full parsing every delta is expensive and can cause visual churn.
154
+ - Final render should prioritize quality and correct layout.
155
+
156
+ ---
157
+
158
+ ## 7. Markdown Quality
159
+
160
+ ### Code Fences
161
+
162
+ Code fences support syntax highlighting with `highlight.js`.
163
+
164
+ Policy:
165
+
166
+ - If language is known, use language-specific highlighting.
167
+ - If language is unknown, use auto-detect only when cheap enough; otherwise render as plain code.
168
+ - Preserve indentation.
169
+ - Wrap long lines only when the pane width requires it.
170
+ - Show the language label when present.
171
+
172
+ ### Tables
173
+
174
+ v1 table support is simple alignment:
175
+
176
+ - parse Markdown table rows
177
+ - calculate display width per column
178
+ - align with spaces
179
+ - wrap very long cells conservatively
180
+
181
+ v1.x improvements:
182
+
183
+ - column truncation
184
+ - horizontal scrolling
185
+ - border styles
186
+ - alignment markers (`:---`, `---:`, `:---:`)
187
+
188
+ ### Links
189
+
190
+ Links render as `label (url)` initially. v1.x can add terminal hyperlink escape sequences where supported.
191
+
192
+ ### Width
193
+
194
+ All layout must use terminal display width, not string length. CJK and ANSI styling require `string-width`, `wrap-ansi`, and `slice-ansi`.
195
+
196
+ ---
197
+
198
+ ## 8. TUI Layout
199
+
200
+ Default layout:
201
+
202
+ ```text
203
+ ┌ status ─ provider / model / agent / cwd / sandbox ───────────┐
204
+ │ settings ─ p provider · m model · enter submit · ctrl+c exit ─│
205
+ │ transcript │
206
+ │ user prompt │
207
+ │ assistant markdown │
208
+ │ tool activity summaries │
209
+ │ │
210
+ ├ activity / diagnostics ─ tool, retry, usage, transcript path ─┤
211
+ │ permission prompt or current action │
212
+ └ composer ─ first message input or permission keys ────────────┘
213
+ ```
214
+
215
+ The first implementation can be a single-column layout with:
216
+
217
+ - status bar
218
+ - settings/help bar
219
+ - scroll-like transcript area using recent events
220
+ - activity/status line
221
+ - initial prompt input line
222
+ - permission prompt line
223
+
224
+ The architecture should not require a full-screen widget system before the core TUI is useful.
225
+
226
+ ### Interactive Message Loop
227
+
228
+ TUI prompt handling is split from the runtime session. The TUI process outlives individual task runs:
229
+
230
+ ```text
231
+ demian
232
+ -> render TUI
233
+ -> show resolved provider/model defaults
234
+ -> allow provider/model edits before first submit
235
+ -> wait for first message in input line
236
+ -> submit prompt to SessionRunner
237
+ -> render assistant answer
238
+ -> return to standby composer
239
+ -> repeat until /exit or /quit
240
+ ```
241
+
242
+ The controller keeps prior non-system messages in memory and passes them as `SessionRunner.history` on the next run. The system prompt is rebuilt for each run so environment notes stay fresh.
243
+
244
+ If a prompt argument is supplied as a compatibility shortcut:
245
+
246
+ ```text
247
+ demian [flags] "prompt"
248
+ -> render TUI
249
+ -> show selected provider/model briefly
250
+ -> start SessionRunner with that prompt
251
+ -> return to standby composer after the answer
252
+ ```
253
+
254
+ Keys in composer mode:
255
+
256
+ - `Enter`: submit non-empty prompt
257
+ - `Ctrl+U`: clear input
258
+ - `Up`: recall the previous submitted prompt
259
+ - `Down`: move toward newer submitted prompts, then restore the current draft
260
+ - `p`: open provider selector when the composer is empty
261
+ - `m`: open model editor when the composer is empty
262
+ - `/exit` or `/quit`: exit the TUI
263
+ - `/stop`: report that no task is running while standby
264
+
265
+ Policy:
266
+
267
+ - The TUI owns initial settings selection and message collection.
268
+ - Submitted prompt history is stored only in the in-memory TUI store. It is capped and is not written to transcript or grants storage.
269
+ - Prompt history stores user prompts only. `/stop`, `/exit`, and `/quit` are control commands and are not recall entries.
270
+ - `SessionRunner` receives a normal `prompt: string` plus optional prior `history`, and remains unaware of TUI input state.
271
+ - Provider/model changes are allowed only before a run starts. A running tool loop has a stable provider and model.
272
+
273
+ During a running task, the bottom composer switches to command mode:
274
+
275
+ - `/stop`: abort the active run and return to standby.
276
+ - `/exit` or `/quit`: abort the active run and exit the TUI.
277
+ - Other text is rejected until the current run finishes.
278
+
279
+ This keeps the UI available for control commands while avoiding concurrent task runs in v1.
280
+
281
+ ### Provider And Model Selection
282
+
283
+ The TUI must make configured defaults visible before the first prompt.
284
+
285
+ Status/settings display:
286
+
287
+ ```text
288
+ provider openai model gpt-model-name agent build cwd ./repo
289
+ [p] provider [m] model [enter] send [ctrl+c] exit
290
+ ```
291
+
292
+ Provider selector:
293
+
294
+ - Opens with `p` while the composer is empty and no session is running.
295
+ - Lists configured provider keys from resolved config.
296
+ - Highlights the current provider and marks the source as `config`, `saved`, `flag`, or `interactive`.
297
+ - `Up`/`Down` moves selection.
298
+ - `Enter` selects provider.
299
+ - `Esc` returns to composer without changes.
300
+
301
+ Model editor:
302
+
303
+ - Opens with `m` while the composer is empty and no session is running.
304
+ - Shows the selected provider's configured model as the default.
305
+ - Accepts free-form model or deployment name text.
306
+ - `Enter` accepts the typed model or keeps the default when blank.
307
+ - `Esc` returns to composer without changes.
308
+
309
+ Provider/model state:
310
+
311
+ ```ts
312
+ interface InteractiveModelSelection {
313
+ providerName: string
314
+ providerSource: "config" | "saved" | "flag" | "interactive"
315
+ model: string
316
+ modelSource: "config" | "saved" | "flag" | "interactive"
317
+ }
318
+ ```
319
+
320
+ Rules:
321
+
322
+ - Saved UI preferences from `.demian/preferences.json` override config defaults.
323
+ - CLI flags preselect provider/model, override saved preferences, and mark values as flag-sourced.
324
+ - `config` means the resolved value after built-in defaults, user config, workspace config, and `--config` merge. The current config loader does not expose finer provenance.
325
+ - `saved` means the last explicit provider/model selection from this workspace. Only provider key and model name are stored; API keys and provider config are never copied into preferences.
326
+ - If the user changes provider, the model resets to that provider's configured default.
327
+ - If the new provider has no configured model, the TUI opens model editor immediately.
328
+ - Provider/model selection is committed into the `SessionRunner` options only when the message is submitted.
329
+ - The selector must not import provider SDKs or make network calls. It only reads resolved local config.
330
+ - The selected provider/model is saved after an interactive or flag-sourced selection.
331
+
332
+ ---
333
+
334
+ ## 9. Event Mapping
335
+
336
+ TUI subscribes to `RuntimeEvent`:
337
+
338
+ | Event | TUI behavior |
339
+ |-------|--------------|
340
+ | `session.started` | initialize status |
341
+ | `user.message` | append user block |
342
+ | `model.request` | show model activity |
343
+ | `model.text.delta` | update streaming fallback buffer |
344
+ | `model.text` | finalize rich Markdown block |
345
+ | `model.usage` | update usage summary |
346
+ | `model.content_filter` | show warning block |
347
+ | `provider.retry` | show retry diagnostics |
348
+ | `tool.requested` | append pending tool item |
349
+ | `tool.started` | mark tool running |
350
+ | `tool.completed` | mark tool done and preview |
351
+ | `tool.failed` | mark tool failed |
352
+ | `permission.requested` | show permission prompt and make it the bottom bar priority |
353
+ | `permission.granted` | show grant key |
354
+ | `permission.denied` | show denial |
355
+ | `session.ended` | show completion state, then return to standby composer |
356
+
357
+ ---
358
+
359
+ ## 10. Permission UX
360
+
361
+ TUI provides `SessionRunner.permissionPrompt`.
362
+
363
+ Permission prompts are modal inside the TUI. When a tool is waiting for approval, the bottom bar must show the permission prompt instead of the generic running command bar. This is important because keypresses are handled as permission answers while the session is otherwise still in `running` mode.
364
+
365
+ The prompt displays a human-readable summary of the tool input before shortcuts. For `bash`, the command is shown explicitly; for file tools, the path is shown explicitly; for text-heavy inputs, content is summarized by character count and a short preview. Raw JSON is only a fallback for unknown shapes.
366
+
367
+ Keys:
368
+
369
+ - `y`: allow once
370
+ - `a`: allow and persist according to configured grant scope
371
+ - `n`: deny
372
+ - `Enter`: deny by default
373
+ - `Ctrl+C`: emergency exit from the TUI
374
+
375
+ Policy:
376
+
377
+ - TUI permission answers must call the same permission engine path as plain CLI.
378
+ - Deny and hook blocks still win over `y`, `a`, and `--yes`.
379
+ - When stdin is not a TTY, TUI should refuse to start and suggest `demian-plain`.
380
+
381
+ ---
382
+
383
+ ## 11. Failure And Fallback
384
+
385
+ If TUI dependencies are missing:
386
+
387
+ - `demian` and `demian-cli` should print a clear install hint.
388
+ - `demian-plain` should still work.
389
+
390
+ If terminal is not interactive:
391
+
392
+ - TUI should fail fast with a message.
393
+ - Plain CLI remains the supported non-interactive path only when an explicit automation input path is provided.
394
+ - Interactive provider/model prompts require a TTY.
395
+
396
+ If Markdown rendering fails:
397
+
398
+ - render raw Markdown in the TUI block
399
+ - show a low-noise warning in diagnostics
400
+ - do not fail the session
401
+
402
+ ---
403
+
404
+ ## 12. Implementation Sequence
405
+
406
+ 1. Add `architecture-tui.md`.
407
+ 2. Add TUI dependencies to `package.json`.
408
+ 3. Add `bin/demian-plain.js` for existing plain CLI.
409
+ 4. Change `bin/demian.js` to TUI entrypoint.
410
+ 5. Add `bin/demian-cli.js` as TUI alias.
411
+ 6. Implement `src/tui.ts`.
412
+ 7. Implement Ink app/controller/state.
413
+ 8. Implement initial TUI prompt input for promptless startup.
414
+ 9. Implement provider selector and model editor before session start.
415
+ 10. Implement plain CLI interactive provider/model/message flow.
416
+ 11. Keep CLI/TUI alive after each run and support `/stop`, `/exit`, and `/quit`.
417
+ 12. Implement independent Markdown renderer with parser and syntax highlighting.
418
+ 13. Wire TUI permission prompt.
419
+ 14. Add smoke tests:
420
+ - `demian-plain --help`
421
+ - `demian --help`
422
+ - TUI promptless startup in a pseudo-TTY
423
+ - plain CLI promptless startup in a pseudo-TTY
424
+ - Markdown renderer fixture tests without running an interactive TUI.
425
+
426
+ ---
427
+
428
+ ## 13. Non-goals
429
+
430
+ - Replacing JSONL transcripts.
431
+ - Changing provider/tool/session contracts.
432
+ - Implementing a full editor inside the TUI beyond the initial single-line prompt input.
433
+ - Building an IDE-like file explorer in v1.
434
+ - Perfect Markdown/GFM rendering in the first implementation.
435
+
436
+ ---
437
+
438
+ ## 14. Decision Log
439
+
440
+ | Decision | Choice | Reason |
441
+ |----------|--------|--------|
442
+ | Default command | `demian` opens TUI | Human-facing default should be high quality |
443
+ | Plain command | `demian-plain` | Preserve raw Markdown and automation |
444
+ | TUI alias | `demian-cli` | Explicit terminal UI command requested |
445
+ | Promptless start | all commands enter UI/CLI first | Interactive use should not require long shell-argument prompts |
446
+ | TUI provider selector | `p` before run | Defaults are visible and can be corrected without editing config |
447
+ | TUI model editor | `m` before run | Model names and Azure deployment names are often deployment-specific |
448
+ | Provider/model source labels | `config`, `saved`, `flag`, `interactive` | Shows whether the value came from config, remembered UI preferences, flags, or current UI edits |
449
+ | Provider/model lock | locked during a run | Keeps one tool loop deterministic and transcript-readable |
450
+ | Persistent UI loop | return to standby after each run | The command should feel like an interactive session, not a one-shot wrapper |
451
+ | Stop command | `/stop` | User needs a clear in-UI way to abort active work without closing the whole UI |
452
+ | Exit command | `/exit` and `/quit` | User needs an explicit way to close the interactive CLI/TUI |
453
+ | Prompt recall | `Up`/`Down` over in-memory submitted prompts | Fast prompt reuse without adding persistence or leaking commands into history |
454
+ | Provider/model preferences | project-local `.demian/preferences.json` | Reuse the last explicit selection without mutating user config or storing secrets |
455
+ | Framework | Ink + React | Best balance of UI quality and implementation speed |
456
+ | Markdown parser | `marked` | Practical parser with simple token API |
457
+ | Syntax highlight | `highlight.js` | Good quality with reasonable weight |
458
+ | Streaming render | incremental fallback | Streaming Markdown is often incomplete |
459
+ | Final render | full Markdown parse | Quality matters once message is complete |
460
+ | Tables | v1 simple alignment | Useful baseline, advanced layout later |
461
+
462
+ ---
463
+
464
+ ## 15. Summary
465
+
466
+ The TUI is a rich presentation layer over the existing agent runtime. It shows provider/model defaults before the first message and lets the user change them with focused shortcuts. Plain CLI remains raw and scriptable, but also offers a TTY-based provider/model/message flow. Markdown rendering is independent, high-quality final rendering is prioritized, and streaming stays responsive through a fallback renderer.
467
+
468
+ `demian` and `demian-cli` are interactive TUI commands. `demian-plain` is the stable plain CLI with interactive startup when a TTY is available.