@agi-fans/dsh-tui 0.20.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 +249 -3
- 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 +18 -0
- package/lib/chrome/main-screen-renderer.js +58 -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/renderer.d.ts +9 -0
- package/lib/chrome/renderer.js +1 -1
- package/lib/chrome/renderer.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 +616 -520
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-args.d.ts +1 -1
- package/lib/chrome/tool-args.js +3 -1
- package/lib/chrome/tool-args.js.map +1 -1
- package/lib/chrome/tool-renderers.d.ts +2 -0
- package/lib/chrome/tool-renderers.js +69 -3
- 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 -7
- package/lib/chrome/welcome-tips.js +22 -41
- 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 +16 -22
- package/lib/commands/auth.js +346 -299
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/btw.d.ts +27 -0
- package/lib/commands/btw.js +59 -0
- package/lib/commands/btw.js.map +1 -0
- package/lib/commands/diff.js +30 -5
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +1 -1
- package/lib/commands/export.js +5 -3
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.d.ts +17 -1
- package/lib/commands/files.js +69 -61
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.js +18 -6
- package/lib/commands/integrations.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 +109 -76
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/permission.js +46 -13
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +196 -42
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule.d.ts +29 -0
- package/lib/commands/schedule.js +228 -0
- package/lib/commands/schedule.js.map +1 -0
- package/lib/commands/session-configuration.d.ts +1 -1
- package/lib/commands/session-configuration.js +29 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +75 -80
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.d.ts +24 -4
- package/lib/commands/terminal.js +38 -94
- 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 +150 -11
- 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-discovery.js +2 -2
- package/lib/input/editor-discovery.js.map +1 -1
- 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 +15 -4
- package/lib/input/keybindings-config.js +57 -35
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +18 -0
- package/lib/input/keys.js +48 -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/agent-profile.d.ts +5 -0
- package/lib/runtime/agent-profile.js +12 -0
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/approval-rules.d.ts +59 -0
- package/lib/runtime/approval-rules.js +179 -0
- package/lib/runtime/approval-rules.js.map +1 -0
- package/lib/runtime/btw-policy.d.ts +7 -0
- package/lib/runtime/btw-policy.js +18 -0
- package/lib/runtime/btw-policy.js.map +1 -0
- package/lib/runtime/btw.d.ts +47 -0
- package/lib/runtime/btw.js +200 -0
- package/lib/runtime/btw.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 +30 -0
- package/lib/runtime/composer-shell.js +150 -0
- package/lib/runtime/composer-shell.js.map +1 -0
- package/lib/runtime/file-recovery-local.d.ts +73 -0
- package/lib/runtime/file-recovery-local.js +351 -0
- package/lib/runtime/file-recovery-local.js.map +1 -0
- package/lib/runtime/file-recovery-recording.d.ts +23 -0
- package/lib/runtime/file-recovery-recording.js +131 -0
- package/lib/runtime/file-recovery-recording.js.map +1 -0
- package/lib/runtime/file-recovery-review.d.ts +3 -0
- package/lib/runtime/file-recovery-review.js +130 -0
- package/lib/runtime/file-recovery-review.js.map +1 -0
- package/lib/runtime/file-recovery-store.d.ts +83 -0
- package/lib/runtime/file-recovery-store.js +329 -0
- package/lib/runtime/file-recovery-store.js.map +1 -0
- package/lib/runtime/file-recovery-worker.d.ts +22 -0
- package/lib/runtime/file-recovery-worker.js +153 -0
- package/lib/runtime/file-recovery-worker.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -3
- package/lib/runtime/file-review.js +44 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/herdr-agent.d.ts +4 -13
- package/lib/runtime/herdr-agent.js +19 -63
- package/lib/runtime/herdr-agent.js.map +1 -1
- 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 +25 -2
- package/lib/runtime/plugin-settings.js +329 -31
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +81 -24
- package/lib/runtime/provider-local.js +1807 -608
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +52 -12
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-archive.d.ts +6 -0
- package/lib/runtime/session-archive.js +129 -3
- package/lib/runtime/session-archive.js.map +1 -1
- package/lib/runtime/session-runtime.js +1 -1
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/settings-preferences.d.ts +5 -0
- package/lib/runtime/settings-preferences.js +34 -0
- package/lib/runtime/settings-preferences.js.map +1 -0
- 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/runtime/terminal-state.d.ts +29 -0
- package/lib/runtime/terminal-state.js +62 -0
- package/lib/runtime/terminal-state.js.map +1 -0
- package/lib/runtime/tool-presentation.js +50 -14
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/external-agents.d.ts +5 -0
- package/lib/session/external-agents.js +27 -0
- package/lib/session/external-agents.js.map +1 -0
- package/lib/session/feature-toggles.d.ts +1 -1
- package/lib/session/feature-toggles.js +1 -1
- package/lib/session/interaction-adapter.d.ts +1 -6
- package/lib/session/interaction-adapter.js +95 -18
- 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/model-readiness.d.ts +16 -0
- package/lib/session/model-readiness.js +43 -0
- package/lib/session/model-readiness.js.map +1 -0
- 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/project-context.d.ts +5 -0
- package/lib/session/project-context.js +36 -0
- package/lib/session/project-context.js.map +1 -1
- package/lib/session/session-controller.d.ts +10 -13
- package/lib/session/session-controller.js +183 -98
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.js +2 -0
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/session-tree.d.ts +3 -0
- package/lib/session/session-tree.js +3 -1
- package/lib/session/session-tree.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +2 -1
- package/lib/session/subagent-roster.js +24 -3
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +6 -2
- package/lib/session/subagent-tracker.js +49 -2
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +47 -13
- package/lib/session/tui-settings.js +76 -34
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/working-directory.d.ts +4 -0
- package/lib/session/working-directory.js +4 -0
- package/lib/session/working-directory.js.map +1 -0
- package/lib/session/worktrees.d.ts +13 -0
- package/lib/session/worktrees.js +49 -0
- package/lib/session/worktrees.js.map +1 -0
- package/lib/views/approval-rule.d.ts +3 -0
- package/lib/views/approval-rule.js +4 -0
- package/lib/views/approval-rule.js.map +1 -0
- package/lib/views/at-complete.d.ts +8 -0
- package/lib/views/at-complete.js +64 -2
- package/lib/views/at-complete.js.map +1 -1
- package/lib/views/autocomplete.d.ts +22 -2
- package/lib/views/autocomplete.js +70 -40
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/copy-targets.d.ts +13 -14
- package/lib/views/copy-targets.js +42 -54
- 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.d.ts +1 -1
- package/lib/views/event-views.js +60 -6
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.d.ts +12 -0
- package/lib/views/file-actions.js +72 -0
- package/lib/views/file-actions.js.map +1 -0
- package/lib/views/history-search.js +15 -21
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkeys.d.ts +8 -1
- package/lib/views/hotkeys.js +53 -26
- 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 +34 -4
- package/lib/views/prompt-selector.js +270 -134
- 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 +51 -10
- package/lib/views/settings-list.js +267 -315
- 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-export.js +9 -0
- package/lib/views/transcript-export.js.map +1 -1
- package/lib/views/transcript-render.d.ts +97 -25
- package/lib/views/transcript-render.js +542 -170
- 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 +95 -65
- 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/steer.d.ts +0 -5
- package/lib/commands/steer.js +0 -24
- package/lib/commands/steer.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 -206
- package/lib/views/agent-hub.js.map +0 -1
- package/lib/views/copy-selector.d.ts +0 -51
- package/lib/views/copy-selector.js +0 -139
- package/lib/views/copy-selector.js.map +0 -1
|
@@ -6,30 +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
|
-
import { workspaceBlockLines } from "./workspace-changes.js";
|
|
21
|
+
import { workspaceBlockLines, WORKSPACE_COLLAPSED_LINES } from "./workspace-changes.js";
|
|
22
|
+
import { blockFileAction } from "./file-actions.js";
|
|
23
23
|
import { renderTool, toolArgSubject } from "../chrome/tool-renderers.js";
|
|
24
24
|
import { toolArgsObject } from "../chrome/tool-args.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { renderCommandOutput, renderCommandSeparator, renderShell } from "../chrome/command-output.js";
|
|
26
|
+
import { terminalColorsKey } from "../chrome/terminal-colors.js";
|
|
27
27
|
import { renderPathMentionRows } from "../chrome/path-mentions.js";
|
|
28
|
-
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 };
|
|
29
33
|
import { contentToText, prettyArgs, reasoningKey, } from "./transcript-types.js";
|
|
30
34
|
function isBlockPending(block) {
|
|
31
35
|
return (block.kind === 'assistant' && block.streaming) || (block.kind === 'tool' && block.status === 'running');
|
|
32
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
|
+
}
|
|
33
45
|
function exclusiveDiffs(presentation) {
|
|
34
46
|
if (presentation?.result?.card === 'diff')
|
|
35
47
|
return presentation.result.diffs;
|
|
@@ -107,7 +119,9 @@ function assistantMarkdown(source, theme, width, style) {
|
|
|
107
119
|
*/
|
|
108
120
|
function userBubble(text, theme, width) {
|
|
109
121
|
const gutter = ' ';
|
|
110
|
-
|
|
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);
|
|
111
125
|
const rows = ['', ...wrapped, ''];
|
|
112
126
|
// The padding rows share the prompt's tint, so the prompt is one slab that
|
|
113
127
|
// opens the turn. Unpainted, they were two blank rows of their own on top of
|
|
@@ -116,7 +130,8 @@ function userBubble(text, theme, width) {
|
|
|
116
130
|
return rows.map((row, index) => {
|
|
117
131
|
if (row === '')
|
|
118
132
|
return paintSurface(theme, 'userMessageBg', 'userMessageText', padToWidth('', width));
|
|
119
|
-
const
|
|
133
|
+
const promptMark = index === 1 && theme.colors ? '\x1b]133;A\x07' : '';
|
|
134
|
+
const content = padToWidth((index === 1 ? promptMark + gutter : '') + row, width);
|
|
120
135
|
return paintSurface(theme, 'userMessageBg', 'userMessageText', content);
|
|
121
136
|
});
|
|
122
137
|
}
|
|
@@ -275,7 +290,7 @@ function toolRowSubject(block, presentation, label) {
|
|
|
275
290
|
return firstLineOf(verb !== '' && title.startsWith(verb + ' ') ? title.slice(verb.length + 1) : title);
|
|
276
291
|
}
|
|
277
292
|
/** Tool content shared by a running turn and a completed turn opened for reading. */
|
|
278
|
-
function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt+O') {
|
|
293
|
+
function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt+O', surface = 'toolSuccessBg') {
|
|
279
294
|
const full = mode !== 'preview';
|
|
280
295
|
const presentation = renderTool({
|
|
281
296
|
name: block.name, arguments: prettyArgs(block.args), output: block.output,
|
|
@@ -301,8 +316,15 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
301
316
|
: call?.card === 'generic' || call?.card === 'diff' ? call.title
|
|
302
317
|
: !read && presentation.title !== undefined && presentation.title !== block.name ? presentation.title
|
|
303
318
|
: `${block.name}${subject === '' ? '' : ` ${subject}`}`;
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
306
328
|
let body = [];
|
|
307
329
|
if (mode === 'details') {
|
|
308
330
|
let input = block.args;
|
|
@@ -311,20 +333,26 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
311
333
|
}
|
|
312
334
|
catch { /* Partial arguments retain their received text. */ }
|
|
313
335
|
const output = presentation.output.length > 0 ? presentation.output : block.output.split('\n');
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
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));
|
|
317
343
|
body = [
|
|
318
|
-
...
|
|
319
|
-
|
|
320
|
-
|
|
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),
|
|
321
349
|
...outputRows,
|
|
322
350
|
];
|
|
323
351
|
// Keep the tool's rich presentation, but expose received text it replaced.
|
|
324
352
|
if (block.output !== '' && (diffs !== undefined || block.output !== output.join('\n'))) {
|
|
325
|
-
body.push('', ...
|
|
353
|
+
body.push('', ...wrapTextStable(theme.fg('dim', 'Raw result'), room));
|
|
326
354
|
for (const line of block.output.split('\n')) {
|
|
327
|
-
body.push(...
|
|
355
|
+
body.push(...wrapTextStable(theme.fg(block.status === 'error' ? 'error' : 'toolOutput', line), room));
|
|
328
356
|
}
|
|
329
357
|
}
|
|
330
358
|
}
|
|
@@ -332,13 +360,21 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
332
360
|
body = wrapPaintedDiffRows(alignFileDiffs(diffs), theme, room);
|
|
333
361
|
}
|
|
334
362
|
else if (full || !read || block.status === 'error') {
|
|
335
|
-
|
|
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);
|
|
336
371
|
const input = !shell && !read && (call?.card === 'generic' || (call === undefined && presentation.title !== block.name))
|
|
337
|
-
? presentation.input
|
|
338
|
-
|
|
372
|
+
? highlightedLines(presentation.input, language ?? 'text', theme, undefined, room)
|
|
373
|
+
: [];
|
|
374
|
+
body = [...input, ...output.flatMap(line => wrapTextStable(line, room))];
|
|
339
375
|
// Writes carry their content in arguments before the result arrives.
|
|
340
376
|
if (body.length === 0 && block.name === 'write' && typeof args?.['content'] === 'string') {
|
|
341
|
-
body = args['content'].split('\n')
|
|
377
|
+
body = highlightedLines(args['content'].split('\n'), toolPayloadLanguage('write', subject) ?? 'text', theme, undefined, room);
|
|
342
378
|
}
|
|
343
379
|
}
|
|
344
380
|
while (body.length > 0 && stripAnsi(body[body.length - 1]).trim() === '')
|
|
@@ -351,12 +387,12 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
351
387
|
const shortcut = detailsKey === 'Disabled' ? '' : ` · ${detailsKey}: Expand`;
|
|
352
388
|
const hint = theme.fg('dim', `… ${omitted} ${shell ? 'earlier' : 'more'} lines${shortcut}`);
|
|
353
389
|
if (shell)
|
|
354
|
-
body.unshift(...
|
|
390
|
+
body.unshift(...wrapTextStable(hint, room));
|
|
355
391
|
else
|
|
356
|
-
body.push(...
|
|
392
|
+
body.push(...wrapTextStable(hint, room));
|
|
357
393
|
}
|
|
358
394
|
if (!full && read && block.status !== 'error' && block.output !== '' && detailsKey !== 'Disabled') {
|
|
359
|
-
body.push(...
|
|
395
|
+
body.push(...wrapTextStable(theme.fg('dim', `${detailsKey}: Expand result`), room));
|
|
360
396
|
}
|
|
361
397
|
if (full && block.status === 'error' && call?.card === 'diff')
|
|
362
398
|
rows.push('', ...wrapPaintedDiffRows(alignFileDiffs(call.diffs), theme, room));
|
|
@@ -366,10 +402,15 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
366
402
|
rows.push(row);
|
|
367
403
|
}
|
|
368
404
|
if (block.status === 'error')
|
|
369
|
-
rows.push(...
|
|
370
|
-
if (block.job !== undefined)
|
|
371
|
-
|
|
372
|
-
|
|
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;
|
|
373
414
|
return ['', ...rows, ''].map(row => paintSurface(theme, bg, 'toolOutput', padToWidth(' '.repeat(padding + nest) + row, width)));
|
|
374
415
|
}
|
|
375
416
|
function firstLineOf(text) {
|
|
@@ -380,6 +421,18 @@ function firstLineOf(text) {
|
|
|
380
421
|
}
|
|
381
422
|
return '';
|
|
382
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
|
+
}
|
|
383
436
|
/** Thinking uses the same quiet Markdown style throughout the turn. */
|
|
384
437
|
function openReasoningLines(block, theme, width, mathMode = 'auto') {
|
|
385
438
|
return assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true, mathMode });
|
|
@@ -400,27 +453,103 @@ function groupMembers(group) {
|
|
|
400
453
|
* last row and its words are the answer painted below the run, so the run
|
|
401
454
|
* paints the thought and nothing else.
|
|
402
455
|
*/
|
|
403
|
-
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) {
|
|
404
459
|
const { blocks, options, theme, width } = context;
|
|
405
460
|
const block = blocks[index];
|
|
406
461
|
// Search must reveal a match even when it falls outside a tool's normal preview.
|
|
407
462
|
const full = block.kind === 'tool' && (context.matches.has(index) || options.expandedTools?.has(block.callId) === true);
|
|
408
|
-
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');
|
|
409
467
|
const cached = groupMemberCache.get(block);
|
|
410
468
|
let lines;
|
|
411
469
|
if (cached?.signature === signature)
|
|
412
470
|
lines = cached.lines;
|
|
413
471
|
else {
|
|
414
|
-
lines = block.kind === 'tool' ? toolBlockLines(block, theme, width, full ? 'details' : 'preview', options.toolDetailsKey)
|
|
415
|
-
: block.kind === 'assistant' ? thoughtOnly
|
|
416
|
-
|
|
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
|
+
})
|
|
417
483
|
: block.kind === 'notice' ? blockLines(block, theme, width) : [];
|
|
418
484
|
groupMemberCache.set(block, { signature, lines });
|
|
419
485
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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));
|
|
424
553
|
}
|
|
425
554
|
/** A notice that reports on the run's work, as one of its rows. */
|
|
426
555
|
function processNoticeLine(block, theme, width, indent) {
|
|
@@ -439,25 +568,21 @@ const groupMemberCache = new WeakMap();
|
|
|
439
568
|
function processGroupBody(group, context) {
|
|
440
569
|
const lines = [];
|
|
441
570
|
const offsets = new Map();
|
|
571
|
+
const spans = [];
|
|
442
572
|
for (const index of groupMembers(group)) {
|
|
443
|
-
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;
|
|
444
577
|
if (rows.length > 0 && lines.length > 0)
|
|
445
578
|
lines.push('');
|
|
446
579
|
offsets.set(index, lines.length);
|
|
580
|
+
if (context.blocks[index]?.kind === 'tool')
|
|
581
|
+
spans.push({ index, start: lines.length, end: lines.length + rows.length });
|
|
447
582
|
for (const row of rows)
|
|
448
583
|
lines.push(row);
|
|
449
584
|
}
|
|
450
|
-
return { lines, offsets };
|
|
451
|
-
}
|
|
452
|
-
/** A settled turn's length, the way a person would say it. */
|
|
453
|
-
export function formatElapsed(ms) {
|
|
454
|
-
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
455
|
-
if (seconds < 60)
|
|
456
|
-
return `${seconds}s`;
|
|
457
|
-
const minutes = Math.floor(seconds / 60);
|
|
458
|
-
if (minutes < 60)
|
|
459
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
460
|
-
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
585
|
+
return { lines, offsets, spans };
|
|
461
586
|
}
|
|
462
587
|
/** A completed turn's duration and any failures remain visible when folded. */
|
|
463
588
|
function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O') {
|
|
@@ -475,13 +600,16 @@ function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O')
|
|
|
475
600
|
* to direct callers; turn inspection uses previews. omitReasoning excludes
|
|
476
601
|
* thinking already drawn by a run.
|
|
477
602
|
*/
|
|
478
|
-
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' } = {}) {
|
|
479
604
|
if (block.kind === 'user')
|
|
480
605
|
return userBubble(block.text, theme, width);
|
|
481
606
|
if (block.kind === 'assistant') {
|
|
482
607
|
const lines = [];
|
|
483
608
|
if (block.reasoning !== '' && !omitReasoning) {
|
|
484
|
-
|
|
609
|
+
const collapsed = hideThinking && !thinkingOpen;
|
|
610
|
+
lines.push(...(collapsed
|
|
611
|
+
? [reasoningSummaryRow(block.reasoning, theme, width)]
|
|
612
|
+
: openReasoningLines(block, theme, width, mathMode)));
|
|
485
613
|
if (block.text !== '')
|
|
486
614
|
lines.push('');
|
|
487
615
|
}
|
|
@@ -501,12 +629,30 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
501
629
|
if (block.kind === 'tool') {
|
|
502
630
|
return toolBlockLines(block, theme, width, toolsExpanded ? 'full' : 'preview', toolDetailsKey);
|
|
503
631
|
}
|
|
504
|
-
if (block.kind === 'toolCatalog')
|
|
505
|
-
return renderToolsPanel(block.tools, theme, width, toolsExpanded);
|
|
506
632
|
if (block.kind === 'workspace')
|
|
507
633
|
return workspaceBlockLines(block, theme, width, toolsExpanded);
|
|
508
|
-
if (block.kind === 'commandOutput')
|
|
509
|
-
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);
|
|
510
656
|
if (block.boundary === true)
|
|
511
657
|
return [renderTranscriptBoundary(block.text, theme, width)];
|
|
512
658
|
if (block.process !== undefined && block.framed !== true)
|
|
@@ -515,11 +661,11 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
515
661
|
const prefix = ' ';
|
|
516
662
|
const continuation = ' '.repeat(visibleWidth(prefix));
|
|
517
663
|
const tone = block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'dim';
|
|
518
|
-
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));
|
|
519
665
|
}
|
|
520
666
|
const state = block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'info';
|
|
521
667
|
const paint = (text) => theme.fg(block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'dim', text);
|
|
522
|
-
const wrapped =
|
|
668
|
+
const wrapped = wrapTextStable(block.text, Math.max(1, width - 4));
|
|
523
669
|
const title = paint(wrapped[0] ?? '');
|
|
524
670
|
return renderFramedBlock({
|
|
525
671
|
header: title,
|
|
@@ -563,6 +709,55 @@ function renderTranscriptBoundary(label, theme, width) {
|
|
|
563
709
|
const left = Math.floor(room / 2);
|
|
564
710
|
return rule.repeat(left) + ' ' + labelText + ' ' + rule.repeat(room - left);
|
|
565
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
|
+
}
|
|
566
761
|
/** Plain text of one block, used by transcript search and match painting. */
|
|
567
762
|
export function blockSearchText(block) {
|
|
568
763
|
if (block.kind === 'assistant') {
|
|
@@ -570,14 +765,13 @@ export function blockSearchText(block) {
|
|
|
570
765
|
}
|
|
571
766
|
if (block.kind === 'tool')
|
|
572
767
|
return `${block.args}\n${block.output}${block.job === undefined ? '' : `\n${block.job.id} ${block.job.status} ${block.job.detail ?? ''}`}`;
|
|
573
|
-
if (block.kind === 'toolCatalog') {
|
|
574
|
-
return block.tools.map(tool => `${tool.name} ${tool.description}`).join('\n');
|
|
575
|
-
}
|
|
576
768
|
if (block.kind === 'workspace') {
|
|
577
769
|
// Only the served file paths are searchable; a turn whose summary the
|
|
578
770
|
// host no longer holds contributes no path text.
|
|
579
771
|
return block.summary?.files.map(file => file.display).join('\n') ?? `turn ${block.turn}`;
|
|
580
772
|
}
|
|
773
|
+
if (block.kind === 'shell')
|
|
774
|
+
return `${block.command}\n${block.output}`;
|
|
581
775
|
return block.text;
|
|
582
776
|
}
|
|
583
777
|
function fitFrame(lines, width, stablePrefix = 0) {
|
|
@@ -601,35 +795,53 @@ function fitFrame(lines, width, stablePrefix = 0) {
|
|
|
601
795
|
const transcriptBodyCache = new WeakMap();
|
|
602
796
|
/** Settled immutable blocks keep their expensive Markdown/tool layout. */
|
|
603
797
|
const blockLinesCache = new WeakMap();
|
|
604
|
-
function cachedBlockLines(block, options, theme, themeName,
|
|
798
|
+
function cachedBlockLines(block, options, theme, themeName, depth, expanded, omitReasoning = false) {
|
|
605
799
|
const cached = blockLinesCache.get(block);
|
|
606
800
|
if (cached !== undefined
|
|
607
801
|
&& cached.width === options.width
|
|
608
802
|
&& cached.colors === options.colors
|
|
609
|
-
&& cached.
|
|
803
|
+
&& cached.depth === depth
|
|
610
804
|
&& cached.themeName === themeName
|
|
805
|
+
&& cached.terminalKey === terminalKey(options)
|
|
611
806
|
&& cached.expanded === expanded
|
|
612
807
|
&& cached.omitReasoning === omitReasoning
|
|
808
|
+
&& cached.hideThinking === (options.hideThinking === true)
|
|
809
|
+
&& cached.thinkingOpen === (block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true)
|
|
613
810
|
&& cached.mathMode === options.mathMode
|
|
614
811
|
&& cached.mermaidMode === options.mermaidMode
|
|
615
812
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
616
|
-
&& 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))
|
|
617
815
|
return cached.lines;
|
|
618
816
|
const toolDetails = block.kind === 'tool' && options.expandedTools?.has(block.callId) === true;
|
|
619
817
|
const lines = block.kind === 'tool' && toolDetails
|
|
620
818
|
? toolBlockLines(block, theme, options.width, 'details', options.toolDetailsKey)
|
|
621
|
-
: 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
|
+
});
|
|
622
830
|
blockLinesCache.set(block, {
|
|
623
831
|
mathMode: options.mathMode,
|
|
624
832
|
mermaidMode: options.mermaidMode,
|
|
625
833
|
toolDetailsKey: options.toolDetailsKey,
|
|
626
834
|
toolDetails,
|
|
835
|
+
expandedOutput: options.expandedOutputs?.has(block) === true,
|
|
627
836
|
width: options.width,
|
|
628
837
|
colors: options.colors,
|
|
629
|
-
|
|
838
|
+
depth,
|
|
630
839
|
themeName,
|
|
840
|
+
terminalKey: terminalKey(options),
|
|
631
841
|
expanded,
|
|
632
842
|
omitReasoning,
|
|
843
|
+
hideThinking: options.hideThinking === true,
|
|
844
|
+
thinkingOpen: block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true,
|
|
633
845
|
lines,
|
|
634
846
|
});
|
|
635
847
|
return lines;
|
|
@@ -653,7 +865,7 @@ const PROCESS_LABELS = [
|
|
|
653
865
|
{ match: /^(web_search)$/, label: 'Search web' },
|
|
654
866
|
{ match: /^(web_fetch)$/, label: 'Fetch page' },
|
|
655
867
|
{
|
|
656
|
-
match: /^(subagent|subagent_fork|subagent_isolated|agent|ralph|workflow_run)$/, label: 'Run subagent',
|
|
868
|
+
match: /^(subagent|subagent_fork|subagent_isolated|subagent_codex|subagent_claude_code|agent|ralph|workflow_run)$/, label: 'Run subagent',
|
|
657
869
|
},
|
|
658
870
|
{
|
|
659
871
|
match: /^(session_search|session_trace|session_event_read|session_event_search|session_event_trace)$/,
|
|
@@ -861,29 +1073,46 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
861
1073
|
const toolsExpanded = options.toolsExpanded === true;
|
|
862
1074
|
const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
|
|
863
1075
|
const expandedReasoning = [...(options.expandedReasoning ?? [])].sort().join('\0');
|
|
1076
|
+
const hideThinking = options.hideThinking === true;
|
|
864
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');
|
|
865
1081
|
const themeName = options.themeName ?? 'dark';
|
|
866
|
-
const
|
|
1082
|
+
const depth = colorDepthOf(options);
|
|
867
1083
|
const animatedSpinnerFrame = state.blocks.some(block => block.kind === 'tool' && block.status === 'running')
|
|
868
1084
|
? spinnerFrame
|
|
869
1085
|
: -1;
|
|
870
1086
|
const search = options.transcriptSearch;
|
|
871
|
-
const searchKey = search === undefined
|
|
872
|
-
|
|
873
|
-
|
|
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);
|
|
874
1100
|
const cached = transcriptBodyCache.get(state.blocks);
|
|
875
1101
|
if (cached !== undefined
|
|
876
1102
|
&& cached.width === options.width
|
|
877
1103
|
&& cached.colors === options.colors
|
|
878
|
-
&& cached.
|
|
1104
|
+
&& cached.depth === depth
|
|
879
1105
|
&& cached.themeName === themeName
|
|
1106
|
+
&& cached.terminalKey === terminalKey(options)
|
|
880
1107
|
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
881
1108
|
&& cached.mathMode === options.mathMode
|
|
882
1109
|
&& cached.mermaidMode === options.mermaidMode
|
|
883
1110
|
&& cached.toolsExpanded === toolsExpanded
|
|
884
1111
|
&& cached.expandedTools === expandedTools
|
|
885
1112
|
&& cached.expandedReasoning === expandedReasoning
|
|
1113
|
+
&& cached.hideThinking === hideThinking
|
|
886
1114
|
&& cached.openedGroups === openedGroups
|
|
1115
|
+
&& cached.expandedOutputs === expandedOutputs
|
|
887
1116
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
888
1117
|
&& cached.turnSpans === state.turnSpans
|
|
889
1118
|
&& cached.turnDetailsKey === options.turnDetailsKey
|
|
@@ -892,15 +1121,33 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
892
1121
|
&& cached.focusBlock === options.focusBlock) {
|
|
893
1122
|
return {
|
|
894
1123
|
lines: cached.lines,
|
|
1124
|
+
fileRegions: cached.fileRegions,
|
|
895
1125
|
blockStarts: cached.blockStarts,
|
|
896
1126
|
blockDrawStarts: cached.blockDrawStarts,
|
|
897
1127
|
foldShape: cached.foldShape,
|
|
898
1128
|
foldMarks: cached.foldMarks,
|
|
899
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,
|
|
900
1133
|
};
|
|
901
1134
|
}
|
|
902
|
-
const matches = new Set(
|
|
1135
|
+
const matches = new Set(occurrences.map(match => match.block));
|
|
903
1136
|
const lines = [];
|
|
1137
|
+
const fileRegions = [];
|
|
1138
|
+
const filesAt = (index, start, length, expanded = false) => {
|
|
1139
|
+
const block = state.blocks[index];
|
|
1140
|
+
if (length === 0 || blockFileAction(block) === undefined)
|
|
1141
|
+
return;
|
|
1142
|
+
if (block.kind === 'workspace') {
|
|
1143
|
+
fileRegions.push({ block: index, start, end: start + 1 });
|
|
1144
|
+
const visible = Math.min(block.summary.files.length, expanded ? length - 1 : WORKSPACE_COLLAPSED_LINES);
|
|
1145
|
+
for (let file = 0; file < visible; file += 1)
|
|
1146
|
+
fileRegions.push({ block: index, start: start + file + 1, end: start + file + 2, file });
|
|
1147
|
+
}
|
|
1148
|
+
else
|
|
1149
|
+
fileRegions.push({ block: index, start, end: start + length });
|
|
1150
|
+
};
|
|
904
1151
|
const blockStarts = [];
|
|
905
1152
|
// Where each block is drawn, apart from the offsets above: a search can aim a
|
|
906
1153
|
// block's offset at the thought it also carries, but its drawn span does not
|
|
@@ -960,10 +1207,16 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
960
1207
|
const foldMarks = [];
|
|
961
1208
|
if (toolsExpanded)
|
|
962
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 });
|
|
963
1214
|
let liveFrom;
|
|
964
1215
|
const context = {
|
|
965
|
-
blocks: state.blocks, theme, themeName,
|
|
966
|
-
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 },
|
|
967
1220
|
};
|
|
968
1221
|
const compactProcessRow = (block) => {
|
|
969
1222
|
if (block?.kind === 'notice')
|
|
@@ -993,21 +1246,34 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
993
1246
|
lines.push('');
|
|
994
1247
|
const headerRow = lines.length;
|
|
995
1248
|
const opened = openGroups.has(groupIndex);
|
|
996
|
-
|
|
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';
|
|
997
1261
|
foldMarks.push({ key: `run:${group.key}:${shape}`, row: headerRow });
|
|
998
1262
|
if (group.live)
|
|
999
1263
|
liveFrom ??= headerRow;
|
|
1000
1264
|
// A live run has no header: it becomes a group when its turn ends.
|
|
1001
|
-
if (!group.live && !
|
|
1265
|
+
if (!group.live && !shown) {
|
|
1002
1266
|
lines.push(...processGroupHeader(group, theme, options.width, elapsedOf(groupIndex), options.turnDetailsKey));
|
|
1003
1267
|
}
|
|
1004
1268
|
const bodyRow = lines.length;
|
|
1005
1269
|
let offsets = new Map();
|
|
1006
|
-
if (group.live ||
|
|
1270
|
+
if (group.live || shown) {
|
|
1007
1271
|
const body = processGroupBody(group, context);
|
|
1008
1272
|
for (const line of body.lines)
|
|
1009
1273
|
lines.push(line);
|
|
1010
1274
|
offsets = body.offsets;
|
|
1275
|
+
for (const span of body.spans)
|
|
1276
|
+
filesAt(span.index, bodyRow + span.start, span.end - span.start);
|
|
1011
1277
|
}
|
|
1012
1278
|
else {
|
|
1013
1279
|
for (let at = group.start; at < (group.answer ?? group.until); at += 1) {
|
|
@@ -1016,7 +1282,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1016
1282
|
&& !(member.kind === 'notice' && member.level === 'error'))
|
|
1017
1283
|
continue;
|
|
1018
1284
|
offsets.set(at, lines.length - bodyRow);
|
|
1019
|
-
|
|
1285
|
+
const rows = member.kind === 'tool' ? toolRowLine(member, theme, options.width, spinnerFrame) : groupMemberLines(at, context);
|
|
1286
|
+
filesAt(at, lines.length, rows.length);
|
|
1287
|
+
lines.push(...rows);
|
|
1020
1288
|
}
|
|
1021
1289
|
}
|
|
1022
1290
|
// The turn's answer, painted as a row of its own between the run's work
|
|
@@ -1045,10 +1313,8 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1045
1313
|
blockDrawStarts[group.answer] = lines.length;
|
|
1046
1314
|
offsets.set(group.answer, lines.length - bodyRow);
|
|
1047
1315
|
const answer = state.blocks[group.answer];
|
|
1048
|
-
const rendered = cachedBlockLines(answer, options, theme, themeName,
|
|
1049
|
-
for (const line of (
|
|
1050
|
-
? rendered.map(row => blockMatchesQuery(stripAnsi(row), search.query) ? theme.inverse(row) : row)
|
|
1051
|
-
: 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))
|
|
1052
1318
|
lines.push(line);
|
|
1053
1319
|
if (thoughtHit && thoughtRow !== undefined)
|
|
1054
1320
|
blockStarts[group.answer] = bodyRow + thoughtRow;
|
|
@@ -1074,7 +1340,8 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1074
1340
|
offsets.set(at, lines.length - bodyRow);
|
|
1075
1341
|
const rows = !group.live && !opened && trailing?.kind === 'tool'
|
|
1076
1342
|
? toolRowLine(trailing, theme, options.width, spinnerFrame)
|
|
1077
|
-
: groupMemberLines(at, context);
|
|
1343
|
+
: groupMemberLines(at, context, false, 0, group.live || opened);
|
|
1344
|
+
filesAt(at, lines.length, rows.length);
|
|
1078
1345
|
for (const line of rows)
|
|
1079
1346
|
lines.push(line);
|
|
1080
1347
|
}
|
|
@@ -1135,10 +1402,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1135
1402
|
liveFrom ??= lines.length;
|
|
1136
1403
|
const expanded = (block.kind === 'workspace' && groups.some((group, at) => group.turn === block.turn && openGroups.has(at))) || matches.has(index) || toolsExpanded
|
|
1137
1404
|
|| (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
|
|
1138
|
-
const rendered = cachedBlockLines(block, options, theme, themeName,
|
|
1139
|
-
lines.
|
|
1140
|
-
|
|
1141
|
-
: rendered));
|
|
1405
|
+
const rendered = cachedBlockLines(block, options, theme, themeName, depth, expanded, absorbed.has(index));
|
|
1406
|
+
filesAt(index, lines.length, rendered.length, expanded);
|
|
1407
|
+
lines.push(...paintSearch(rendered, index, occurrences, theme));
|
|
1142
1408
|
blockDrawStarts[index] = blockStarts[index];
|
|
1143
1409
|
previous = block;
|
|
1144
1410
|
}
|
|
@@ -1146,6 +1412,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1146
1412
|
// this; opening, folding, or a run settling at turn end do.
|
|
1147
1413
|
const foldShape = foldMarks.map(mark => mark.key).join('\u0001');
|
|
1148
1414
|
transcriptBodyCache.set(state.blocks, {
|
|
1415
|
+
fileRegions,
|
|
1149
1416
|
mathMode: options.mathMode,
|
|
1150
1417
|
mermaidMode: options.mermaidMode,
|
|
1151
1418
|
toolDetailsKey: options.toolDetailsKey,
|
|
@@ -1153,13 +1420,16 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1153
1420
|
blockDrawStarts,
|
|
1154
1421
|
width: options.width,
|
|
1155
1422
|
colors: options.colors,
|
|
1156
|
-
|
|
1423
|
+
depth,
|
|
1157
1424
|
themeName,
|
|
1425
|
+
terminalKey: terminalKey(options),
|
|
1158
1426
|
spinnerFrame: animatedSpinnerFrame,
|
|
1159
1427
|
toolsExpanded,
|
|
1160
1428
|
expandedTools,
|
|
1161
1429
|
expandedReasoning,
|
|
1430
|
+
hideThinking,
|
|
1162
1431
|
openedGroups,
|
|
1432
|
+
expandedOutputs,
|
|
1163
1433
|
searchKey,
|
|
1164
1434
|
turnSpans: state.turnSpans,
|
|
1165
1435
|
status: state.status,
|
|
@@ -1170,11 +1440,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1170
1440
|
lines,
|
|
1171
1441
|
blockStarts,
|
|
1172
1442
|
});
|
|
1173
|
-
return { lines, blockStarts, blockDrawStarts, foldShape, foldMarks, liveFrom };
|
|
1443
|
+
return { lines, blockStarts, blockDrawStarts, fileRegions, foldShape, foldMarks, liveFrom, occurrences };
|
|
1174
1444
|
}
|
|
1175
1445
|
function commandSurfaceName(block) {
|
|
1176
|
-
if (block?.kind === 'toolCatalog')
|
|
1177
|
-
return 'tools';
|
|
1178
1446
|
if (block?.kind === 'commandOutput')
|
|
1179
1447
|
return block.command;
|
|
1180
1448
|
return undefined;
|
|
@@ -1353,6 +1621,39 @@ function todoPreviewStart(todos) {
|
|
|
1353
1621
|
const pending = todos.findIndex(todo => todo.status === 'pending');
|
|
1354
1622
|
return pending >= 0 ? pending : Math.max(0, todos.length - TODO_PREVIEW);
|
|
1355
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
|
+
}
|
|
1356
1657
|
/** Compact, unframed Todo tree placed above queued messages. */
|
|
1357
1658
|
export function renderTodos(todos, theme, width) {
|
|
1358
1659
|
if (todos.length === 0 || width <= 0)
|
|
@@ -1382,7 +1683,8 @@ export function renderTodos(todos, theme, width) {
|
|
|
1382
1683
|
})];
|
|
1383
1684
|
return lines.map(line => truncateToWidth(line, width));
|
|
1384
1685
|
}
|
|
1385
|
-
|
|
1686
|
+
/** First row the bar shows; the provider starts its selection here. */
|
|
1687
|
+
export function subagentPreviewStart(agents) {
|
|
1386
1688
|
if (agents.length <= SUBAGENT_PREVIEW)
|
|
1387
1689
|
return 0;
|
|
1388
1690
|
const active = agents.findIndex(agent => agent.phase === 'running' || agent.phase === 'starting');
|
|
@@ -1406,12 +1708,24 @@ function subagentPhaseGlyph(phase, theme, spinnerFrame) {
|
|
|
1406
1708
|
const SUBAGENT_PHASE_LABELS = {
|
|
1407
1709
|
starting: 'Starting', running: 'Running', waiting: 'Waiting', completed: 'Done', error: 'Failed',
|
|
1408
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
|
+
}
|
|
1409
1723
|
function subagentRowText(agent) {
|
|
1410
1724
|
const indent = agent.depth > 1 ? ' '.repeat(agent.depth - 1) : '';
|
|
1411
1725
|
return `${indent}${agent.label} · ${SUBAGENT_PHASE_LABELS[agent.phase]}`;
|
|
1412
1726
|
}
|
|
1413
1727
|
/** Compact, unframed descendant-subagent tree placed above Todos. */
|
|
1414
|
-
export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspectedId,
|
|
1728
|
+
export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspectedId, selected) {
|
|
1415
1729
|
const agents = roster?.agents ?? [];
|
|
1416
1730
|
if (agents.length === 0 || width <= 0)
|
|
1417
1731
|
return [];
|
|
@@ -1425,10 +1739,16 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1425
1739
|
done === 0 ? undefined : `${done} done`,
|
|
1426
1740
|
failed === 0 ? undefined : `${failed} failed`,
|
|
1427
1741
|
].filter((part) => part !== undefined);
|
|
1742
|
+
const pointing = selected !== undefined;
|
|
1428
1743
|
const headerBody = theme.bold(theme.fg('accent', 'Agents'))
|
|
1429
1744
|
+ (counts.length === 0 ? '' : theme.fg('dim', ` · ${counts.join(' · ')}`))
|
|
1430
|
-
|
|
1431
|
-
|
|
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}`}`));
|
|
1432
1752
|
const start = subagentPreviewStart(agents);
|
|
1433
1753
|
const end = Math.min(agents.length, start + SUBAGENT_PREVIEW);
|
|
1434
1754
|
const rows = [
|
|
@@ -1436,11 +1756,11 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1436
1756
|
...agents.slice(start, end),
|
|
1437
1757
|
...(end === agents.length ? [] : [`… ${agents.length - end} more`]),
|
|
1438
1758
|
];
|
|
1439
|
-
const lines = [header, ...rows.map((row, index) => {
|
|
1759
|
+
const lines = [header, ...workflows, ...rows.map((row, index) => {
|
|
1440
1760
|
const branch = ' ' + theme.fg('dim', index === rows.length - 1 ? '└─' : '├─') + ' ';
|
|
1441
1761
|
if (typeof row === 'string')
|
|
1442
1762
|
return branch + theme.fg('dim', row);
|
|
1443
|
-
const
|
|
1763
|
+
const current = inspectedId === row.id;
|
|
1444
1764
|
const paint = row.phase === 'completed'
|
|
1445
1765
|
? (text) => theme.fg('dim', theme.strikethrough(text))
|
|
1446
1766
|
: row.phase === 'error'
|
|
@@ -1448,9 +1768,10 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1448
1768
|
: row.phase === 'waiting'
|
|
1449
1769
|
? (text) => theme.fg('dim', text)
|
|
1450
1770
|
: (text) => text;
|
|
1451
|
-
const
|
|
1771
|
+
const pointed = index === selected;
|
|
1772
|
+
const marker = current || pointed ? theme.fg('accent', SYMBOL.cursor) + ' ' : '';
|
|
1452
1773
|
const body = marker + subagentPhaseGlyph(row.phase, theme, spinnerFrame) + ' ' + paint(subagentRowText(row));
|
|
1453
|
-
return branch + (
|
|
1774
|
+
return branch + (current || pointed ? theme.bold(body) : body);
|
|
1454
1775
|
})];
|
|
1455
1776
|
return lines.map(line => truncateToWidth(line, width));
|
|
1456
1777
|
}
|
|
@@ -1540,27 +1861,32 @@ export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
|
1540
1861
|
const paintedLeft = truncateToWidth(left, width - actionWidth - 2);
|
|
1541
1862
|
return paintedLeft + ' ' + action;
|
|
1542
1863
|
};
|
|
1864
|
+
const editSingleHint = width >= 72 ? '↑/Alt+Up edit · Alt+Q queue' : '↑ edit';
|
|
1865
|
+
const editMultiHint = width >= 72 ? '↑/Alt+Up edit latest · Alt+Q queue' : '↑ edit latest';
|
|
1543
1866
|
if (labels.length === 1) {
|
|
1544
|
-
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))];
|
|
1545
1871
|
}
|
|
1546
|
-
const summary = ` · ${labels.length} ·
|
|
1872
|
+
const summary = ` · ${labels.length} · follow-up${hidden === 0 ? '' : ` · ${hidden} earlier`}`;
|
|
1547
1873
|
const visibleLabels = labels.slice(start);
|
|
1548
1874
|
const lines = [
|
|
1549
|
-
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim',
|
|
1875
|
+
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim', editMultiHint), false),
|
|
1550
1876
|
...visibleLabels.map((label, index) => rail + theme.fg(index === visibleLabels.length - 1 ? 'accent' : 'dim', String(start + index + 1))
|
|
1551
1877
|
+ ' ' + theme.fg('text', label)),
|
|
1552
1878
|
];
|
|
1553
1879
|
return lines.map(line => truncateToWidth(line, width));
|
|
1554
1880
|
}
|
|
1555
1881
|
/**
|
|
1556
|
-
* Compose the
|
|
1882
|
+
* Compose the startup header, transcript, working row, and ruled editor into
|
|
1557
1883
|
* one frame — the oh-my-pi surface.
|
|
1558
1884
|
* @param state - transcript state.
|
|
1559
1885
|
* @param options - terminal geometry and input state.
|
|
1560
1886
|
* @returns the frame to hand to a renderer.
|
|
1561
1887
|
*/
|
|
1562
1888
|
export function renderView(state, options) {
|
|
1563
|
-
const theme = createTheme(options.colors, options.
|
|
1889
|
+
const theme = createTheme(options.colors === true, options.colorDepth ?? 'truecolor', options.themeName ?? 'dark', options.terminal);
|
|
1564
1890
|
const width = options.width;
|
|
1565
1891
|
const height = options.height;
|
|
1566
1892
|
const appName = options.appName ?? 'omdsh';
|
|
@@ -1568,16 +1894,6 @@ export function renderView(state, options) {
|
|
|
1568
1894
|
const pwd = options.pwd ?? '';
|
|
1569
1895
|
const motion = options.motion ?? 'full';
|
|
1570
1896
|
const spinnerFrame = motion === 'off' ? -1 : options.spinnerFrame ?? 0;
|
|
1571
|
-
if (options.agentHub !== undefined) {
|
|
1572
|
-
const hub = renderAgentHub(options.agentHub, theme, width, height);
|
|
1573
|
-
return {
|
|
1574
|
-
lines: fitFrame(hub.lines, width),
|
|
1575
|
-
cursor: hub.cursor,
|
|
1576
|
-
cursorVisible: hub.cursorVisible,
|
|
1577
|
-
// A full-screen surface drew no transcript body.
|
|
1578
|
-
documentRows: null,
|
|
1579
|
-
};
|
|
1580
|
-
}
|
|
1581
1897
|
if (options.trajectory !== undefined) {
|
|
1582
1898
|
const trajectory = renderTrajectory(options.trajectory, theme, width, height);
|
|
1583
1899
|
return {
|
|
@@ -1598,17 +1914,6 @@ export function renderView(state, options) {
|
|
|
1598
1914
|
documentRows: null,
|
|
1599
1915
|
};
|
|
1600
1916
|
}
|
|
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);
|
|
1603
|
-
return {
|
|
1604
|
-
lines: fitFrame(selector.lines, width),
|
|
1605
|
-
...(selector.document === undefined ? {} : { promptDocument: selector.document }),
|
|
1606
|
-
cursor: selector.cursor,
|
|
1607
|
-
cursorVisible: true,
|
|
1608
|
-
// A full-screen surface drew no transcript body.
|
|
1609
|
-
documentRows: null,
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
1917
|
if (options.promptSelector?.request.presentation === 'plan-review' || options.promptSelector?.request.presentation === 'document') {
|
|
1613
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);
|
|
1614
1919
|
return {
|
|
@@ -1623,12 +1928,8 @@ export function renderView(state, options) {
|
|
|
1623
1928
|
}
|
|
1624
1929
|
const welcome = renderWelcome({
|
|
1625
1930
|
width,
|
|
1626
|
-
model: options.model,
|
|
1627
|
-
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
|
|
1628
1931
|
version,
|
|
1629
|
-
appName,
|
|
1630
|
-
...(options.recentSessions === undefined ? {} : { recentSessions: options.recentSessions }),
|
|
1631
|
-
...(options.welcomeTips === undefined ? {} : { tips: options.welcomeTips }),
|
|
1932
|
+
appName: options.displayName ?? appName,
|
|
1632
1933
|
}, theme);
|
|
1633
1934
|
const transcript = renderTranscriptBody(state, options, theme, spinnerFrame);
|
|
1634
1935
|
const body = [...welcome];
|
|
@@ -1639,22 +1940,37 @@ export function renderView(state, options) {
|
|
|
1639
1940
|
transcriptStart = body.length;
|
|
1640
1941
|
body.push(...transcript.lines);
|
|
1641
1942
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
|
1644
1948
|
: state.status === 'compacting'
|
|
1645
|
-
?
|
|
1646
|
-
:
|
|
1949
|
+
? options.workingLabel ?? 'Compacting'
|
|
1950
|
+
: undefined;
|
|
1951
|
+
const working = [];
|
|
1647
1952
|
const statusBar = resolveStatusBarConfig(options.statusBar, options.statusPreset);
|
|
1953
|
+
const placeholder = options.inspected === undefined && state.status === 'idle'
|
|
1954
|
+
&& options.input === '' && (options.inputImages ?? 0) === 0
|
|
1955
|
+
? options.modelReady === false
|
|
1956
|
+
? 'Run /login to connect a model'
|
|
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'
|
|
1962
|
+
: null;
|
|
1648
1963
|
const inlineHint = options.transcriptSearch !== undefined
|
|
1649
1964
|
? transcriptSearchHint({
|
|
1650
1965
|
query: options.transcriptSearch.query,
|
|
1651
1966
|
editing: options.transcriptSearch.editing,
|
|
1652
1967
|
focus: options.transcriptSearch.focus,
|
|
1653
|
-
},
|
|
1968
|
+
}, transcript.occurrences.length)
|
|
1654
1969
|
: options.inspected === undefined
|
|
1655
1970
|
? slashInlineHint(options.input, options.inputCursor, options.commands)
|
|
1971
|
+
?? placeholder
|
|
1656
1972
|
?? (state.status === 'running' && options.steerKey !== undefined
|
|
1657
|
-
? `Enter:
|
|
1973
|
+
? `Enter: follow-up · ${options.steerKey}: next step` : null)
|
|
1658
1974
|
: options.inspected.writable
|
|
1659
1975
|
? slashInlineHint(options.input, options.inputCursor, options.commands) ?? 'Enter to steer · Esc to return'
|
|
1660
1976
|
: 'Read-only · Esc to return';
|
|
@@ -1677,29 +1993,52 @@ export function renderView(state, options) {
|
|
|
1677
1993
|
inputCursor: options.inputCursor,
|
|
1678
1994
|
status: ' ' + theme.fg('accent', '🐳') + ' ',
|
|
1679
1995
|
...(permissionBadge === '' ? {} : { statusRight: ' ' + permissionBadge + ' ' }),
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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',
|
|
1684
2006
|
...(theme.colors && (leadingSlashCommandNameRange(options.input) !== null
|
|
1685
2007
|
|| (options.inputImages !== undefined && options.inputImages !== 0))
|
|
1686
2008
|
? { paintInput: (text, start) => paintComposerInputSlice(options.input, text, start, theme) }
|
|
1687
2009
|
: {}),
|
|
1688
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
|
+
}),
|
|
1689
2020
|
};
|
|
1690
|
-
|
|
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
|
|
1691
2033
|
? undefined
|
|
1692
|
-
: 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))));
|
|
1693
2035
|
const settings = promptSelector !== undefined || options.settings === undefined
|
|
1694
2036
|
? undefined
|
|
1695
2037
|
: renderSettings(options.settings, theme, width);
|
|
1696
|
-
const
|
|
1697
|
-
? undefined
|
|
1698
|
-
: renderCopySelector(options.copySelector, theme, width);
|
|
1699
|
-
const search = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || options.historySearch === undefined
|
|
2038
|
+
const search = promptSelector !== undefined || settings !== undefined || options.historySearch === undefined
|
|
1700
2039
|
? undefined
|
|
1701
2040
|
: renderHistorySearch(options.historySearch, theme, width, Math.max(1, Math.min(HISTORY_SEARCH_MAX_VISIBLE, height - working.length - 8)));
|
|
1702
|
-
const editor = promptSelector === undefined && settings === undefined &&
|
|
2041
|
+
const editor = promptSelector === undefined && settings === undefined && search === undefined
|
|
1703
2042
|
? renderEditor(editorOpts, theme)
|
|
1704
2043
|
: undefined;
|
|
1705
2044
|
const queuedSubmissions = editor === undefined || options.inspected !== undefined
|
|
@@ -1714,6 +2053,9 @@ export function renderView(state, options) {
|
|
|
1714
2053
|
? []
|
|
1715
2054
|
: renderTurnError(state.turnError, theme, width);
|
|
1716
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)));
|
|
1717
2059
|
const goal = editor === undefined || options.inspected !== undefined || options.sessionControls?.goal === undefined
|
|
1718
2060
|
? []
|
|
1719
2061
|
: renderGoalBar(options.sessionControls.goal, theme, width);
|
|
@@ -1724,15 +2066,15 @@ export function renderView(state, options) {
|
|
|
1724
2066
|
const inspect = editor === undefined ? [] : renderInspectBanner(options.inspected, theme, width, spinnerFrame);
|
|
1725
2067
|
const subagents = editor === undefined
|
|
1726
2068
|
? []
|
|
1727
|
-
: renderSubagents(options.subagents, theme, width, spinnerFrame, options.inspected?.id, options.
|
|
1728
|
-
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
|
|
1729
2071
|
|| options.autocomplete === undefined
|
|
1730
2072
|
? []
|
|
1731
2073
|
: renderAutocomplete(options.autocomplete.items, options.autocomplete.selected, theme, width);
|
|
1732
|
-
const inputLines = promptSelector?.lines ?? settings?.lines ??
|
|
2074
|
+
const inputLines = promptSelector?.lines ?? settings?.lines ?? search?.lines
|
|
1733
2075
|
?? (editor === undefined ? [] : editor.lines);
|
|
1734
2076
|
const spacer = 1;
|
|
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;
|
|
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;
|
|
1736
2078
|
const budget = Math.max(0, height - reserved);
|
|
1737
2079
|
const focusIndex = options.focusBlock === undefined
|
|
1738
2080
|
? undefined
|
|
@@ -1746,7 +2088,7 @@ export function renderView(state, options) {
|
|
|
1746
2088
|
const requestedStart = focusStart === undefined
|
|
1747
2089
|
? (options.scrollStart ?? Number.POSITIVE_INFINITY)
|
|
1748
2090
|
: transcriptStart + focusStart;
|
|
1749
|
-
const hasOverlay = promptSelector !== undefined || settings !== undefined ||
|
|
2091
|
+
const hasOverlay = promptSelector !== undefined || settings !== undefined || search !== undefined;
|
|
1750
2092
|
const isFollowing = requestedStart === Number.POSITIVE_INFINITY && !hasOverlay;
|
|
1751
2093
|
// A run still at work reshapes as it goes — its newest rows scroll, it folds
|
|
1752
2094
|
// when the turn ends — so none of it may freeze into history until it
|
|
@@ -1766,9 +2108,29 @@ export function renderView(state, options) {
|
|
|
1766
2108
|
if (visible.length > 0)
|
|
1767
2109
|
lines.push(showJump
|
|
1768
2110
|
? ' '.repeat(jumpColumn) + theme.inverse(jumpText) : '');
|
|
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;
|
|
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;
|
|
1770
2112
|
const fill = Math.max(0, height - lines.length - bottomRows);
|
|
1771
|
-
|
|
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);
|
|
1772
2134
|
lines.push(...working);
|
|
1773
2135
|
lines.push(...goal);
|
|
1774
2136
|
lines.push(...questions);
|
|
@@ -1789,10 +2151,10 @@ export function renderView(state, options) {
|
|
|
1789
2151
|
? pendingRow
|
|
1790
2152
|
: Math.min(pendingRow ?? transcript.liveFrom, transcript.liveFrom);
|
|
1791
2153
|
if (liveRow !== undefined) {
|
|
1792
|
-
liveStart = transcriptStart + liveRow;
|
|
2154
|
+
liveStart = transcriptStart + liveRow + topPad;
|
|
1793
2155
|
}
|
|
1794
2156
|
else {
|
|
1795
|
-
liveStart = lines.length - bottomRows;
|
|
2157
|
+
liveStart = lines.length - bottomRows + topPad;
|
|
1796
2158
|
}
|
|
1797
2159
|
// liveStart marks the first row that is still live/mutable. Rows above it
|
|
1798
2160
|
// are committed. Off-screen live rows are bounded by the renderer: it only
|
|
@@ -1802,18 +2164,23 @@ export function renderView(state, options) {
|
|
|
1802
2164
|
else {
|
|
1803
2165
|
liveStart = 0;
|
|
1804
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)];
|
|
1805
2172
|
// Every settled block was already rendered against this width. Mirroring
|
|
1806
2173
|
// OMP's stable-prefix preparation, only validate the mutable suffix instead
|
|
1807
2174
|
// of measuring the complete native-scrollback history on every frame.
|
|
1808
|
-
const trimmed = fitFrame(
|
|
1809
|
-
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 };
|
|
1810
2177
|
// Which document rows this frame's body came from. `body` already includes the
|
|
1811
2178
|
// header, so a window's own range is already in document coordinates and must
|
|
1812
2179
|
// not have the header added again. No window means the whole body was drawn —
|
|
1813
2180
|
// that is a known range, not a frame that failed to report one, and treating
|
|
1814
2181
|
// it as "no body" would clear the record on every ordinary follow frame.
|
|
1815
2182
|
const windowRange = windowed === undefined
|
|
1816
|
-
? (body.length === 0 ? null : { documentStart: 0, documentEnd: body.length, frameStart: 0 })
|
|
2183
|
+
? (body.length === 0 ? null : { documentStart: 0, documentEnd: body.length + topPad, frameStart: 0 })
|
|
1817
2184
|
: windowed.documentRows;
|
|
1818
2185
|
const bodySource = windowRange === undefined
|
|
1819
2186
|
? null
|
|
@@ -1826,11 +2193,11 @@ export function renderView(state, options) {
|
|
|
1826
2193
|
continue;
|
|
1827
2194
|
const previous = stickyHeaders.at(-1);
|
|
1828
2195
|
if (previous !== undefined)
|
|
1829
|
-
previous.end = transcriptStart + transcript.blockStarts[at];
|
|
2196
|
+
previous.end = transcriptStart + topPad + transcript.blockStarts[at];
|
|
1830
2197
|
const padding = width > 4 ? 2 : 0;
|
|
1831
2198
|
const label = truncateToWidth(promptSummary(block), Math.max(1, width - padding * 2));
|
|
1832
2199
|
stickyHeaders.push({
|
|
1833
|
-
start: transcriptStart + (transcript.blockStarts[at + 1] ?? transcript.lines.length),
|
|
2200
|
+
start: transcriptStart + topPad + (transcript.blockStarts[at + 1] ?? transcript.lines.length),
|
|
1834
2201
|
end: body.length,
|
|
1835
2202
|
text: paintSurface(theme, 'userMessageBg', 'userMessageText', padToWidth(' '.repeat(padding) + label, width)),
|
|
1836
2203
|
});
|
|
@@ -1839,16 +2206,20 @@ export function renderView(state, options) {
|
|
|
1839
2206
|
return {
|
|
1840
2207
|
lines: trimmed,
|
|
1841
2208
|
stickyHeaders,
|
|
1842
|
-
...(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 }),
|
|
1843
2214
|
documentRows: bodySource,
|
|
1844
2215
|
cursor: {
|
|
1845
|
-
row: editorStart + caret.row,
|
|
2216
|
+
row: editorStart + caret.row + topPad,
|
|
1846
2217
|
column: Math.min(caret.column, width),
|
|
1847
2218
|
},
|
|
1848
2219
|
cursorVisible: state.status === 'compacting'
|
|
1849
2220
|
|| (options.inspected !== undefined && options.inspected.writable !== true)
|
|
1850
2221
|
? false
|
|
1851
|
-
: promptSelector?.cursorVisible ?? (settings === undefined
|
|
2222
|
+
: promptSelector?.cursorVisible ?? (settings === undefined),
|
|
1852
2223
|
liveStart,
|
|
1853
2224
|
livePinned,
|
|
1854
2225
|
// A transient frame is either a full-screen surface or the user browsing
|
|
@@ -1864,10 +2235,11 @@ export function renderView(state, options) {
|
|
|
1864
2235
|
hiddenBelow: 0,
|
|
1865
2236
|
}),
|
|
1866
2237
|
blockStarts: transcript.blockStarts,
|
|
2238
|
+
fileRegions: transcript.fileRegions.map(region => ({ ...region, start: transcriptStart + topPad + region.start, end: transcriptStart + topPad + region.end })),
|
|
1867
2239
|
blockDrawStarts: transcript.blockDrawStarts,
|
|
1868
2240
|
foldShape: transcript.foldShape,
|
|
1869
2241
|
foldMarks: transcript.foldMarks,
|
|
1870
|
-
bodyRow: transcriptStart,
|
|
2242
|
+
bodyRow: transcriptStart + topPad,
|
|
1871
2243
|
},
|
|
1872
2244
|
};
|
|
1873
2245
|
}
|