@agi-fans/dsh-tui 0.19.0 → 0.20.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/lib/CHANGELOG.md +84 -1
- package/lib/chrome/box.js +2 -12
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.d.ts +26 -18
- package/lib/chrome/code-highlight.js +114 -39
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/diff-render.d.ts +1 -2
- package/lib/chrome/diff-render.js +41 -90
- package/lib/chrome/diff-render.js.map +1 -1
- package/lib/chrome/main-screen-renderer.d.ts +4 -32
- package/lib/chrome/main-screen-renderer.js +63 -15
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.js +128 -93
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/math.d.ts +4 -0
- package/lib/chrome/math.js +340 -0
- package/lib/chrome/math.js.map +1 -0
- package/lib/chrome/md-style.d.ts +3 -0
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/mermaid.d.ts +6 -0
- package/lib/chrome/mermaid.js +56 -0
- package/lib/chrome/mermaid.js.map +1 -0
- package/lib/chrome/renderer.d.ts +3 -0
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-config.d.ts +5 -0
- package/lib/chrome/status-config.js +3 -0
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/status-line.d.ts +0 -21
- package/lib/chrome/status-line.js +8 -65
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/terminal-images.d.ts +31 -0
- package/lib/chrome/terminal-images.js +34 -0
- package/lib/chrome/terminal-images.js.map +1 -0
- package/lib/chrome/theme.d.ts +5 -6
- package/lib/chrome/theme.js +199 -109
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/welcome-tips.js +1 -1
- package/lib/chrome/width.d.ts +7 -3
- package/lib/chrome/width.js +78 -14
- package/lib/chrome/width.js.map +1 -1
- package/lib/commands/diff.js +49 -3
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.js +28 -5
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.d.ts +8 -0
- package/lib/commands/files.js +137 -0
- package/lib/commands/files.js.map +1 -0
- package/lib/commands/plugins.d.ts +8 -0
- package/lib/commands/plugins.js +158 -0
- package/lib/commands/plugins.js.map +1 -0
- package/lib/commands/session-configuration.d.ts +4 -0
- package/lib/commands/session-configuration.js +39 -8
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +47 -15
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/steer.js +2 -2
- package/lib/commands/steer.js.map +1 -1
- package/lib/commands/terminal.d.ts +7 -0
- package/lib/commands/terminal.js +99 -0
- package/lib/commands/terminal.js.map +1 -0
- package/lib/definition.d.ts +121 -7
- package/lib/input/clipboard.d.ts +12 -7
- package/lib/input/clipboard.js +79 -20
- package/lib/input/clipboard.js.map +1 -1
- package/lib/input/composer-files.d.ts +26 -0
- package/lib/input/composer-files.js +50 -0
- package/lib/input/composer-files.js.map +1 -0
- package/lib/input/editor-discovery.d.ts +21 -0
- package/lib/input/editor-discovery.js +99 -0
- package/lib/input/editor-discovery.js.map +1 -0
- package/lib/input/editor.d.ts +26 -1
- package/lib/input/editor.js +126 -12
- package/lib/input/editor.js.map +1 -1
- package/lib/input/external-editor.d.ts +5 -2
- package/lib/input/external-editor.js +14 -12
- package/lib/input/external-editor.js.map +1 -1
- package/lib/input/fuzzy-search.d.ts +9 -0
- package/lib/input/fuzzy-search.js +113 -0
- package/lib/input/fuzzy-search.js.map +1 -0
- package/lib/input/keybindings-config.d.ts +1 -1
- package/lib/input/keybindings-config.js +6 -1
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +11 -1
- package/lib/input/keys.js +17 -8
- package/lib/input/keys.js.map +1 -1
- package/lib/input/mouse-policy.d.ts +3 -0
- package/lib/input/mouse-policy.js +14 -0
- package/lib/input/mouse-policy.js.map +1 -0
- package/lib/input/paste-burst.d.ts +16 -0
- package/lib/input/paste-burst.js +61 -0
- package/lib/input/paste-burst.js.map +1 -0
- package/lib/input/text-selection.d.ts +20 -0
- package/lib/input/text-selection.js +195 -0
- package/lib/input/text-selection.js.map +1 -0
- package/lib/runtime/composer-images.d.ts +1 -0
- package/lib/runtime/composer-images.js +11 -3
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/file-open.d.ts +2 -0
- package/lib/runtime/file-open.js +12 -0
- package/lib/runtime/file-open.js.map +1 -0
- package/lib/runtime/file-review.d.ts +24 -0
- package/lib/runtime/file-review.js +188 -0
- package/lib/runtime/file-review.js.map +1 -0
- package/lib/runtime/herdr-agent.d.ts +7 -15
- package/lib/runtime/herdr-agent.js +151 -79
- package/lib/runtime/herdr-agent.js.map +1 -1
- package/lib/runtime/hook-settings.d.ts +17 -0
- package/lib/runtime/hook-settings.js +57 -0
- package/lib/runtime/hook-settings.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/image-preview.d.ts +8 -0
- package/lib/runtime/image-preview.js +119 -0
- package/lib/runtime/image-preview.js.map +1 -0
- package/lib/runtime/plain-tui.d.ts +2 -0
- package/lib/runtime/plain-tui.js +2 -2
- package/lib/runtime/plain-tui.js.map +1 -1
- package/lib/runtime/plugin-settings.d.ts +17 -0
- package/lib/runtime/plugin-settings.js +154 -0
- package/lib/runtime/plugin-settings.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +39 -4
- package/lib/runtime/provider-local.js +977 -92
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +3 -3
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-archive.d.ts +5 -0
- package/lib/runtime/session-archive.js +114 -0
- package/lib/runtime/session-archive.js.map +1 -0
- package/lib/runtime/subagent-model-selection.d.ts +5 -0
- package/lib/runtime/subagent-model-selection.js +15 -0
- package/lib/runtime/subagent-model-selection.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +8 -0
- package/lib/runtime/terminal-notifications.js +16 -0
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-reader.d.ts +11 -0
- package/lib/runtime/terminal-reader.js +87 -0
- package/lib/runtime/terminal-reader.js.map +1 -0
- package/lib/session/composer-drafts.d.ts +20 -0
- package/lib/session/composer-drafts.js +103 -0
- package/lib/session/composer-drafts.js.map +1 -0
- package/lib/session/conversation-turns.d.ts +16 -0
- package/lib/session/conversation-turns.js +33 -0
- package/lib/session/conversation-turns.js.map +1 -0
- package/lib/session/feature-toggles.d.ts +7 -28
- package/lib/session/feature-toggles.js +14 -45
- package/lib/session/feature-toggles.js.map +1 -1
- package/lib/session/fold-policy.d.ts +0 -17
- package/lib/session/fold-policy.js +0 -11
- package/lib/session/fold-policy.js.map +1 -1
- package/lib/session/interaction-adapter.d.ts +2 -2
- package/lib/session/interaction-adapter.js +156 -25
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +22 -0
- package/lib/session/message-queue.js +157 -0
- package/lib/session/message-queue.js.map +1 -0
- package/lib/session/project-file-search.js +3 -21
- package/lib/session/project-file-search.js.map +1 -1
- package/lib/session/session-controller.d.ts +11 -18
- package/lib/session/session-controller.js +195 -87
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +11 -3
- package/lib/session/session-library.js +48 -8
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/session-tree.d.ts +26 -0
- package/lib/session/session-tree.js +112 -0
- package/lib/session/session-tree.js.map +1 -0
- package/lib/session/steering.d.ts +3 -0
- package/lib/session/steering.js +9 -0
- package/lib/session/steering.js.map +1 -0
- package/lib/session/tui-settings.d.ts +17 -0
- package/lib/session/tui-settings.js +12 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +9 -0
- package/lib/session/workflow-projection.js +44 -0
- package/lib/session/workflow-projection.js.map +1 -0
- package/lib/views/agent-hub.js +9 -4
- package/lib/views/agent-hub.js.map +1 -1
- package/lib/views/autocomplete.js +5 -25
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/document-reader.d.ts +34 -0
- package/lib/views/document-reader.js +145 -0
- package/lib/views/document-reader.js.map +1 -0
- package/lib/views/event-views.js +39 -2
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/hotkeys.js +13 -5
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +33 -3
- package/lib/views/prompt-selector.js +287 -43
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/prompt-tree.d.ts +11 -0
- package/lib/views/prompt-tree.js +74 -0
- package/lib/views/prompt-tree.js.map +1 -0
- package/lib/views/session-files.d.ts +7 -0
- package/lib/views/session-files.js +62 -0
- package/lib/views/session-files.js.map +1 -0
- package/lib/views/settings-list.d.ts +24 -2
- package/lib/views/settings-list.js +115 -27
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-render.d.ts +23 -39
- package/lib/views/transcript-render.js +101 -210
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +1 -1
- package/package.json +77 -42
- package/lib/chrome/style.d.ts +0 -22
- package/lib/chrome/style.js +0 -23
- package/lib/chrome/style.js.map +0 -1
|
@@ -12,12 +12,12 @@ import { renderCopySelector } from "./copy-selector.js";
|
|
|
12
12
|
import { renderSettings } from "./settings-list.js";
|
|
13
13
|
import { renderTrajectory } from "./trajectory.js";
|
|
14
14
|
import { renderAgentHub } from "./agent-hub.js";
|
|
15
|
-
import { renderPlanReviewPage, renderPromptSelector, renderPromptSelectorPage, } from "./prompt-selector.js";
|
|
15
|
+
import { renderImagePreviewPage, renderPlanReviewPage, renderPromptSelector, renderPromptSelectorPage, renderPromptTreePage, } from "./prompt-selector.js";
|
|
16
16
|
import { resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
17
17
|
import { renderPermissionBadge, renderStatusFooter } from "../chrome/status-line.js";
|
|
18
|
-
import { createTheme, SPINNER, SYMBOL, BOX } from "../chrome/theme.js";
|
|
18
|
+
import { createTheme, paintSurface, SPINNER, SYMBOL, BOX } from "../chrome/theme.js";
|
|
19
19
|
import { renderGoalBar } from "../chrome/goal-bar.js";
|
|
20
|
-
import {
|
|
20
|
+
import { padToWidth, stripAnsi, truncateToWidth, visibleWidth, wrapText } from "../chrome/width.js";
|
|
21
21
|
import { alignFileDiffs, wrapPaintedDiffRows, } from "../chrome/diff-render.js";
|
|
22
22
|
import { workspaceBlockLines } from "./workspace-changes.js";
|
|
23
23
|
import { renderTool, toolArgSubject } from "../chrome/tool-renderers.js";
|
|
@@ -26,7 +26,6 @@ import { renderToolsPanel } from "../chrome/tools-list.js";
|
|
|
26
26
|
import { renderCommandOutput, renderCommandSeparator } from "../chrome/command-output.js";
|
|
27
27
|
import { renderPathMentionRows } from "../chrome/path-mentions.js";
|
|
28
28
|
import { blockMatchesQuery, transcriptSearchHint } from "./transcript-search.js";
|
|
29
|
-
import { DEFAULT_FOLD_DENSITY, foldPolicy, foldPolicyKey } from "../session/fold-policy.js";
|
|
30
29
|
import { contentToText, prettyArgs, reasoningKey, } from "./transcript-types.js";
|
|
31
30
|
function isBlockPending(block) {
|
|
32
31
|
return (block.kind === 'assistant' && block.streaming) || (block.kind === 'tool' && block.status === 'running');
|
|
@@ -116,9 +115,9 @@ function userBubble(text, theme, width) {
|
|
|
116
115
|
// anything else on the screen.
|
|
117
116
|
return rows.map((row, index) => {
|
|
118
117
|
if (row === '')
|
|
119
|
-
return theme
|
|
118
|
+
return paintSurface(theme, 'userMessageBg', 'userMessageText', padToWidth('', width));
|
|
120
119
|
const content = padToWidth((index === 1 ? gutter : '') + row, width);
|
|
121
|
-
return theme
|
|
120
|
+
return paintSurface(theme, 'userMessageBg', 'userMessageText', content);
|
|
122
121
|
});
|
|
123
122
|
}
|
|
124
123
|
function toolIcon(status, theme, spinnerFrame) {
|
|
@@ -171,7 +170,7 @@ function toolFailureFact(output, command, fact) {
|
|
|
171
170
|
* Errors reserve space for their reason; full commands and output stay in the
|
|
172
171
|
* expanded block. Process rows share an 80-cell measure and right padding.
|
|
173
172
|
*/
|
|
174
|
-
function toolRowLine(block, theme, width, spinnerFrame
|
|
173
|
+
function toolRowLine(block, theme, width, spinnerFrame) {
|
|
175
174
|
const presentation = renderTool({
|
|
176
175
|
name: block.name,
|
|
177
176
|
arguments: prettyArgs(block.args),
|
|
@@ -184,7 +183,7 @@ function toolRowLine(block, theme, width, spinnerFrame, showSubject = true, inde
|
|
|
184
183
|
// A PTC sub-call ran inside a program, not because the model asked for it
|
|
185
184
|
// directly. Indenting says that without a second visual vocabulary, and the
|
|
186
185
|
// width budget shrinks to match so the row still ends at the same column.
|
|
187
|
-
const nest =
|
|
186
|
+
const nest = block.parentCallId === undefined ? '' : ' ';
|
|
188
187
|
// No gutter column. A settled successful call carries no mark at all — a
|
|
189
188
|
// column of identical bullets says only that the row above exists — and the
|
|
190
189
|
// two that remain are the states a reader has to act on: motion while the
|
|
@@ -193,19 +192,18 @@ function toolRowLine(block, theme, width, spinnerFrame, showSubject = true, inde
|
|
|
193
192
|
const head = nest + mark;
|
|
194
193
|
const room = processRoom(width, head);
|
|
195
194
|
const label = processLabel(block.name);
|
|
196
|
-
const
|
|
197
|
-
const subject = showSubject ? detail : '';
|
|
195
|
+
const subject = toolRowSubject(block, presentation, label);
|
|
198
196
|
const args = toolArgsObject(block.args);
|
|
199
197
|
const call = block.presentation?.call;
|
|
200
198
|
const description = args?.['description'] ?? (call?.card === 'terminal' ? call.description : undefined);
|
|
201
|
-
const descriptionLed =
|
|
199
|
+
const descriptionLed = (COMMAND_TOOL.test(block.name) || call?.card === 'terminal')
|
|
202
200
|
&& typeof description === 'string' && description.trim() !== '';
|
|
203
201
|
// The echo a failure skips is the command line, which a description-led row
|
|
204
202
|
// no longer carries as its subject.
|
|
205
|
-
const argCommand =
|
|
203
|
+
const argCommand = args?.['command'];
|
|
206
204
|
const command = typeof argCommand === 'string'
|
|
207
205
|
? argCommand
|
|
208
|
-
: block.presentation?.call?.card === 'terminal' ? presentation.input[0] ??
|
|
206
|
+
: block.presentation?.call?.card === 'terminal' ? presentation.input[0] ?? subject : subject;
|
|
209
207
|
const fact = block.status === 'error'
|
|
210
208
|
? firstLineOf(block.job !== undefined && block.job.status !== 'completed'
|
|
211
209
|
? block.job.detail ?? block.job.status
|
|
@@ -216,14 +214,9 @@ function toolRowLine(block, theme, width, spinnerFrame, showSubject = true, inde
|
|
|
216
214
|
// as a separate column that belonged to no row in particular. It is reserved
|
|
217
215
|
// first so a long subject cannot push it off; a failure gets the larger
|
|
218
216
|
// share, because its fact is the reason the reader stopped.
|
|
219
|
-
// The running clock is reserved before either of them: a call that has been
|
|
220
|
-
// running is what the reader is waiting on, and a subject that pushed its age
|
|
221
|
-
// off the row would leave the one live number in the transcript unreported.
|
|
222
|
-
const clock = elapsed === undefined || elapsed === '' ? '' : theme.dim(elapsed);
|
|
223
|
-
const clockRoom = clock === '' ? 0 : visibleWidth(clock) + ROW_JOIN.length;
|
|
224
217
|
const factShare = block.status === 'error' ? 0.5 : 1 / 3;
|
|
225
|
-
const factRoom = Math.min(visibleWidth(fact), Math.floor(Math.max(1, room
|
|
226
|
-
const afterFact = Math.max(1, room -
|
|
218
|
+
const factRoom = Math.min(visibleWidth(fact), Math.floor(Math.max(1, room) * factShare));
|
|
219
|
+
const afterFact = Math.max(1, room - (factRoom === 0 ? 0 : factRoom + ROW_JOIN.length));
|
|
227
220
|
const labelRoom = descriptionLed ? 0 : Math.min(visibleWidth(label), afterFact);
|
|
228
221
|
const detailRoom = descriptionLed ? afterFact : afterFact - labelRoom - ROW_JOIN.length;
|
|
229
222
|
const labelText = descriptionLed ? '' : truncateToWidth(label, Math.max(1, labelRoom));
|
|
@@ -235,9 +228,7 @@ function toolRowLine(block, theme, width, spinnerFrame, showSubject = true, inde
|
|
|
235
228
|
// A failure is the one line in a run that has to stay findable while scrolling
|
|
236
229
|
// past, so its fact carries the error colour rather than the row's dim.
|
|
237
230
|
const tail = tailText === '' ? '' : block.status === 'error' ? theme.fg('error', tailText) : theme.dim(tailText);
|
|
238
|
-
|
|
239
|
-
: clock === '' ? '' : ROW_JOIN + clock;
|
|
240
|
-
return [padToWidth(truncateToWidth(head + theme.fg('muted', text) + tail + clockText, Math.max(1, width - 2)), width)];
|
|
231
|
+
return [padToWidth(truncateToWidth(head + theme.fg('muted', text) + tail, Math.max(1, width - 2)), width)];
|
|
241
232
|
}
|
|
242
233
|
/** Separator between a process subject and its outcome. */
|
|
243
234
|
const ROW_JOIN = ' · ';
|
|
@@ -379,10 +370,8 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
379
370
|
if (block.job !== undefined)
|
|
380
371
|
rows.push(...wrapText(theme.fg(block.status === 'error' ? 'error' : 'muted', `Job ${block.job.id} · ${block.job.detail ?? block.job.status}`), room));
|
|
381
372
|
const bg = block.status === 'running' ? 'toolPendingBg' : block.status === 'error' ? 'toolErrorBg' : 'toolSuccessBg';
|
|
382
|
-
return ['', ...rows, ''].map(row => theme
|
|
373
|
+
return ['', ...rows, ''].map(row => paintSurface(theme, bg, 'toolOutput', padToWidth(' '.repeat(padding + nest) + row, width)));
|
|
383
374
|
}
|
|
384
|
-
/** How wide a preview may grow before it is cut even at a sentence end. */
|
|
385
|
-
const PREVIEW_CELLS = 96;
|
|
386
375
|
function firstLineOf(text) {
|
|
387
376
|
for (const line of text.split('\n')) {
|
|
388
377
|
const trimmed = line.trim();
|
|
@@ -391,91 +380,9 @@ function firstLineOf(text) {
|
|
|
391
380
|
}
|
|
392
381
|
return '';
|
|
393
382
|
}
|
|
394
|
-
/**
|
|
395
|
-
* The first whole sentence of a line, or the line cut to a readable width.
|
|
396
|
-
*
|
|
397
|
-
* Taking the first *line* of a thought produced a run-on clause that always ran
|
|
398
|
-
* off the right edge and always ended in an ellipsis, so a long turn came back
|
|
399
|
-
* as a wall of identically truncated rows. A sentence is the unit a reader can
|
|
400
|
-
* hold, and it is usually short enough to fit without a mark saying it was cut.
|
|
401
|
-
* Prose that never closes on a sentence falls back to a width, and says so.
|
|
402
|
-
*/
|
|
403
|
-
function firstSentenceOf(line) {
|
|
404
|
-
const limit = Math.min(visibleWidth(line), PREVIEW_CELLS);
|
|
405
|
-
// The running width is accumulated rather than re-measured per character. A
|
|
406
|
-
// thought paragraph runs to thousands of characters, and re-measuring the
|
|
407
|
-
// prefix on every step made looking for a sentence that is not there
|
|
408
|
-
// quadratic — the common case, since most thoughts do not close on one.
|
|
409
|
-
let widthSoFar = 0;
|
|
410
|
-
for (let at = 1; at < line.length; at += 1) {
|
|
411
|
-
const char = line[at];
|
|
412
|
-
const next = line[at + 1];
|
|
413
|
-
widthSoFar += charWidth(line.charCodeAt(at));
|
|
414
|
-
const wide = char === '\u3002' || char === '\uff01' || char === '\uff1f';
|
|
415
|
-
const closes = wide || char === '.' || char === '!' || char === '?';
|
|
416
|
-
// A full stop inside an ellipsis, a version tag or a decimal is not an end.
|
|
417
|
-
// A CJK stop is never followed by a space, so it ends wherever it stands.
|
|
418
|
-
const endsHere = closes && (wide || next === undefined || SENTENCE_FOLLOWER.test(next));
|
|
419
|
-
if (!endsHere || widthSoFar > limit)
|
|
420
|
-
continue;
|
|
421
|
-
// A sentence that closes inside a quotation takes the closing mark with it;
|
|
422
|
-
// cutting before it leaves the row with an opening quote and no partner.
|
|
423
|
-
let end = at + 1;
|
|
424
|
-
while (end < line.length && CLOSING_MARK.test(line[end]))
|
|
425
|
-
end += 1;
|
|
426
|
-
return line.slice(0, end).trimEnd();
|
|
427
|
-
}
|
|
428
|
-
return truncateToWidth(line, Math.max(1, limit));
|
|
429
|
-
}
|
|
430
|
-
const SENTENCE_FOLLOWER = /[\s"'\u201d\u2019)\]\u300d\u300f]/u;
|
|
431
|
-
const CLOSING_MARK = /["'\u201d\u2019)\]\u300d\u300f]/u;
|
|
432
|
-
/**
|
|
433
|
-
* Sentences that carry no content on their own.
|
|
434
|
-
*
|
|
435
|
-
* A model opens a thought with an acknowledgement — `Good.`, `Hmm.`, `Now,` —
|
|
436
|
-
* far more often than a writer would, and a preview that stops there reads as
|
|
437
|
-
* a row of noise down the run. Skipping them costs nothing: the next sentence
|
|
438
|
-
* is the one the acknowledgement was introducing.
|
|
439
|
-
*/
|
|
440
|
-
const FILLER_SENTENCE = /^(good|great|ok|okay|hmm+|now|so|right|alright|yes|interesting|perfect|done|nice)[\s.!,:;\u2014-]*$/iu;
|
|
441
|
-
/** The first sentence of a line that says something, skipping acknowledgements. */
|
|
442
|
-
function firstTellingSentenceOf(line) {
|
|
443
|
-
let rest = line;
|
|
444
|
-
for (let hop = 0; hop < 3; hop += 1) {
|
|
445
|
-
const sentence = firstSentenceOf(rest);
|
|
446
|
-
const after = rest.slice(sentence.length).trim();
|
|
447
|
-
if (!FILLER_SENTENCE.test(sentence) || after === '')
|
|
448
|
-
return sentence;
|
|
449
|
-
rest = after;
|
|
450
|
-
}
|
|
451
|
-
return firstSentenceOf(rest);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* One-line preview of a reasoning run.
|
|
455
|
-
*
|
|
456
|
-
* Slicing the raw text would show half a sentence and then rewrite it under the
|
|
457
|
-
* reader's eyes on the next token. A paragraph is the unit the model actually
|
|
458
|
-
* finishes, so the preview advances to the first line of the last *completed*
|
|
459
|
-
* paragraph and waits there until another one closes; before any paragraph has
|
|
460
|
-
* closed there is nothing safe to show, and the row says so. Once the block
|
|
461
|
-
* settles there is no incomplete tail to avoid, so the whole text's first line
|
|
462
|
-
* is used. Inline markers are stripped because the row is prose, not rendered
|
|
463
|
-
* markdown: leaving them reads as stray punctuation in a plain line.
|
|
464
|
-
*/
|
|
465
|
-
export function reasoningPreview(text, streaming) {
|
|
466
|
-
const clean = text.replaceAll('**', '').replaceAll('`', '').trim();
|
|
467
|
-
if (clean === '')
|
|
468
|
-
return '';
|
|
469
|
-
if (!streaming)
|
|
470
|
-
return firstTellingSentenceOf(firstLineOf(clean));
|
|
471
|
-
const paragraphs = clean.split(/\n[ \t]*\n/u).map(paragraph => paragraph.trim()).filter(paragraph => paragraph !== '');
|
|
472
|
-
if (paragraphs.length < 2)
|
|
473
|
-
return '';
|
|
474
|
-
return firstTellingSentenceOf(firstLineOf(paragraphs[paragraphs.length - 2] ?? ''));
|
|
475
|
-
}
|
|
476
383
|
/** Thinking uses the same quiet Markdown style throughout the turn. */
|
|
477
|
-
function openReasoningLines(block, theme, width) {
|
|
478
|
-
return assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true });
|
|
384
|
+
function openReasoningLines(block, theme, width, mathMode = 'auto') {
|
|
385
|
+
return assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true, mathMode });
|
|
479
386
|
}
|
|
480
387
|
/** The block indexes a run shows as rows, in order: its members, then the thought it took from its answer. */
|
|
481
388
|
function groupMembers(group) {
|
|
@@ -498,15 +405,15 @@ function groupMemberLines(index, context, thoughtOnly = false) {
|
|
|
498
405
|
const block = blocks[index];
|
|
499
406
|
// Search must reveal a match even when it falls outside a tool's normal preview.
|
|
500
407
|
const full = block.kind === 'tool' && (context.matches.has(index) || options.expandedTools?.has(block.callId) === true);
|
|
501
|
-
const signature = [width, options.colors, context.trueColor, context.themeName, thoughtOnly, full, options.toolDetailsKey].join('\0');
|
|
408
|
+
const signature = [width, options.colors, context.trueColor, context.themeName, options.mathMode, options.mermaidMode, thoughtOnly, full, options.toolDetailsKey].join('\0');
|
|
502
409
|
const cached = groupMemberCache.get(block);
|
|
503
410
|
let lines;
|
|
504
411
|
if (cached?.signature === signature)
|
|
505
412
|
lines = cached.lines;
|
|
506
413
|
else {
|
|
507
414
|
lines = block.kind === 'tool' ? toolBlockLines(block, theme, width, full ? 'details' : 'preview', options.toolDetailsKey)
|
|
508
|
-
: block.kind === 'assistant' ? thoughtOnly ? openReasoningLines(block, theme, width)
|
|
509
|
-
: blockLines(block, theme, width,
|
|
415
|
+
: block.kind === 'assistant' ? thoughtOnly ? openReasoningLines(block, theme, width, options.mathMode)
|
|
416
|
+
: blockLines(block, theme, width, { mathMode: options.mathMode ?? 'auto', mermaidMode: options.mermaidMode ?? 'auto' })
|
|
510
417
|
: block.kind === 'notice' ? blockLines(block, theme, width) : [];
|
|
511
418
|
groupMemberCache.set(block, { signature, lines });
|
|
512
419
|
}
|
|
@@ -553,24 +460,28 @@ export function formatElapsed(ms) {
|
|
|
553
460
|
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
554
461
|
}
|
|
555
462
|
/** A completed turn's duration and any failures remain visible when folded. */
|
|
556
|
-
function processGroupHeader(group, theme, width, elapsed) {
|
|
463
|
+
function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O') {
|
|
557
464
|
const label = elapsed === undefined ? 'Worked' : `Worked for ${elapsed}`;
|
|
558
465
|
const failure = group.failures === 0 ? '' : theme.fg('error', ` · ${group.failures} failed`);
|
|
559
466
|
const labelRoom = Math.max(1, width - 4 - visibleWidth(failure));
|
|
560
|
-
|
|
467
|
+
const summary = `${SYMBOL.folded} ${theme.fg('dim', truncateToWidth(label, labelRoom))}${failure}`;
|
|
468
|
+
const hint = ` · ${expandKey} to expand`;
|
|
469
|
+
const suffix = expandKey !== 'Disabled' && visibleWidth(summary) + visibleWidth(hint) <= width - 2
|
|
470
|
+
? theme.fg('dim', hint) : '';
|
|
471
|
+
return [padToWidth(truncateToWidth(summary + suffix, Math.max(1, width - 2)), width)];
|
|
561
472
|
}
|
|
562
473
|
/**
|
|
563
474
|
* Render a block for the transcript or plain printer. Full output is available
|
|
564
|
-
* to direct callers; turn inspection uses previews.
|
|
565
|
-
*
|
|
475
|
+
* to direct callers; turn inspection uses previews. omitReasoning excludes
|
|
476
|
+
* thinking already drawn by a run.
|
|
566
477
|
*/
|
|
567
|
-
export function blockLines(block, theme, width,
|
|
478
|
+
export function blockLines(block, theme, width, { toolsExpanded = false, omitReasoning = false, toolDetailsKey = 'Alt+O', mathMode = 'auto', mermaidMode = 'auto' } = {}) {
|
|
568
479
|
if (block.kind === 'user')
|
|
569
480
|
return userBubble(block.text, theme, width);
|
|
570
481
|
if (block.kind === 'assistant') {
|
|
571
482
|
const lines = [];
|
|
572
483
|
if (block.reasoning !== '' && !omitReasoning) {
|
|
573
|
-
lines.push(...openReasoningLines(block, theme, width));
|
|
484
|
+
lines.push(...openReasoningLines(block, theme, width, mathMode));
|
|
574
485
|
if (block.text !== '')
|
|
575
486
|
lines.push('');
|
|
576
487
|
}
|
|
@@ -579,7 +490,7 @@ export function blockLines(block, theme, width, _spinnerFrame = 0, toolsExpanded
|
|
|
579
490
|
lines.push(...assistantContentLines([theme.fg('dim', '…')], width, paddingX));
|
|
580
491
|
}
|
|
581
492
|
else if (block.text !== '') {
|
|
582
|
-
lines.push(...assistantMarkdown(block.text, theme, width, hasExplicitTextColor(theme) ? { color: 'text' } :
|
|
493
|
+
lines.push(...assistantMarkdown(block.text, theme, width, { ...(hasExplicitTextColor(theme) ? { color: 'text' } : {}), mathMode, mermaidMode }));
|
|
583
494
|
}
|
|
584
495
|
if (block.interrupted === true) {
|
|
585
496
|
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
@@ -690,74 +601,71 @@ function fitFrame(lines, width, stablePrefix = 0) {
|
|
|
690
601
|
const transcriptBodyCache = new WeakMap();
|
|
691
602
|
/** Settled immutable blocks keep their expensive Markdown/tool layout. */
|
|
692
603
|
const blockLinesCache = new WeakMap();
|
|
693
|
-
function cachedBlockLines(block, options, theme, themeName, trueColor,
|
|
694
|
-
const animatedSpinnerFrame = block.kind === 'tool' && block.status === 'running' ? spinnerFrame : -1;
|
|
604
|
+
function cachedBlockLines(block, options, theme, themeName, trueColor, expanded, omitReasoning = false) {
|
|
695
605
|
const cached = blockLinesCache.get(block);
|
|
696
606
|
if (cached !== undefined
|
|
697
607
|
&& cached.width === options.width
|
|
698
608
|
&& cached.colors === options.colors
|
|
699
609
|
&& cached.trueColor === trueColor
|
|
700
610
|
&& cached.themeName === themeName
|
|
701
|
-
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
702
611
|
&& cached.expanded === expanded
|
|
703
|
-
&& cached.reasoningExpanded === reasoningExpanded
|
|
704
|
-
&& cached.toolSubject === toolSubject
|
|
705
612
|
&& cached.omitReasoning === omitReasoning
|
|
613
|
+
&& cached.mathMode === options.mathMode
|
|
614
|
+
&& cached.mermaidMode === options.mermaidMode
|
|
706
615
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
707
616
|
&& cached.toolDetails === (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true))
|
|
708
617
|
return cached.lines;
|
|
709
618
|
const toolDetails = block.kind === 'tool' && options.expandedTools?.has(block.callId) === true;
|
|
710
619
|
const lines = block.kind === 'tool' && toolDetails
|
|
711
620
|
? toolBlockLines(block, theme, options.width, 'details', options.toolDetailsKey)
|
|
712
|
-
: blockLines(block, theme, options.width,
|
|
621
|
+
: blockLines(block, theme, options.width, { toolsExpanded: expanded, omitReasoning, toolDetailsKey: options.toolDetailsKey ?? 'Alt+O', mathMode: options.mathMode ?? 'auto', mermaidMode: options.mermaidMode ?? 'auto' });
|
|
713
622
|
blockLinesCache.set(block, {
|
|
623
|
+
mathMode: options.mathMode,
|
|
624
|
+
mermaidMode: options.mermaidMode,
|
|
714
625
|
toolDetailsKey: options.toolDetailsKey,
|
|
715
626
|
toolDetails,
|
|
716
627
|
width: options.width,
|
|
717
628
|
colors: options.colors,
|
|
718
629
|
trueColor,
|
|
719
630
|
themeName,
|
|
720
|
-
spinnerFrame: animatedSpinnerFrame,
|
|
721
631
|
expanded,
|
|
722
|
-
reasoningExpanded,
|
|
723
|
-
toolSubject,
|
|
724
632
|
omitReasoning,
|
|
725
633
|
lines,
|
|
726
634
|
});
|
|
727
635
|
return lines;
|
|
728
636
|
}
|
|
729
|
-
/**
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
{ match: /^
|
|
739
|
-
{ match: /^(
|
|
740
|
-
{ match: /^(
|
|
741
|
-
{ match: /^(
|
|
742
|
-
{ match: /^(
|
|
637
|
+
/**
|
|
638
|
+
* What a stretch of the transcript was doing, as a short English label.
|
|
639
|
+
*
|
|
640
|
+
* Status strings are English until a language layer exists, so every label a
|
|
641
|
+
* reader can see is resolved from this one table rather than spelled into the
|
|
642
|
+
* rendering code. A tool with no family is reported by its own name, which is
|
|
643
|
+
* still more informative than dropping it.
|
|
644
|
+
*/
|
|
645
|
+
const PROCESS_LABELS = [
|
|
646
|
+
{ match: /^read$/, label: 'Read file' },
|
|
647
|
+
{ match: /^(read_image|readImage)$/, label: 'View image' },
|
|
648
|
+
{ match: /^(write|notebook_edit|apply_patch)$/, label: 'Write file' },
|
|
649
|
+
{ match: /^(edit|multi_edit|str_replace_editor)$/, label: 'Edit file' },
|
|
650
|
+
{ match: /^(grep|glob|list_dir|search)$/, label: 'Search code' },
|
|
651
|
+
{ match: /^(bash|shell|pwsh|exec|write_stdin)$/, label: 'Run command' },
|
|
652
|
+
{ match: /^(run_code)$/, label: 'Run code' },
|
|
653
|
+
{ match: /^(web_search)$/, label: 'Search web' },
|
|
654
|
+
{ match: /^(web_fetch)$/, label: 'Fetch page' },
|
|
743
655
|
{
|
|
744
656
|
match: /^(subagent|subagent_fork|subagent_isolated|agent|ralph|workflow_run)$/, label: 'Run subagent',
|
|
745
|
-
noun: plural('subagent', 'subagents'),
|
|
746
|
-
phrase: plural('Coordinated a subagent', 'Coordinated subagents'),
|
|
747
657
|
},
|
|
748
658
|
{
|
|
749
659
|
match: /^(session_search|session_trace|session_event_read|session_event_search|session_event_trace)$/,
|
|
750
|
-
label: 'Search sessions',
|
|
660
|
+
label: 'Search sessions',
|
|
751
661
|
},
|
|
752
662
|
{
|
|
753
663
|
match: /^(todo_write|update_goal|create_goal|get_goal)$/, label: 'Update plan',
|
|
754
|
-
noun: plural('plan update', 'plan updates'), phrase: flat('Updated the plan'),
|
|
755
664
|
},
|
|
756
665
|
{
|
|
757
666
|
match: /^(ask_user_question|ask_user|user_question)$/, label: 'Ask question',
|
|
758
|
-
noun: plural('question', 'questions'), phrase: plural('Asked a question', 'Asked questions'),
|
|
759
667
|
},
|
|
760
|
-
{ match: /^skill$/, label: 'Load skill'
|
|
668
|
+
{ match: /^skill$/, label: 'Load skill' },
|
|
761
669
|
];
|
|
762
670
|
/**
|
|
763
671
|
* The phrase for one tool, or the tool's own name when it belongs to no
|
|
@@ -765,32 +673,12 @@ const PROCESS_CATEGORIES = [
|
|
|
765
673
|
* category for it would not be.
|
|
766
674
|
*/
|
|
767
675
|
function processLabel(name) {
|
|
768
|
-
for (const category of
|
|
676
|
+
for (const category of PROCESS_LABELS) {
|
|
769
677
|
if (category.match.test(name))
|
|
770
678
|
return category.label;
|
|
771
679
|
}
|
|
772
680
|
return name;
|
|
773
681
|
}
|
|
774
|
-
function processPhrase(name) {
|
|
775
|
-
for (const category of PROCESS_CATEGORIES) {
|
|
776
|
-
if (category.match.test(name))
|
|
777
|
-
return category.phrase;
|
|
778
|
-
}
|
|
779
|
-
return () => name;
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* The bare noun a folded run counts this tool in: `16 commands`.
|
|
783
|
-
*
|
|
784
|
-
* A tool that belongs to no declared category is counted as a call rather than
|
|
785
|
-
* dropped, so the run's totals always add up to what the reader can see.
|
|
786
|
-
*/
|
|
787
|
-
function processNoun(name) {
|
|
788
|
-
for (const category of PROCESS_CATEGORIES) {
|
|
789
|
-
if (category.match.test(name))
|
|
790
|
-
return category.noun;
|
|
791
|
-
}
|
|
792
|
-
return plural('call', 'calls');
|
|
793
|
-
}
|
|
794
682
|
/** A group has to stand for repetition; one row is not repetition. */
|
|
795
683
|
const MIN_GROUP_BLOCKS = 2;
|
|
796
684
|
function blockIdentity(block, index) {
|
|
@@ -889,8 +777,6 @@ export function processGroups(blocks) {
|
|
|
889
777
|
const size = memberEnd - start + (absorbs === undefined ? 0 : 1);
|
|
890
778
|
const answerIndex = answer;
|
|
891
779
|
if (size >= MIN_GROUP_BLOCKS || (size > 0 && segmentTurn !== undefined)) {
|
|
892
|
-
const categories = [];
|
|
893
|
-
const stats = [];
|
|
894
780
|
let calls = 0;
|
|
895
781
|
let failures = 0;
|
|
896
782
|
let live = false;
|
|
@@ -910,18 +796,6 @@ export function processGroups(blocks) {
|
|
|
910
796
|
calls += 1;
|
|
911
797
|
if (block.status === 'error')
|
|
912
798
|
failures += 1;
|
|
913
|
-
const phrase = processPhrase(block.name);
|
|
914
|
-
const existing = categories.find(entry => entry.phrase(1) === phrase(1));
|
|
915
|
-
if (existing === undefined)
|
|
916
|
-
categories.push({ phrase, count: 1 });
|
|
917
|
-
else
|
|
918
|
-
existing.count += 1;
|
|
919
|
-
const noun = processNoun(block.name)(1);
|
|
920
|
-
const counted = stats.find(entry => entry.noun === noun);
|
|
921
|
-
if (counted === undefined)
|
|
922
|
-
stats.push({ noun, count: 1 });
|
|
923
|
-
else
|
|
924
|
-
counted.count += 1;
|
|
925
799
|
}
|
|
926
800
|
const turn = segmentTurn ?? (blocks[end]?.kind === 'workspace' ? blocks[end].turn : undefined);
|
|
927
801
|
groups.push({
|
|
@@ -932,8 +806,6 @@ export function processGroups(blocks) {
|
|
|
932
806
|
// painted without it. A separate flag rather than a second index: the
|
|
933
807
|
// block is always the answer.
|
|
934
808
|
tookThought: absorbs !== undefined,
|
|
935
|
-
categories,
|
|
936
|
-
stats,
|
|
937
809
|
calls,
|
|
938
810
|
failures,
|
|
939
811
|
live,
|
|
@@ -987,8 +859,6 @@ export function turnGroups(state) {
|
|
|
987
859
|
}
|
|
988
860
|
function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
989
861
|
const toolsExpanded = options.toolsExpanded === true;
|
|
990
|
-
const fold = foldPolicy(DEFAULT_FOLD_DENSITY);
|
|
991
|
-
const foldKey = foldPolicyKey(fold);
|
|
992
862
|
const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
|
|
993
863
|
const expandedReasoning = [...(options.expandedReasoning ?? [])].sort().join('\0');
|
|
994
864
|
const openedGroups = [...(options.openedGroups ?? [])].sort().join('\0');
|
|
@@ -1008,13 +878,15 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1008
878
|
&& cached.trueColor === trueColor
|
|
1009
879
|
&& cached.themeName === themeName
|
|
1010
880
|
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
881
|
+
&& cached.mathMode === options.mathMode
|
|
882
|
+
&& cached.mermaidMode === options.mermaidMode
|
|
1011
883
|
&& cached.toolsExpanded === toolsExpanded
|
|
1012
884
|
&& cached.expandedTools === expandedTools
|
|
1013
885
|
&& cached.expandedReasoning === expandedReasoning
|
|
1014
886
|
&& cached.openedGroups === openedGroups
|
|
1015
887
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
1016
|
-
&& cached.foldKey === foldKey
|
|
1017
888
|
&& cached.turnSpans === state.turnSpans
|
|
889
|
+
&& cached.turnDetailsKey === options.turnDetailsKey
|
|
1018
890
|
&& cached.searchKey === searchKey
|
|
1019
891
|
&& cached.status === state.status
|
|
1020
892
|
&& cached.focusBlock === options.focusBlock) {
|
|
@@ -1074,7 +946,6 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1074
946
|
openedThoughts.add(reasoningKey(state.blocks[group.answer]));
|
|
1075
947
|
}
|
|
1076
948
|
const asked = toolsExpanded
|
|
1077
|
-
|| !fold.groups
|
|
1078
949
|
|| options.openedGroups?.has(group.key) === true
|
|
1079
950
|
|| state.blocks.slice(group.start, group.until).some(block => block.kind === 'tool' && options.expandedTools?.has(block.callId))
|
|
1080
951
|
|| reasoningHit
|
|
@@ -1089,7 +960,6 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1089
960
|
const foldMarks = [];
|
|
1090
961
|
if (toolsExpanded)
|
|
1091
962
|
foldMarks.push({ key: 'all', row: 0 });
|
|
1092
|
-
foldMarks.push({ key: `density:${foldKey}`, row: 0 });
|
|
1093
963
|
let liveFrom;
|
|
1094
964
|
const context = {
|
|
1095
965
|
blocks: state.blocks, theme, themeName, trueColor, width: options.width,
|
|
@@ -1098,7 +968,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1098
968
|
const compactProcessRow = (block) => {
|
|
1099
969
|
if (block?.kind === 'notice')
|
|
1100
970
|
return block.process !== undefined && block.framed !== true;
|
|
1101
|
-
return block?.kind === 'tool' &&
|
|
971
|
+
return block?.kind === 'tool' && !toolsExpanded && !options.expandedTools?.has(block.callId);
|
|
1102
972
|
};
|
|
1103
973
|
// Only a turn's last run is summarized by how long the turn took; an earlier
|
|
1104
974
|
// run in the same turn did not take all of it.
|
|
@@ -1129,7 +999,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1129
999
|
liveFrom ??= headerRow;
|
|
1130
1000
|
// A live run has no header: it becomes a group when its turn ends.
|
|
1131
1001
|
if (!group.live && !opened) {
|
|
1132
|
-
lines.push(...processGroupHeader(group, theme, options.width, elapsedOf(groupIndex)));
|
|
1002
|
+
lines.push(...processGroupHeader(group, theme, options.width, elapsedOf(groupIndex), options.turnDetailsKey));
|
|
1133
1003
|
}
|
|
1134
1004
|
const bodyRow = lines.length;
|
|
1135
1005
|
let offsets = new Map();
|
|
@@ -1175,7 +1045,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1175
1045
|
blockDrawStarts[group.answer] = lines.length;
|
|
1176
1046
|
offsets.set(group.answer, lines.length - bodyRow);
|
|
1177
1047
|
const answer = state.blocks[group.answer];
|
|
1178
|
-
const rendered = cachedBlockLines(answer, options, theme, themeName, trueColor,
|
|
1048
|
+
const rendered = cachedBlockLines(answer, options, theme, themeName, trueColor, toolsExpanded, absorbed.has(group.answer));
|
|
1179
1049
|
for (const line of (search !== undefined && matches.has(group.answer)
|
|
1180
1050
|
? rendered.map(row => blockMatchesQuery(stripAnsi(row), search.query) ? theme.inverse(row) : row)
|
|
1181
1051
|
: rendered))
|
|
@@ -1264,12 +1134,8 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1264
1134
|
if (liveBlock)
|
|
1265
1135
|
liveFrom ??= lines.length;
|
|
1266
1136
|
const expanded = (block.kind === 'workspace' && groups.some((group, at) => group.turn === block.turn && openGroups.has(at))) || matches.has(index) || toolsExpanded
|
|
1267
|
-
|| !fold.tools
|
|
1268
1137
|
|| (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
|
|
1269
|
-
const
|
|
1270
|
-
|| !fold.reasoning
|
|
1271
|
-
|| (block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true);
|
|
1272
|
-
const rendered = cachedBlockLines(block, options, theme, themeName, trueColor, spinnerFrame, expanded, reasoningExpanded, fold.subject, absorbed.has(index));
|
|
1138
|
+
const rendered = cachedBlockLines(block, options, theme, themeName, trueColor, expanded, absorbed.has(index));
|
|
1273
1139
|
lines.push(...(search !== undefined && matches.has(index)
|
|
1274
1140
|
? rendered.map(line => blockMatchesQuery(stripAnsi(line), search.query) ? theme.inverse(line) : line)
|
|
1275
1141
|
: rendered));
|
|
@@ -1280,7 +1146,10 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1280
1146
|
// this; opening, folding, or a run settling at turn end do.
|
|
1281
1147
|
const foldShape = foldMarks.map(mark => mark.key).join('\u0001');
|
|
1282
1148
|
transcriptBodyCache.set(state.blocks, {
|
|
1149
|
+
mathMode: options.mathMode,
|
|
1150
|
+
mermaidMode: options.mermaidMode,
|
|
1283
1151
|
toolDetailsKey: options.toolDetailsKey,
|
|
1152
|
+
turnDetailsKey: options.turnDetailsKey,
|
|
1284
1153
|
blockDrawStarts,
|
|
1285
1154
|
width: options.width,
|
|
1286
1155
|
colors: options.colors,
|
|
@@ -1291,7 +1160,6 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1291
1160
|
expandedTools,
|
|
1292
1161
|
expandedReasoning,
|
|
1293
1162
|
openedGroups,
|
|
1294
|
-
foldKey,
|
|
1295
1163
|
searchKey,
|
|
1296
1164
|
turnSpans: state.turnSpans,
|
|
1297
1165
|
status: state.status,
|
|
@@ -1638,6 +1506,17 @@ export function renderTurnError(text, theme, width) {
|
|
|
1638
1506
|
+ (suffix === '' ? '' : theme.fg('dim', suffix));
|
|
1639
1507
|
return [padToWidth(line, width)];
|
|
1640
1508
|
}
|
|
1509
|
+
/** Human guidance remains visible until the Harness claims it for a model step. */
|
|
1510
|
+
export function renderSteering(inbox, theme, width) {
|
|
1511
|
+
const messages = inbox.filter(message => message.source.kind === 'user');
|
|
1512
|
+
if (messages.length === 0 || width <= 0)
|
|
1513
|
+
return [];
|
|
1514
|
+
const latest = queuedMessageLabel(messages.at(-1));
|
|
1515
|
+
const count = messages.length > 1 ? ` · ${messages.length}` : '';
|
|
1516
|
+
return [truncateToWidth(' ' + theme.fg('border', '│') + ' '
|
|
1517
|
+
+ theme.bold(theme.fg('accent', 'Guidance · next step' + count))
|
|
1518
|
+
+ theme.fg('dim', ' · ') + theme.fg('text', latest), width)];
|
|
1519
|
+
}
|
|
1641
1520
|
/** Compact, unframed pending-message view placed immediately above the composer. */
|
|
1642
1521
|
export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
1643
1522
|
const labels = [
|
|
@@ -1662,9 +1541,9 @@ export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
|
1662
1541
|
return paintedLeft + ' ' + action;
|
|
1663
1542
|
};
|
|
1664
1543
|
if (labels.length === 1) {
|
|
1665
|
-
return [alignAction(rail + queueLabel + theme.fg('dim', ' · ') + theme.fg('text', labels[0] ?? ''), theme.fg('dim', '↑ edit'))];
|
|
1544
|
+
return [alignAction(rail + queueLabel + theme.fg('dim', ' · ') + theme.fg('dim', 'next turn · ') + theme.fg('text', labels[0] ?? ''), theme.fg('dim', '↑ edit'))];
|
|
1666
1545
|
}
|
|
1667
|
-
const summary = ` · ${labels.length}${hidden === 0 ? '' : ` · ${hidden} earlier`}`;
|
|
1546
|
+
const summary = ` · ${labels.length} · next turn${hidden === 0 ? '' : ` · ${hidden} earlier`}`;
|
|
1668
1547
|
const visibleLabels = labels.slice(start);
|
|
1669
1548
|
const lines = [
|
|
1670
1549
|
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim', '↑ edit latest'), false),
|
|
@@ -1719,19 +1598,21 @@ export function renderView(state, options) {
|
|
|
1719
1598
|
documentRows: null,
|
|
1720
1599
|
};
|
|
1721
1600
|
}
|
|
1722
|
-
if (options.promptSelector?.request.presentation === 'fullscreen-list') {
|
|
1723
|
-
const selector = renderPromptSelectorPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
|
|
1601
|
+
if (options.promptSelector?.request.presentation === 'fullscreen-list' || options.promptSelector?.request.presentation === 'fullscreen-tree') {
|
|
1602
|
+
const selector = (options.promptSelector.request.presentation === 'fullscreen-tree' ? renderPromptTreePage : renderPromptSelectorPage)(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
|
|
1724
1603
|
return {
|
|
1725
1604
|
lines: fitFrame(selector.lines, width),
|
|
1605
|
+
...(selector.document === undefined ? {} : { promptDocument: selector.document }),
|
|
1726
1606
|
cursor: selector.cursor,
|
|
1727
1607
|
cursorVisible: true,
|
|
1728
1608
|
// A full-screen surface drew no transcript body.
|
|
1729
1609
|
documentRows: null,
|
|
1730
1610
|
};
|
|
1731
1611
|
}
|
|
1732
|
-
if (options.promptSelector?.request.presentation === 'plan-review') {
|
|
1733
|
-
const review = renderPlanReviewPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
|
|
1612
|
+
if (options.promptSelector?.request.presentation === 'plan-review' || options.promptSelector?.request.presentation === 'document') {
|
|
1613
|
+
const review = options.promptSelector.request.documentImage === undefined ? renderPlanReviewPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName) : renderImagePreviewPage(options.promptSelector, theme, width, height, appName, options.imageProtocol, options.imageCellSize);
|
|
1734
1614
|
return {
|
|
1615
|
+
...(review.image === undefined ? {} : { image: review.image }),
|
|
1735
1616
|
lines: fitFrame(review.lines, width),
|
|
1736
1617
|
cursor: review.cursor,
|
|
1737
1618
|
cursorVisible: review.cursorVisible === true,
|
|
@@ -1772,6 +1653,8 @@ export function renderView(state, options) {
|
|
|
1772
1653
|
}, options.transcriptSearch.matches.length)
|
|
1773
1654
|
: options.inspected === undefined
|
|
1774
1655
|
? slashInlineHint(options.input, options.inputCursor, options.commands)
|
|
1656
|
+
?? (state.status === 'running' && options.steerKey !== undefined
|
|
1657
|
+
? `Enter: next turn · ${options.steerKey}: next step` : null)
|
|
1775
1658
|
: options.inspected.writable
|
|
1776
1659
|
? slashInlineHint(options.input, options.inputCursor, options.commands) ?? 'Enter to steer · Esc to return'
|
|
1777
1660
|
: 'Read-only · Esc to return';
|
|
@@ -1821,7 +1704,10 @@ export function renderView(state, options) {
|
|
|
1821
1704
|
: undefined;
|
|
1822
1705
|
const queuedSubmissions = editor === undefined || options.inspected !== undefined
|
|
1823
1706
|
? []
|
|
1824
|
-
:
|
|
1707
|
+
: [
|
|
1708
|
+
...renderSteering(state.nextStepInbox, theme, width),
|
|
1709
|
+
...renderQueuedSubmissions(options.queuedSubmissions ?? [], theme, width, state.nextTurnInbox),
|
|
1710
|
+
];
|
|
1825
1711
|
// Above the composer and below the queue: the failure explains why the queue
|
|
1826
1712
|
// is not moving, and the row survives until the next submission.
|
|
1827
1713
|
const turnError = editor === undefined || options.inspected !== undefined || state.turnError === undefined
|
|
@@ -1831,6 +1717,10 @@ export function renderView(state, options) {
|
|
|
1831
1717
|
const goal = editor === undefined || options.inspected !== undefined || options.sessionControls?.goal === undefined
|
|
1832
1718
|
? []
|
|
1833
1719
|
: renderGoalBar(options.sessionControls.goal, theme, width);
|
|
1720
|
+
const pendingCount = options.sessionControls?.pendingQuestions ?? 0;
|
|
1721
|
+
const questions = editor === undefined || options.inspected !== undefined || pendingCount === 0
|
|
1722
|
+
? []
|
|
1723
|
+
: [truncateToWidth(theme.fg('warning', ` ${pendingCount} pending ${pendingCount === 1 ? 'question' : 'questions'} · /questions to answer`), width)];
|
|
1834
1724
|
const inspect = editor === undefined ? [] : renderInspectBanner(options.inspected, theme, width, spinnerFrame);
|
|
1835
1725
|
const subagents = editor === undefined
|
|
1836
1726
|
? []
|
|
@@ -1842,7 +1732,7 @@ export function renderView(state, options) {
|
|
|
1842
1732
|
const inputLines = promptSelector?.lines ?? settings?.lines ?? copySelector?.lines ?? search?.lines
|
|
1843
1733
|
?? (editor === undefined ? [] : editor.lines);
|
|
1844
1734
|
const spacer = 1;
|
|
1845
|
-
const reserved = inputLines.length + working.length + inspect.length + subagents.length + todos.length + goal.length + queuedSubmissions.length + turnError.length + spacer + autocomplete.length + statusFooter.length;
|
|
1735
|
+
const reserved = inputLines.length + working.length + inspect.length + subagents.length + todos.length + goal.length + questions.length + queuedSubmissions.length + turnError.length + spacer + autocomplete.length + statusFooter.length;
|
|
1846
1736
|
const budget = Math.max(0, height - reserved);
|
|
1847
1737
|
const focusIndex = options.focusBlock === undefined
|
|
1848
1738
|
? undefined
|
|
@@ -1876,11 +1766,12 @@ export function renderView(state, options) {
|
|
|
1876
1766
|
if (visible.length > 0)
|
|
1877
1767
|
lines.push(showJump
|
|
1878
1768
|
? ' '.repeat(jumpColumn) + theme.inverse(jumpText) : '');
|
|
1879
|
-
const bottomRows = working.length + goal.length + inspect.length + subagents.length + todos.length + queuedSubmissions.length + turnError.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
1769
|
+
const bottomRows = working.length + goal.length + questions.length + inspect.length + subagents.length + todos.length + queuedSubmissions.length + turnError.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
1880
1770
|
const fill = Math.max(0, height - lines.length - bottomRows);
|
|
1881
1771
|
lines.push(...Array.from({ length: fill }, () => ''));
|
|
1882
1772
|
lines.push(...working);
|
|
1883
1773
|
lines.push(...goal);
|
|
1774
|
+
lines.push(...questions);
|
|
1884
1775
|
lines.push(...inspect);
|
|
1885
1776
|
lines.push(...subagents);
|
|
1886
1777
|
lines.push(...todos);
|
|
@@ -1941,7 +1832,7 @@ export function renderView(state, options) {
|
|
|
1941
1832
|
stickyHeaders.push({
|
|
1942
1833
|
start: transcriptStart + (transcript.blockStarts[at + 1] ?? transcript.lines.length),
|
|
1943
1834
|
end: body.length,
|
|
1944
|
-
text: theme
|
|
1835
|
+
text: paintSurface(theme, 'userMessageBg', 'userMessageText', padToWidth(' '.repeat(padding) + label, width)),
|
|
1945
1836
|
});
|
|
1946
1837
|
}
|
|
1947
1838
|
}
|