@aiwayds/dsh-tui-pi 0.1.0

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +324 -0
  3. package/bin/dsh-tui-pi +5 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/append-system.d.ts +66 -0
  6. package/lib/append-system.js +161 -0
  7. package/lib/append-system.js.map +1 -0
  8. package/lib/commands.d.ts +53 -0
  9. package/lib/commands.js +167 -0
  10. package/lib/commands.js.map +1 -0
  11. package/lib/dsh-events.d.ts +106 -0
  12. package/lib/dsh-events.js +30 -0
  13. package/lib/dsh-events.js.map +1 -0
  14. package/lib/editor.d.ts +28 -0
  15. package/lib/editor.js +70 -0
  16. package/lib/editor.js.map +1 -0
  17. package/lib/footer.d.ts +36 -0
  18. package/lib/footer.js +112 -0
  19. package/lib/footer.js.map +1 -0
  20. package/lib/frame.d.ts +35 -0
  21. package/lib/frame.js +75 -0
  22. package/lib/frame.js.map +1 -0
  23. package/lib/git.d.ts +17 -0
  24. package/lib/git.js +51 -0
  25. package/lib/git.js.map +1 -0
  26. package/lib/index.d.ts +15 -0
  27. package/lib/index.js +781 -0
  28. package/lib/index.js.map +1 -0
  29. package/lib/instructions.d.ts +29 -0
  30. package/lib/instructions.js +67 -0
  31. package/lib/instructions.js.map +1 -0
  32. package/lib/live-widgets.d.ts +85 -0
  33. package/lib/live-widgets.js +218 -0
  34. package/lib/live-widgets.js.map +1 -0
  35. package/lib/messages.d.ts +277 -0
  36. package/lib/messages.js +734 -0
  37. package/lib/messages.js.map +1 -0
  38. package/lib/permission.d.ts +27 -0
  39. package/lib/permission.js +48 -0
  40. package/lib/permission.js.map +1 -0
  41. package/lib/provider-catalog.d.ts +114 -0
  42. package/lib/provider-catalog.js +124 -0
  43. package/lib/provider-catalog.js.map +1 -0
  44. package/lib/quotes.d.ts +28 -0
  45. package/lib/quotes.js +144 -0
  46. package/lib/quotes.js.map +1 -0
  47. package/lib/reload.d.ts +23 -0
  48. package/lib/reload.js +171 -0
  49. package/lib/reload.js.map +1 -0
  50. package/lib/selectors.d.ts +48 -0
  51. package/lib/selectors.js +261 -0
  52. package/lib/selectors.js.map +1 -0
  53. package/lib/session.d.ts +157 -0
  54. package/lib/session.js +555 -0
  55. package/lib/session.js.map +1 -0
  56. package/lib/sessions.d.ts +73 -0
  57. package/lib/sessions.js +253 -0
  58. package/lib/sessions.js.map +1 -0
  59. package/lib/settings.d.ts +180 -0
  60. package/lib/settings.js +1328 -0
  61. package/lib/settings.js.map +1 -0
  62. package/lib/text.d.ts +22 -0
  63. package/lib/text.js +45 -0
  64. package/lib/text.js.map +1 -0
  65. package/lib/theme/index.d.ts +79 -0
  66. package/lib/theme/index.js +121 -0
  67. package/lib/theme/index.js.map +1 -0
  68. package/lib/theme/palette.d.ts +56 -0
  69. package/lib/theme/palette.js +154 -0
  70. package/lib/theme/palette.js.map +1 -0
  71. package/lib/theme-settings.d.ts +68 -0
  72. package/lib/theme-settings.js +223 -0
  73. package/lib/theme-settings.js.map +1 -0
  74. package/lib/tui.d.ts +70 -0
  75. package/lib/tui.js +206 -0
  76. package/lib/tui.js.map +1 -0
  77. package/lib/welcome.d.ts +91 -0
  78. package/lib/welcome.js +281 -0
  79. package/lib/welcome.js.map +1 -0
  80. package/package.json +50 -0
  81. package/patches/@earendil-works__pi-tui.patch +72 -0
  82. package/pnpm-workspace.yaml +2 -0
  83. package/templates/APPEND_SYSTEM.md +34 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Transcript rendering: turns dsh session events into pi-tui components.
