@agi-fans/dsh-tui 0.21.0 → 0.22.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 +158 -1
- package/lib/chrome/box.d.ts +47 -16
- package/lib/chrome/box.js +172 -130
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.d.ts +15 -0
- package/lib/chrome/code-highlight.js +60 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +29 -2
- package/lib/chrome/command-output.js +100 -4
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/main-screen-renderer.d.ts +16 -0
- package/lib/chrome/main-screen-renderer.js +53 -5
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.js +30 -10
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/relative-time.d.ts +17 -0
- package/lib/chrome/relative-time.js +36 -0
- package/lib/chrome/relative-time.js.map +1 -1
- package/lib/chrome/secret-mask.d.ts +35 -0
- package/lib/chrome/secret-mask.js +180 -0
- package/lib/chrome/secret-mask.js.map +1 -0
- package/lib/chrome/status-config.d.ts +161 -51
- package/lib/chrome/status-config.js +237 -126
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -2
- package/lib/chrome/status-line.js +105 -63
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/terminal-colors.d.ts +85 -0
- package/lib/chrome/terminal-colors.js +230 -0
- package/lib/chrome/terminal-colors.js.map +1 -0
- package/lib/chrome/terminal-images.d.ts +9 -1
- package/lib/chrome/terminal-images.js +27 -4
- package/lib/chrome/terminal-images.js.map +1 -1
- package/lib/chrome/theme.d.ts +32 -9
- package/lib/chrome/theme.js +595 -498
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-renderers.js +69 -13
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/chrome/tools-list.d.ts +1 -4
- package/lib/chrome/tools-list.js +1 -41
- package/lib/chrome/tools-list.js.map +1 -1
- package/lib/chrome/welcome-tips.d.ts +20 -8
- package/lib/chrome/welcome-tips.js +22 -42
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/commands/auth-ids.d.ts +25 -0
- package/lib/commands/auth-ids.js +27 -0
- package/lib/commands/auth-ids.js.map +1 -0
- package/lib/commands/auth-input.d.ts +34 -0
- package/lib/commands/auth-input.js +96 -0
- package/lib/commands/auth-input.js.map +1 -0
- package/lib/commands/auth-providers.d.ts +39 -0
- package/lib/commands/auth-providers.js +125 -0
- package/lib/commands/auth-providers.js.map +1 -0
- package/lib/commands/auth.d.ts +12 -20
- package/lib/commands/auth.js +42 -229
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/btw.d.ts +13 -1
- package/lib/commands/btw.js +37 -104
- package/lib/commands/btw.js.map +1 -1
- package/lib/commands/diff.js +1 -96
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +1 -1
- package/lib/commands/export.js +3 -27
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.d.ts +17 -1
- package/lib/commands/files.js +67 -107
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/jobs.d.ts +19 -4
- package/lib/commands/jobs.js +50 -7
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.js +108 -76
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/permission.js +15 -16
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +133 -16
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule.d.ts +10 -0
- package/lib/commands/schedule.js +45 -14
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.js +2 -10
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +14 -38
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.d.ts +14 -8
- package/lib/commands/terminal.js +19 -178
- package/lib/commands/terminal.js.map +1 -1
- package/lib/commands/trajectory.js +4 -1
- package/lib/commands/trajectory.js.map +1 -1
- package/lib/definition.d.ts +122 -22
- package/lib/definition.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +51 -0
- package/lib/input/actions.js +120 -0
- package/lib/input/actions.js.map +1 -0
- package/lib/input/editor.d.ts +0 -10
- package/lib/input/editor.js +11 -9
- package/lib/input/editor.js.map +1 -1
- package/lib/input/image-paste.d.ts +19 -1
- package/lib/input/image-paste.js +82 -1
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/keybindings-config.d.ts +12 -5
- package/lib/input/keybindings-config.js +30 -57
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +16 -0
- package/lib/input/keys.js +38 -3
- package/lib/input/keys.js.map +1 -1
- package/lib/input/wheel-scroll.d.ts +46 -0
- package/lib/input/wheel-scroll.js +69 -0
- package/lib/input/wheel-scroll.js.map +1 -0
- package/lib/prompt-intents.d.ts +182 -0
- package/lib/prompt-intents.js +127 -0
- package/lib/prompt-intents.js.map +1 -0
- package/lib/runtime/cancellable.d.ts +75 -0
- package/lib/runtime/cancellable.js +76 -0
- package/lib/runtime/cancellable.js.map +1 -0
- package/lib/runtime/composer-images.js +5 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.d.ts +17 -2
- package/lib/runtime/composer-shell.js +109 -43
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-recovery-review.d.ts +0 -1
- package/lib/runtime/file-recovery-review.js +29 -30
- package/lib/runtime/file-recovery-review.js.map +1 -1
- package/lib/runtime/file-recovery-store.js +5 -1
- package/lib/runtime/file-recovery-store.js.map +1 -1
- package/lib/runtime/file-review.js +1 -1
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/hook-settings.d.ts +2 -2
- package/lib/runtime/plain-tui.d.ts +2 -0
- package/lib/runtime/plain-tui.js +7 -1
- package/lib/runtime/plain-tui.js.map +1 -1
- package/lib/runtime/plugin-settings.d.ts +1 -1
- package/lib/runtime/plugin-settings.js +10 -8
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +57 -15
- package/lib/runtime/provider-local.js +1112 -471
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +5 -11
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.js +2 -0
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/terminal-modes.d.ts +47 -0
- package/lib/runtime/terminal-modes.js +86 -0
- package/lib/runtime/terminal-modes.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -5
- package/lib/runtime/terminal-notifications.js +4 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +36 -0
- package/lib/runtime/terminal-probe.js +37 -0
- package/lib/runtime/terminal-probe.js.map +1 -0
- package/lib/runtime/terminal-session.d.ts +57 -0
- package/lib/runtime/terminal-session.js +164 -0
- package/lib/runtime/terminal-session.js.map +1 -0
- package/lib/session/interaction-adapter.js +7 -8
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.js +1 -1
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/package-metadata.d.ts +8 -0
- package/lib/session/package-metadata.js +8 -0
- package/lib/session/package-metadata.js.map +1 -1
- package/lib/session/session-controller.d.ts +10 -11
- package/lib/session/session-controller.js +95 -13
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-tree.d.ts +2 -0
- package/lib/session/session-tree.js +2 -0
- package/lib/session/session-tree.js.map +1 -1
- package/lib/session/tui-settings.d.ts +40 -11
- package/lib/session/tui-settings.js +73 -34
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/autocomplete.d.ts +18 -2
- package/lib/views/autocomplete.js +51 -38
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/copy-targets.d.ts +12 -13
- package/lib/views/copy-targets.js +16 -46
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.js +18 -5
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +42 -6
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/history-search.js +15 -21
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkeys.d.ts +7 -1
- package/lib/views/hotkeys.js +49 -25
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-navigation.d.ts +62 -0
- package/lib/views/list-navigation.js +73 -0
- package/lib/views/list-navigation.js.map +1 -0
- package/lib/views/list-view.d.ts +99 -0
- package/lib/views/list-view.js +165 -0
- package/lib/views/list-view.js.map +1 -0
- package/lib/views/prompt-selector.d.ts +31 -3
- package/lib/views/prompt-selector.js +249 -125
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/session-files.d.ts +1 -2
- package/lib/views/session-files.js +1 -11
- package/lib/views/session-files.js.map +1 -1
- package/lib/views/settings-list.d.ts +10 -8
- package/lib/views/settings-list.js +116 -283
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/trajectory-timeline.d.ts +72 -0
- package/lib/views/trajectory-timeline.js +133 -0
- package/lib/views/trajectory-timeline.js.map +1 -0
- package/lib/views/trajectory.d.ts +15 -4
- package/lib/views/trajectory.js +557 -123
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +95 -25
- package/lib/views/transcript-render.js +501 -167
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-search.d.ts +39 -6
- package/lib/views/transcript-search.js +46 -11
- package/lib/views/transcript-search.js.map +1 -1
- package/lib/views/transcript-types.d.ts +63 -5
- package/lib/views/transcript-types.js +23 -11
- package/lib/views/transcript-types.js.map +1 -1
- package/package.json +67 -86
- package/lib/commands/advisor.d.ts +0 -15
- package/lib/commands/advisor.js +0 -200
- package/lib/commands/advisor.js.map +0 -1
- package/lib/commands/loop.d.ts +0 -52
- package/lib/commands/loop.js +0 -307
- package/lib/commands/loop.js.map +0 -1
- package/lib/commands/rate.d.ts +0 -33
- package/lib/commands/rate.js +0 -212
- package/lib/commands/rate.js.map +0 -1
- package/lib/commands/steer.d.ts +0 -5
- package/lib/commands/steer.js +0 -24
- package/lib/commands/steer.js.map +0 -1
- package/lib/commands/worktree.d.ts +0 -5
- package/lib/commands/worktree.js +0 -103
- package/lib/commands/worktree.js.map +0 -1
- package/lib/runtime/terminal-reader.d.ts +0 -11
- package/lib/runtime/terminal-reader.js +0 -87
- package/lib/runtime/terminal-reader.js.map +0 -1
- package/lib/views/agent-hub.d.ts +0 -32
- package/lib/views/agent-hub.js +0 -209
- package/lib/views/agent-hub.js.map +0 -1
- package/lib/views/copy-selector.d.ts +0 -54
- package/lib/views/copy-selector.js +0 -170
- package/lib/views/copy-selector.js.map +0 -1
|
@@ -6,31 +6,42 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { leadingSlashCommandNameRange, renderAutocomplete, slashInlineHint } from "./autocomplete.js";
|
|
8
8
|
import { HISTORY_SEARCH_MAX_VISIBLE, renderHistorySearch } from "./history-search.js";
|
|
9
|
-
import { renderEditor, renderFramedBlock, renderWelcome
|
|
9
|
+
import { DEEP_DRIVING_LABEL, renderEditor, renderFramedBlock, renderWelcome } from "../chrome/box.js";
|
|
10
10
|
import { renderMarkdown } from "../chrome/markdown.js";
|
|
11
|
-
import { renderCopySelector } from "./copy-selector.js";
|
|
12
11
|
import { renderSettings } from "./settings-list.js";
|
|
13
12
|
import { renderTrajectory } from "./trajectory.js";
|
|
14
|
-
import {
|
|
15
|
-
import { renderImagePreviewPage, renderPlanReviewPage, renderPromptSelector, renderPromptSelectorPage, renderPromptTreePage, } from "./prompt-selector.js";
|
|
13
|
+
import { renderImagePreviewPage, renderPlanReviewPage, renderPromptSelector, renderPromptTreePage, } from "./prompt-selector.js";
|
|
16
14
|
import { resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
17
15
|
import { renderPermissionBadge, renderStatusFooter } from "../chrome/status-line.js";
|
|
18
16
|
import { createTheme, paintSurface, SPINNER, SYMBOL, BOX } from "../chrome/theme.js";
|
|
17
|
+
import { highlightToolText, toolPayloadLanguage } from "../chrome/code-highlight.js";
|
|
19
18
|
import { renderGoalBar } from "../chrome/goal-bar.js";
|
|
20
|
-
import { padToWidth, stripAnsi, truncateToWidth, visibleWidth,
|
|
19
|
+
import { padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
|
|
21
20
|
import { alignFileDiffs, wrapPaintedDiffRows, } from "../chrome/diff-render.js";
|
|
22
21
|
import { workspaceBlockLines, WORKSPACE_COLLAPSED_LINES } from "./workspace-changes.js";
|
|
23
22
|
import { blockFileAction } from "./file-actions.js";
|
|
24
23
|
import { renderTool, toolArgSubject } from "../chrome/tool-renderers.js";
|
|
25
24
|
import { toolArgsObject } from "../chrome/tool-args.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { renderCommandOutput, renderCommandSeparator, renderShell } from "../chrome/command-output.js";
|
|
26
|
+
import { terminalColorsKey } from "../chrome/terminal-colors.js";
|
|
28
27
|
import { renderPathMentionRows } from "../chrome/path-mentions.js";
|
|
29
|
-
import {
|
|
28
|
+
import { formatElapsed } from "../chrome/relative-time.js";
|
|
29
|
+
import { maskSecrets } from "../chrome/secret-mask.js";
|
|
30
|
+
import { formatOverlayHint } from "./hotkey-format.js";
|
|
31
|
+
import { blockMatchesQuery, matchedRows, searchMatches, transcriptSearchHint, } from "./transcript-search.js";
|
|
32
|
+
export { formatElapsed };
|
|
30
33
|
import { contentToText, prettyArgs, reasoningKey, } from "./transcript-types.js";
|
|
31
34
|
function isBlockPending(block) {
|
|
32
35
|
return (block.kind === 'assistant' && block.streaming) || (block.kind === 'tool' && block.status === 'running');
|
|
33
36
|
}
|
|
37
|
+
/** The depth a frame encodes with. */
|
|
38
|
+
function colorDepthOf(options) {
|
|
39
|
+
return options.colorDepth ?? 'ansi16';
|
|
40
|
+
}
|
|
41
|
+
/** Cache key for everything the color depth and the reported terminal colors can change. */
|
|
42
|
+
function terminalKey(options) {
|
|
43
|
+
return `${colorDepthOf(options)}|${terminalColorsKey(options.terminal)}`;
|
|
44
|
+
}
|
|
34
45
|
function exclusiveDiffs(presentation) {
|
|
35
46
|
if (presentation?.result?.card === 'diff')
|
|
36
47
|
return presentation.result.diffs;
|
|
@@ -108,7 +119,9 @@ function assistantMarkdown(source, theme, width, style) {
|
|
|
108
119
|
*/
|
|
109
120
|
function userBubble(text, theme, width) {
|
|
110
121
|
const gutter = ' ';
|
|
111
|
-
|
|
122
|
+
// Drawn masked, stored plain: the key still reaches the model and the copy
|
|
123
|
+
// action still returns what the reader sent, but a shared screen shows stars.
|
|
124
|
+
const wrapped = renderPathMentionRows(maskSecrets(text), Math.max(1, width - gutter.length), theme);
|
|
112
125
|
const rows = ['', ...wrapped, ''];
|
|
113
126
|
// The padding rows share the prompt's tint, so the prompt is one slab that
|
|
114
127
|
// opens the turn. Unpainted, they were two blank rows of their own on top of
|
|
@@ -277,7 +290,7 @@ function toolRowSubject(block, presentation, label) {
|
|
|
277
290
|
return firstLineOf(verb !== '' && title.startsWith(verb + ' ') ? title.slice(verb.length + 1) : title);
|
|
278
291
|
}
|
|
279
292
|
/** Tool content shared by a running turn and a completed turn opened for reading. */
|
|
280
|
-
function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt+O') {
|
|
293
|
+
function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt+O', surface = 'toolSuccessBg') {
|
|
281
294
|
const full = mode !== 'preview';
|
|
282
295
|
const presentation = renderTool({
|
|
283
296
|
name: block.name, arguments: prettyArgs(block.args), output: block.output,
|
|
@@ -303,8 +316,15 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
303
316
|
: call?.card === 'generic' || call?.card === 'diff' ? call.title
|
|
304
317
|
: !read && presentation.title !== undefined && presentation.title !== block.name ? presentation.title
|
|
305
318
|
: `${block.name}${subject === '' ? '' : ` ${subject}`}`;
|
|
306
|
-
|
|
307
|
-
|
|
319
|
+
// The command line is one ink, not a highlighted program.
|
|
320
|
+
//
|
|
321
|
+
// A command is the reader's own intent, and it has to be recognisable at a
|
|
322
|
+
// glance; syntax colouring it competes with the output below, which is the
|
|
323
|
+
// part that actually has to be read. Pi splits the two the same way: a flat
|
|
324
|
+
// bold `toolTitle` line for the call, and language highlighting for whatever
|
|
325
|
+
// the call returned.
|
|
326
|
+
const titleInk = block.status === 'error' ? 'error' : 'toolTitle';
|
|
327
|
+
const rows = wrapTextStable(theme.bold(theme.fg(titleInk, title)), room);
|
|
308
328
|
let body = [];
|
|
309
329
|
if (mode === 'details') {
|
|
310
330
|
let input = block.args;
|
|
@@ -313,20 +333,26 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
313
333
|
}
|
|
314
334
|
catch { /* Partial arguments retain their received text. */ }
|
|
315
335
|
const output = presentation.output.length > 0 ? presentation.output : block.output.split('\n');
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
336
|
+
const detailLanguage = shell ? 'bash' : toolPayloadLanguage(block.name, subject);
|
|
337
|
+
const outputRows = diffs !== undefined
|
|
338
|
+
? wrapPaintedDiffRows(alignFileDiffs(diffs), theme, room)
|
|
339
|
+
: (detailLanguage === undefined
|
|
340
|
+
? output.map(line => theme.fg(block.status === 'error' ? 'error' : 'toolOutput', line))
|
|
341
|
+
: highlightedLines(output, detailLanguage, theme, block.status === 'error' ? 'error' : undefined))
|
|
342
|
+
.flatMap(line => wrapTextStable(line, room));
|
|
319
343
|
body = [
|
|
320
|
-
...
|
|
321
|
-
|
|
322
|
-
|
|
344
|
+
...wrapTextStable(theme.fg('dim', 'Input'), room),
|
|
345
|
+
// The arguments are JSON here (they were re-serialized above), so they are
|
|
346
|
+
// highlighted as JSON whatever the tool itself reads.
|
|
347
|
+
...highlightedLines(input.split('\n'), 'json', theme, undefined, room),
|
|
348
|
+
'', ...wrapTextStable(theme.fg('dim', 'Output'), room),
|
|
323
349
|
...outputRows,
|
|
324
350
|
];
|
|
325
351
|
// Keep the tool's rich presentation, but expose received text it replaced.
|
|
326
352
|
if (block.output !== '' && (diffs !== undefined || block.output !== output.join('\n'))) {
|
|
327
|
-
body.push('', ...
|
|
353
|
+
body.push('', ...wrapTextStable(theme.fg('dim', 'Raw result'), room));
|
|
328
354
|
for (const line of block.output.split('\n')) {
|
|
329
|
-
body.push(...
|
|
355
|
+
body.push(...wrapTextStable(theme.fg(block.status === 'error' ? 'error' : 'toolOutput', line), room));
|
|
330
356
|
}
|
|
331
357
|
}
|
|
332
358
|
}
|
|
@@ -334,13 +360,21 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
334
360
|
body = wrapPaintedDiffRows(alignFileDiffs(diffs), theme, room);
|
|
335
361
|
}
|
|
336
362
|
else if (full || !read || block.status === 'error') {
|
|
337
|
-
|
|
363
|
+
// One highlight pass over the whole payload. Painting line by line handed
|
|
364
|
+
// the tokenizer a fragment with no opening construct, so a second and third
|
|
365
|
+
// command row came back bare — the "only the first line is highlighted"
|
|
366
|
+
// report. A file payload uses the subject's own language.
|
|
367
|
+
const language = shell ? 'bash' : toolPayloadLanguage(block.name, subject);
|
|
368
|
+
const output = language === undefined
|
|
369
|
+
? presentation.output.map(line => theme.fg(block.status === 'error' ? 'error' : 'toolOutput', line))
|
|
370
|
+
: highlightedLines(presentation.output, language, theme, block.status === 'error' ? 'error' : undefined);
|
|
338
371
|
const input = !shell && !read && (call?.card === 'generic' || (call === undefined && presentation.title !== block.name))
|
|
339
|
-
? presentation.input
|
|
340
|
-
|
|
372
|
+
? highlightedLines(presentation.input, language ?? 'text', theme, undefined, room)
|
|
373
|
+
: [];
|
|
374
|
+
body = [...input, ...output.flatMap(line => wrapTextStable(line, room))];
|
|
341
375
|
// Writes carry their content in arguments before the result arrives.
|
|
342
376
|
if (body.length === 0 && block.name === 'write' && typeof args?.['content'] === 'string') {
|
|
343
|
-
body = args['content'].split('\n')
|
|
377
|
+
body = highlightedLines(args['content'].split('\n'), toolPayloadLanguage('write', subject) ?? 'text', theme, undefined, room);
|
|
344
378
|
}
|
|
345
379
|
}
|
|
346
380
|
while (body.length > 0 && stripAnsi(body[body.length - 1]).trim() === '')
|
|
@@ -353,12 +387,12 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
353
387
|
const shortcut = detailsKey === 'Disabled' ? '' : ` · ${detailsKey}: Expand`;
|
|
354
388
|
const hint = theme.fg('dim', `… ${omitted} ${shell ? 'earlier' : 'more'} lines${shortcut}`);
|
|
355
389
|
if (shell)
|
|
356
|
-
body.unshift(...
|
|
390
|
+
body.unshift(...wrapTextStable(hint, room));
|
|
357
391
|
else
|
|
358
|
-
body.push(...
|
|
392
|
+
body.push(...wrapTextStable(hint, room));
|
|
359
393
|
}
|
|
360
394
|
if (!full && read && block.status !== 'error' && block.output !== '' && detailsKey !== 'Disabled') {
|
|
361
|
-
body.push(...
|
|
395
|
+
body.push(...wrapTextStable(theme.fg('dim', `${detailsKey}: Expand result`), room));
|
|
362
396
|
}
|
|
363
397
|
if (full && block.status === 'error' && call?.card === 'diff')
|
|
364
398
|
rows.push('', ...wrapPaintedDiffRows(alignFileDiffs(call.diffs), theme, room));
|
|
@@ -368,10 +402,15 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
368
402
|
rows.push(row);
|
|
369
403
|
}
|
|
370
404
|
if (block.status === 'error')
|
|
371
|
-
rows.push(...
|
|
372
|
-
if (block.job !== undefined)
|
|
373
|
-
|
|
374
|
-
|
|
405
|
+
rows.push(...wrapTextStable(theme.fg('error', 'Tool failed'), room));
|
|
406
|
+
if (block.job !== undefined) {
|
|
407
|
+
// The settlement line belongs to the call, not to its output: without a gap
|
|
408
|
+
// it reads as one more line of whatever the command printed.
|
|
409
|
+
if (rows.length > 0)
|
|
410
|
+
rows.push('');
|
|
411
|
+
rows.push(...wrapTextStable(theme.fg(block.status === 'error' ? 'error' : 'muted', `Job ${block.job.id} · ${block.job.detail ?? block.job.status}`), room));
|
|
412
|
+
}
|
|
413
|
+
const bg = block.status === 'running' ? 'toolPendingBg' : block.status === 'error' ? 'toolErrorBg' : surface;
|
|
375
414
|
return ['', ...rows, ''].map(row => paintSurface(theme, bg, 'toolOutput', padToWidth(' '.repeat(padding + nest) + row, width)));
|
|
376
415
|
}
|
|
377
416
|
function firstLineOf(text) {
|
|
@@ -382,6 +421,18 @@ function firstLineOf(text) {
|
|
|
382
421
|
}
|
|
383
422
|
return '';
|
|
384
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* One row standing in for a thought the reader has not opened.
|
|
426
|
+
*
|
|
427
|
+
* The first line is kept so a collapsed thought still says what it was about —
|
|
428
|
+
* a labelled placeholder would make every thought look the same.
|
|
429
|
+
*/
|
|
430
|
+
function reasoningSummaryRow(reasoning, theme, width) {
|
|
431
|
+
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
432
|
+
const label = `Thinking: ${firstLineOf(reasoning)}`;
|
|
433
|
+
return assistantContentLines([theme.fg('dim', truncateToWidth(label, Math.max(1, width - paddingX * 2)))], width, paddingX)[0]
|
|
434
|
+
?? '';
|
|
435
|
+
}
|
|
385
436
|
/** Thinking uses the same quiet Markdown style throughout the turn. */
|
|
386
437
|
function openReasoningLines(block, theme, width, mathMode = 'auto') {
|
|
387
438
|
return assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true, mathMode });
|
|
@@ -402,27 +453,103 @@ function groupMembers(group) {
|
|
|
402
453
|
* last row and its words are the answer painted below the run, so the run
|
|
403
454
|
* paints the thought and nothing else.
|
|
404
455
|
*/
|
|
405
|
-
function groupMemberLines(index, context, thoughtOnly = false
|
|
456
|
+
function groupMemberLines(index, context, thoughtOnly = false, toolOrdinal = 0,
|
|
457
|
+
/** The reader opened this run, so its thoughts are laid out in full. */
|
|
458
|
+
openRun = false) {
|
|
406
459
|
const { blocks, options, theme, width } = context;
|
|
407
460
|
const block = blocks[index];
|
|
408
461
|
// Search must reveal a match even when it falls outside a tool's normal preview.
|
|
409
462
|
const full = block.kind === 'tool' && (context.matches.has(index) || options.expandedTools?.has(block.callId) === true);
|
|
410
|
-
const
|
|
463
|
+
const surface = toolOrdinal % 2 === 0 ? 'toolSuccessBg' : 'toolAltBg';
|
|
464
|
+
const thinkingOpen = openRun
|
|
465
|
+
|| (block.kind === 'assistant' && context.openedThoughts.has(reasoningKey(block)));
|
|
466
|
+
const signature = [width, options.colors, context.depth, context.themeName, terminalKey(options), options.mathMode, options.mermaidMode, thoughtOnly, full, options.toolDetailsKey, surface, thinkingOpen].join('\0');
|
|
411
467
|
const cached = groupMemberCache.get(block);
|
|
412
468
|
let lines;
|
|
413
469
|
if (cached?.signature === signature)
|
|
414
470
|
lines = cached.lines;
|
|
415
471
|
else {
|
|
416
|
-
lines = block.kind === 'tool' ? toolBlockLines(block, theme, width, full ? 'details' : 'preview', options.toolDetailsKey)
|
|
417
|
-
: block.kind === 'assistant' ? thoughtOnly
|
|
418
|
-
|
|
472
|
+
lines = block.kind === 'tool' ? toolBlockLines(block, theme, width, full ? 'details' : 'preview', options.toolDetailsKey, surface)
|
|
473
|
+
: block.kind === 'assistant' ? thoughtOnly
|
|
474
|
+
? (options.hideThinking !== true || thinkingOpen
|
|
475
|
+
? openReasoningLines(block, theme, width, options.mathMode)
|
|
476
|
+
: [reasoningSummaryRow(block.reasoning, theme, width)])
|
|
477
|
+
: blockLines(block, theme, width, {
|
|
478
|
+
mathMode: options.mathMode ?? 'auto',
|
|
479
|
+
mermaidMode: options.mermaidMode ?? 'auto',
|
|
480
|
+
hideThinking: options.hideThinking === true,
|
|
481
|
+
thinkingOpen,
|
|
482
|
+
})
|
|
419
483
|
: block.kind === 'notice' ? blockLines(block, theme, width) : [];
|
|
420
484
|
groupMemberCache.set(block, { signature, lines });
|
|
421
485
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
486
|
+
return [...paintSearch(lines, index, context.occurrences, theme)];
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Paint the query occurrences of one block onto its rows.
|
|
490
|
+
*
|
|
491
|
+
* One painter for both the run path and the block path: painting the row instead
|
|
492
|
+
* of the occurrence was the original defect, and having two copies of the fix
|
|
493
|
+
* would let one of them regress back to it.
|
|
494
|
+
*/
|
|
495
|
+
function paintSearch(lines, block, occurrences, theme) {
|
|
496
|
+
const spansByRow = matchedRows(occurrences, block);
|
|
497
|
+
if (spansByRow.size === 0)
|
|
498
|
+
return lines;
|
|
499
|
+
return lines.map((line, row) => {
|
|
500
|
+
const spans = spansByRow.get(row);
|
|
501
|
+
return spans === undefined ? line : inverseSpans(line, spans, theme);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Invert the given display-cell spans of one rendered row.
|
|
506
|
+
*
|
|
507
|
+
* `sliceCells` is what makes this safe for CJK, emoji, and combining marks: the
|
|
508
|
+
* spans are in display cells, which is the only measure the reader's eye agrees
|
|
509
|
+
* with. Offsets computed from string indexes land mid-glyph and shear the row.
|
|
510
|
+
*/
|
|
511
|
+
function inverseSpans(line, spans, theme) {
|
|
512
|
+
const plain = stripAnsi(line);
|
|
513
|
+
const total = visibleWidth(plain);
|
|
514
|
+
// Merge overlapping spans, then walk them left to right so no cell is painted
|
|
515
|
+
// twice and the untouched gaps keep their original styling.
|
|
516
|
+
const ordered = [...spans].sort((left, right) => left.start - right.start);
|
|
517
|
+
const merged = [];
|
|
518
|
+
for (const span of ordered) {
|
|
519
|
+
const last = merged.at(-1);
|
|
520
|
+
const end = span.start + span.width;
|
|
521
|
+
if (last !== undefined && span.start <= last.start + last.width) {
|
|
522
|
+
last.width = Math.max(last.width, end - last.start);
|
|
523
|
+
}
|
|
524
|
+
else
|
|
525
|
+
merged.push({ start: span.start, width: span.width });
|
|
526
|
+
}
|
|
527
|
+
let out = '';
|
|
528
|
+
let at = 0;
|
|
529
|
+
for (const span of merged) {
|
|
530
|
+
const start = Math.max(at, Math.min(span.start, total));
|
|
531
|
+
const end = Math.min(total, Math.max(start, span.start + span.width));
|
|
532
|
+
if (start > at)
|
|
533
|
+
out += sliceCells(line, at, start - at);
|
|
534
|
+
if (end > start)
|
|
535
|
+
out += theme.inverse(sliceCells(line, start, end - start));
|
|
536
|
+
at = end;
|
|
537
|
+
}
|
|
538
|
+
if (at < total)
|
|
539
|
+
out += sliceCells(line, at, total - at);
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Highlight a payload once, then wrap it.
|
|
544
|
+
*
|
|
545
|
+
* Every caller goes through this so no tool view can quietly fall back to
|
|
546
|
+
* per-line painting — the shape that produced a single highlighted row above a
|
|
547
|
+
* block of plain ones.
|
|
548
|
+
*/
|
|
549
|
+
function highlightedLines(lines, language, theme, errorInk, width) {
|
|
550
|
+
const highlighted = highlightToolText(lines.join('\n'), language, theme);
|
|
551
|
+
const rows = highlighted.map(line => errorInk === undefined ? line : theme.fg(errorInk, line));
|
|
552
|
+
return width === undefined ? rows : rows.flatMap(line => wrapTextStable(line, width));
|
|
426
553
|
}
|
|
427
554
|
/** A notice that reports on the run's work, as one of its rows. */
|
|
428
555
|
function processNoticeLine(block, theme, width, indent) {
|
|
@@ -443,7 +570,10 @@ function processGroupBody(group, context) {
|
|
|
443
570
|
const offsets = new Map();
|
|
444
571
|
const spans = [];
|
|
445
572
|
for (const index of groupMembers(group)) {
|
|
446
|
-
const
|
|
573
|
+
const isTool = context.blocks[index]?.kind === 'tool';
|
|
574
|
+
const rows = groupMemberLines(index, context, index === group.answer && group.tookThought, context.toolCards.count, true);
|
|
575
|
+
if (isTool && rows.length > 0)
|
|
576
|
+
context.toolCards.count += 1;
|
|
447
577
|
if (rows.length > 0 && lines.length > 0)
|
|
448
578
|
lines.push('');
|
|
449
579
|
offsets.set(index, lines.length);
|
|
@@ -454,16 +584,6 @@ function processGroupBody(group, context) {
|
|
|
454
584
|
}
|
|
455
585
|
return { lines, offsets, spans };
|
|
456
586
|
}
|
|
457
|
-
/** A settled turn's length, the way a person would say it. */
|
|
458
|
-
export function formatElapsed(ms) {
|
|
459
|
-
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
460
|
-
if (seconds < 60)
|
|
461
|
-
return `${seconds}s`;
|
|
462
|
-
const minutes = Math.floor(seconds / 60);
|
|
463
|
-
if (minutes < 60)
|
|
464
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
465
|
-
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
466
|
-
}
|
|
467
587
|
/** A completed turn's duration and any failures remain visible when folded. */
|
|
468
588
|
function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O') {
|
|
469
589
|
const label = elapsed === undefined ? 'Worked' : `Worked for ${elapsed}`;
|
|
@@ -480,13 +600,16 @@ function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O')
|
|
|
480
600
|
* to direct callers; turn inspection uses previews. omitReasoning excludes
|
|
481
601
|
* thinking already drawn by a run.
|
|
482
602
|
*/
|
|
483
|
-
export function blockLines(block, theme, width, { toolsExpanded = false, omitReasoning = false, toolDetailsKey = 'Alt+O', mathMode = 'auto', mermaidMode = 'auto' } = {}) {
|
|
603
|
+
export function blockLines(block, theme, width, { toolsExpanded = false, omitReasoning = false, toolDetailsKey = 'Alt+O', mathMode = 'auto', mermaidMode = 'auto', hideThinking = false, thinkingOpen = false, expandedOutput = false, outputExpandKey = 'Ctrl+O' } = {}) {
|
|
484
604
|
if (block.kind === 'user')
|
|
485
605
|
return userBubble(block.text, theme, width);
|
|
486
606
|
if (block.kind === 'assistant') {
|
|
487
607
|
const lines = [];
|
|
488
608
|
if (block.reasoning !== '' && !omitReasoning) {
|
|
489
|
-
|
|
609
|
+
const collapsed = hideThinking && !thinkingOpen;
|
|
610
|
+
lines.push(...(collapsed
|
|
611
|
+
? [reasoningSummaryRow(block.reasoning, theme, width)]
|
|
612
|
+
: openReasoningLines(block, theme, width, mathMode)));
|
|
490
613
|
if (block.text !== '')
|
|
491
614
|
lines.push('');
|
|
492
615
|
}
|
|
@@ -506,12 +629,30 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
506
629
|
if (block.kind === 'tool') {
|
|
507
630
|
return toolBlockLines(block, theme, width, toolsExpanded ? 'full' : 'preview', toolDetailsKey);
|
|
508
631
|
}
|
|
509
|
-
if (block.kind === 'toolCatalog')
|
|
510
|
-
return renderToolsPanel(block.tools, theme, width, toolsExpanded);
|
|
511
632
|
if (block.kind === 'workspace')
|
|
512
633
|
return workspaceBlockLines(block, theme, width, toolsExpanded);
|
|
513
|
-
if (block.kind === 'commandOutput')
|
|
514
|
-
return renderCommandOutput(block.command, block.text, theme, width
|
|
634
|
+
if (block.kind === 'commandOutput') {
|
|
635
|
+
return renderCommandOutput(block.command, block.text, theme, width, {
|
|
636
|
+
...(block.surface === undefined ? {} : { surface: block.surface }),
|
|
637
|
+
expanded: expandedOutput,
|
|
638
|
+
...(expandedOutput ? {} : { expandKey: outputExpandKey }),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (block.kind === 'aside') {
|
|
642
|
+
// One line when closed, the whole answer when opened — the same fold a long
|
|
643
|
+
// command result uses, so there is one expand gesture in the product.
|
|
644
|
+
const head = ' ' + theme.bold(theme.fg('accent', '/btw')) + ' ' + theme.fg('text', block.question);
|
|
645
|
+
if (expandedOutput) {
|
|
646
|
+
return [
|
|
647
|
+
truncateToWidth(head, width),
|
|
648
|
+
'',
|
|
649
|
+
...renderMarkdown(block.text, theme, Math.max(1, width - 2)).map(line => ' ' + line),
|
|
650
|
+
];
|
|
651
|
+
}
|
|
652
|
+
return [truncateToWidth(head + theme.fg('dim', ` · ${outputExpandKey}: Expand`), width)];
|
|
653
|
+
}
|
|
654
|
+
if (block.kind === 'shell')
|
|
655
|
+
return renderShell(block, theme, width);
|
|
515
656
|
if (block.boundary === true)
|
|
516
657
|
return [renderTranscriptBoundary(block.text, theme, width)];
|
|
517
658
|
if (block.process !== undefined && block.framed !== true)
|
|
@@ -520,11 +661,11 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
520
661
|
const prefix = ' ';
|
|
521
662
|
const continuation = ' '.repeat(visibleWidth(prefix));
|
|
522
663
|
const tone = block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'dim';
|
|
523
|
-
return
|
|
664
|
+
return wrapTextStable(block.text, Math.max(1, width - visibleWidth(prefix))).map((line, index) => truncateToWidth((index === 0 ? prefix : continuation) + theme.fg(tone, line), width));
|
|
524
665
|
}
|
|
525
666
|
const state = block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'info';
|
|
526
667
|
const paint = (text) => theme.fg(block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'dim', text);
|
|
527
|
-
const wrapped =
|
|
668
|
+
const wrapped = wrapTextStable(block.text, Math.max(1, width - 4));
|
|
528
669
|
const title = paint(wrapped[0] ?? '');
|
|
529
670
|
return renderFramedBlock({
|
|
530
671
|
header: title,
|
|
@@ -568,6 +709,55 @@ function renderTranscriptBoundary(label, theme, width) {
|
|
|
568
709
|
const left = Math.floor(room / 2);
|
|
569
710
|
return rule.repeat(left) + ' ' + labelText + ' ' + rule.repeat(room - left);
|
|
570
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Rendered rows of every block, as the search index sees them.
|
|
714
|
+
*
|
|
715
|
+
* The searchable text used to be the block's source (`blockSearchText`), which is
|
|
716
|
+
* not what the reader is looking at: Markdown source carries `**` and backticks,
|
|
717
|
+
* a table is one source line but several rendered rows, and a folded run hides
|
|
718
|
+
* rows that the source still contains. Matching rendered rows is the only way the
|
|
719
|
+
* reported hit and the painted span agree.
|
|
720
|
+
*
|
|
721
|
+
* Plain text only — the caller strips SGR — and cached on the block array, which
|
|
722
|
+
* is replaced rather than mutated whenever the transcript changes.
|
|
723
|
+
*/
|
|
724
|
+
const renderedBlockCache = new WeakMap();
|
|
725
|
+
export function renderedBlocks(state, options) {
|
|
726
|
+
const theme = createTheme(options.colors === true, options.colorDepth ?? 'truecolor', options.themeName ?? 'dark', options.terminal);
|
|
727
|
+
const key = [
|
|
728
|
+
options.width,
|
|
729
|
+
options.toolsExpanded === true,
|
|
730
|
+
options.hideThinking === true,
|
|
731
|
+
[...(options.expandedTools ?? [])].sort().join(','),
|
|
732
|
+
[...(options.expandedReasoning ?? [])].sort().join(','),
|
|
733
|
+
[...(options.openedGroups ?? [])].sort().join(','),
|
|
734
|
+
].join('\u0000');
|
|
735
|
+
const cached = renderedBlockCache.get(state.blocks)?.get(key);
|
|
736
|
+
if (cached !== undefined)
|
|
737
|
+
return cached;
|
|
738
|
+
const blocks = state.blocks.map((block, index) => ({
|
|
739
|
+
block: index,
|
|
740
|
+
// The source text is the fallback for a block kind the painter does not lay
|
|
741
|
+
// out as rows of its own; matching something is better than matching nothing.
|
|
742
|
+
lines: renderedRows(block, theme, options).map(stripAnsi),
|
|
743
|
+
}));
|
|
744
|
+
const byKey = new Map(renderedBlockCache.get(state.blocks) ?? []);
|
|
745
|
+
byKey.set(key, blocks);
|
|
746
|
+
renderedBlockCache.set(state.blocks, byKey);
|
|
747
|
+
return blocks;
|
|
748
|
+
}
|
|
749
|
+
/** Plain rows one block contributes, without the search highlighting applied. */
|
|
750
|
+
function renderedRows(block, theme, options) {
|
|
751
|
+
if (block.kind === 'assistant') {
|
|
752
|
+
const thinkingOpen = options.hideThinking !== true || options.expandedReasoning?.has(reasoningKey(block)) === true;
|
|
753
|
+
return thinkingOpen && block.reasoning !== ''
|
|
754
|
+
? openReasoningLines(block, theme, options.width, options.mathMode)
|
|
755
|
+
: assistantMarkdown(block.text, theme, options.width, {
|
|
756
|
+
...(options.mathMode === undefined ? {} : { mathMode: options.mathMode }),
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return blockSearchText(block).split('\n');
|
|
760
|
+
}
|
|
571
761
|
/** Plain text of one block, used by transcript search and match painting. */
|
|
572
762
|
export function blockSearchText(block) {
|
|
573
763
|
if (block.kind === 'assistant') {
|
|
@@ -575,14 +765,13 @@ export function blockSearchText(block) {
|
|
|
575
765
|
}
|
|
576
766
|
if (block.kind === 'tool')
|
|
577
767
|
return `${block.args}\n${block.output}${block.job === undefined ? '' : `\n${block.job.id} ${block.job.status} ${block.job.detail ?? ''}`}`;
|
|
578
|
-
if (block.kind === 'toolCatalog') {
|
|
579
|
-
return block.tools.map(tool => `${tool.name} ${tool.description}`).join('\n');
|
|
580
|
-
}
|
|
581
768
|
if (block.kind === 'workspace') {
|
|
582
769
|
// Only the served file paths are searchable; a turn whose summary the
|
|
583
770
|
// host no longer holds contributes no path text.
|
|
584
771
|
return block.summary?.files.map(file => file.display).join('\n') ?? `turn ${block.turn}`;
|
|
585
772
|
}
|
|
773
|
+
if (block.kind === 'shell')
|
|
774
|
+
return `${block.command}\n${block.output}`;
|
|
586
775
|
return block.text;
|
|
587
776
|
}
|
|
588
777
|
function fitFrame(lines, width, stablePrefix = 0) {
|
|
@@ -606,35 +795,53 @@ function fitFrame(lines, width, stablePrefix = 0) {
|
|
|
606
795
|
const transcriptBodyCache = new WeakMap();
|
|
607
796
|
/** Settled immutable blocks keep their expensive Markdown/tool layout. */
|
|
608
797
|
const blockLinesCache = new WeakMap();
|
|
609
|
-
function cachedBlockLines(block, options, theme, themeName,
|
|
798
|
+
function cachedBlockLines(block, options, theme, themeName, depth, expanded, omitReasoning = false) {
|
|
610
799
|
const cached = blockLinesCache.get(block);
|
|
611
800
|
if (cached !== undefined
|
|
612
801
|
&& cached.width === options.width
|
|
613
802
|
&& cached.colors === options.colors
|
|
614
|
-
&& cached.
|
|
803
|
+
&& cached.depth === depth
|
|
615
804
|
&& cached.themeName === themeName
|
|
805
|
+
&& cached.terminalKey === terminalKey(options)
|
|
616
806
|
&& cached.expanded === expanded
|
|
617
807
|
&& cached.omitReasoning === omitReasoning
|
|
808
|
+
&& cached.hideThinking === (options.hideThinking === true)
|
|
809
|
+
&& cached.thinkingOpen === (block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true)
|
|
618
810
|
&& cached.mathMode === options.mathMode
|
|
619
811
|
&& cached.mermaidMode === options.mermaidMode
|
|
620
812
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
621
|
-
&& cached.toolDetails === (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true)
|
|
813
|
+
&& cached.toolDetails === (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true)
|
|
814
|
+
&& cached.expandedOutput === (options.expandedOutputs?.has(block) === true))
|
|
622
815
|
return cached.lines;
|
|
623
816
|
const toolDetails = block.kind === 'tool' && options.expandedTools?.has(block.callId) === true;
|
|
624
817
|
const lines = block.kind === 'tool' && toolDetails
|
|
625
818
|
? toolBlockLines(block, theme, options.width, 'details', options.toolDetailsKey)
|
|
626
|
-
: blockLines(block, theme, options.width, {
|
|
819
|
+
: blockLines(block, theme, options.width, {
|
|
820
|
+
toolsExpanded: expanded,
|
|
821
|
+
omitReasoning,
|
|
822
|
+
toolDetailsKey: options.toolDetailsKey ?? 'Alt+O',
|
|
823
|
+
mathMode: options.mathMode ?? 'auto',
|
|
824
|
+
mermaidMode: options.mermaidMode ?? 'auto',
|
|
825
|
+
hideThinking: options.hideThinking === true,
|
|
826
|
+
thinkingOpen: block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true,
|
|
827
|
+
expandedOutput: options.expandedOutputs?.has(block) === true,
|
|
828
|
+
outputExpandKey: options.turnDetailsKey ?? 'Ctrl+O',
|
|
829
|
+
});
|
|
627
830
|
blockLinesCache.set(block, {
|
|
628
831
|
mathMode: options.mathMode,
|
|
629
832
|
mermaidMode: options.mermaidMode,
|
|
630
833
|
toolDetailsKey: options.toolDetailsKey,
|
|
631
834
|
toolDetails,
|
|
835
|
+
expandedOutput: options.expandedOutputs?.has(block) === true,
|
|
632
836
|
width: options.width,
|
|
633
837
|
colors: options.colors,
|
|
634
|
-
|
|
838
|
+
depth,
|
|
635
839
|
themeName,
|
|
840
|
+
terminalKey: terminalKey(options),
|
|
636
841
|
expanded,
|
|
637
842
|
omitReasoning,
|
|
843
|
+
hideThinking: options.hideThinking === true,
|
|
844
|
+
thinkingOpen: block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true,
|
|
638
845
|
lines,
|
|
639
846
|
});
|
|
640
847
|
return lines;
|
|
@@ -660,7 +867,6 @@ const PROCESS_LABELS = [
|
|
|
660
867
|
{
|
|
661
868
|
match: /^(subagent|subagent_fork|subagent_isolated|subagent_codex|subagent_claude_code|agent|ralph|workflow_run)$/, label: 'Run subagent',
|
|
662
869
|
},
|
|
663
|
-
{ match: /^consult_advisor$/, label: 'Consult advisor' },
|
|
664
870
|
{
|
|
665
871
|
match: /^(session_search|session_trace|session_event_read|session_event_search|session_event_trace)$/,
|
|
666
872
|
label: 'Search sessions',
|
|
@@ -867,29 +1073,46 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
867
1073
|
const toolsExpanded = options.toolsExpanded === true;
|
|
868
1074
|
const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
|
|
869
1075
|
const expandedReasoning = [...(options.expandedReasoning ?? [])].sort().join('\0');
|
|
1076
|
+
const hideThinking = options.hideThinking === true;
|
|
870
1077
|
const openedGroups = [...(options.openedGroups ?? [])].sort().join('\0');
|
|
1078
|
+
// A command output the reader opened paints different rows, so the body cache
|
|
1079
|
+
// has to see it; without this the frame is reused and the key looks dead.
|
|
1080
|
+
const expandedOutputs = [...(options.expandedOutputs ?? [])].join('\u0000');
|
|
871
1081
|
const themeName = options.themeName ?? 'dark';
|
|
872
|
-
const
|
|
1082
|
+
const depth = colorDepthOf(options);
|
|
873
1083
|
const animatedSpinnerFrame = state.blocks.some(block => block.kind === 'tool' && block.status === 'running')
|
|
874
1084
|
? spinnerFrame
|
|
875
1085
|
: -1;
|
|
876
1086
|
const search = options.transcriptSearch;
|
|
877
|
-
const searchKey = search === undefined
|
|
878
|
-
|
|
879
|
-
|
|
1087
|
+
const searchKey = search === undefined ? '' : `${search.query}\u0000${search.focus}`;
|
|
1088
|
+
/*
|
|
1089
|
+
* The fold logic asks "does this range contain a hit", which is a question about
|
|
1090
|
+
* blocks; the painter asks "which cells on this row". Both are answered from one
|
|
1091
|
+
* occurrence list, computed from the rendered rows so a hit cannot exist in the
|
|
1092
|
+
* count and be missing from the paint.
|
|
1093
|
+
*
|
|
1094
|
+
* It is computed before the cache check because the composer hint reports its
|
|
1095
|
+
* length: a cache hit still has to say how many matches there are.
|
|
1096
|
+
*/
|
|
1097
|
+
const occurrences = search === undefined || search.query.trim() === ''
|
|
1098
|
+
? []
|
|
1099
|
+
: searchMatches(renderedBlocks(state, options), search.query);
|
|
880
1100
|
const cached = transcriptBodyCache.get(state.blocks);
|
|
881
1101
|
if (cached !== undefined
|
|
882
1102
|
&& cached.width === options.width
|
|
883
1103
|
&& cached.colors === options.colors
|
|
884
|
-
&& cached.
|
|
1104
|
+
&& cached.depth === depth
|
|
885
1105
|
&& cached.themeName === themeName
|
|
1106
|
+
&& cached.terminalKey === terminalKey(options)
|
|
886
1107
|
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
887
1108
|
&& cached.mathMode === options.mathMode
|
|
888
1109
|
&& cached.mermaidMode === options.mermaidMode
|
|
889
1110
|
&& cached.toolsExpanded === toolsExpanded
|
|
890
1111
|
&& cached.expandedTools === expandedTools
|
|
891
1112
|
&& cached.expandedReasoning === expandedReasoning
|
|
1113
|
+
&& cached.hideThinking === hideThinking
|
|
892
1114
|
&& cached.openedGroups === openedGroups
|
|
1115
|
+
&& cached.expandedOutputs === expandedOutputs
|
|
893
1116
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
894
1117
|
&& cached.turnSpans === state.turnSpans
|
|
895
1118
|
&& cached.turnDetailsKey === options.turnDetailsKey
|
|
@@ -904,9 +1127,12 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
904
1127
|
foldShape: cached.foldShape,
|
|
905
1128
|
foldMarks: cached.foldMarks,
|
|
906
1129
|
liveFrom: cached.liveFrom,
|
|
1130
|
+
// Recomputed on a cache hit from the same input, so the hint's count is
|
|
1131
|
+
// never a stale number left over from an earlier query.
|
|
1132
|
+
occurrences,
|
|
907
1133
|
};
|
|
908
1134
|
}
|
|
909
|
-
const matches = new Set(
|
|
1135
|
+
const matches = new Set(occurrences.map(match => match.block));
|
|
910
1136
|
const lines = [];
|
|
911
1137
|
const fileRegions = [];
|
|
912
1138
|
const filesAt = (index, start, length, expanded = false) => {
|
|
@@ -981,10 +1207,16 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
981
1207
|
const foldMarks = [];
|
|
982
1208
|
if (toolsExpanded)
|
|
983
1209
|
foldMarks.push({ key: 'all', row: 0 });
|
|
1210
|
+
// Collapsing or revealing every thought reshapes from the top, like the
|
|
1211
|
+
// global tool switch above, so it reports the same kind of mark.
|
|
1212
|
+
if (hideThinking)
|
|
1213
|
+
foldMarks.push({ key: 'thinking', row: 0 });
|
|
984
1214
|
let liveFrom;
|
|
985
1215
|
const context = {
|
|
986
|
-
blocks: state.blocks, theme, themeName,
|
|
987
|
-
matches, options: { ...options, expandedReasoning: openedThoughts },
|
|
1216
|
+
blocks: state.blocks, theme, themeName, depth, width: options.width,
|
|
1217
|
+
matches, occurrences, options: { ...options, expandedReasoning: openedThoughts },
|
|
1218
|
+
openedThoughts,
|
|
1219
|
+
toolCards: { count: 0 },
|
|
988
1220
|
};
|
|
989
1221
|
const compactProcessRow = (block) => {
|
|
990
1222
|
if (block?.kind === 'notice')
|
|
@@ -1014,17 +1246,28 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1014
1246
|
lines.push('');
|
|
1015
1247
|
const headerRow = lines.length;
|
|
1016
1248
|
const opened = openGroups.has(groupIndex);
|
|
1017
|
-
|
|
1249
|
+
/**
|
|
1250
|
+
* A run the reader stopped keeps the rows it had while it ran.
|
|
1251
|
+
*
|
|
1252
|
+
* Folding it at the moment of the interrupt would take those rows out from
|
|
1253
|
+
* under the reader, and — because a fold shortens the document — leave the
|
|
1254
|
+
* viewport starting inside rows already committed to native history, which
|
|
1255
|
+
* cannot be repainted. It stays open until the next submission, which is
|
|
1256
|
+
* the point at which the reader has moved on.
|
|
1257
|
+
*/
|
|
1258
|
+
const stopped = group.turn !== undefined && (state.stoppedTurns ?? []).includes(group.turn);
|
|
1259
|
+
const shown = opened || stopped;
|
|
1260
|
+
const shape = group.live ? 'live' : shown ? 'open' : 'folded';
|
|
1018
1261
|
foldMarks.push({ key: `run:${group.key}:${shape}`, row: headerRow });
|
|
1019
1262
|
if (group.live)
|
|
1020
1263
|
liveFrom ??= headerRow;
|
|
1021
1264
|
// A live run has no header: it becomes a group when its turn ends.
|
|
1022
|
-
if (!group.live && !
|
|
1265
|
+
if (!group.live && !shown) {
|
|
1023
1266
|
lines.push(...processGroupHeader(group, theme, options.width, elapsedOf(groupIndex), options.turnDetailsKey));
|
|
1024
1267
|
}
|
|
1025
1268
|
const bodyRow = lines.length;
|
|
1026
1269
|
let offsets = new Map();
|
|
1027
|
-
if (group.live ||
|
|
1270
|
+
if (group.live || shown) {
|
|
1028
1271
|
const body = processGroupBody(group, context);
|
|
1029
1272
|
for (const line of body.lines)
|
|
1030
1273
|
lines.push(line);
|
|
@@ -1070,10 +1313,8 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1070
1313
|
blockDrawStarts[group.answer] = lines.length;
|
|
1071
1314
|
offsets.set(group.answer, lines.length - bodyRow);
|
|
1072
1315
|
const answer = state.blocks[group.answer];
|
|
1073
|
-
const rendered = cachedBlockLines(answer, options, theme, themeName,
|
|
1074
|
-
for (const line of (
|
|
1075
|
-
? rendered.map(row => blockMatchesQuery(stripAnsi(row), search.query) ? theme.inverse(row) : row)
|
|
1076
|
-
: rendered))
|
|
1316
|
+
const rendered = cachedBlockLines(answer, options, theme, themeName, depth, toolsExpanded, absorbed.has(group.answer));
|
|
1317
|
+
for (const line of paintSearch(rendered, group.answer, occurrences, theme))
|
|
1077
1318
|
lines.push(line);
|
|
1078
1319
|
if (thoughtHit && thoughtRow !== undefined)
|
|
1079
1320
|
blockStarts[group.answer] = bodyRow + thoughtRow;
|
|
@@ -1099,7 +1340,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1099
1340
|
offsets.set(at, lines.length - bodyRow);
|
|
1100
1341
|
const rows = !group.live && !opened && trailing?.kind === 'tool'
|
|
1101
1342
|
? toolRowLine(trailing, theme, options.width, spinnerFrame)
|
|
1102
|
-
: groupMemberLines(at, context);
|
|
1343
|
+
: groupMemberLines(at, context, false, 0, group.live || opened);
|
|
1103
1344
|
filesAt(at, lines.length, rows.length);
|
|
1104
1345
|
for (const line of rows)
|
|
1105
1346
|
lines.push(line);
|
|
@@ -1161,11 +1402,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1161
1402
|
liveFrom ??= lines.length;
|
|
1162
1403
|
const expanded = (block.kind === 'workspace' && groups.some((group, at) => group.turn === block.turn && openGroups.has(at))) || matches.has(index) || toolsExpanded
|
|
1163
1404
|
|| (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
|
|
1164
|
-
const rendered = cachedBlockLines(block, options, theme, themeName,
|
|
1405
|
+
const rendered = cachedBlockLines(block, options, theme, themeName, depth, expanded, absorbed.has(index));
|
|
1165
1406
|
filesAt(index, lines.length, rendered.length, expanded);
|
|
1166
|
-
lines.push(...(
|
|
1167
|
-
? rendered.map(line => blockMatchesQuery(stripAnsi(line), search.query) ? theme.inverse(line) : line)
|
|
1168
|
-
: rendered));
|
|
1407
|
+
lines.push(...paintSearch(rendered, index, occurrences, theme));
|
|
1169
1408
|
blockDrawStarts[index] = blockStarts[index];
|
|
1170
1409
|
previous = block;
|
|
1171
1410
|
}
|
|
@@ -1181,13 +1420,16 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1181
1420
|
blockDrawStarts,
|
|
1182
1421
|
width: options.width,
|
|
1183
1422
|
colors: options.colors,
|
|
1184
|
-
|
|
1423
|
+
depth,
|
|
1185
1424
|
themeName,
|
|
1425
|
+
terminalKey: terminalKey(options),
|
|
1186
1426
|
spinnerFrame: animatedSpinnerFrame,
|
|
1187
1427
|
toolsExpanded,
|
|
1188
1428
|
expandedTools,
|
|
1189
1429
|
expandedReasoning,
|
|
1430
|
+
hideThinking,
|
|
1190
1431
|
openedGroups,
|
|
1432
|
+
expandedOutputs,
|
|
1191
1433
|
searchKey,
|
|
1192
1434
|
turnSpans: state.turnSpans,
|
|
1193
1435
|
status: state.status,
|
|
@@ -1198,11 +1440,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1198
1440
|
lines,
|
|
1199
1441
|
blockStarts,
|
|
1200
1442
|
});
|
|
1201
|
-
return { lines, blockStarts, blockDrawStarts, fileRegions, foldShape, foldMarks, liveFrom };
|
|
1443
|
+
return { lines, blockStarts, blockDrawStarts, fileRegions, foldShape, foldMarks, liveFrom, occurrences };
|
|
1202
1444
|
}
|
|
1203
1445
|
function commandSurfaceName(block) {
|
|
1204
|
-
if (block?.kind === 'toolCatalog')
|
|
1205
|
-
return 'tools';
|
|
1206
1446
|
if (block?.kind === 'commandOutput')
|
|
1207
1447
|
return block.command;
|
|
1208
1448
|
return undefined;
|
|
@@ -1381,6 +1621,39 @@ function todoPreviewStart(todos) {
|
|
|
1381
1621
|
const pending = todos.findIndex(todo => todo.status === 'pending');
|
|
1382
1622
|
return pending >= 0 ? pending : Math.max(0, todos.length - TODO_PREVIEW);
|
|
1383
1623
|
}
|
|
1624
|
+
/** How many rows the pane may take before it starts scrolling. */
|
|
1625
|
+
const ASIDE_MAX_ROWS = 12;
|
|
1626
|
+
/**
|
|
1627
|
+
* The side-conversation pane: a rule, the question, the answer, and how to leave.
|
|
1628
|
+
*
|
|
1629
|
+
* A pane rather than a transcript block because the exchange belongs to a
|
|
1630
|
+
* conversation that is not this one: the reader is working in the transcript, and
|
|
1631
|
+
* an answer from elsewhere should not become a row of it. Docked above the
|
|
1632
|
+
* composer, `Esc` closes it and the session looks exactly as it did.
|
|
1633
|
+
*/
|
|
1634
|
+
export function renderAside(panel, theme, width, available = ASIDE_MAX_ROWS) {
|
|
1635
|
+
if (width <= 0)
|
|
1636
|
+
return [];
|
|
1637
|
+
const rule = theme.fg('border', '─'.repeat(width));
|
|
1638
|
+
const head = ' ' + theme.bold(theme.fg('accent', '/btw')) + ' ' + theme.fg('text', panel.question);
|
|
1639
|
+
const body = panel.text === ''
|
|
1640
|
+
? [theme.fg('dim', panel.status === 'running' ? '…' : 'No answer.')]
|
|
1641
|
+
: renderMarkdown(panel.text, theme, Math.max(1, width - 2));
|
|
1642
|
+
const detail = panel.detail === undefined || panel.detail === ''
|
|
1643
|
+
? []
|
|
1644
|
+
: ['', theme.fg('dim', panel.detail)];
|
|
1645
|
+
const hint = ' ' + theme.fg('dim', formatOverlayHint([{ keys: '↑↓', action: 'scroll' }, { keys: '/copy', action: 'copy' }, { keys: 'Esc', action: 'close' }]));
|
|
1646
|
+
// Two rules, the question, its blank line, and the hint are always drawn. What
|
|
1647
|
+
// is left is the answer's window, so the pane never grows past its cap.
|
|
1648
|
+
const cap = Math.max(6, Math.min(ASIDE_MAX_ROWS, available));
|
|
1649
|
+
const windowRows = Math.max(1, cap - 4);
|
|
1650
|
+
const content = [...body, ...detail];
|
|
1651
|
+
const maxStart = Math.max(0, content.length - windowRows);
|
|
1652
|
+
const first = Math.max(0, Math.min(panel.scroll, maxStart));
|
|
1653
|
+
const rows = [head, '', ...content.slice(first, first + windowRows)];
|
|
1654
|
+
return [rule, ...rows.map(line => ' ' + line).map(line => truncateToWidth(line, width)),
|
|
1655
|
+
truncateToWidth(hint, width), rule];
|
|
1656
|
+
}
|
|
1384
1657
|
/** Compact, unframed Todo tree placed above queued messages. */
|
|
1385
1658
|
export function renderTodos(todos, theme, width) {
|
|
1386
1659
|
if (todos.length === 0 || width <= 0)
|
|
@@ -1410,7 +1683,8 @@ export function renderTodos(todos, theme, width) {
|
|
|
1410
1683
|
})];
|
|
1411
1684
|
return lines.map(line => truncateToWidth(line, width));
|
|
1412
1685
|
}
|
|
1413
|
-
|
|
1686
|
+
/** First row the bar shows; the provider starts its selection here. */
|
|
1687
|
+
export function subagentPreviewStart(agents) {
|
|
1414
1688
|
if (agents.length <= SUBAGENT_PREVIEW)
|
|
1415
1689
|
return 0;
|
|
1416
1690
|
const active = agents.findIndex(agent => agent.phase === 'running' || agent.phase === 'starting');
|
|
@@ -1434,12 +1708,24 @@ function subagentPhaseGlyph(phase, theme, spinnerFrame) {
|
|
|
1434
1708
|
const SUBAGENT_PHASE_LABELS = {
|
|
1435
1709
|
starting: 'Starting', running: 'Running', waiting: 'Waiting', completed: 'Done', error: 'Failed',
|
|
1436
1710
|
};
|
|
1711
|
+
/** How long a child has been (or was) running, as one label. */
|
|
1712
|
+
export function formatAgentElapsed(agent, now = Date.now()) {
|
|
1713
|
+
if (agent.startedAt === undefined)
|
|
1714
|
+
return '—';
|
|
1715
|
+
const live = agent.phase === 'running' || agent.phase === 'starting';
|
|
1716
|
+
const ms = Math.max(0, (live ? now : agent.updatedAt ?? now) - agent.startedAt);
|
|
1717
|
+
if (ms < 1_000)
|
|
1718
|
+
return `${Math.round(ms)}ms`;
|
|
1719
|
+
if (ms < 60_000)
|
|
1720
|
+
return `${(ms / 1_000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
1721
|
+
return `${Math.floor(ms / 60_000)}m${String(Math.floor((ms % 60_000) / 1_000)).padStart(2, '0')}s`;
|
|
1722
|
+
}
|
|
1437
1723
|
function subagentRowText(agent) {
|
|
1438
1724
|
const indent = agent.depth > 1 ? ' '.repeat(agent.depth - 1) : '';
|
|
1439
1725
|
return `${indent}${agent.label} · ${SUBAGENT_PHASE_LABELS[agent.phase]}`;
|
|
1440
1726
|
}
|
|
1441
1727
|
/** Compact, unframed descendant-subagent tree placed above Todos. */
|
|
1442
|
-
export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspectedId,
|
|
1728
|
+
export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspectedId, selected) {
|
|
1443
1729
|
const agents = roster?.agents ?? [];
|
|
1444
1730
|
if (agents.length === 0 || width <= 0)
|
|
1445
1731
|
return [];
|
|
@@ -1453,10 +1739,16 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1453
1739
|
done === 0 ? undefined : `${done} done`,
|
|
1454
1740
|
failed === 0 ? undefined : `${failed} failed`,
|
|
1455
1741
|
].filter((part) => part !== undefined);
|
|
1742
|
+
const pointing = selected !== undefined;
|
|
1456
1743
|
const headerBody = theme.bold(theme.fg('accent', 'Agents'))
|
|
1457
1744
|
+ (counts.length === 0 ? '' : theme.fg('dim', ` · ${counts.join(' · ')}`))
|
|
1458
|
-
|
|
1459
|
-
|
|
1745
|
+
// Short enough to survive a narrow bar: the counts already use the width.
|
|
1746
|
+
+ theme.fg('dim', pointing ? ' · ↑↓ · Enter open · Esc' : ' · ↓ select');
|
|
1747
|
+
const header = ' ' + (pointing ? theme.inverse(headerBody) : headerBody);
|
|
1748
|
+
// A workflow run belongs to the same roster: it is the one place a reader learns
|
|
1749
|
+
// that a workflow is still going, so it stays as the bar's first row rather than
|
|
1750
|
+
// living in the screen this bar replaced.
|
|
1751
|
+
const workflows = (roster?.workflows ?? []).map(run => ' ' + theme.fg(run.status === 'completed' ? 'success' : run.status === 'open' ? 'accent' : 'warning', `Workflow · ${run.name} · ${run.status}${run.phase === undefined ? '' : ` · ${run.phase}`}`));
|
|
1460
1752
|
const start = subagentPreviewStart(agents);
|
|
1461
1753
|
const end = Math.min(agents.length, start + SUBAGENT_PREVIEW);
|
|
1462
1754
|
const rows = [
|
|
@@ -1464,11 +1756,11 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1464
1756
|
...agents.slice(start, end),
|
|
1465
1757
|
...(end === agents.length ? [] : [`… ${agents.length - end} more`]),
|
|
1466
1758
|
];
|
|
1467
|
-
const lines = [header, ...rows.map((row, index) => {
|
|
1759
|
+
const lines = [header, ...workflows, ...rows.map((row, index) => {
|
|
1468
1760
|
const branch = ' ' + theme.fg('dim', index === rows.length - 1 ? '└─' : '├─') + ' ';
|
|
1469
1761
|
if (typeof row === 'string')
|
|
1470
1762
|
return branch + theme.fg('dim', row);
|
|
1471
|
-
const
|
|
1763
|
+
const current = inspectedId === row.id;
|
|
1472
1764
|
const paint = row.phase === 'completed'
|
|
1473
1765
|
? (text) => theme.fg('dim', theme.strikethrough(text))
|
|
1474
1766
|
: row.phase === 'error'
|
|
@@ -1476,9 +1768,10 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1476
1768
|
: row.phase === 'waiting'
|
|
1477
1769
|
? (text) => theme.fg('dim', text)
|
|
1478
1770
|
: (text) => text;
|
|
1479
|
-
const
|
|
1771
|
+
const pointed = index === selected;
|
|
1772
|
+
const marker = current || pointed ? theme.fg('accent', SYMBOL.cursor) + ' ' : '';
|
|
1480
1773
|
const body = marker + subagentPhaseGlyph(row.phase, theme, spinnerFrame) + ' ' + paint(subagentRowText(row));
|
|
1481
|
-
return branch + (
|
|
1774
|
+
return branch + (current || pointed ? theme.bold(body) : body);
|
|
1482
1775
|
})];
|
|
1483
1776
|
return lines.map(line => truncateToWidth(line, width));
|
|
1484
1777
|
}
|
|
@@ -1571,9 +1864,12 @@ export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
|
1571
1864
|
const editSingleHint = width >= 72 ? '↑/Alt+Up edit · Alt+Q queue' : '↑ edit';
|
|
1572
1865
|
const editMultiHint = width >= 72 ? '↑/Alt+Up edit latest · Alt+Q queue' : '↑ edit latest';
|
|
1573
1866
|
if (labels.length === 1) {
|
|
1574
|
-
return [alignAction(
|
|
1867
|
+
return [alignAction(
|
|
1868
|
+
// Pi's vocabulary: what Enter typed was a follow-up, and the row says so
|
|
1869
|
+
// in the same word the composer hint uses.
|
|
1870
|
+
rail + queueLabel + theme.fg('dim', ' · ') + theme.fg('dim', 'follow-up · ') + theme.fg('text', labels[0] ?? ''), theme.fg('dim', editSingleHint))];
|
|
1575
1871
|
}
|
|
1576
|
-
const summary = ` · ${labels.length} ·
|
|
1872
|
+
const summary = ` · ${labels.length} · follow-up${hidden === 0 ? '' : ` · ${hidden} earlier`}`;
|
|
1577
1873
|
const visibleLabels = labels.slice(start);
|
|
1578
1874
|
const lines = [
|
|
1579
1875
|
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim', editMultiHint), false),
|
|
@@ -1583,14 +1879,14 @@ export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
|
1583
1879
|
return lines.map(line => truncateToWidth(line, width));
|
|
1584
1880
|
}
|
|
1585
1881
|
/**
|
|
1586
|
-
* Compose the
|
|
1882
|
+
* Compose the startup header, transcript, working row, and ruled editor into
|
|
1587
1883
|
* one frame — the oh-my-pi surface.
|
|
1588
1884
|
* @param state - transcript state.
|
|
1589
1885
|
* @param options - terminal geometry and input state.
|
|
1590
1886
|
* @returns the frame to hand to a renderer.
|
|
1591
1887
|
*/
|
|
1592
1888
|
export function renderView(state, options) {
|
|
1593
|
-
const theme = createTheme(options.colors, options.
|
|
1889
|
+
const theme = createTheme(options.colors === true, options.colorDepth ?? 'truecolor', options.themeName ?? 'dark', options.terminal);
|
|
1594
1890
|
const width = options.width;
|
|
1595
1891
|
const height = options.height;
|
|
1596
1892
|
const appName = options.appName ?? 'omdsh';
|
|
@@ -1598,16 +1894,6 @@ export function renderView(state, options) {
|
|
|
1598
1894
|
const pwd = options.pwd ?? '';
|
|
1599
1895
|
const motion = options.motion ?? 'full';
|
|
1600
1896
|
const spinnerFrame = motion === 'off' ? -1 : options.spinnerFrame ?? 0;
|
|
1601
|
-
if (options.agentHub !== undefined) {
|
|
1602
|
-
const hub = renderAgentHub(options.agentHub, theme, width, height);
|
|
1603
|
-
return {
|
|
1604
|
-
lines: fitFrame(hub.lines, width),
|
|
1605
|
-
cursor: hub.cursor,
|
|
1606
|
-
cursorVisible: hub.cursorVisible,
|
|
1607
|
-
// A full-screen surface drew no transcript body.
|
|
1608
|
-
documentRows: null,
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
1897
|
if (options.trajectory !== undefined) {
|
|
1612
1898
|
const trajectory = renderTrajectory(options.trajectory, theme, width, height);
|
|
1613
1899
|
return {
|
|
@@ -1628,17 +1914,6 @@ export function renderView(state, options) {
|
|
|
1628
1914
|
documentRows: null,
|
|
1629
1915
|
};
|
|
1630
1916
|
}
|
|
1631
|
-
if (options.promptSelector?.request.presentation === 'fullscreen-list' || options.promptSelector?.request.presentation === 'fullscreen-tree') {
|
|
1632
|
-
const selector = (options.promptSelector.request.presentation === 'fullscreen-tree' ? renderPromptTreePage : renderPromptSelectorPage)(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
|
|
1633
|
-
return {
|
|
1634
|
-
lines: fitFrame(selector.lines, width),
|
|
1635
|
-
...(selector.document === undefined ? {} : { promptDocument: selector.document }),
|
|
1636
|
-
cursor: selector.cursor,
|
|
1637
|
-
cursorVisible: true,
|
|
1638
|
-
// A full-screen surface drew no transcript body.
|
|
1639
|
-
documentRows: null,
|
|
1640
|
-
};
|
|
1641
|
-
}
|
|
1642
1917
|
if (options.promptSelector?.request.presentation === 'plan-review' || options.promptSelector?.request.presentation === 'document') {
|
|
1643
1918
|
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);
|
|
1644
1919
|
return {
|
|
@@ -1653,12 +1928,8 @@ export function renderView(state, options) {
|
|
|
1653
1928
|
}
|
|
1654
1929
|
const welcome = renderWelcome({
|
|
1655
1930
|
width,
|
|
1656
|
-
model: options.model,
|
|
1657
|
-
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
|
|
1658
1931
|
version,
|
|
1659
|
-
appName,
|
|
1660
|
-
...(options.recentSessions === undefined ? {} : { recentSessions: options.recentSessions }),
|
|
1661
|
-
...(options.welcomeTips === undefined ? {} : { tips: options.welcomeTips }),
|
|
1932
|
+
appName: options.displayName ?? appName,
|
|
1662
1933
|
}, theme);
|
|
1663
1934
|
const transcript = renderTranscriptBody(state, options, theme, spinnerFrame);
|
|
1664
1935
|
const body = [...welcome];
|
|
@@ -1669,29 +1940,37 @@ export function renderView(state, options) {
|
|
|
1669
1940
|
transcriptStart = body.length;
|
|
1670
1941
|
body.push(...transcript.lines);
|
|
1671
1942
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1943
|
+
// The activity is drawn inside the composer frame rather than as its own row.
|
|
1944
|
+
// A separate row cost a line, read as another component, and moved when the
|
|
1945
|
+
// composer grew; the frame is where the reader is already looking.
|
|
1946
|
+
const activityLabelText = state.status === 'running'
|
|
1947
|
+
? options.workingLabel ?? DEEP_DRIVING_LABEL
|
|
1674
1948
|
: state.status === 'compacting'
|
|
1675
|
-
?
|
|
1676
|
-
:
|
|
1949
|
+
? options.workingLabel ?? 'Compacting'
|
|
1950
|
+
: undefined;
|
|
1951
|
+
const working = [];
|
|
1677
1952
|
const statusBar = resolveStatusBarConfig(options.statusBar, options.statusPreset);
|
|
1678
1953
|
const placeholder = options.inspected === undefined && state.status === 'idle'
|
|
1679
1954
|
&& options.input === '' && (options.inputImages ?? 0) === 0
|
|
1680
1955
|
? options.modelReady === false
|
|
1681
1956
|
? 'Run /login to connect a model'
|
|
1682
|
-
|
|
1957
|
+
// `/ commands` and `@ files` are already on the header's hint line, and this
|
|
1958
|
+
// one is the copy that costs every frame: the header's scrolls into history
|
|
1959
|
+
// once the conversation starts, this one does not. A bare prompt is enough
|
|
1960
|
+
// to say the composer is ready for words.
|
|
1961
|
+
: 'Ask anything'
|
|
1683
1962
|
: null;
|
|
1684
1963
|
const inlineHint = options.transcriptSearch !== undefined
|
|
1685
1964
|
? transcriptSearchHint({
|
|
1686
1965
|
query: options.transcriptSearch.query,
|
|
1687
1966
|
editing: options.transcriptSearch.editing,
|
|
1688
1967
|
focus: options.transcriptSearch.focus,
|
|
1689
|
-
},
|
|
1968
|
+
}, transcript.occurrences.length)
|
|
1690
1969
|
: options.inspected === undefined
|
|
1691
1970
|
? slashInlineHint(options.input, options.inputCursor, options.commands)
|
|
1692
1971
|
?? placeholder
|
|
1693
1972
|
?? (state.status === 'running' && options.steerKey !== undefined
|
|
1694
|
-
? `Enter:
|
|
1973
|
+
? `Enter: follow-up · ${options.steerKey}: next step` : null)
|
|
1695
1974
|
: options.inspected.writable
|
|
1696
1975
|
? slashInlineHint(options.input, options.inputCursor, options.commands) ?? 'Enter to steer · Esc to return'
|
|
1697
1976
|
: 'Read-only · Esc to return';
|
|
@@ -1714,29 +1993,52 @@ export function renderView(state, options) {
|
|
|
1714
1993
|
inputCursor: options.inputCursor,
|
|
1715
1994
|
status: ' ' + theme.fg('accent', '🐳') + ' ',
|
|
1716
1995
|
...(permissionBadge === '' ? {} : { statusRight: ' ' + permissionBadge + ' ' }),
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1996
|
+
// A composer starting with `!` is not talking to the model, and the border
|
|
1997
|
+
// says so the moment the character is typed — before the reader commits to
|
|
1998
|
+
// running anything. Green is the one ink that means "this runs on your
|
|
1999
|
+
// machine", which is also what a successful tool card uses.
|
|
2000
|
+
border: options.input.trimStart().startsWith('!')
|
|
2001
|
+
? 'shellMode'
|
|
2002
|
+
: options.inspected !== undefined && options.inspected.writable !== true
|
|
2003
|
+
|| state.status === 'idle'
|
|
2004
|
+
? 'border'
|
|
2005
|
+
: 'accent',
|
|
1721
2006
|
...(theme.colors && (leadingSlashCommandNameRange(options.input) !== null
|
|
1722
2007
|
|| (options.inputImages !== undefined && options.inputImages !== 0))
|
|
1723
2008
|
? { paintInput: (text, start) => paintComposerInputSlice(options.input, text, start, theme) }
|
|
1724
2009
|
: {}),
|
|
1725
2010
|
...(inlineHint !== null ? { inlineHint } : {}),
|
|
2011
|
+
...(options.transientStatus === undefined ? {} : { note: options.transientStatus }),
|
|
2012
|
+
...(activityLabelText === undefined ? {} : {
|
|
2013
|
+
activity: {
|
|
2014
|
+
frame: motion === 'off' ? SYMBOL.running : SPINNER[Math.max(0, spinnerFrame) % SPINNER.length] ?? SYMBOL.running,
|
|
2015
|
+
label: activityLabelText,
|
|
2016
|
+
shimmer: activityLabelText === DEEP_DRIVING_LABEL && motion === 'full',
|
|
2017
|
+
},
|
|
2018
|
+
spinnerFrame: Math.max(0, spinnerFrame),
|
|
2019
|
+
}),
|
|
1726
2020
|
};
|
|
1727
|
-
|
|
2021
|
+
/**
|
|
2022
|
+
* A tree is not a page.
|
|
2023
|
+
*
|
|
2024
|
+
* It used to take the whole frame, which hid the conversation the reader was
|
|
2025
|
+
* about to rewind to — the one thing they need to see while choosing a point in
|
|
2026
|
+
* it. Bounded to a little over half the screen it sits above the composer with
|
|
2027
|
+
* the transcript still behind it, the way pi mounts its tree selector.
|
|
2028
|
+
*/
|
|
2029
|
+
const treeRows = options.promptSelector === undefined || options.promptSelector.request.presentation !== 'tree'
|
|
2030
|
+
? undefined
|
|
2031
|
+
: renderPromptTreePage(options.promptSelector, theme, width, Math.max(10, Math.min(height - 8, Math.floor(height * 0.6))), options.input, options.inputCursor, appName);
|
|
2032
|
+
const promptSelector = treeRows ?? (options.promptSelector === undefined
|
|
1728
2033
|
? undefined
|
|
1729
|
-
: renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor, Math.max(3, Math.min(10, height - working.length - 14)));
|
|
2034
|
+
: renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor, Math.max(3, Math.min(10, height - working.length - 14))));
|
|
1730
2035
|
const settings = promptSelector !== undefined || options.settings === undefined
|
|
1731
2036
|
? undefined
|
|
1732
2037
|
: renderSettings(options.settings, theme, width);
|
|
1733
|
-
const
|
|
1734
|
-
? undefined
|
|
1735
|
-
: renderCopySelector(options.copySelector, theme, width);
|
|
1736
|
-
const search = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || options.historySearch === undefined
|
|
2038
|
+
const search = promptSelector !== undefined || settings !== undefined || options.historySearch === undefined
|
|
1737
2039
|
? undefined
|
|
1738
2040
|
: renderHistorySearch(options.historySearch, theme, width, Math.max(1, Math.min(HISTORY_SEARCH_MAX_VISIBLE, height - working.length - 8)));
|
|
1739
|
-
const editor = promptSelector === undefined && settings === undefined &&
|
|
2041
|
+
const editor = promptSelector === undefined && settings === undefined && search === undefined
|
|
1740
2042
|
? renderEditor(editorOpts, theme)
|
|
1741
2043
|
: undefined;
|
|
1742
2044
|
const queuedSubmissions = editor === undefined || options.inspected !== undefined
|
|
@@ -1751,6 +2053,9 @@ export function renderView(state, options) {
|
|
|
1751
2053
|
? []
|
|
1752
2054
|
: renderTurnError(state.turnError, theme, width);
|
|
1753
2055
|
const todos = editor === undefined || options.inspected !== undefined ? [] : renderTodos(state.todos, theme, width);
|
|
2056
|
+
const aside = editor === undefined || options.inspected !== undefined || options.aside === undefined
|
|
2057
|
+
? []
|
|
2058
|
+
: renderAside(options.aside, theme, width, Math.max(4, Math.floor(height / 2)));
|
|
1754
2059
|
const goal = editor === undefined || options.inspected !== undefined || options.sessionControls?.goal === undefined
|
|
1755
2060
|
? []
|
|
1756
2061
|
: renderGoalBar(options.sessionControls.goal, theme, width);
|
|
@@ -1761,15 +2066,15 @@ export function renderView(state, options) {
|
|
|
1761
2066
|
const inspect = editor === undefined ? [] : renderInspectBanner(options.inspected, theme, width, spinnerFrame);
|
|
1762
2067
|
const subagents = editor === undefined
|
|
1763
2068
|
? []
|
|
1764
|
-
: renderSubagents(options.subagents, theme, width, spinnerFrame, options.inspected?.id, options.
|
|
1765
|
-
const autocomplete = promptSelector !== undefined || settings !== undefined ||
|
|
2069
|
+
: renderSubagents(options.subagents, theme, width, spinnerFrame, options.inspected?.id, options.subagentSelection);
|
|
2070
|
+
const autocomplete = promptSelector !== undefined || settings !== undefined || search !== undefined
|
|
1766
2071
|
|| options.autocomplete === undefined
|
|
1767
2072
|
? []
|
|
1768
2073
|
: renderAutocomplete(options.autocomplete.items, options.autocomplete.selected, theme, width);
|
|
1769
|
-
const inputLines = promptSelector?.lines ?? settings?.lines ??
|
|
2074
|
+
const inputLines = promptSelector?.lines ?? settings?.lines ?? search?.lines
|
|
1770
2075
|
?? (editor === undefined ? [] : editor.lines);
|
|
1771
2076
|
const spacer = 1;
|
|
1772
|
-
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;
|
|
2077
|
+
const reserved = aside.length + inputLines.length + working.length + inspect.length + subagents.length + todos.length + goal.length + questions.length + queuedSubmissions.length + turnError.length + spacer + autocomplete.length + statusFooter.length;
|
|
1773
2078
|
const budget = Math.max(0, height - reserved);
|
|
1774
2079
|
const focusIndex = options.focusBlock === undefined
|
|
1775
2080
|
? undefined
|
|
@@ -1783,7 +2088,7 @@ export function renderView(state, options) {
|
|
|
1783
2088
|
const requestedStart = focusStart === undefined
|
|
1784
2089
|
? (options.scrollStart ?? Number.POSITIVE_INFINITY)
|
|
1785
2090
|
: transcriptStart + focusStart;
|
|
1786
|
-
const hasOverlay = promptSelector !== undefined || settings !== undefined ||
|
|
2091
|
+
const hasOverlay = promptSelector !== undefined || settings !== undefined || search !== undefined;
|
|
1787
2092
|
const isFollowing = requestedStart === Number.POSITIVE_INFINITY && !hasOverlay;
|
|
1788
2093
|
// A run still at work reshapes as it goes — its newest rows scroll, it folds
|
|
1789
2094
|
// when the turn ends — so none of it may freeze into history until it
|
|
@@ -1803,9 +2108,29 @@ export function renderView(state, options) {
|
|
|
1803
2108
|
if (visible.length > 0)
|
|
1804
2109
|
lines.push(showJump
|
|
1805
2110
|
? ' '.repeat(jumpColumn) + theme.inverse(jumpText) : '');
|
|
1806
|
-
const bottomRows = working.length + goal.length + questions.length + inspect.length + subagents.length + todos.length + queuedSubmissions.length + turnError.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
2111
|
+
const bottomRows = aside.length + working.length + goal.length + questions.length + inspect.length + subagents.length + todos.length + queuedSubmissions.length + turnError.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
1807
2112
|
const fill = Math.max(0, height - lines.length - bottomRows);
|
|
1808
|
-
|
|
2113
|
+
/**
|
|
2114
|
+
* Spare rows sit between the header and the conversation while at the tail.
|
|
2115
|
+
*
|
|
2116
|
+
* A short transcript used to leave its slack below the last row, so folding a
|
|
2117
|
+
* run — which shortens the transcript — widened a gap in the middle of the
|
|
2118
|
+
* screen while nothing the reader was looking at moved. Moving the slack to the
|
|
2119
|
+
* top of the *frame* fixed that but dragged the header down with it, and a
|
|
2120
|
+
* header that drops to the bottom the moment the first message arrives is worse
|
|
2121
|
+
* than the gap.
|
|
2122
|
+
*
|
|
2123
|
+
* Between the header and the transcript, both stay put: the header keeps the
|
|
2124
|
+
* row it is read on, the newest rows keep the row they are watched on, and the
|
|
2125
|
+
* slack absorbs whatever a fold removes.
|
|
2126
|
+
*
|
|
2127
|
+
* Not while scrolled: a window is already full, and padding a scrollback view
|
|
2128
|
+
* would shift the rows the reader deliberately positioned.
|
|
2129
|
+
*/
|
|
2130
|
+
const topPad = windowed === undefined && transcript.lines.length > 0 ? fill : 0;
|
|
2131
|
+
if (topPad === 0)
|
|
2132
|
+
lines.push(...Array.from({ length: fill }, () => ''));
|
|
2133
|
+
lines.push(...aside);
|
|
1809
2134
|
lines.push(...working);
|
|
1810
2135
|
lines.push(...goal);
|
|
1811
2136
|
lines.push(...questions);
|
|
@@ -1826,10 +2151,10 @@ export function renderView(state, options) {
|
|
|
1826
2151
|
? pendingRow
|
|
1827
2152
|
: Math.min(pendingRow ?? transcript.liveFrom, transcript.liveFrom);
|
|
1828
2153
|
if (liveRow !== undefined) {
|
|
1829
|
-
liveStart = transcriptStart + liveRow;
|
|
2154
|
+
liveStart = transcriptStart + liveRow + topPad;
|
|
1830
2155
|
}
|
|
1831
2156
|
else {
|
|
1832
|
-
liveStart = lines.length - bottomRows;
|
|
2157
|
+
liveStart = lines.length - bottomRows + topPad;
|
|
1833
2158
|
}
|
|
1834
2159
|
// liveStart marks the first row that is still live/mutable. Rows above it
|
|
1835
2160
|
// are committed. Off-screen live rows are bounded by the renderer: it only
|
|
@@ -1839,18 +2164,23 @@ export function renderView(state, options) {
|
|
|
1839
2164
|
else {
|
|
1840
2165
|
liveStart = 0;
|
|
1841
2166
|
}
|
|
2167
|
+
// Padding rows are not document rows: `frameStart` below tells the renderer
|
|
2168
|
+
// which frame row the document begins at, so they never map to content.
|
|
2169
|
+
const frameLines = topPad === 0
|
|
2170
|
+
? lines
|
|
2171
|
+
: [...lines.slice(0, transcriptStart), ...Array.from({ length: topPad }, () => ''), ...lines.slice(transcriptStart)];
|
|
1842
2172
|
// Every settled block was already rendered against this width. Mirroring
|
|
1843
2173
|
// OMP's stable-prefix preparation, only validate the mutable suffix instead
|
|
1844
2174
|
// of measuring the complete native-scrollback history on every frame.
|
|
1845
|
-
const trimmed = fitFrame(
|
|
1846
|
-
const caret = promptSelector?.cursor ?? settings?.cursor ??
|
|
2175
|
+
const trimmed = fitFrame(frameLines, width, isFollowing ? liveStart + topPad : 0);
|
|
2176
|
+
const caret = promptSelector?.cursor ?? settings?.cursor ?? search?.cursor ?? editor?.cursor ?? { row: 0, column: 0 };
|
|
1847
2177
|
// Which document rows this frame's body came from. `body` already includes the
|
|
1848
2178
|
// header, so a window's own range is already in document coordinates and must
|
|
1849
2179
|
// not have the header added again. No window means the whole body was drawn —
|
|
1850
2180
|
// that is a known range, not a frame that failed to report one, and treating
|
|
1851
2181
|
// it as "no body" would clear the record on every ordinary follow frame.
|
|
1852
2182
|
const windowRange = windowed === undefined
|
|
1853
|
-
? (body.length === 0 ? null : { documentStart: 0, documentEnd: body.length, frameStart: 0 })
|
|
2183
|
+
? (body.length === 0 ? null : { documentStart: 0, documentEnd: body.length + topPad, frameStart: 0 })
|
|
1854
2184
|
: windowed.documentRows;
|
|
1855
2185
|
const bodySource = windowRange === undefined
|
|
1856
2186
|
? null
|
|
@@ -1863,11 +2193,11 @@ export function renderView(state, options) {
|
|
|
1863
2193
|
continue;
|
|
1864
2194
|
const previous = stickyHeaders.at(-1);
|
|
1865
2195
|
if (previous !== undefined)
|
|
1866
|
-
previous.end = transcriptStart + transcript.blockStarts[at];
|
|
2196
|
+
previous.end = transcriptStart + topPad + transcript.blockStarts[at];
|
|
1867
2197
|
const padding = width > 4 ? 2 : 0;
|
|
1868
2198
|
const label = truncateToWidth(promptSummary(block), Math.max(1, width - padding * 2));
|
|
1869
2199
|
stickyHeaders.push({
|
|
1870
|
-
start: transcriptStart + (transcript.blockStarts[at + 1] ?? transcript.lines.length),
|
|
2200
|
+
start: transcriptStart + topPad + (transcript.blockStarts[at + 1] ?? transcript.lines.length),
|
|
1871
2201
|
end: body.length,
|
|
1872
2202
|
text: paintSurface(theme, 'userMessageBg', 'userMessageText', padToWidth(' '.repeat(padding) + label, width)),
|
|
1873
2203
|
});
|
|
@@ -1876,16 +2206,20 @@ export function renderView(state, options) {
|
|
|
1876
2206
|
return {
|
|
1877
2207
|
lines: trimmed,
|
|
1878
2208
|
stickyHeaders,
|
|
1879
|
-
...(jumpToLatest === undefined ? {} : { jumpToLatest }),
|
|
2209
|
+
...(jumpToLatest === undefined ? {} : { jumpToLatest: { ...jumpToLatest, row: jumpToLatest.row + topPad } }),
|
|
2210
|
+
// The tree's preview is a scrollable document too: without its coordinates
|
|
2211
|
+
// the advertised Ctrl+↑↓ had nothing to move, because the card path never
|
|
2212
|
+
// reported them.
|
|
2213
|
+
...(promptSelector?.document === undefined ? {} : { promptDocument: promptSelector.document }),
|
|
1880
2214
|
documentRows: bodySource,
|
|
1881
2215
|
cursor: {
|
|
1882
|
-
row: editorStart + caret.row,
|
|
2216
|
+
row: editorStart + caret.row + topPad,
|
|
1883
2217
|
column: Math.min(caret.column, width),
|
|
1884
2218
|
},
|
|
1885
2219
|
cursorVisible: state.status === 'compacting'
|
|
1886
2220
|
|| (options.inspected !== undefined && options.inspected.writable !== true)
|
|
1887
2221
|
? false
|
|
1888
|
-
: promptSelector?.cursorVisible ?? (settings === undefined
|
|
2222
|
+
: promptSelector?.cursorVisible ?? (settings === undefined),
|
|
1889
2223
|
liveStart,
|
|
1890
2224
|
livePinned,
|
|
1891
2225
|
// A transient frame is either a full-screen surface or the user browsing
|
|
@@ -1901,11 +2235,11 @@ export function renderView(state, options) {
|
|
|
1901
2235
|
hiddenBelow: 0,
|
|
1902
2236
|
}),
|
|
1903
2237
|
blockStarts: transcript.blockStarts,
|
|
1904
|
-
fileRegions: transcript.fileRegions.map(region => ({ ...region, start: transcriptStart + region.start, end: transcriptStart + region.end })),
|
|
2238
|
+
fileRegions: transcript.fileRegions.map(region => ({ ...region, start: transcriptStart + topPad + region.start, end: transcriptStart + topPad + region.end })),
|
|
1905
2239
|
blockDrawStarts: transcript.blockDrawStarts,
|
|
1906
2240
|
foldShape: transcript.foldShape,
|
|
1907
2241
|
foldMarks: transcript.foldMarks,
|
|
1908
|
-
bodyRow: transcriptStart,
|
|
2242
|
+
bodyRow: transcriptStart + topPad,
|
|
1909
2243
|
},
|
|
1910
2244
|
};
|
|
1911
2245
|
}
|