@agi-fans/dsh-tui 0.1.1 → 0.3.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.
- package/README.md +2 -2
- package/lib/autocomplete.js +0 -2
- package/lib/autocomplete.js.map +1 -1
- package/lib/{command-queue.d.ts → command-export.d.ts} +2 -2
- package/lib/command-export.js +39 -0
- package/lib/command-export.js.map +1 -0
- package/lib/{command-transcript.d.ts → command-permission.d.ts} +2 -2
- package/lib/{command-mode.js → command-permission.js} +28 -21
- package/lib/command-permission.js.map +1 -0
- package/lib/command-session.js +1 -1
- package/lib/command-session.js.map +1 -1
- package/lib/{command-mode.d.ts → command-steer.d.ts} +2 -2
- package/lib/command-steer.js +18 -0
- package/lib/command-steer.js.map +1 -0
- package/lib/definition.d.ts +14 -0
- package/lib/event-views.d.ts +32 -8
- package/lib/event-views.js +249 -45
- package/lib/event-views.js.map +1 -1
- package/lib/hotkeys.d.ts +2 -5
- package/lib/hotkeys.js +4 -14
- package/lib/hotkeys.js.map +1 -1
- package/lib/provider-local.d.ts +4 -1
- package/lib/provider-local.js +171 -43
- package/lib/provider-local.js.map +1 -1
- package/lib/renderer.d.ts +2 -0
- package/lib/renderer.js +48 -25
- package/lib/renderer.js.map +1 -1
- package/lib/runner.js +28 -0
- package/lib/runner.js.map +1 -1
- package/lib/session-controller.d.ts +27 -3
- package/lib/session-controller.js +206 -8
- package/lib/session-controller.js.map +1 -1
- package/lib/welcome-tips.js +2 -2
- package/lib/welcome-tips.js.map +1 -1
- package/package.json +13 -12
- package/lib/command-mode.js.map +0 -1
- package/lib/command-queue.js +0 -62
- package/lib/command-queue.js.map +0 -1
- package/lib/command-transcript.js +0 -85
- package/lib/command-transcript.js.map +0 -1
- package/lib/pwd.d.ts +0 -13
- package/lib/pwd.js +0 -14
- package/lib/pwd.js.map +0 -1
package/lib/event-views.js
CHANGED
|
@@ -21,12 +21,19 @@ import { createTheme, SPINNER, SYMBOL } from "./theme.js";
|
|
|
21
21
|
import { padToWidth, truncateToWidth, visibleWidth, wrapText } from "./width.js";
|
|
22
22
|
import { renderTool } from "./tool-renderers.js";
|
|
23
23
|
import { renderToolsPanel } from "./tools-list.js";
|
|
24
|
-
import { renderHotkeysPanel } from "./hotkeys.js";
|
|
25
24
|
import { renderCommandOutput, renderCommandSeparator } from "./command-output.js";
|
|
26
25
|
import { renderPathMentionRows } from "./path-mentions.js";
|
|
27
26
|
/** Empty starting state. */
|
|
28
27
|
export function initialTranscript() {
|
|
29
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
blocks: [],
|
|
30
|
+
status: 'idle',
|
|
31
|
+
turn: 0,
|
|
32
|
+
todos: [],
|
|
33
|
+
compactCommandId: undefined,
|
|
34
|
+
nextTurnInbox: [],
|
|
35
|
+
nextStepInbox: [],
|
|
36
|
+
};
|
|
30
37
|
}
|
|
31
38
|
/** Extract plain text from text blocks, ignoring other block kinds. */
|
|
32
39
|
function contentToText(content) {
|
|
@@ -66,8 +73,11 @@ function streamingBlock(state, turn, step) {
|
|
|
66
73
|
return undefined;
|
|
67
74
|
}
|
|
68
75
|
/** Replace the trailing streaming block with a settled one, or append. */
|
|
69
|
-
function
|
|
70
|
-
|
|
76
|
+
function editableBlocks(state, mutable) {
|
|
77
|
+
return mutable ? state.blocks : state.blocks.slice();
|
|
78
|
+
}
|
|
79
|
+
function settleAssistant(state, turn, step, text, reasoning, mutable) {
|
|
80
|
+
const blocks = editableBlocks(state, mutable);
|
|
71
81
|
const last = blocks[blocks.length - 1];
|
|
72
82
|
if (last?.kind === 'assistant' && last.streaming && last.turn === turn && last.step === step) {
|
|
73
83
|
blocks[blocks.length - 1] = { kind: 'assistant', turn, step, text, reasoning, streaming: false };
|
|
@@ -84,23 +94,41 @@ function settleAssistant(state, turn, step, text, reasoning) {
|
|
|
84
94
|
* @returns the next state.
|
|
85
95
|
*/
|
|
86
96
|
export function applyEvent(state, event, presentation) {
|
|
97
|
+
return foldEvent(state, event, presentation, false);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Replay one immutable event log without repeatedly copying its growing block
|
|
101
|
+
* array. The mutable array is private to this fold and becomes readonly when
|
|
102
|
+
* the completed state escapes.
|
|
103
|
+
*/
|
|
104
|
+
export function replayEvents(events, presentations) {
|
|
105
|
+
let state = initialTranscript();
|
|
106
|
+
const indexes = { toolByCallId: new Map() };
|
|
107
|
+
for (const event of events)
|
|
108
|
+
state = foldEvent(state, event, presentations?.get(event.seq), true, indexes);
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
function foldEvent(state, event, presentation, mutable, indexes) {
|
|
87
112
|
switch (event.type) {
|
|
88
113
|
case 'turn/start':
|
|
89
|
-
return { ...state, status: 'running', turn: event.data.turn };
|
|
114
|
+
return { ...state, status: 'running', turn: event.data.turn, todos: [], compactCommandId: undefined };
|
|
90
115
|
case 'turn/end': {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
116
|
+
const last = state.blocks[state.blocks.length - 1];
|
|
117
|
+
const reason = event.data.reason;
|
|
118
|
+
const changesBlocks = (last?.kind === 'assistant' && last.streaming)
|
|
119
|
+
|| reason.kind === 'error'
|
|
120
|
+
|| reason.kind === 'aborted';
|
|
121
|
+
const blocks = changesBlocks ? editableBlocks(state, mutable) : state.blocks;
|
|
93
122
|
if (last?.kind === 'assistant' && last.streaming) {
|
|
94
123
|
blocks[blocks.length - 1] = { ...last, streaming: false };
|
|
95
124
|
}
|
|
96
|
-
const reason = event.data.reason;
|
|
97
125
|
if (reason.kind === 'error') {
|
|
98
126
|
blocks.push({ kind: 'notice', level: 'error', text: 'error: ' + reason.error.code + ': ' + reason.error.message });
|
|
99
127
|
}
|
|
100
128
|
else if (reason.kind === 'aborted') {
|
|
101
129
|
blocks.push({ kind: 'notice', level: 'info', text: 'interrupted' });
|
|
102
130
|
}
|
|
103
|
-
return { ...state, blocks, status: 'idle' };
|
|
131
|
+
return { ...state, blocks, status: 'idle', compactCommandId: undefined };
|
|
104
132
|
}
|
|
105
133
|
case 'user/message': {
|
|
106
134
|
// Synthetic plugin injections (system-prompt runtime context, skill
|
|
@@ -111,12 +139,14 @@ export function applyEvent(state, event, presentation) {
|
|
|
111
139
|
const text = contentToText(event.data.content);
|
|
112
140
|
if (text === '')
|
|
113
141
|
return state;
|
|
114
|
-
|
|
142
|
+
const blocks = editableBlocks(state, mutable);
|
|
143
|
+
blocks.push({ kind: 'user', text });
|
|
144
|
+
return { ...state, blocks };
|
|
115
145
|
}
|
|
116
146
|
case 'assistant/chunk': {
|
|
117
147
|
const { turn, step, chunk } = event.data;
|
|
118
148
|
if (chunk.type === 'text-delta') {
|
|
119
|
-
const blocks = state
|
|
149
|
+
const blocks = editableBlocks(state, mutable);
|
|
120
150
|
const last = streamingBlock(state, turn, step);
|
|
121
151
|
if (last !== undefined) {
|
|
122
152
|
const idx = blocks.length - 1;
|
|
@@ -130,7 +160,7 @@ export function applyEvent(state, event, presentation) {
|
|
|
130
160
|
return { ...state, blocks };
|
|
131
161
|
}
|
|
132
162
|
if (chunk.type === 'reasoning-delta') {
|
|
133
|
-
const blocks = state
|
|
163
|
+
const blocks = editableBlocks(state, mutable);
|
|
134
164
|
const last = streamingBlock(state, turn, step);
|
|
135
165
|
if (last !== undefined) {
|
|
136
166
|
const idx = blocks.length - 1;
|
|
@@ -144,8 +174,10 @@ export function applyEvent(state, event, presentation) {
|
|
|
144
174
|
return { ...state, blocks };
|
|
145
175
|
}
|
|
146
176
|
if (chunk.type === 'tool-call-delta') {
|
|
147
|
-
const blocks = state
|
|
148
|
-
const index =
|
|
177
|
+
const blocks = editableBlocks(state, mutable);
|
|
178
|
+
const index = indexes === undefined
|
|
179
|
+
? blocks.findIndex(block => block.kind === 'tool' && block.callId === chunk.id)
|
|
180
|
+
: indexes.toolByCallId.get(chunk.id) ?? -1;
|
|
149
181
|
const existing = blocks[index];
|
|
150
182
|
if (existing?.kind === 'tool') {
|
|
151
183
|
blocks[index] = {
|
|
@@ -160,6 +192,7 @@ export function applyEvent(state, event, presentation) {
|
|
|
160
192
|
kind: 'tool', callId: chunk.id, name: chunk.name ?? 'tool',
|
|
161
193
|
args: chunk.argumentsDelta, status: 'running', output: '', partial: true,
|
|
162
194
|
});
|
|
195
|
+
indexes?.toolByCallId.set(chunk.id, blocks.length - 1);
|
|
163
196
|
}
|
|
164
197
|
return { ...state, blocks };
|
|
165
198
|
}
|
|
@@ -167,7 +200,7 @@ export function applyEvent(state, event, presentation) {
|
|
|
167
200
|
}
|
|
168
201
|
case 'assistant/message': {
|
|
169
202
|
const { turn, step, message } = event.data;
|
|
170
|
-
return settleAssistant(state, turn, step, contentToText(message.content), contentToReasoning(message.content));
|
|
203
|
+
return settleAssistant(state, turn, step, contentToText(message.content), contentToReasoning(message.content), mutable);
|
|
171
204
|
}
|
|
172
205
|
case 'tool/call': {
|
|
173
206
|
const block = {
|
|
@@ -179,16 +212,43 @@ export function applyEvent(state, event, presentation) {
|
|
|
179
212
|
output: '',
|
|
180
213
|
...(presentation === undefined ? {} : { presentation }),
|
|
181
214
|
};
|
|
182
|
-
const blocks = state
|
|
183
|
-
const partial =
|
|
215
|
+
const blocks = editableBlocks(state, mutable);
|
|
216
|
+
const partial = indexes === undefined
|
|
217
|
+
? blocks.findIndex(item => item.kind === 'tool' && item.callId === event.data.callId)
|
|
218
|
+
: indexes.toolByCallId.get(event.data.callId) ?? -1;
|
|
184
219
|
if (partial >= 0)
|
|
185
220
|
blocks[partial] = block;
|
|
186
|
-
else
|
|
221
|
+
else {
|
|
187
222
|
blocks.push(block);
|
|
223
|
+
indexes?.toolByCallId.set(event.data.callId, blocks.length - 1);
|
|
224
|
+
}
|
|
188
225
|
return { ...state, blocks };
|
|
189
226
|
}
|
|
190
227
|
case 'tool/result':
|
|
191
|
-
return applyToolResult(state, event.data.message, event.data.error, presentation);
|
|
228
|
+
return applyToolResult(state, event.data.message, event.data.error, presentation, mutable, indexes);
|
|
229
|
+
case 'todo/write':
|
|
230
|
+
return { ...state, todos: event.data.todos.map(todo => ({ ...todo })) };
|
|
231
|
+
case 'command/run':
|
|
232
|
+
if (event.data.name !== 'compact')
|
|
233
|
+
return state;
|
|
234
|
+
return {
|
|
235
|
+
...state,
|
|
236
|
+
status: 'compacting',
|
|
237
|
+
compactCommandId: event.data.commandId,
|
|
238
|
+
};
|
|
239
|
+
case 'command/done':
|
|
240
|
+
if (state.compactCommandId !== event.data.commandId)
|
|
241
|
+
return state;
|
|
242
|
+
return { ...state, status: 'idle', compactCommandId: undefined };
|
|
243
|
+
case 'agent/inbox/spliced': {
|
|
244
|
+
const key = event.data.target === 'next-turn' ? 'nextTurnInbox' : 'nextStepInbox';
|
|
245
|
+
return {
|
|
246
|
+
...state,
|
|
247
|
+
[key]: state[key].toSpliced(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
case 'session/end-seed':
|
|
251
|
+
return { ...state, nextTurnInbox: [], nextStepInbox: [] };
|
|
192
252
|
// Log-only vocabulary (boundaries, usage, compaction, approvals, ...):
|
|
193
253
|
// nothing to display; the recognized core events above own the surface.
|
|
194
254
|
default:
|
|
@@ -196,27 +256,38 @@ export function applyEvent(state, event, presentation) {
|
|
|
196
256
|
}
|
|
197
257
|
}
|
|
198
258
|
/** Fold one tool result into its tool block. */
|
|
199
|
-
function applyToolResult(state, message, error, presentation) {
|
|
259
|
+
function applyToolResult(state, message, error, presentation, mutable, indexes) {
|
|
200
260
|
// A tool-result message carries exactly one tool-result block; the call
|
|
201
261
|
// identity and outcome live on that inner block.
|
|
202
262
|
const inner = message.content[0];
|
|
203
263
|
if (inner?.type !== 'tool-result')
|
|
204
264
|
return state;
|
|
205
|
-
const blocks = state
|
|
265
|
+
const blocks = editableBlocks(state, mutable);
|
|
266
|
+
const indexed = indexes?.toolByCallId.get(inner.toolCallId);
|
|
267
|
+
if (indexed !== undefined) {
|
|
268
|
+
const block = blocks[indexed];
|
|
269
|
+
if (block?.kind === 'tool') {
|
|
270
|
+
blocks[indexed] = settleTool(block, inner, error, presentation);
|
|
271
|
+
return { ...state, blocks };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
206
274
|
for (let i = blocks.length - 1; i >= 0; i -= 1) {
|
|
207
275
|
const block = blocks[i];
|
|
208
276
|
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
|
-
};
|
|
277
|
+
blocks[i] = settleTool(block, inner, error, presentation);
|
|
215
278
|
return { ...state, blocks };
|
|
216
279
|
}
|
|
217
280
|
}
|
|
218
281
|
return state;
|
|
219
282
|
}
|
|
283
|
+
function settleTool(block, result, error, presentation) {
|
|
284
|
+
return {
|
|
285
|
+
...block,
|
|
286
|
+
status: error !== undefined || result.isError === true ? 'error' : 'ok',
|
|
287
|
+
output: contentToText(result.content),
|
|
288
|
+
...(presentation === undefined ? {} : { presentation }),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
220
291
|
/** Collapsed tool-output preview height (OMP `DEFAULT_TERMINAL_PREVIEW_LINES`). */
|
|
221
292
|
export const TOOL_COLLAPSED_LINES = 10;
|
|
222
293
|
/** OMP AssistantMessage uses one horizontal cell of padding and no vertical padding. */
|
|
@@ -307,8 +378,6 @@ export function blockLines(block, theme, width, spinnerFrame = 0, toolsExpanded
|
|
|
307
378
|
return toolBlockLines(block, theme, width, spinnerFrame, toolsExpanded);
|
|
308
379
|
if (block.kind === 'toolCatalog')
|
|
309
380
|
return renderToolsPanel(block.tools, theme, width, toolsExpanded);
|
|
310
|
-
if (block.kind === 'hotkeyCatalog')
|
|
311
|
-
return renderHotkeysPanel(block.bindings, theme, width);
|
|
312
381
|
if (block.kind === 'commandOutput')
|
|
313
382
|
return renderCommandOutput(block.command, block.text, theme, width);
|
|
314
383
|
if (block.framed !== true) {
|
|
@@ -337,23 +406,52 @@ function fitFrame(lines, width) {
|
|
|
337
406
|
});
|
|
338
407
|
}
|
|
339
408
|
/**
|
|
340
|
-
* Scroll
|
|
341
|
-
*
|
|
342
|
-
* rows instead of formatting the complete
|
|
409
|
+
* Scroll, activity, inbox, and composer changes do not alter transcript blocks.
|
|
410
|
+
* Cache the expensive Markdown/tool fold by immutable block-array identity so
|
|
411
|
+
* those frames slice already-rendered rows instead of formatting the complete
|
|
412
|
+
* session again. Spinner frames matter only while a visible tool block runs.
|
|
343
413
|
*/
|
|
344
414
|
const transcriptBodyCache = new WeakMap();
|
|
415
|
+
/** Settled immutable blocks keep their expensive Markdown/tool layout. */
|
|
416
|
+
const blockLinesCache = new WeakMap();
|
|
417
|
+
function cachedBlockLines(block, options, theme, themeName, trueColor, spinnerFrame, expanded) {
|
|
418
|
+
const animatedSpinnerFrame = block.kind === 'tool' && block.status === 'running' ? spinnerFrame : -1;
|
|
419
|
+
const cached = blockLinesCache.get(block);
|
|
420
|
+
if (cached !== undefined
|
|
421
|
+
&& cached.width === options.width
|
|
422
|
+
&& cached.colors === options.colors
|
|
423
|
+
&& cached.trueColor === trueColor
|
|
424
|
+
&& cached.themeName === themeName
|
|
425
|
+
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
426
|
+
&& cached.expanded === expanded)
|
|
427
|
+
return cached.lines;
|
|
428
|
+
const lines = blockLines(block, theme, options.width, spinnerFrame, expanded);
|
|
429
|
+
blockLinesCache.set(block, {
|
|
430
|
+
width: options.width,
|
|
431
|
+
colors: options.colors,
|
|
432
|
+
trueColor,
|
|
433
|
+
themeName,
|
|
434
|
+
spinnerFrame: animatedSpinnerFrame,
|
|
435
|
+
expanded,
|
|
436
|
+
lines,
|
|
437
|
+
});
|
|
438
|
+
return lines;
|
|
439
|
+
}
|
|
345
440
|
function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
346
441
|
const toolsExpanded = options.toolsExpanded === true;
|
|
347
442
|
const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
|
|
348
443
|
const themeName = options.themeName ?? 'dark';
|
|
349
444
|
const trueColor = options.trueColor === true;
|
|
350
|
-
const
|
|
445
|
+
const animatedSpinnerFrame = state.blocks.some(block => block.kind === 'tool' && block.status === 'running')
|
|
446
|
+
? spinnerFrame
|
|
447
|
+
: -1;
|
|
448
|
+
const cached = transcriptBodyCache.get(state.blocks);
|
|
351
449
|
if (cached !== undefined
|
|
352
450
|
&& cached.width === options.width
|
|
353
451
|
&& cached.colors === options.colors
|
|
354
452
|
&& cached.trueColor === trueColor
|
|
355
453
|
&& cached.themeName === themeName
|
|
356
|
-
&& cached.spinnerFrame ===
|
|
454
|
+
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
357
455
|
&& cached.toolsExpanded === toolsExpanded
|
|
358
456
|
&& cached.expandedTools === expandedTools) {
|
|
359
457
|
return { lines: cached.lines, blockStarts: cached.blockStarts };
|
|
@@ -374,15 +472,15 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
374
472
|
}
|
|
375
473
|
blockStarts.push(lines.length);
|
|
376
474
|
const expanded = toolsExpanded || (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
|
|
377
|
-
lines.push(...
|
|
475
|
+
lines.push(...cachedBlockLines(block, options, theme, themeName, trueColor, spinnerFrame, expanded));
|
|
378
476
|
previous = block;
|
|
379
477
|
}
|
|
380
|
-
transcriptBodyCache.set(state, {
|
|
478
|
+
transcriptBodyCache.set(state.blocks, {
|
|
381
479
|
width: options.width,
|
|
382
480
|
colors: options.colors,
|
|
383
481
|
trueColor,
|
|
384
482
|
themeName,
|
|
385
|
-
spinnerFrame,
|
|
483
|
+
spinnerFrame: animatedSpinnerFrame,
|
|
386
484
|
toolsExpanded,
|
|
387
485
|
expandedTools,
|
|
388
486
|
lines,
|
|
@@ -393,8 +491,6 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
393
491
|
function commandSurfaceName(block) {
|
|
394
492
|
if (block?.kind === 'toolCatalog')
|
|
395
493
|
return 'tools';
|
|
396
|
-
if (block?.kind === 'hotkeyCatalog')
|
|
397
|
-
return 'hotkeys';
|
|
398
494
|
if (block?.kind === 'commandOutput')
|
|
399
495
|
return block.command;
|
|
400
496
|
return undefined;
|
|
@@ -508,6 +604,103 @@ function paintImageMarkerSlice(fullText, slice, sourceStart, theme) {
|
|
|
508
604
|
output += fullText.slice(cursor, sourceEnd);
|
|
509
605
|
return output;
|
|
510
606
|
}
|
|
607
|
+
/** Maximum number of queued composer submissions kept visible above the editor. */
|
|
608
|
+
export const QUEUED_SUBMISSION_PREVIEW = 3;
|
|
609
|
+
/** Maximum number of Todo items kept visible above the composer. */
|
|
610
|
+
export const TODO_PREVIEW = 5;
|
|
611
|
+
function todoPreviewStart(todos) {
|
|
612
|
+
if (todos.length <= TODO_PREVIEW)
|
|
613
|
+
return 0;
|
|
614
|
+
const active = todos.findIndex(todo => todo.status === 'in_progress');
|
|
615
|
+
if (active >= 0)
|
|
616
|
+
return active;
|
|
617
|
+
const pending = todos.findIndex(todo => todo.status === 'pending');
|
|
618
|
+
return pending >= 0 ? pending : Math.max(0, todos.length - TODO_PREVIEW);
|
|
619
|
+
}
|
|
620
|
+
/** Compact, unframed Todo tree placed above queued messages. */
|
|
621
|
+
export function renderTodos(todos, theme, width) {
|
|
622
|
+
if (todos.length === 0 || width <= 0)
|
|
623
|
+
return [];
|
|
624
|
+
const completed = todos.filter(todo => todo.status === 'completed').length;
|
|
625
|
+
const header = ' ' + theme.bold(theme.fg('accent', 'Todos'))
|
|
626
|
+
+ theme.fg('dim', ` · ${completed}/${todos.length}`);
|
|
627
|
+
const start = todoPreviewStart(todos);
|
|
628
|
+
const end = Math.min(todos.length, start + TODO_PREVIEW);
|
|
629
|
+
const rows = [
|
|
630
|
+
...(start === 0 ? [] : [`… ${start} earlier`]),
|
|
631
|
+
...todos.slice(start, end),
|
|
632
|
+
...(end === todos.length ? [] : [`… ${todos.length - end} more`]),
|
|
633
|
+
];
|
|
634
|
+
const lines = [header, ...rows.map((row, index) => {
|
|
635
|
+
const branch = ' ' + theme.fg('dim', index === rows.length - 1 ? '└─' : '├─') + ' ';
|
|
636
|
+
if (typeof row === 'string')
|
|
637
|
+
return branch + theme.fg('dim', row);
|
|
638
|
+
const todo = row;
|
|
639
|
+
if (todo.status === 'completed') {
|
|
640
|
+
return branch + theme.fg('success', '☑ ' + theme.strikethrough(todo.content));
|
|
641
|
+
}
|
|
642
|
+
if (todo.status === 'in_progress') {
|
|
643
|
+
return branch + theme.fg('accent', '☐ ' + todo.content);
|
|
644
|
+
}
|
|
645
|
+
return branch + theme.fg('dim', '☐ ' + todo.content);
|
|
646
|
+
})];
|
|
647
|
+
return lines.map(line => truncateToWidth(line, width));
|
|
648
|
+
}
|
|
649
|
+
function queuedSubmissionLabel(submission) {
|
|
650
|
+
const text = submission.text.replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
651
|
+
.replaceAll('\n', ' ↵ ')
|
|
652
|
+
.replace(/\s+/gu, ' ')
|
|
653
|
+
.trim();
|
|
654
|
+
if (text !== '')
|
|
655
|
+
return text;
|
|
656
|
+
const count = submission.images.length;
|
|
657
|
+
return count === 0 ? '(empty message)' : `${count} image${count === 1 ? '' : 's'}`;
|
|
658
|
+
}
|
|
659
|
+
function queuedMessageLabel(message) {
|
|
660
|
+
const text = contentToText(message.content).replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
661
|
+
.replaceAll('\n', ' ↵ ')
|
|
662
|
+
.replace(/\s+/gu, ' ')
|
|
663
|
+
.trim();
|
|
664
|
+
if (text !== '')
|
|
665
|
+
return text;
|
|
666
|
+
const count = message.content.filter(block => block.type === 'image').length;
|
|
667
|
+
return count === 0 ? '(empty message)' : `${count} image${count === 1 ? '' : 's'}`;
|
|
668
|
+
}
|
|
669
|
+
/** Compact, unframed pending-message view placed immediately above the composer. */
|
|
670
|
+
export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
671
|
+
const labels = [
|
|
672
|
+
...inbox.filter(message => message.source.kind === 'user').map(queuedMessageLabel),
|
|
673
|
+
...submissions.map(queuedSubmissionLabel),
|
|
674
|
+
];
|
|
675
|
+
if (labels.length === 0 || width <= 0)
|
|
676
|
+
return [];
|
|
677
|
+
const start = Math.max(0, labels.length - QUEUED_SUBMISSION_PREVIEW);
|
|
678
|
+
const hidden = start;
|
|
679
|
+
const queueLabel = theme.bold(theme.fg('accent', 'Queued'));
|
|
680
|
+
const rail = ' ' + theme.fg('border', '│') + ' ';
|
|
681
|
+
const alignAction = (left, action, preserveAction = true) => {
|
|
682
|
+
const leftWidth = visibleWidth(left);
|
|
683
|
+
const actionWidth = visibleWidth(action);
|
|
684
|
+
if (leftWidth + actionWidth + 2 <= width) {
|
|
685
|
+
return left + ' '.repeat(width - leftWidth - actionWidth) + action;
|
|
686
|
+
}
|
|
687
|
+
if (!preserveAction || actionWidth + 4 >= width)
|
|
688
|
+
return truncateToWidth(left, width);
|
|
689
|
+
const paintedLeft = truncateToWidth(left, width - actionWidth - 2);
|
|
690
|
+
return paintedLeft + ' ' + action;
|
|
691
|
+
};
|
|
692
|
+
if (labels.length === 1) {
|
|
693
|
+
return [alignAction(rail + queueLabel + theme.fg('dim', ' · ') + theme.fg('text', labels[0] ?? ''), theme.fg('dim', '↑ edit'))];
|
|
694
|
+
}
|
|
695
|
+
const summary = ` · ${labels.length}${hidden === 0 ? '' : ` · ${hidden} earlier`}`;
|
|
696
|
+
const visibleLabels = labels.slice(start);
|
|
697
|
+
const lines = [
|
|
698
|
+
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim', '↑ edit latest'), false),
|
|
699
|
+
...visibleLabels.map((label, index) => rail + theme.fg(index === visibleLabels.length - 1 ? 'accent' : 'dim', String(start + index + 1))
|
|
700
|
+
+ ' ' + theme.fg('text', label)),
|
|
701
|
+
];
|
|
702
|
+
return lines.map(line => truncateToWidth(line, width));
|
|
703
|
+
}
|
|
511
704
|
/**
|
|
512
705
|
* Compose the welcome card, transcript, working row, and rounded editor into
|
|
513
706
|
* one frame — the oh-my-pi surface.
|
|
@@ -578,7 +771,11 @@ export function renderView(state, options) {
|
|
|
578
771
|
transcriptStart = body.length;
|
|
579
772
|
body.push(...transcript.lines);
|
|
580
773
|
}
|
|
581
|
-
const working = state.status === 'running'
|
|
774
|
+
const working = state.status === 'running'
|
|
775
|
+
? renderWorking(theme, spinnerFrame, undefined, width)
|
|
776
|
+
: state.status === 'compacting'
|
|
777
|
+
? renderWorking(theme, spinnerFrame, 'Compacting', width)
|
|
778
|
+
: [];
|
|
582
779
|
const statusBar = resolveStatusBarConfig(options.statusBar, options.statusPreset);
|
|
583
780
|
const inlineHint = slashInlineHint(options.input, options.inputCursor, options.commands);
|
|
584
781
|
const statusFooter = renderStatusFooter({
|
|
@@ -596,7 +793,7 @@ export function renderView(state, options) {
|
|
|
596
793
|
input: options.input,
|
|
597
794
|
inputCursor: options.inputCursor,
|
|
598
795
|
status: ' ' + theme.fg('accent', '🐳') + ' ',
|
|
599
|
-
border: state.status === '
|
|
796
|
+
border: state.status === 'idle' ? 'border' : 'accent',
|
|
600
797
|
...(options.inputImages === undefined || options.inputImages === 0
|
|
601
798
|
? {}
|
|
602
799
|
: { paintInput: (text, start) => paintImageMarkerSlice(options.input, text, start, theme) }),
|
|
@@ -617,6 +814,10 @@ export function renderView(state, options) {
|
|
|
617
814
|
const editor = promptSelector === undefined && settings === undefined && copySelector === undefined && search === undefined
|
|
618
815
|
? renderEditor(editorOpts, theme)
|
|
619
816
|
: undefined;
|
|
817
|
+
const queuedSubmissions = editor === undefined
|
|
818
|
+
? []
|
|
819
|
+
: renderQueuedSubmissions(options.queuedSubmissions ?? [], theme, width, state.nextTurnInbox);
|
|
820
|
+
const todos = editor === undefined ? [] : renderTodos(state.todos, theme, width);
|
|
620
821
|
const autocomplete = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || search !== undefined
|
|
621
822
|
|| options.autocomplete === undefined
|
|
622
823
|
? []
|
|
@@ -624,7 +825,7 @@ export function renderView(state, options) {
|
|
|
624
825
|
const inputLines = promptSelector?.lines ?? settings?.lines ?? copySelector?.lines ?? search?.lines
|
|
625
826
|
?? (editor === undefined ? [] : editor.lines);
|
|
626
827
|
const spacer = 1;
|
|
627
|
-
const reserved = inputLines.length + working.length + spacer + autocomplete.length + statusFooter.length;
|
|
828
|
+
const reserved = inputLines.length + working.length + todos.length + queuedSubmissions.length + spacer + autocomplete.length + statusFooter.length;
|
|
628
829
|
const budget = Math.max(0, height - reserved);
|
|
629
830
|
const focusStart = options.focusBlock === undefined
|
|
630
831
|
? undefined
|
|
@@ -637,10 +838,12 @@ export function renderView(state, options) {
|
|
|
637
838
|
const lines = [...visible];
|
|
638
839
|
if (visible.length > 0)
|
|
639
840
|
lines.push('');
|
|
640
|
-
const bottomRows = working.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
841
|
+
const bottomRows = working.length + todos.length + queuedSubmissions.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
641
842
|
const fill = Math.max(0, height - lines.length - bottomRows);
|
|
642
843
|
lines.push(...Array.from({ length: fill }, () => ''));
|
|
643
844
|
lines.push(...working);
|
|
845
|
+
lines.push(...todos);
|
|
846
|
+
lines.push(...queuedSubmissions);
|
|
644
847
|
const editorStart = lines.length;
|
|
645
848
|
lines.push(...inputLines);
|
|
646
849
|
lines.push(...autocomplete);
|
|
@@ -653,8 +856,9 @@ export function renderView(state, options) {
|
|
|
653
856
|
row: editorStart + caret.row,
|
|
654
857
|
column: Math.min(caret.column, width),
|
|
655
858
|
},
|
|
656
|
-
cursorVisible:
|
|
657
|
-
|
|
859
|
+
cursorVisible: state.status === 'compacting'
|
|
860
|
+
? false
|
|
861
|
+
: promptSelector?.cursorVisible ?? (settings === undefined && copySelector === undefined),
|
|
658
862
|
...(promptSelector?.editor !== undefined
|
|
659
863
|
? { editor: { start: editorStart + promptSelector.editor.start, rows: promptSelector.editor.rows } }
|
|
660
864
|
: editor === undefined ? {} : { editor: { start: editorStart, rows: editor.lines.length } }),
|