3
+ *
4
+ * Incremental by design (pi-turbo lesson): every event does O(event) work —
5
+ * streaming deltas update one Text in place, tool cards are keyed by callId,
6
+ * and nothing ever re-scans the session log.
7
+ *
8
+ * Streaming strategy: during `assistant/chunk` the raw text grows in a plain
9
+ * Text component; on the assembled `assistant/message` the streaming component
10
+ * is replaced by proper Markdown rendering. This keeps per-token cost at
11
+ * O(accumulated text) instead of re-parsing markdown on every delta.
12
+ *
13
+ * Panels: think blocks and tool cards render as boxed rows — a full box
14
+ * border (top border + header row + body rows + bottom border). The
15
+ * configured height counts the DISPLAYED rows — the header line plus the
16
+ * content rows ('5' shows five rows; the two box borders add two more
17
+ * physical rows, so a '5' box is seven terminal rows tall). The height is
18
+ * configurable through the `dsh-tui` settings namespace ('5'/'7'/'10'
19
+ * displayed rows, or 'all' to print the full body without a row cap). The
20
+ * transcript doc is a plain Container
21
+ * inside the outer ScrollView, so pi-tui 0.84.2 never lays out nested
22
+ * components (a Container without a layout node renders by simple
23
+ * concatenation — verified in dist/layout.js) and an inner ScrollView can
24
+ * never obtain a viewport. The body is therefore a padded tail of the last
25
+ * body-row budget (or every row, in 'all' mode) rather than an internal
26
+ * scroll; every row (borders included) carries the panel background. In
27
+ * 'all' mode the unbounded content stays bounded on screen: a streaming
28
+ * reasoning panel boxes only a STREAMING_TAIL_LINES live tail while chunks
29
+ * are in flight (the assembled message renders the full body), and a settled
30
+ * tool card keeps at most ALL_TOOL_RESULT_LINES rows with a drop marker.
31
+ *
32
+ * Theme hot-switch: every applied operation is appended to `replay` (O(1)
33
+ * per event — never a render-path scan). `setTheme` is an explicit user
34
+ * action, so it may do a one-off full rebuild: clear the doc and re-apply
35
+ * the buffered operations against the new theme. Streaming and tool cards
36
+ * rebuild exactly as they were applied, so an in-flight stream simply
37
+ * continues `setText` on its rebuilt component. (The live Todos/Agents
38
+ * widgets live outside the transcript — see live-widgets.ts.)
39
+ *
40
+ * Welcome banner: the first replay op, pushed at construction (the doc is
41
+ * cleared first, replacing tui.ts's startup placeholder). It stays at the
42
+ * top of the doc — the event flow appends below it, and every rebuild
43
+ * (relayout/setTheme) reproduces it first with the current theme.
44
+ */
45
+ import { Container } from '@earendil-works/pi-tui';
46
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
47
+ import { type TuiTheme } from './theme/index.ts';
48
+ /**
49
+ * Configurable think/tool panel height. Fixed values count the DISPLAYED
50
+ * rows — the header line plus the content rows (the two box borders are not
51
+ * counted; they add two more physical rows); 'all' prints the full body
52
+ * with no row cap (every row still clips to one terminal line).
53
+ */
54
+ export type PanelHeight = '5' | '7' | '10' | 'all';
55
+ /**
56
+ * Default displayed height of a think/tool panel: the header line plus the
57
+ * content rows. The single default for every '5' fallback (the renderer
58
+ * constructor, the settings schema default/entry/narrowing) — other heights
59
+ * are set through the `panelHeight` setting.
60
+ */
61
+ export declare const DEFAULT_PANEL_HEIGHT: PanelHeight;
62
+ /**
63
+ * 'all' streaming cap: while a reasoning stream is in flight, the panel boxes
64
+ * only this many trailing rows. Without the cap every chunk would re-box the
65
+ * whole accumulated body — O(accumulated) per chunk, O(n²) over the stream
66
+ * (3000 lines ≈ 22s vs 155ms at a fixed height). The live tail is transient:
67
+ * the assembled `assistant/message` reasoning block (and the replay rebuilds)
68
+ * render the full body.
69
+ */
70
+ export declare const STREAMING_TAIL_LINES = 200;
71
+ /**
72
+ * 'all' settle cap: a settled tool card keeps at most this many body rows,
73
+ * with a `… (+N lines)` marker for the drop. The unlimited body would
74
+ * otherwise hitch the frame and balloon memory on a 50k-line tool result.
75
+ */
76
+ export declare const ALL_TOOL_RESULT_LINES = 2000;
77
+ /**
78
+ * Terminal columns a panel body row's CONTENT may occupy so the whole
79
+ * bordered row renders on exactly one physical line: the body Text wraps at
80
+ * `width - paddingX*2` (paddingX = 1), every row carries 4 columns of box
81
+ * chrome (`│ ` … ` │`), and tool rows add a 2-column indent — hence the -6
82
+ * (think) and -8 (tool, indent = 2) headroom.
83
+ */
84
+ export declare function panelLineCap(columns: number | undefined, indent?: number): number;
85
+ /** Full visible width of one bordered panel row, box chrome included. */
86
+ export declare function panelBoxWidth(columns: number | undefined): number;
87
+ /**
88
+ * Clip an unstyled line to one physical panel row. Must run BEFORE styling:
89
+ * clipToWidth counts per grapheme, so the ASCII fragments of an SGR code
90
+ * would count as visible columns (verified against pi-tui 0.84.2) — clipping
91
+ * plain text first, then applying ANSI, keeps the accounting exact.
92
+ * `indent` is the leading content indent the row carries (2 for tool rows).
93
+ * Carriage returns are stripped first: pi-tui's wrapTextWithAnsi splits on
94
+ * `/\r\n|\r|\n/`, so a bare \r (progress bars, CRLF tool output) would break
95
+ * the fixed panel rows just like a wrap would — the panel line is one row,
96
+ * not a line record.
97
+ */
98
+ export declare function clipPanelLine(text: string, indent?: number): string;
99
+ /**
100
+ * Compose the bordered body Text content (boxed rows plus the bottom border)
101
+ * from already-styled, already-clipped lines: keep the tail — newest rows
102
+ * win — pad short content with empty boxed rows, then append the bottom
103
+ * border. `bodyRows` is the panel's body-row budget (default PANEL_BODY_LINES)
104
+ * or 'all': with 'all' every line is kept verbatim, nothing is padded, and
105
+ * only the bottom border is appended (the box stays closed). Every row is
106
+ * one `boxWidth`-wide boxed line (`│ ` … ` │`, see borderedRow);
107
+ * `borderFg` is the panelBorder SGR prefix (no trailing RESET — the panel
108
+ * bg function terminates the row). Pad rows carry the box characters, so
109
+ * they survive Text's `text.trim() === ''` fast path, which would otherwise
110
+ * drop a body of only empty rows; the border SGR does not touch the
111
+ * background, so the panel bg function still paints the full row width.
112
+ * Callers clip each line with `clipPanelLine` BEFORE styling — otherwise a
113
+ * styled line that outgrows `width - paddingX*2` wraps and the panel
114
+ * exceeds its configured rows.
115
+ */
116
+ export declare function panelBodyText(lines: readonly string[], boxWidth: number, borderFg: string, bodyRows?: number | 'all'): string;
117
+ /**
118
+ * The tool header's subject word: the file path for read/write-style tools,
119
+ * the command's first word for cli-style tools ('git', 'python') — the first
120
+ * whitespace token of the highest-priority string argument (same key
121
+ * priority as callDetail's summary). '' when the arguments carry no usable
122
+ * string (the header then shows the bare tool name).
123
+ */
124
+ export declare function toolSubject(rawArguments: string): string;
125
+ export declare class TranscriptRenderer {
126
+ private readonly doc;
127
+ private theme;
128
+ private readonly requestRender;
129
+ /** Configured panel height ('5'/'7'/'10' total rows, or 'all' = uncapped). */
130
+ private panelHeight;
131
+ private streaming;
132
+ private readonly toolCards;
133
+ /** Text of the prompt echoed locally on submit; the matching session event is deduped. */
134
+ private lastEcho;
135
+ /**
136
+ * Append-only buffer of every applied operation (O(1) per event). The
137
+ * render path never scans it; `setTheme` — an explicit user action — is
138
+ * the only reader, replaying it once against the new theme.
139
+ */
140
+ private readonly replay;
141
+ /**
142
+ * The session's daily quote — rolled once here, so every rebuild
143
+ * (relayout/setTheme replay) re-renders the same line and only a fresh
144
+ * session rolls a new one (see quotes.ts).
145
+ */
146
+ private readonly dailyQuote;
147
+ constructor(doc: Container, theme: TuiTheme, requestRender: () => void, panelHeight?: PanelHeight);
148
+ /**
149
+ * Content-row budget for the configured panel height: the displayed row
150
+ * count minus the header row ('5' → 4 content rows), or 'all' when the
151
+ * panel prints its full body. The box borders are not part of the budget.
152
+ */
153
+ private panelBodyRows;
154
+ /**
155
+ * Switch the configured panel height. Returns whether the height actually
156
+ * changed — the settings watch sink relayouts only then; `relayout` is the
157
+ * replay rebuild that repaints every panel (streaming, tool cards, settled
158
+ * cards) at the new row budget.
159
+ */
160
+ setPanelHeight(panelHeight: PanelHeight): boolean;
161
+ applyEvent(event: SessionEvent): void;
162
+ /** Render a submitted prompt immediately, before the session echoes it back. */
163
+ renderPromptEcho(text: string): void;
164
+ /** Render one executed slash command line with its outcome. */
165
+ renderCommandEcho(line: string, error?: string, text?: string): void;
166
+ /**
167
+ * Append a transcript line that has no matching session event (a
168
+ * transient status notice or the sole on-screen record of an error).
169
+ * Buffered as a replay op like echoes, so a theme-switch rebuild keeps it.
170
+ * `error` lines get the ✘ danger treatment; `info` lines the attention
171
+ * color (the Ctrl+C cancel hint) without a prefix.
172
+ */
173
+ renderNotice(text: string, level?: 'error' | 'info'): void;
174
+ /**
175
+ * Repaint the whole transcript against a new theme: clear the doc and
176
+ * replay the buffered operations. Per-op requestRenders coalesce into a
177
+ * single pi-tui frame (requestRender is nextTick-throttled), so the switch
178
+ * repaints once, with no intermediate flicker. An in-flight stream keeps
179
+ * its accumulated text — the replay rebuilds its Text and later chunks
180
+ * continue setText on it. No-op when the theme bundle is unchanged
181
+ * (themes are module singletons; the settings watcher may echo our own
182
+ * write).
183
+ */
184
+ setTheme(theme: TuiTheme): void;
185
+ /**
186
+ * Repaint the whole transcript at the current terminal width — the resize
187
+ * counterpart of `setTheme`. On stdout `resize` pi-tui re-renders every
188
+ * component with the new columns, but bordered panel rows were padded to
189
+ * the OLD box width, so a narrowing terminal wraps every row and shatters
190
+ * the fixed-height panels. Clear and re-apply the buffered operations
191
+ * exactly like a theme switch: an in-flight stream keeps its accumulated
192
+ * text (the replay rebuilds its Text and later chunks continue setText on
193
+ * it), tool cards keep their settle state, todos reappear. No-op when the
194
+ * replay is empty — that guards the doc emptied by /new (clear()), which
195
+ * must stay empty until the next prompt: the welcome banner is the startup
196
+ * screen of a TUI run and must not resurrect here.
197
+ */
198
+ relayout(): void;
199
+ /**
200
+ * Drop everything rendered so far (`/new`). The next prompt opens a fresh
201
+ * agent; the welcome banner goes with the rest — it is the startup screen
202
+ * of a TUI run, not persistent transcript chrome.
203
+ */
204
+ clear(): void;
205
+ /** Re-apply one buffered operation against the current theme. */
206
+ private applyOp;
207
+ /**
208
+ * The startup welcome banner (whale pixel art + pixel-letter wordmark)
209
+ * with the daily quote caption beneath it, as the doc's first content:
210
+ * a leading spacer, the banner Text, a spacer, the quote Text, then the
211
+ * trailing spacer that matches the message-block rhythm. The leading
212
+ * spacer keeps the banner from pressing against the top of the transcript
213
+ * (the startup placeholder line it replaces sat flush at row 0). The
214
+ * whale and the letters keep their brand blue across themes — the banner
215
+ * is theme-independent (gaps stay transparent over the terminal default
216
+ * background — see welcome.ts); the quote is the one theme-tinted line
217
+ * (fgSubtle, rebuilt with the live theme by the replay). The banner is
218
+ * built at the current terminal width: below 96 columns it degrades to
219
+ * the whale alone, and every rebuild (relayout/setTheme replay) reads the
220
+ * width afresh, so narrowing drops the wordmark and widening restores it.
221
+ * The quote line is clipped to the terminal width before styling (the
222
+ * repo rule — ANSI never goes through the clipper), so it never wraps.
223
+ */
224
+ private renderWelcome;
225
+ private renderUserMessage;
226
+ private renderUserText;
227
+ private applyChunk;
228
+ private finalizeStreaming;
229
+ /** Keep streaming components as-is but detach state (user message arrived). */
230
+ private dropStreaming;
231
+ /** Full box width and panelBorder SGR prefix for one panel, per current theme. */
232
+ private panelBox;
233
+ /** Top border line plus bordered header row — the header Text's two lines. */
234
+ private panelTop;
235
+ /**
236
+ * Thinking color style, italic-on. The style terminates with a targeted
237
+ * italic-off (`\x1b[23m`) — NOT a full RESET: the panel bg function paints
238
+ * the whole row width, and a `\x1b[0m` here would clear the background and
239
+ * leave the row's right side unpainted. Without the italic-off the leak is
240
+ * visible in the box chrome: wrapTextWithAnsi carries ANSI state across
241
+ * lines within one Text, so the row's right border, the following body
242
+ * rows and the bottom border would all render italic.
243
+ */
244
+ private thinkStyle;
245
+ /**
246
+ * Styled, boxed tail of a reasoning text at the configured height (bottom
247
+ * border included). `streaming` marks the in-flight live path (per-chunk
248
+ * setBody): 'all' then boxes only the bounded STREAMING_TAIL_LINES tail so
249
+ * every chunk stays O(tail) — the full body renders once the assembled
250
+ * `assistant/message` (and the replay rebuilds) call without the flag.
251
+ * Fixed heights are already tail-bounded and behave identically either way.
252
+ */
253
+ private thinkingBody;
254
+ /**
255
+ * Build the thinking panel (default 5 rows, configurable height): top
256
+ * border + header row + body rows + bottom border, all on the thinking
257
+ * panel background.
258
+ * Header icon: '⟡' (U+27E1) renders as a tofu box on the user's terminal;
259
+ * emoji render fine there (footer ⚙✔✘⏹ all verified), so '💭' is used.
260
+ * The fixed header text is clipped at the plain-text stage like every
261
+ * other panel line — below 17 terminal columns it would otherwise outgrow
262
+ * the header row's budget and wrap, breaking the panel shape.
263
+ */
264
+ private createThinkingPanel;
265
+ private renderAssistantMessage;
266
+ /**
267
+ * Styled tool header content (icon + name + subject, no box chrome, no
268
+ * trailing RESET). The subject is the argument's first word — the file
269
+ * path for read/write, the command for cli (see toolSubject) — so the
270
+ * first line reads like "⚙ read src/welcome.ts" / "⚙ cli python".
271
+ */
272
+ private toolHeader;
273
+ private addToolCard;
274
+ private settleToolCard;
275
+ private renderTurnEnd;
276
+ private appendLine;
277
+ }