@agi-fans/dsh-tui 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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/lib/autocomplete.d.ts +80 -0
  4. package/lib/autocomplete.js +320 -0
  5. package/lib/autocomplete.js.map +1 -0
  6. package/lib/box.d.ts +79 -0
  7. package/lib/box.js +276 -0
  8. package/lib/box.js.map +1 -0
  9. package/lib/clipboard.d.ts +19 -0
  10. package/lib/clipboard.js +77 -0
  11. package/lib/clipboard.js.map +1 -0
  12. package/lib/command-integrations.d.ts +10 -0
  13. package/lib/command-integrations.js +49 -0
  14. package/lib/command-integrations.js.map +1 -0
  15. package/lib/command-mode.d.ts +5 -0
  16. package/lib/command-mode.js +73 -0
  17. package/lib/command-mode.js.map +1 -0
  18. package/lib/command-model.d.ts +5 -0
  19. package/lib/command-model.js +87 -0
  20. package/lib/command-model.js.map +1 -0
  21. package/lib/command-output.d.ts +6 -0
  22. package/lib/command-output.js +33 -0
  23. package/lib/command-output.js.map +1 -0
  24. package/lib/command-queue.d.ts +5 -0
  25. package/lib/command-queue.js +62 -0
  26. package/lib/command-queue.js.map +1 -0
  27. package/lib/command-registration.d.ts +5 -0
  28. package/lib/command-registration.js +20 -0
  29. package/lib/command-registration.js.map +1 -0
  30. package/lib/command-session.d.ts +5 -0
  31. package/lib/command-session.js +153 -0
  32. package/lib/command-session.js.map +1 -0
  33. package/lib/command-transcript.d.ts +5 -0
  34. package/lib/command-transcript.js +85 -0
  35. package/lib/command-transcript.js.map +1 -0
  36. package/lib/copy-selector.d.ts +52 -0
  37. package/lib/copy-selector.js +136 -0
  38. package/lib/copy-selector.js.map +1 -0
  39. package/lib/copy-targets.d.ts +34 -0
  40. package/lib/copy-targets.js +170 -0
  41. package/lib/copy-targets.js.map +1 -0
  42. package/lib/definition.d.ts +171 -0
  43. package/lib/definition.js +14 -0
  44. package/lib/definition.js.map +1 -0
  45. package/lib/editor.d.ts +66 -0
  46. package/lib/editor.js +365 -0
  47. package/lib/editor.js.map +1 -0
  48. package/lib/event-views.d.ts +188 -0
  49. package/lib/event-views.js +678 -0
  50. package/lib/event-views.js.map +1 -0
  51. package/lib/external-editor.d.ts +2 -0
  52. package/lib/external-editor.js +31 -0
  53. package/lib/external-editor.js.map +1 -0
  54. package/lib/history-search.d.ts +61 -0
  55. package/lib/history-search.js +239 -0
  56. package/lib/history-search.js.map +1 -0
  57. package/lib/history-store.d.ts +8 -0
  58. package/lib/history-store.js +36 -0
  59. package/lib/history-store.js.map +1 -0
  60. package/lib/hotkeys.d.ts +14 -0
  61. package/lib/hotkeys.js +115 -0
  62. package/lib/hotkeys.js.map +1 -0
  63. package/lib/human-interaction.d.ts +5 -0
  64. package/lib/human-interaction.js +8 -0
  65. package/lib/human-interaction.js.map +1 -0
  66. package/lib/image-paste.d.ts +21 -0
  67. package/lib/image-paste.js +277 -0
  68. package/lib/image-paste.js.map +1 -0
  69. package/lib/index.d.ts +10 -0
  70. package/lib/index.js +11 -0
  71. package/lib/index.js.map +1 -0
  72. package/lib/interaction-adapter.d.ts +18 -0
  73. package/lib/interaction-adapter.js +93 -0
  74. package/lib/interaction-adapter.js.map +1 -0
  75. package/lib/keybindings-config.d.ts +5 -0
  76. package/lib/keybindings-config.js +28 -0
  77. package/lib/keybindings-config.js.map +1 -0
  78. package/lib/keys.d.ts +59 -0
  79. package/lib/keys.js +263 -0
  80. package/lib/keys.js.map +1 -0
  81. package/lib/markdown.d.ts +13 -0
  82. package/lib/markdown.js +453 -0
  83. package/lib/markdown.js.map +1 -0
  84. package/lib/path-complete.d.ts +85 -0
  85. package/lib/path-complete.js +231 -0
  86. package/lib/path-complete.js.map +1 -0
  87. package/lib/path-mentions.d.ts +4 -0
  88. package/lib/path-mentions.js +50 -0
  89. package/lib/path-mentions.js.map +1 -0
  90. package/lib/project-context.d.ts +13 -0
  91. package/lib/project-context.js +52 -0
  92. package/lib/project-context.js.map +1 -0
  93. package/lib/project-file-search.d.ts +25 -0
  94. package/lib/project-file-search.js +207 -0
  95. package/lib/project-file-search.js.map +1 -0
  96. package/lib/prompt-selector.d.ts +56 -0
  97. package/lib/prompt-selector.js +297 -0
  98. package/lib/prompt-selector.js.map +1 -0
  99. package/lib/provider-local.d.ts +115 -0
  100. package/lib/provider-local.js +1760 -0
  101. package/lib/provider-local.js.map +1 -0
  102. package/lib/pwd.d.ts +13 -0
  103. package/lib/pwd.js +14 -0
  104. package/lib/pwd.js.map +1 -0
  105. package/lib/relative-time.d.ts +2 -0
  106. package/lib/relative-time.js +11 -0
  107. package/lib/relative-time.js.map +1 -0
  108. package/lib/renderer.d.ts +112 -0
  109. package/lib/renderer.js +181 -0
  110. package/lib/renderer.js.map +1 -0
  111. package/lib/runner.d.ts +9 -0
  112. package/lib/runner.js +94 -0
  113. package/lib/runner.js.map +1 -0
  114. package/lib/session-controller.d.ts +92 -0
  115. package/lib/session-controller.js +571 -0
  116. package/lib/session-controller.js.map +1 -0
  117. package/lib/session-runtime.d.ts +16 -0
  118. package/lib/session-runtime.js +17 -0
  119. package/lib/session-runtime.js.map +1 -0
  120. package/lib/settings-list.d.ts +67 -0
  121. package/lib/settings-list.js +380 -0
  122. package/lib/settings-list.js.map +1 -0
  123. package/lib/status-config.d.ts +32 -0
  124. package/lib/status-config.js +52 -0
  125. package/lib/status-config.js.map +1 -0
  126. package/lib/status-line.d.ts +41 -0
  127. package/lib/status-line.js +351 -0
  128. package/lib/status-line.js.map +1 -0
  129. package/lib/style.d.ts +22 -0
  130. package/lib/style.js +23 -0
  131. package/lib/style.js.map +1 -0
  132. package/lib/theme.d.ts +83 -0
  133. package/lib/theme.js +317 -0
  134. package/lib/theme.js.map +1 -0
  135. package/lib/tool-presentation.d.ts +20 -0
  136. package/lib/tool-presentation.js +79 -0
  137. package/lib/tool-presentation.js.map +1 -0
  138. package/lib/tool-renderers.d.ts +21 -0
  139. package/lib/tool-renderers.js +133 -0
  140. package/lib/tool-renderers.js.map +1 -0
  141. package/lib/tools-list.d.ts +14 -0
  142. package/lib/tools-list.js +61 -0
  143. package/lib/tools-list.js.map +1 -0
  144. package/lib/transcript-export.d.ts +4 -0
  145. package/lib/transcript-export.js +66 -0
  146. package/lib/transcript-export.js.map +1 -0
  147. package/lib/tui-settings.d.ts +20 -0
  148. package/lib/tui-settings.js +23 -0
  149. package/lib/tui-settings.js.map +1 -0
  150. package/lib/welcome-tips.d.ts +10 -0
  151. package/lib/welcome-tips.js +36 -0
  152. package/lib/welcome-tips.js.map +1 -0
  153. package/lib/width.d.ts +54 -0
  154. package/lib/width.js +323 -0
  155. package/lib/width.js.map +1 -0
  156. package/package.json +119 -0
@@ -0,0 +1,678 @@
1
+ /**
2
+ * Transcript state machine: SessionEvent -> display blocks, and the pure
3
+ * view that composes blocks + status + input into a render frame.
4
+
5
+ * applyEvent is the single writer of TranscriptState; renderView is the
6
+ * single reader. windowTranscript clips the body the way OMP's ScrollView
7
+ * does. Both are pure so the whole rendering pipeline is testable without
8
+ * a terminal.
9
+ * @module @agi-fans/dsh-tui
10
+ */
11
+ import { renderAutocomplete, slashInlineHint } from "./autocomplete.js";
12
+ import { HISTORY_SEARCH_MAX_VISIBLE, renderHistorySearch } from "./history-search.js";
13
+ import { renderEditor, renderFramedBlock, renderWelcome, renderWorking } from "./box.js";
14
+ import { renderMarkdown } from "./markdown.js";
15
+ import { renderCopySelector } from "./copy-selector.js";
16
+ import { renderSettings } from "./settings-list.js";
17
+ import { renderPlanReviewPage, renderPromptSelector, renderPromptSelectorPage, } from "./prompt-selector.js";
18
+ import { resolveStatusBarConfig } from "./status-config.js";
19
+ import { renderStatusFooter } from "./status-line.js";
20
+ import { createTheme, SPINNER, SYMBOL } from "./theme.js";
21
+ import { padToWidth, truncateToWidth, visibleWidth, wrapText } from "./width.js";
22
+ import { renderTool } from "./tool-renderers.js";
23
+ import { renderToolsPanel } from "./tools-list.js";
24
+ import { renderHotkeysPanel } from "./hotkeys.js";
25
+ import { renderCommandOutput, renderCommandSeparator } from "./command-output.js";
26
+ import { renderPathMentionRows } from "./path-mentions.js";
27
+ /** Empty starting state. */
28
+ export function initialTranscript() {
29
+ return { blocks: [], status: 'idle', turn: 0 };
30
+ }
31
+ /** Extract plain text from text blocks, ignoring other block kinds. */
32
+ function contentToText(content) {
33
+ return content
34
+ .flatMap((block) => {
35
+ if (block.type === 'text')
36
+ return [block.text];
37
+ if (block.type === 'image') {
38
+ const ref = block.attachment;
39
+ return [`[image ${ref.width}×${ref.height} · ${ref.mediaType}]`];
40
+ }
41
+ return [];
42
+ })
43
+ .join('');
44
+ }
45
+ /** Extract reasoning text from reasoning blocks. */
46
+ function contentToReasoning(content) {
47
+ return content
48
+ .filter((block) => block.type === 'reasoning')
49
+ .map((block) => block.text)
50
+ .join('');
51
+ }
52
+ /** Compact pretty-print of a tool call's raw arguments JSON. */
53
+ function prettyArgs(raw) {
54
+ try {
55
+ return JSON.stringify(JSON.parse(raw));
56
+ }
57
+ catch {
58
+ return raw;
59
+ }
60
+ }
61
+ /** The streaming assistant block if it is the last block, else undefined. */
62
+ function streamingBlock(state, turn, step) {
63
+ const last = state.blocks[state.blocks.length - 1];
64
+ if (last?.kind === 'assistant' && last.streaming && last.turn === turn && last.step === step)
65
+ return last;
66
+ return undefined;
67
+ }
68
+ /** Replace the trailing streaming block with a settled one, or append. */
69
+ function settleAssistant(state, turn, step, text, reasoning) {
70
+ const blocks = state.blocks.slice();
71
+ const last = blocks[blocks.length - 1];
72
+ if (last?.kind === 'assistant' && last.streaming && last.turn === turn && last.step === step) {
73
+ blocks[blocks.length - 1] = { kind: 'assistant', turn, step, text, reasoning, streaming: false };
74
+ }
75
+ else {
76
+ blocks.push({ kind: 'assistant', turn, step, text, reasoning, streaming: false });
77
+ }
78
+ return { ...state, blocks };
79
+ }
80
+ /**
81
+ * Fold one session-log event into the transcript state.
82
+ * @param state - prior state.
83
+ * @param event - the appended session event.
84
+ * @returns the next state.
85
+ */
86
+ export function applyEvent(state, event, presentation) {
87
+ switch (event.type) {
88
+ case 'turn/start':
89
+ return { ...state, status: 'running', turn: event.data.turn };
90
+ case 'turn/end': {
91
+ const blocks = state.blocks.slice();
92
+ const last = blocks[blocks.length - 1];
93
+ if (last?.kind === 'assistant' && last.streaming) {
94
+ blocks[blocks.length - 1] = { ...last, streaming: false };
95
+ }
96
+ const reason = event.data.reason;
97
+ if (reason.kind === 'error') {
98
+ blocks.push({ kind: 'notice', level: 'error', text: 'error: ' + reason.error.code + ': ' + reason.error.message });
99
+ }
100
+ else if (reason.kind === 'aborted') {
101
+ blocks.push({ kind: 'notice', level: 'info', text: 'interrupted' });
102
+ }
103
+ return { ...state, blocks, status: 'idle' };
104
+ }
105
+ case 'user/message': {
106
+ // Synthetic plugin injections (system-prompt runtime context, skill
107
+ // catalog) reach the surface as user-role messages but are model input,
108
+ // not what the human typed; only human prompts render as transcript.
109
+ if (event.data.source.kind !== 'user')
110
+ return state;
111
+ const text = contentToText(event.data.content);
112
+ if (text === '')
113
+ return state;
114
+ return { ...state, blocks: [...state.blocks, { kind: 'user', text }] };
115
+ }
116
+ case 'assistant/chunk': {
117
+ const { turn, step, chunk } = event.data;
118
+ if (chunk.type === 'text-delta') {
119
+ const blocks = state.blocks.slice();
120
+ const last = streamingBlock(state, turn, step);
121
+ if (last !== undefined) {
122
+ const idx = blocks.length - 1;
123
+ const found = blocks[idx];
124
+ if (found?.kind === 'assistant')
125
+ blocks[idx] = { ...found, text: found.text + chunk.text };
126
+ }
127
+ else {
128
+ blocks.push({ kind: 'assistant', turn, step, text: chunk.text, reasoning: '', streaming: true });
129
+ }
130
+ return { ...state, blocks };
131
+ }
132
+ if (chunk.type === 'reasoning-delta') {
133
+ const blocks = state.blocks.slice();
134
+ const last = streamingBlock(state, turn, step);
135
+ if (last !== undefined) {
136
+ const idx = blocks.length - 1;
137
+ const found = blocks[idx];
138
+ if (found?.kind === 'assistant')
139
+ blocks[idx] = { ...found, reasoning: found.reasoning + chunk.text };
140
+ }
141
+ else {
142
+ blocks.push({ kind: 'assistant', turn, step, text: '', reasoning: chunk.text, streaming: true });
143
+ }
144
+ return { ...state, blocks };
145
+ }
146
+ if (chunk.type === 'tool-call-delta') {
147
+ const blocks = state.blocks.slice();
148
+ const index = blocks.findIndex(block => block.kind === 'tool' && block.callId === chunk.id);
149
+ const existing = blocks[index];
150
+ if (existing?.kind === 'tool') {
151
+ blocks[index] = {
152
+ ...existing,
153
+ name: chunk.name ?? existing.name,
154
+ args: existing.args + chunk.argumentsDelta,
155
+ partial: true,
156
+ };
157
+ }
158
+ else {
159
+ blocks.push({
160
+ kind: 'tool', callId: chunk.id, name: chunk.name ?? 'tool',
161
+ args: chunk.argumentsDelta, status: 'running', output: '', partial: true,
162
+ });
163
+ }
164
+ return { ...state, blocks };
165
+ }
166
+ return state;
167
+ }
168
+ case 'assistant/message': {
169
+ const { turn, step, message } = event.data;
170
+ return settleAssistant(state, turn, step, contentToText(message.content), contentToReasoning(message.content));
171
+ }
172
+ case 'tool/call': {
173
+ const block = {
174
+ kind: 'tool',
175
+ callId: event.data.callId,
176
+ name: event.data.name,
177
+ args: prettyArgs(event.data.arguments),
178
+ status: 'running',
179
+ output: '',
180
+ ...(presentation === undefined ? {} : { presentation }),
181
+ };
182
+ const blocks = state.blocks.slice();
183
+ const partial = blocks.findIndex(item => item.kind === 'tool' && item.callId === event.data.callId);
184
+ if (partial >= 0)
185
+ blocks[partial] = block;
186
+ else
187
+ blocks.push(block);
188
+ return { ...state, blocks };
189
+ }
190
+ case 'tool/result':
191
+ return applyToolResult(state, event.data.message, event.data.error, presentation);
192
+ // Log-only vocabulary (boundaries, usage, compaction, approvals, ...):
193
+ // nothing to display; the recognized core events above own the surface.
194
+ default:
195
+ return state;
196
+ }
197
+ }
198
+ /** Fold one tool result into its tool block. */
199
+ function applyToolResult(state, message, error, presentation) {
200
+ // A tool-result message carries exactly one tool-result block; the call
201
+ // identity and outcome live on that inner block.
202
+ const inner = message.content[0];
203
+ if (inner?.type !== 'tool-result')
204
+ return state;
205
+ const blocks = state.blocks.slice();
206
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
207
+ const block = blocks[i];
208
+ if (block?.kind === 'tool' && block.callId === inner.toolCallId) {
209
+ blocks[i] = {
210
+ ...block,
211
+ status: error !== undefined || inner.isError === true ? 'error' : 'ok',
212
+ output: contentToText(inner.content),
213
+ ...(presentation === undefined ? {} : { presentation }),
214
+ };
215
+ return { ...state, blocks };
216
+ }
217
+ }
218
+ return state;
219
+ }
220
+ /** Collapsed tool-output preview height (OMP `DEFAULT_TERMINAL_PREVIEW_LINES`). */
221
+ export const TOOL_COLLAPSED_LINES = 10;
222
+ /** OMP AssistantMessage uses one horizontal cell of padding and no vertical padding. */
223
+ const ASSISTANT_PADDING_X = 1;
224
+ function assistantContentLines(lines, width, paddingX) {
225
+ const margin = ' '.repeat(paddingX);
226
+ return lines.map((line) => padToWidth(margin + line + margin, width));
227
+ }
228
+ function assistantMarkdown(source, theme, width) {
229
+ const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
230
+ const contentWidth = Math.max(1, width - paddingX * 2);
231
+ return assistantContentLines(renderMarkdown(source, theme, contentWidth), width, paddingX);
232
+ }
233
+ function userBubble(text, theme, width) {
234
+ const inner = Math.max(1, width - 2);
235
+ const wrapped = renderPathMentionRows(text, inner, theme);
236
+ const rows = ['', ...wrapped, ''];
237
+ return rows.map((row) => {
238
+ const content = row === '' ? padToWidth('', width) : padToWidth(' ' + row, width);
239
+ return theme.colors ? theme.bg('userMessageBg', content) : content;
240
+ });
241
+ }
242
+ function toolIcon(status, theme, spinnerFrame) {
243
+ if (status === 'running') {
244
+ return theme.fg('accent', SPINNER[spinnerFrame % SPINNER.length] ?? SYMBOL.running);
245
+ }
246
+ if (status === 'ok')
247
+ return theme.fg('success', SYMBOL.success);
248
+ return theme.fg('error', SYMBOL.error);
249
+ }
250
+ /** Render one tool block as an OMP framed output box. */
251
+ function toolBlockLines(block, theme, width, spinnerFrame, expanded) {
252
+ const icon = toolIcon(block.status, theme, spinnerFrame);
253
+ const presentation = renderTool({
254
+ name: block.name,
255
+ arguments: prettyArgs(block.args),
256
+ output: block.output,
257
+ status: block.status,
258
+ expanded,
259
+ ...(block.presentation === undefined ? {} : { presentation: block.presentation }),
260
+ });
261
+ const summary = presentation.summary === undefined || presentation.summary === ''
262
+ ? ''
263
+ : theme.fg('dim', ' ' + presentation.summary);
264
+ const header = icon + ' ' + theme.bold(presentation.title ?? block.name) + summary;
265
+ const raw = [...(presentation.lines ?? [])];
266
+ const hidden = expanded ? 0 : Math.max(0, raw.length - TOOL_COLLAPSED_LINES);
267
+ const shown = hidden > 0 ? raw.slice(0, TOOL_COLLAPSED_LINES) : raw;
268
+ const output = shown.map((line) => theme.fg('toolOutput', line));
269
+ if (hidden > 0) {
270
+ output.push(theme.fg('dim', '… ' + hidden + ' more lines · ⟨Ctrl+O: Expand⟩'));
271
+ }
272
+ const state = block.status === 'running' ? 'running' : block.status === 'ok' ? 'ok' : 'error';
273
+ return renderFramedBlock({ header, state, lines: output, width }, theme);
274
+ }
275
+ /**
276
+ * Render one transcript block to display lines. Pure; shared by the tty view
277
+ * and the non-tty plain printer.
278
+ * @param block - the block to render.
279
+ * @param theme - active theme.
280
+ * @param width - terminal width in columns.
281
+ * @param spinnerFrame - activity spinner phase for running tools.
282
+ * @param toolsExpanded - paint full tool output instead of the collapsed preview.
283
+ * @returns display lines (already width-fitted).
284
+ */
285
+ export function blockLines(block, theme, width, spinnerFrame = 0, toolsExpanded = false) {
286
+ if (block.kind === 'user')
287
+ return userBubble(block.text, theme, width);
288
+ if (block.kind === 'assistant') {
289
+ const lines = [];
290
+ if (block.reasoning !== '') {
291
+ for (const part of assistantMarkdown(block.reasoning, theme, width)) {
292
+ lines.push(theme.italic(theme.fg('thinkingText', part)));
293
+ }
294
+ if (block.text !== '')
295
+ lines.push('');
296
+ }
297
+ if (block.text === '' && block.streaming) {
298
+ const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
299
+ lines.push(...assistantContentLines([theme.fg('dim', '…')], width, paddingX));
300
+ }
301
+ else if (block.text !== '') {
302
+ lines.push(...assistantMarkdown(block.text, theme, width));
303
+ }
304
+ return lines;
305
+ }
306
+ if (block.kind === 'tool')
307
+ return toolBlockLines(block, theme, width, spinnerFrame, toolsExpanded);
308
+ if (block.kind === 'toolCatalog')
309
+ return renderToolsPanel(block.tools, theme, width, toolsExpanded);
310
+ if (block.kind === 'hotkeyCatalog')
311
+ return renderHotkeysPanel(block.bindings, theme, width);
312
+ if (block.kind === 'commandOutput')
313
+ return renderCommandOutput(block.command, block.text, theme, width);
314
+ if (block.level === 'info' && !block.text.includes('\n')) {
315
+ const prefix = ' ';
316
+ const continuation = ' '.repeat(visibleWidth(prefix));
317
+ return wrapText(block.text, Math.max(1, width - visibleWidth(prefix))).map((line, index) => truncateToWidth((index === 0 ? prefix : continuation) + theme.fg('dim', line), width));
318
+ }
319
+ const state = block.level === 'error' ? 'error' : 'info';
320
+ const paint = (text) => (block.level === 'error' ? theme.fg('error', text) : theme.fg('dim', text));
321
+ const wrapped = wrapText(block.text, Math.max(1, width - 4));
322
+ const title = paint(wrapped[0] ?? '');
323
+ return renderFramedBlock({
324
+ header: title,
325
+ state,
326
+ width,
327
+ lines: wrapped.slice(1).map(paint),
328
+ applyBg: false,
329
+ }, theme);
330
+ }
331
+ function fitFrame(lines, width) {
332
+ return lines.map((line) => {
333
+ if (visibleWidth(line) <= width)
334
+ return line;
335
+ return truncateToWidth(line, width);
336
+ });
337
+ }
338
+ /**
339
+ * Scroll changes only the viewport. Cache the expensive Markdown/tool fold by
340
+ * immutable TranscriptState identity so wheel frames slice already-rendered
341
+ * rows instead of formatting the complete session again.
342
+ */
343
+ const transcriptBodyCache = new WeakMap();
344
+ function renderTranscriptBody(state, options, theme, spinnerFrame) {
345
+ const toolsExpanded = options.toolsExpanded === true;
346
+ const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
347
+ const themeName = options.themeName ?? 'dark';
348
+ const trueColor = options.trueColor === true;
349
+ const cached = transcriptBodyCache.get(state);
350
+ if (cached !== undefined
351
+ && cached.width === options.width
352
+ && cached.colors === options.colors
353
+ && cached.trueColor === trueColor
354
+ && cached.themeName === themeName
355
+ && cached.spinnerFrame === spinnerFrame
356
+ && cached.toolsExpanded === toolsExpanded
357
+ && cached.expandedTools === expandedTools) {
358
+ return { lines: cached.lines, blockStarts: cached.blockStarts };
359
+ }
360
+ const lines = [];
361
+ const blockStarts = [];
362
+ let previous;
363
+ for (const block of state.blocks) {
364
+ if (lines.length > 0) {
365
+ const previousCommand = commandSurfaceName(previous);
366
+ const currentCommand = commandSurfaceName(block);
367
+ if (previousCommand !== undefined && currentCommand !== undefined && previousCommand !== currentCommand) {
368
+ lines.push('', renderCommandSeparator(theme, options.width), '');
369
+ }
370
+ else {
371
+ lines.push('');
372
+ }
373
+ }
374
+ blockStarts.push(lines.length);
375
+ const expanded = toolsExpanded || (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
376
+ lines.push(...blockLines(block, theme, options.width, spinnerFrame, expanded));
377
+ previous = block;
378
+ }
379
+ transcriptBodyCache.set(state, {
380
+ width: options.width,
381
+ colors: options.colors,
382
+ trueColor,
383
+ themeName,
384
+ spinnerFrame,
385
+ toolsExpanded,
386
+ expandedTools,
387
+ lines,
388
+ blockStarts,
389
+ });
390
+ return { lines, blockStarts };
391
+ }
392
+ function commandSurfaceName(block) {
393
+ if (block?.kind === 'toolCatalog')
394
+ return 'tools';
395
+ if (block?.kind === 'hotkeyCatalog')
396
+ return 'hotkeys';
397
+ if (block?.kind === 'commandOutput')
398
+ return block.command;
399
+ return undefined;
400
+ }
401
+ /** Rows moved per Shift+Arrow (OMP ScrollView `fastScrollLines`). */
402
+ export const TRANSCRIPT_FAST_SCROLL = 5;
403
+ /** Rows moved per mouse-wheel notch. */
404
+ export const TRANSCRIPT_WHEEL_SCROLL = 3;
405
+ function earlierLabel(count) {
406
+ return '… ↑ ' + count + ' earlier line' + (count === 1 ? '' : 's') + ' ⟨Pg↑⟩';
407
+ }
408
+ function laterLabel(count) {
409
+ return '… ↓ ' + count + ' later line' + (count === 1 ? '' : 's') + ' ⟨Pg↓⟩';
410
+ }
411
+ /**
412
+ * Window `body` into `budget` rows with ↑/↓ overflow markers.
413
+ * `start` is the first body row to keep; non-finite values pin to the tail.
414
+ */
415
+ export function windowTranscript(body, budget, start, theme) {
416
+ const len = body.length;
417
+ if (budget <= 0) {
418
+ return { lines: [], start: 0, maxStart: 0, budget, hiddenAbove: len, hiddenBelow: 0 };
419
+ }
420
+ if (len <= budget) {
421
+ return { lines: [...body], start: 0, maxStart: 0, budget, hiddenAbove: 0, hiddenBelow: 0 };
422
+ }
423
+ const maxStart = Math.max(0, len - (budget - 1));
424
+ const s = Number.isFinite(start) ? Math.max(0, Math.min(Math.trunc(start), maxStart)) : maxStart;
425
+ const atTail = s >= maxStart;
426
+ const atTop = s <= 0;
427
+ if (budget === 1) {
428
+ const label = atTail
429
+ ? earlierLabel(len)
430
+ : atTop
431
+ ? laterLabel(len)
432
+ : '… ↑ ' + s + ' · ↓ ' + (len - s) + ' ⟨Pg↑/Pg↓⟩';
433
+ return {
434
+ lines: [theme.fg('dim', label)],
435
+ start: s,
436
+ maxStart,
437
+ budget,
438
+ hiddenAbove: atTail ? len : s,
439
+ hiddenBelow: atTail ? 0 : len - s,
440
+ };
441
+ }
442
+ if (atTail) {
443
+ const take = budget - 1;
444
+ const hiddenAbove = len - take;
445
+ return {
446
+ lines: [theme.fg('dim', earlierLabel(hiddenAbove)), ...body.slice(hiddenAbove)],
447
+ start: hiddenAbove,
448
+ maxStart,
449
+ budget,
450
+ hiddenAbove,
451
+ hiddenBelow: 0,
452
+ };
453
+ }
454
+ if (atTop) {
455
+ const take = budget - 1;
456
+ const hiddenBelow = len - take;
457
+ return {
458
+ lines: [...body.slice(0, take), theme.fg('dim', laterLabel(hiddenBelow))],
459
+ start: 0,
460
+ maxStart,
461
+ budget,
462
+ hiddenAbove: 0,
463
+ hiddenBelow,
464
+ };
465
+ }
466
+ if (budget === 2) {
467
+ return {
468
+ lines: [theme.fg('dim', earlierLabel(s)), body[s] ?? ''],
469
+ start: s,
470
+ maxStart,
471
+ budget,
472
+ hiddenAbove: s,
473
+ hiddenBelow: len - s - 1,
474
+ };
475
+ }
476
+ const take = budget - 2;
477
+ const hiddenBelow = len - s - take;
478
+ return {
479
+ lines: [
480
+ theme.fg('dim', earlierLabel(s)),
481
+ ...body.slice(s, s + take),
482
+ theme.fg('dim', laterLabel(hiddenBelow)),
483
+ ],
484
+ start: s,
485
+ maxStart,
486
+ budget,
487
+ hiddenAbove: s,
488
+ hiddenBelow,
489
+ };
490
+ }
491
+ const IMAGE_MARKER = /\[Image #\d+(?:, \d+x\d+)?\]/gu;
492
+ function paintImageMarkerSlice(fullText, slice, sourceStart, theme) {
493
+ const sourceEnd = sourceStart + slice.length;
494
+ let output = '';
495
+ let cursor = sourceStart;
496
+ for (const match of fullText.matchAll(IMAGE_MARKER)) {
497
+ const markerStart = match.index;
498
+ const markerEnd = markerStart + match[0].length;
499
+ if (markerEnd <= sourceStart || markerStart >= sourceEnd)
500
+ continue;
501
+ const overlapStart = Math.max(sourceStart, markerStart);
502
+ const overlapEnd = Math.min(sourceEnd, markerEnd);
503
+ output += fullText.slice(cursor, overlapStart);
504
+ output += theme.underline(theme.bold(theme.fg('accent', fullText.slice(overlapStart, overlapEnd))));
505
+ cursor = overlapEnd;
506
+ }
507
+ output += fullText.slice(cursor, sourceEnd);
508
+ return output;
509
+ }
510
+ /**
511
+ * Compose the welcome card, transcript, working row, and rounded editor into
512
+ * one frame — the oh-my-pi surface.
513
+ * @param state - transcript state.
514
+ * @param options - terminal geometry and input state.
515
+ * @returns the frame to hand to a renderer.
516
+ */
517
+ export function renderView(state, options) {
518
+ const theme = createTheme(options.colors, options.trueColor === true, options.themeName ?? 'dark');
519
+ const width = options.width;
520
+ const height = options.height;
521
+ const appName = options.appName ?? 'omdsh';
522
+ const version = options.version ?? '0.1.0';
523
+ const pwd = options.pwd ?? '';
524
+ const spinnerFrame = options.spinnerFrame ?? 0;
525
+ if (options.settings !== undefined && options.promptSelector === undefined) {
526
+ const settings = renderSettings(options.settings, theme, width, height);
527
+ return {
528
+ lines: fitFrame(settings.lines, width),
529
+ cursor: settings.cursor,
530
+ cursorVisible: false,
531
+ overlay: { kind: 'settings', start: 0, itemRows: settings.itemRows },
532
+ };
533
+ }
534
+ if (options.promptSelector?.request.presentation === 'fullscreen-list') {
535
+ const selector = renderPromptSelectorPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
536
+ return {
537
+ lines: fitFrame(selector.lines, width),
538
+ cursor: selector.cursor,
539
+ cursorVisible: true,
540
+ ...(selector.editor === undefined ? {} : { editor: selector.editor }),
541
+ overlay: {
542
+ kind: 'prompt',
543
+ start: 0,
544
+ ...(selector.itemRows === undefined ? {} : { itemRows: selector.itemRows }),
545
+ },
546
+ };
547
+ }
548
+ if (options.promptSelector?.request.presentation === 'plan-review') {
549
+ const review = renderPlanReviewPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
550
+ return {
551
+ lines: fitFrame(review.lines, width),
552
+ cursor: review.cursor,
553
+ cursorVisible: review.cursorVisible === true,
554
+ ...(review.editor === undefined ? {} : { editor: review.editor }),
555
+ overlay: {
556
+ kind: 'prompt',
557
+ start: 0,
558
+ ...(review.document === undefined ? {} : { document: review.document }),
559
+ },
560
+ };
561
+ }
562
+ const welcome = renderWelcome({
563
+ width,
564
+ model: options.model,
565
+ ...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
566
+ version,
567
+ appName,
568
+ ...(options.recentSessions === undefined ? {} : { recentSessions: options.recentSessions }),
569
+ ...(options.welcomeTips === undefined ? {} : { tips: options.welcomeTips }),
570
+ }, theme);
571
+ const transcript = renderTranscriptBody(state, options, theme, spinnerFrame);
572
+ const body = [...welcome];
573
+ let transcriptStart = body.length;
574
+ if (transcript.lines.length > 0) {
575
+ if (body.length > 0)
576
+ body.push('');
577
+ transcriptStart = body.length;
578
+ body.push(...transcript.lines);
579
+ }
580
+ const working = state.status === 'running' ? renderWorking(theme, spinnerFrame, undefined, width) : [];
581
+ const statusBar = resolveStatusBarConfig(options.statusBar, options.statusPreset);
582
+ const inlineHint = slashInlineHint(options.input, options.inputCursor, options.commands);
583
+ const statusFooter = renderStatusFooter({
584
+ model: options.model,
585
+ ...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
586
+ ...(options.sessionControls === undefined ? {} : { controls: options.sessionControls }),
587
+ ...(pwd === '' ? {} : { pwd }),
588
+ ...(options.branch === undefined || options.branch === '' ? {} : { branch: options.branch }),
589
+ ...(options.sessionStats === undefined ? {} : { stats: options.sessionStats }),
590
+ config: statusBar,
591
+ width,
592
+ }, theme);
593
+ const editorOpts = {
594
+ width,
595
+ input: options.input,
596
+ inputCursor: options.inputCursor,
597
+ status: ' ' + theme.fg('accent', '🐳') + ' ',
598
+ border: state.status === 'running' ? 'accent' : 'border',
599
+ ...(options.inputImages === undefined || options.inputImages === 0
600
+ ? {}
601
+ : { paintInput: (text, start) => paintImageMarkerSlice(options.input, text, start, theme) }),
602
+ ...(inlineHint !== null ? { inlineHint } : {}),
603
+ };
604
+ const promptSelector = options.promptSelector === undefined
605
+ ? undefined
606
+ : renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor, Math.max(3, Math.min(10, height - working.length - 14)));
607
+ const settings = promptSelector !== undefined || options.settings === undefined
608
+ ? undefined
609
+ : renderSettings(options.settings, theme, width);
610
+ const copySelector = promptSelector !== undefined || settings !== undefined || options.copySelector === undefined
611
+ ? undefined
612
+ : renderCopySelector(options.copySelector, theme, width);
613
+ const search = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || options.historySearch === undefined
614
+ ? undefined
615
+ : renderHistorySearch(options.historySearch, theme, width, Math.max(1, Math.min(HISTORY_SEARCH_MAX_VISIBLE, height - working.length - 8)));
616
+ const editor = promptSelector === undefined && settings === undefined && copySelector === undefined && search === undefined
617
+ ? renderEditor(editorOpts, theme)
618
+ : undefined;
619
+ const autocomplete = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || search !== undefined
620
+ || options.autocomplete === undefined
621
+ ? []
622
+ : renderAutocomplete(options.autocomplete.items, options.autocomplete.selected, theme, width);
623
+ const inputLines = promptSelector?.lines ?? settings?.lines ?? copySelector?.lines ?? search?.lines
624
+ ?? (editor === undefined ? [] : editor.lines);
625
+ const spacer = 1;
626
+ const reserved = inputLines.length + working.length + spacer + autocomplete.length + statusFooter.length;
627
+ const budget = Math.max(0, height - reserved);
628
+ const focusStart = options.focusBlock === undefined
629
+ ? undefined
630
+ : transcript.blockStarts[Math.max(0, Math.min(options.focusBlock, transcript.blockStarts.length - 1))];
631
+ const requestedStart = focusStart === undefined
632
+ ? (options.scrollStart ?? Number.POSITIVE_INFINITY)
633
+ : transcriptStart + focusStart;
634
+ const windowed = windowTranscript(body, budget, requestedStart, theme);
635
+ const visible = windowed.lines;
636
+ const lines = [...visible];
637
+ if (visible.length > 0)
638
+ lines.push('');
639
+ const bottomRows = working.length + inputLines.length + autocomplete.length + statusFooter.length;
640
+ const fill = Math.max(0, height - lines.length - bottomRows);
641
+ lines.push(...Array.from({ length: fill }, () => ''));
642
+ lines.push(...working);
643
+ const editorStart = lines.length;
644
+ lines.push(...inputLines);
645
+ lines.push(...autocomplete);
646
+ lines.push(...statusFooter);
647
+ const trimmed = fitFrame(lines, width);
648
+ const caret = promptSelector?.cursor ?? settings?.cursor ?? copySelector?.cursor ?? search?.cursor ?? editor?.cursor ?? { row: 0, column: 0 };
649
+ return {
650
+ lines: trimmed,
651
+ cursor: {
652
+ row: editorStart + caret.row,
653
+ column: Math.min(caret.column, width),
654
+ },
655
+ cursorVisible: promptSelector?.cursorVisible
656
+ ?? (settings === undefined && copySelector === undefined),
657
+ ...(promptSelector?.editor !== undefined
658
+ ? { editor: { start: editorStart + promptSelector.editor.start, rows: promptSelector.editor.rows } }
659
+ : editor === undefined ? {} : { editor: { start: editorStart, rows: editor.lines.length } }),
660
+ ...(settings !== undefined
661
+ ? { overlay: { kind: 'settings', start: editorStart } }
662
+ : copySelector !== undefined
663
+ ? { overlay: { kind: 'copy', start: editorStart } }
664
+ : search !== undefined
665
+ ? { overlay: { kind: 'search', start: editorStart, resultsRow: search.resultsRow } }
666
+ : autocomplete.length > 0
667
+ ? { overlay: { kind: 'autocomplete', start: editorStart + (editor?.lines.length ?? 0) } }
668
+ : {}),
669
+ transcript: {
670
+ start: windowed.start,
671
+ maxStart: windowed.maxStart,
672
+ budget: windowed.budget,
673
+ hiddenAbove: windowed.hiddenAbove,
674
+ hiddenBelow: windowed.hiddenBelow,
675
+ },
676
+ };
677
+ }
678
+ //# sourceMappingURL=event-views.js.map