@agi-fans/dsh-tui 0.22.0 → 0.23.1
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/LICENSE +26 -0
- package/lib/CHANGELOG.md +219 -1
- package/lib/chrome/box.d.ts +1 -18
- package/lib/chrome/box.js +2 -45
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.js +4 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +0 -1
- package/lib/chrome/command-output.js +11 -5
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/goal-bar.js +1 -1
- package/lib/chrome/goal-bar.js.map +1 -1
- package/lib/chrome/main-screen-renderer.js +4 -3
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.d.ts +3 -3
- package/lib/chrome/markdown.js +85 -36
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/md-style.d.ts +5 -2
- package/lib/chrome/md-style.js +13 -4
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/renderer.d.ts +37 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -3
- package/lib/chrome/status-line.js +5 -5
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +8 -3
- package/lib/chrome/theme.js +51 -153
- package/lib/chrome/theme.js.map +1 -1
- package/lib/commands/auth-account.d.ts +9 -0
- package/lib/commands/auth-account.js +202 -0
- package/lib/commands/auth-account.js.map +1 -0
- package/lib/commands/auth.js +238 -414
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/diff.js +22 -16
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +0 -1
- package/lib/commands/export.js +99 -32
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.js +11 -9
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.d.ts +12 -1
- package/lib/commands/integrations.js +120 -23
- package/lib/commands/integrations.js.map +1 -1
- package/lib/commands/jobs-browser.d.ts +3 -0
- package/lib/commands/jobs-browser.js +152 -0
- package/lib/commands/jobs-browser.js.map +1 -0
- package/lib/commands/jobs.d.ts +3 -14
- package/lib/commands/jobs.js +26 -18
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.d.ts +3 -0
- package/lib/commands/model.js +212 -128
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/open-external.d.ts +2 -0
- package/lib/commands/open-external.js +19 -0
- package/lib/commands/open-external.js.map +1 -0
- package/lib/commands/permission.js +65 -54
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +381 -139
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule-catalog.d.ts +12 -0
- package/lib/commands/schedule-catalog.js +41 -0
- package/lib/commands/schedule-catalog.js.map +1 -0
- package/lib/commands/schedule-history.d.ts +4 -0
- package/lib/commands/schedule-history.js +62 -0
- package/lib/commands/schedule-history.js.map +1 -0
- package/lib/commands/schedule.d.ts +12 -15
- package/lib/commands/schedule.js +220 -187
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.d.ts +3 -0
- package/lib/commands/session-configuration.js +62 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +116 -66
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.js +14 -8
- package/lib/commands/terminal.js.map +1 -1
- package/lib/definition.d.ts +115 -15
- 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 +1 -1
- package/lib/input/actions.js +5 -2
- package/lib/input/actions.js.map +1 -1
- package/lib/input/composer-files.d.ts +2 -0
- package/lib/input/composer-files.js +13 -3
- package/lib/input/composer-files.js.map +1 -1
- package/lib/input/image-paste.d.ts +5 -12
- package/lib/input/image-paste.js +32 -28
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/key-test.d.ts +5 -0
- package/lib/input/key-test.js +19 -0
- package/lib/input/key-test.js.map +1 -0
- package/lib/input/keys.d.ts +2 -0
- package/lib/input/keys.js +4 -1
- package/lib/input/keys.js.map +1 -1
- package/lib/locale/index.d.ts +6 -0
- package/lib/locale/index.js +32 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/prompt.d.ts +4 -0
- package/lib/locale/prompt.js +24 -0
- package/lib/locale/prompt.js.map +1 -0
- package/lib/locale/zh.d.ts +1114 -0
- package/lib/locale/zh.js +1115 -0
- package/lib/locale/zh.js.map +1 -0
- package/lib/prompt-intents.d.ts +4 -0
- package/lib/prompt-intents.js +4 -0
- package/lib/prompt-intents.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +18 -0
- package/lib/runtime/account-settings.js +241 -0
- package/lib/runtime/account-settings.js.map +1 -0
- package/lib/runtime/agent-profile.d.ts +3 -3
- package/lib/runtime/agent-profile.js +4 -4
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/btw-policy.js +2 -1
- package/lib/runtime/btw-policy.js.map +1 -1
- package/lib/runtime/btw.js +1 -2
- package/lib/runtime/btw.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +2 -2
- package/lib/runtime/composer-images.js +4 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.js +27 -1
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-browser.d.ts +19 -0
- package/lib/runtime/file-browser.js +291 -0
- package/lib/runtime/file-browser.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -9
- package/lib/runtime/file-review.js +91 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/help-reader.d.ts +12 -0
- package/lib/runtime/help-reader.js +50 -0
- package/lib/runtime/help-reader.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/model-catalog-editor.d.ts +12 -0
- package/lib/runtime/model-catalog-editor.js +215 -0
- package/lib/runtime/model-catalog-editor.js.map +1 -0
- package/lib/runtime/model-settings.d.ts +11 -0
- package/lib/runtime/model-settings.js +404 -0
- package/lib/runtime/model-settings.js.map +1 -0
- package/lib/runtime/plugin-settings.d.ts +4 -1
- package/lib/runtime/plugin-settings.js +164 -88
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/prompt-browser.d.ts +9 -0
- package/lib/runtime/prompt-browser.js +19 -0
- package/lib/runtime/prompt-browser.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +27 -27
- package/lib/runtime/provider-local.js +1288 -308
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/reference-reader.d.ts +15 -0
- package/lib/runtime/reference-reader.js +78 -0
- package/lib/runtime/reference-reader.js.map +1 -0
- package/lib/runtime/runner.js +38 -29
- package/lib/runtime/runner.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-draft.d.ts +21 -0
- package/lib/runtime/settings-draft.js +166 -0
- package/lib/runtime/settings-draft.js.map +1 -0
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.d.ts +0 -1
- package/lib/runtime/startup-notices.js +35 -6
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/subagent-model-selection.js +2 -1
- package/lib/runtime/subagent-model-selection.js.map +1 -1
- package/lib/runtime/subagent-settings.d.ts +14 -0
- package/lib/runtime/subagent-settings.js +288 -0
- package/lib/runtime/subagent-settings.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -0
- package/lib/runtime/terminal-notifications.js +13 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +2 -0
- package/lib/runtime/terminal-probe.js +9 -0
- package/lib/runtime/terminal-probe.js.map +1 -1
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/runtime/tool-reader.d.ts +5 -0
- package/lib/runtime/tool-reader.js +27 -0
- package/lib/runtime/tool-reader.js.map +1 -0
- package/lib/runtime/web-settings.d.ts +14 -0
- package/lib/runtime/web-settings.js +339 -0
- package/lib/runtime/web-settings.js.map +1 -0
- package/lib/session/command-attachments.d.ts +17 -0
- package/lib/session/command-attachments.js +50 -0
- package/lib/session/command-attachments.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +16 -0
- package/lib/session/context-diagnostics.js +15 -0
- package/lib/session/context-diagnostics.js.map +1 -1
- package/lib/session/external-agents.d.ts +3 -1
- package/lib/session/external-agents.js +35 -10
- package/lib/session/external-agents.js.map +1 -1
- package/lib/session/generation-continuation.d.ts +11 -0
- package/lib/session/generation-continuation.js +34 -0
- package/lib/session/generation-continuation.js.map +1 -0
- package/lib/session/goal-controls.d.ts +5 -0
- package/lib/session/goal-controls.js +81 -0
- package/lib/session/goal-controls.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +7 -3
- package/lib/session/interaction-adapter.js +121 -49
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +2 -0
- package/lib/session/message-queue.js +102 -17
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/pending-inputs.d.ts +30 -0
- package/lib/session/pending-inputs.js +183 -0
- package/lib/session/pending-inputs.js.map +1 -0
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +33 -20
- package/lib/session/session-controller.js +500 -129
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +3 -1
- package/lib/session/session-library.js +15 -0
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +1 -0
- package/lib/session/subagent-roster.js +16 -5
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -0
- package/lib/session/subagent-tracker.js +18 -0
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +9 -5
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +2 -1
- package/lib/session/workflow-projection.js +34 -4
- package/lib/session/workflow-projection.js.map +1 -1
- package/lib/views/autocomplete.d.ts +12 -12
- package/lib/views/autocomplete.js +54 -45
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/context-overview.d.ts +4 -0
- package/lib/views/context-overview.js +43 -0
- package/lib/views/context-overview.js.map +1 -0
- package/lib/views/copy-targets.d.ts +6 -2
- package/lib/views/copy-targets.js +19 -14
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.d.ts +10 -1
- package/lib/views/document-reader.js +63 -6
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +29 -11
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.js +21 -0
- package/lib/views/file-actions.js.map +1 -1
- package/lib/views/file-browser.d.ts +98 -0
- package/lib/views/file-browser.js +120 -0
- package/lib/views/file-browser.js.map +1 -0
- package/lib/views/history-search.js +4 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +3 -2
- package/lib/views/hotkey-format.js +4 -3
- package/lib/views/hotkey-format.js.map +1 -1
- package/lib/views/hotkeys.js +10 -12
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-view.js +16 -11
- package/lib/views/list-view.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +23 -1
- package/lib/views/prompt-selector.js +123 -60
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +6 -1
- package/lib/views/settings-list.js +69 -46
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/tool-actions.d.ts +16 -0
- package/lib/views/tool-actions.js +29 -0
- package/lib/views/tool-actions.js.map +1 -0
- package/lib/views/trajectory-timeline.d.ts +5 -6
- package/lib/views/trajectory-timeline.js +25 -15
- package/lib/views/trajectory-timeline.js.map +1 -1
- package/lib/views/trajectory.d.ts +23 -2
- package/lib/views/trajectory.js +404 -186
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +39 -15
- package/lib/views/transcript-render.js +406 -208
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +3 -2
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workflow.d.ts +16 -0
- package/lib/views/workflow.js +42 -0
- package/lib/views/workflow.js.map +1 -0
- package/package.json +61 -58
- package/lib/chrome/welcome-tips.d.ts +0 -23
- package/lib/chrome/welcome-tips.js +0 -24
- package/lib/chrome/welcome-tips.js.map +0 -1
- package/lib/session/steering.d.ts +0 -3
- package/lib/session/steering.js +0 -9
- package/lib/session/steering.js.map +0 -1
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { renderFileBrowser } from "./file-browser.js";
|
|
1
2
|
/**
|
|
2
3
|
* Transcript rendering: Block -> lines. Every function here is pure; the two
|
|
3
4
|
* WeakMap caches key formatted rows on the immutable block objects the fold
|
|
4
5
|
* half produces, so scrolling never reformats settled content.
|
|
5
6
|
* @module @agi-fans/dsh-tui
|
|
6
7
|
*/
|
|
8
|
+
import { uiText } from "../locale/index.js";
|
|
9
|
+
import { rosterItems, workflowStatus } from "./workflow.js";
|
|
7
10
|
import { leadingSlashCommandNameRange, renderAutocomplete, slashInlineHint } from "./autocomplete.js";
|
|
8
11
|
import { HISTORY_SEARCH_MAX_VISIBLE, renderHistorySearch } from "./history-search.js";
|
|
9
12
|
import { DEEP_DRIVING_LABEL, renderEditor, renderFramedBlock, renderWelcome } from "../chrome/box.js";
|
|
10
|
-
import { renderMarkdown } from "../chrome/markdown.js";
|
|
13
|
+
import { renderMarkdown, renderStreamingMarkdown } from "../chrome/markdown.js";
|
|
11
14
|
import { renderSettings } from "./settings-list.js";
|
|
12
15
|
import { renderTrajectory } from "./trajectory.js";
|
|
13
16
|
import { renderImagePreviewPage, renderPlanReviewPage, renderPromptSelector, renderPromptTreePage, } from "./prompt-selector.js";
|
|
@@ -16,7 +19,7 @@ import { renderPermissionBadge, renderStatusFooter } from "../chrome/status-line
|
|
|
16
19
|
import { createTheme, paintSurface, SPINNER, SYMBOL, BOX } from "../chrome/theme.js";
|
|
17
20
|
import { highlightToolText, toolPayloadLanguage } from "../chrome/code-highlight.js";
|
|
18
21
|
import { renderGoalBar } from "../chrome/goal-bar.js";
|
|
19
|
-
import { padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
|
|
22
|
+
import { padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapCode, wrapTextStable } from "../chrome/width.js";
|
|
20
23
|
import { alignFileDiffs, wrapPaintedDiffRows, } from "../chrome/diff-render.js";
|
|
21
24
|
import { workspaceBlockLines, WORKSPACE_COLLAPSED_LINES } from "./workspace-changes.js";
|
|
22
25
|
import { blockFileAction } from "./file-actions.js";
|
|
@@ -28,7 +31,7 @@ import { renderPathMentionRows } from "../chrome/path-mentions.js";
|
|
|
28
31
|
import { formatElapsed } from "../chrome/relative-time.js";
|
|
29
32
|
import { maskSecrets } from "../chrome/secret-mask.js";
|
|
30
33
|
import { formatOverlayHint } from "./hotkey-format.js";
|
|
31
|
-
import {
|
|
34
|
+
import { matchedRows, searchMatches, transcriptSearchHint, } from "./transcript-search.js";
|
|
32
35
|
export { formatElapsed };
|
|
33
36
|
import { contentToText, prettyArgs, reasoningKey, } from "./transcript-types.js";
|
|
34
37
|
function isBlockPending(block) {
|
|
@@ -51,15 +54,7 @@ function exclusiveDiffs(presentation) {
|
|
|
51
54
|
return presentation.call.diffs;
|
|
52
55
|
return undefined;
|
|
53
56
|
}
|
|
54
|
-
/**
|
|
55
|
-
* The transcript's one content column.
|
|
56
|
-
*
|
|
57
|
-
* Every block starts its words in the same column: a reply, a prompt after its
|
|
58
|
-
* `›`, a run's header after its `▸`, a notice. The marks own the two cells to
|
|
59
|
-
* the left of it. A reply used to sit one cell in while the marked rows put
|
|
60
|
-
* their words at three, so a screen had four left edges and read as clutter
|
|
61
|
-
* however tidy each block was on its own.
|
|
62
|
-
*/
|
|
57
|
+
/** Replies and user text have a two-cell margin; process details align after the disclosure glyph. */
|
|
63
58
|
const ASSISTANT_PADDING_X = 2;
|
|
64
59
|
/** Process summaries keep a readable measure even on wide terminals. */
|
|
65
60
|
const PROCESS_WIDTH = 80;
|
|
@@ -87,9 +82,33 @@ function shortPath(path, width) {
|
|
|
87
82
|
}
|
|
88
83
|
return prefix + tail;
|
|
89
84
|
}
|
|
90
|
-
|
|
85
|
+
// Streaming reuses most painted rows even though the enclosing block changes.
|
|
86
|
+
// Bound both row count and retained text, including ANSI and hyperlink bytes.
|
|
87
|
+
const assistantRowCache = new Map();
|
|
88
|
+
let assistantRowChars = 0;
|
|
89
|
+
const ASSISTANT_ROW_CACHE_CHARS = 1024 * 1024;
|
|
90
|
+
function assistantContentLines(lines, width, paddingX, streaming = false) {
|
|
91
91
|
const margin = ' '.repeat(paddingX);
|
|
92
|
-
|
|
92
|
+
if (!streaming)
|
|
93
|
+
return lines.map(line => padToWidth(margin + line + margin, width));
|
|
94
|
+
return lines.map(line => {
|
|
95
|
+
const key = `${width}:${paddingX}:${line}`;
|
|
96
|
+
const cached = assistantRowCache.get(key);
|
|
97
|
+
if (cached !== undefined)
|
|
98
|
+
return cached.row;
|
|
99
|
+
const row = padToWidth(margin + line + margin, width);
|
|
100
|
+
const size = key.length + row.length;
|
|
101
|
+
if (size <= ASSISTANT_ROW_CACHE_CHARS) {
|
|
102
|
+
while (assistantRowCache.size >= 4096 || assistantRowChars + size > ASSISTANT_ROW_CACHE_CHARS) {
|
|
103
|
+
const first = assistantRowCache.keys().next().value;
|
|
104
|
+
assistantRowChars -= assistantRowCache.get(first).size;
|
|
105
|
+
assistantRowCache.delete(first);
|
|
106
|
+
}
|
|
107
|
+
assistantRowCache.set(key, { row, size });
|
|
108
|
+
assistantRowChars += size;
|
|
109
|
+
}
|
|
110
|
+
return row;
|
|
111
|
+
});
|
|
93
112
|
}
|
|
94
113
|
function lockThinkingLine(line, theme) {
|
|
95
114
|
if (line === '' || !theme.colors)
|
|
@@ -100,13 +119,13 @@ function hasExplicitTextColor(theme) {
|
|
|
100
119
|
const ansi = theme.getFgAnsi('text');
|
|
101
120
|
return ansi !== '' && ansi !== '\x1b[39m';
|
|
102
121
|
}
|
|
103
|
-
function assistantMarkdown(source, theme, width, style) {
|
|
122
|
+
function assistantMarkdown(source, theme, width, style, streaming = false) {
|
|
104
123
|
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
105
124
|
const contentWidth = Math.max(1, width - paddingX * 2);
|
|
106
|
-
const rendered = renderMarkdown(source, theme, contentWidth, style);
|
|
125
|
+
const rendered = (streaming ? renderStreamingMarkdown : renderMarkdown)(source, theme, contentWidth, style);
|
|
107
126
|
if (style?.color !== 'thinkingText')
|
|
108
|
-
return assistantContentLines(rendered, width, paddingX);
|
|
109
|
-
return assistantContentLines(rendered.map(line => lockThinkingLine(line, theme)), width, paddingX);
|
|
127
|
+
return assistantContentLines(rendered, width, paddingX, streaming);
|
|
128
|
+
return assistantContentLines(rendered.map(line => lockThinkingLine(line, theme)), width, paddingX, streaming);
|
|
110
129
|
}
|
|
111
130
|
/**
|
|
112
131
|
* What the reader typed.
|
|
@@ -185,7 +204,7 @@ function toolFailureFact(output, command, fact) {
|
|
|
185
204
|
* Errors reserve space for their reason; full commands and output stay in the
|
|
186
205
|
* expanded block. Process rows share an 80-cell measure and right padding.
|
|
187
206
|
*/
|
|
188
|
-
function toolRowLine(block, theme, width, spinnerFrame) {
|
|
207
|
+
function toolRowLine(block, theme, width, spinnerFrame, open = false) {
|
|
189
208
|
const presentation = renderTool({
|
|
190
209
|
name: block.name,
|
|
191
210
|
arguments: prettyArgs(block.args),
|
|
@@ -204,9 +223,9 @@ function toolRowLine(block, theme, width, spinnerFrame) {
|
|
|
204
223
|
// two that remain are the states a reader has to act on: motion while the
|
|
205
224
|
// call is in flight, and the error mark.
|
|
206
225
|
const mark = block.status === 'ok' ? '' : toolStatusMark(block.status, theme, spinnerFrame) + ' ';
|
|
207
|
-
const head = nest + mark;
|
|
226
|
+
const head = ' ' + nest + (open ? SYMBOL.unfolded : SYMBOL.folded) + ' ' + mark;
|
|
208
227
|
const room = processRoom(width, head);
|
|
209
|
-
const label = processLabel(block.name);
|
|
228
|
+
const label = theme.text(processLabel(block.name));
|
|
210
229
|
const subject = toolRowSubject(block, presentation, label);
|
|
211
230
|
const args = toolArgsObject(block.args);
|
|
212
231
|
const call = block.presentation?.call;
|
|
@@ -289,8 +308,35 @@ function toolRowSubject(block, presentation, label) {
|
|
|
289
308
|
const verb = label.split(' ')[0] ?? '';
|
|
290
309
|
return firstLineOf(verb !== '' && title.startsWith(verb + ' ') ? title.slice(verb.length + 1) : title);
|
|
291
310
|
}
|
|
311
|
+
/** Keep file line numbers outside tokenization and wrapping, including continuation rows. */
|
|
312
|
+
function readContentLines(view, theme, width, error) {
|
|
313
|
+
const language = toolPayloadLanguage('read', view.path);
|
|
314
|
+
const source = view.lines.map(line => line.text);
|
|
315
|
+
const painted = language === undefined
|
|
316
|
+
? source.map(line => theme.fg(error ? 'error' : 'toolOutput', line))
|
|
317
|
+
: highlightedLines(source, language, theme, error ? 'error' : undefined);
|
|
318
|
+
const digits = view.lines.reduce((max, line) => Math.max(max, String(line.number).length), 1);
|
|
319
|
+
// Extremely narrow windows retain the file text rather than spending their
|
|
320
|
+
// final cell on a gutter. Ordinary widths reserve the same column on every row.
|
|
321
|
+
const gutter = width > digits + 2 ? digits + 2 : 0;
|
|
322
|
+
return view.lines.flatMap((line, index) => wrapCode(painted[index] ?? '', Math.max(1, width - gutter)).map((row, part) => {
|
|
323
|
+
const number = part === 0 ? String(line.number).padStart(digits) + ' ' : ' '.repeat(gutter);
|
|
324
|
+
return (gutter === 0 ? '' : theme.fg('dim', number)) + row;
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
/** Results are plain output unless a read explicitly identifies source code. */
|
|
328
|
+
function toolOutputLines(block, output, subject, theme, width) {
|
|
329
|
+
const result = block.presentation?.result;
|
|
330
|
+
if (result?.card === 'read')
|
|
331
|
+
return readContentLines(result, theme, width, block.status === 'error');
|
|
332
|
+
const language = block.name === 'read' && result === undefined ? toolPayloadLanguage('read', subject) : undefined;
|
|
333
|
+
const painted = language === undefined
|
|
334
|
+
? output.map(line => theme.fg(block.status === 'error' ? 'error' : 'toolOutput', line))
|
|
335
|
+
: highlightedLines(output, language, theme, block.status === 'error' ? 'error' : undefined);
|
|
336
|
+
return painted.flatMap(line => wrapTextStable(line, width));
|
|
337
|
+
}
|
|
292
338
|
/** Tool content shared by a running turn and a completed turn opened for reading. */
|
|
293
|
-
function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt+O', surface = 'toolSuccessBg') {
|
|
339
|
+
function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt+O', surface = 'toolSuccessBg', showTitle = true) {
|
|
294
340
|
const full = mode !== 'preview';
|
|
295
341
|
const presentation = renderTool({
|
|
296
342
|
name: block.name, arguments: prettyArgs(block.args), output: block.output,
|
|
@@ -304,9 +350,9 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
304
350
|
const shell = COMMAND_TOOL.test(block.name) || call?.card === 'terminal';
|
|
305
351
|
const read = block.name === 'read' || result?.card === 'read';
|
|
306
352
|
const diffs = exclusiveDiffs(block.presentation);
|
|
307
|
-
const padding = width > 4 ? 2 : 0;
|
|
353
|
+
const padding = width > 8 && !showTitle ? 4 : width > 4 ? 2 : 0;
|
|
308
354
|
const nest = block.parentCallId === undefined || width < 10 ? 0 : 2;
|
|
309
|
-
const room = Math.max(1, width - padding
|
|
355
|
+
const room = Math.max(1, width - padding - (width > 4 ? 2 : 0) - nest);
|
|
310
356
|
const subject = toolArgSubject(block.args, block.partial === true)
|
|
311
357
|
|| (result?.card === 'read' ? result.path : '')
|
|
312
358
|
|| (args === undefined ? '' : Object.values(args).find(value => typeof value === 'string')) || '';
|
|
@@ -316,15 +362,13 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
316
362
|
: call?.card === 'generic' || call?.card === 'diff' ? call.title
|
|
317
363
|
: !read && presentation.title !== undefined && presentation.title !== block.name ? presentation.title
|
|
318
364
|
: `${block.name}${subject === '' ? '' : ` ${subject}`}`;
|
|
319
|
-
//
|
|
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.
|
|
365
|
+
// Commands and their output use plain ink; only identified file content is tokenized.
|
|
326
366
|
const titleInk = block.status === 'error' ? 'error' : 'toolTitle';
|
|
327
|
-
const rows =
|
|
367
|
+
const rows = showTitle || mode !== 'details' && shell && command !== ''
|
|
368
|
+
? wrapTextStable(theme.bold(theme.fg(titleInk, title)), room) : [];
|
|
369
|
+
if (!showTitle && mode !== 'details' && result?.card === 'read' && result.lines.length > 0) {
|
|
370
|
+
rows.push(...wrapTextStable(theme.dim(`${result.path} · ${result.lines[0].number}–${result.lines.at(-1).number} / ${result.totalLines}`), room));
|
|
371
|
+
}
|
|
328
372
|
let body = [];
|
|
329
373
|
if (mode === 'details') {
|
|
330
374
|
let input = block.args;
|
|
@@ -333,19 +377,15 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
333
377
|
}
|
|
334
378
|
catch { /* Partial arguments retain their received text. */ }
|
|
335
379
|
const output = presentation.output.length > 0 ? presentation.output : block.output.split('\n');
|
|
336
|
-
const detailLanguage = shell ? 'bash' : toolPayloadLanguage(block.name, subject);
|
|
337
380
|
const outputRows = diffs !== undefined
|
|
338
381
|
? wrapPaintedDiffRows(alignFileDiffs(diffs), theme, room)
|
|
339
|
-
: (
|
|
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));
|
|
382
|
+
: toolOutputLines(block, output, subject, theme, room);
|
|
343
383
|
body = [
|
|
344
|
-
...wrapTextStable(theme.fg('dim', 'Input'), room),
|
|
384
|
+
...wrapTextStable(theme.fg('dim', theme.text('Input')), room),
|
|
345
385
|
// The arguments are JSON here (they were re-serialized above), so they are
|
|
346
386
|
// highlighted as JSON whatever the tool itself reads.
|
|
347
387
|
...highlightedLines(input.split('\n'), 'json', theme, undefined, room),
|
|
348
|
-
'', ...wrapTextStable(theme.fg('dim', 'Output'), room),
|
|
388
|
+
'', ...wrapTextStable(theme.fg('dim', theme.text('Output')), room),
|
|
349
389
|
...outputRows,
|
|
350
390
|
];
|
|
351
391
|
// Keep the tool's rich presentation, but expose received text it replaced.
|
|
@@ -358,20 +398,15 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
358
398
|
}
|
|
359
399
|
else if (diffs !== undefined) {
|
|
360
400
|
body = wrapPaintedDiffRows(alignFileDiffs(diffs), theme, room);
|
|
401
|
+
if (block.status === 'error' && block.output !== '')
|
|
402
|
+
body.push(...wrapTextStable(theme.fg('error', block.output), room));
|
|
361
403
|
}
|
|
362
404
|
else if (full || !read || block.status === 'error') {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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);
|
|
371
|
-
const input = !shell && !read && (call?.card === 'generic' || (call === undefined && presentation.title !== block.name))
|
|
372
|
-
? highlightedLines(presentation.input, language ?? 'text', theme, undefined, room)
|
|
405
|
+
const output = toolOutputLines(block, presentation.output, subject, theme, room);
|
|
406
|
+
const input = presentation.input.length > 0 && !shell && !read && (call?.card === 'generic' || (call === undefined && !/^(grep|glob|find|ls|search|write|edit)$/.test(block.name)))
|
|
407
|
+
? presentation.input.flatMap(line => wrapCode(theme.fg('toolOutput', line), room))
|
|
373
408
|
: [];
|
|
374
|
-
body = [...input, ...output
|
|
409
|
+
body = [...input, ...output];
|
|
375
410
|
// Writes carry their content in arguments before the result arrives.
|
|
376
411
|
if (body.length === 0 && block.name === 'write' && typeof args?.['content'] === 'string') {
|
|
377
412
|
body = highlightedLines(args['content'].split('\n'), toolPayloadLanguage('write', subject) ?? 'text', theme, undefined, room);
|
|
@@ -394,13 +429,17 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
394
429
|
if (!full && read && block.status !== 'error' && block.output !== '' && detailsKey !== 'Disabled') {
|
|
395
430
|
body.push(...wrapTextStable(theme.fg('dim', `${detailsKey}: Expand result`), room));
|
|
396
431
|
}
|
|
397
|
-
if (full && block.status === 'error' && call?.card === 'diff')
|
|
432
|
+
if (full && diffs === undefined && block.status === 'error' && call?.card === 'diff')
|
|
398
433
|
rows.push('', ...wrapPaintedDiffRows(alignFileDiffs(call.diffs), theme, room));
|
|
399
434
|
if (body.length > 0) {
|
|
400
|
-
rows.
|
|
435
|
+
if (rows.length > 0)
|
|
436
|
+
rows.push('');
|
|
401
437
|
for (const row of body)
|
|
402
438
|
rows.push(row);
|
|
403
439
|
}
|
|
440
|
+
if (!showTitle && mode !== 'details' && presentation.summary && (result?.card === 'terminal' || result?.card === 'search')) {
|
|
441
|
+
rows.push(...wrapTextStable(theme.dim(presentation.summary), room));
|
|
442
|
+
}
|
|
404
443
|
if (block.status === 'error')
|
|
405
444
|
rows.push(...wrapTextStable(theme.fg('error', 'Tool failed'), room));
|
|
406
445
|
if (block.job !== undefined) {
|
|
@@ -410,7 +449,7 @@ function toolBlockLines(block, theme, width, mode = 'preview', detailsKey = 'Alt
|
|
|
410
449
|
rows.push('');
|
|
411
450
|
rows.push(...wrapTextStable(theme.fg(block.status === 'error' ? 'error' : 'muted', `Job ${block.job.id} · ${block.job.detail ?? block.job.status}`), room));
|
|
412
451
|
}
|
|
413
|
-
const bg = block.status === 'running' ? 'toolPendingBg' : block.status === 'error' ? 'toolErrorBg' : surface;
|
|
452
|
+
const bg = !showTitle ? 'toolAltBg' : block.status === 'running' ? 'toolPendingBg' : block.status === 'error' ? 'toolErrorBg' : surface;
|
|
414
453
|
return ['', ...rows, ''].map(row => paintSurface(theme, bg, 'toolOutput', padToWidth(' '.repeat(padding + nest) + row, width)));
|
|
415
454
|
}
|
|
416
455
|
function firstLineOf(text) {
|
|
@@ -421,21 +460,43 @@ function firstLineOf(text) {
|
|
|
421
460
|
}
|
|
422
461
|
return '';
|
|
423
462
|
}
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
463
|
+
/** Latest stable paragraph heading; streaming fragments never flicker into the preview. */
|
|
464
|
+
function reasoningPreview(block) {
|
|
465
|
+
const paragraphs = block.reasoning.replace(/\r\n/g, '\n').split(/\n[ \t]*\n/);
|
|
466
|
+
for (let index = paragraphs.length - 1; index >= 0; index--) {
|
|
467
|
+
const paragraph = paragraphs[index];
|
|
468
|
+
if (block.streaming && index === paragraphs.length - 1 && !paragraph.includes('\n'))
|
|
469
|
+
continue;
|
|
470
|
+
const first = firstLineOf(paragraph);
|
|
471
|
+
if (first !== '')
|
|
472
|
+
return first;
|
|
473
|
+
}
|
|
474
|
+
return '';
|
|
475
|
+
}
|
|
476
|
+
function reasoningSummaryRow(block, theme, width) {
|
|
431
477
|
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
432
|
-
const
|
|
478
|
+
const preview = reasoningPreview(block);
|
|
479
|
+
const label = `${SYMBOL.folded} ${theme.text('Thinking')}${preview === '' ? '' : ROW_JOIN + preview}`;
|
|
433
480
|
return assistantContentLines([theme.fg('dim', truncateToWidth(label, Math.max(1, width - paddingX * 2)))], width, paddingX)[0]
|
|
434
481
|
?? '';
|
|
435
482
|
}
|
|
483
|
+
/** A disclosure header stays small; complete tool payloads are laid out only on demand. */
|
|
484
|
+
function toolDisclosureLines(block, theme, width, open, detailsKey = 'Alt+O', spinnerFrame = 0, raw = false) {
|
|
485
|
+
const rows = toolRowLine(block, theme, width, spinnerFrame, open);
|
|
486
|
+
if (open)
|
|
487
|
+
rows.push(...toolBlockLines(block, theme, width, raw ? 'details' : 'full', detailsKey, 'toolAltBg', false));
|
|
488
|
+
return rows;
|
|
489
|
+
}
|
|
490
|
+
function reasoningDisclosureLines(block, theme, width, open, mathMode = 'auto') {
|
|
491
|
+
if (!open)
|
|
492
|
+
return [reasoningSummaryRow(block, theme, width)];
|
|
493
|
+
const indent = width > 8 ? 2 : 0;
|
|
494
|
+
return [padToWidth(truncateToWidth(` ${SYMBOL.unfolded} ${theme.text('Thinking')}`, Math.max(1, width - 2)), width),
|
|
495
|
+
...openReasoningLines(block, theme, width - indent, mathMode).map(line => ' '.repeat(indent) + line)];
|
|
496
|
+
}
|
|
436
497
|
/** Thinking uses the same quiet Markdown style throughout the turn. */
|
|
437
498
|
function openReasoningLines(block, theme, width, mathMode = 'auto') {
|
|
438
|
-
return assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true, mathMode });
|
|
499
|
+
return assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true, mathMode }, block.streaming);
|
|
439
500
|
}
|
|
440
501
|
/** The block indexes a run shows as rows, in order: its members, then the thought it took from its answer. */
|
|
441
502
|
function groupMembers(group) {
|
|
@@ -453,37 +514,38 @@ function groupMembers(group) {
|
|
|
453
514
|
* last row and its words are the answer painted below the run, so the run
|
|
454
515
|
* paints the thought and nothing else.
|
|
455
516
|
*/
|
|
456
|
-
function groupMemberLines(index, context, thoughtOnly = false
|
|
457
|
-
/** The reader opened this run, so its thoughts are laid out in full. */
|
|
458
|
-
openRun = false) {
|
|
517
|
+
function groupMemberLines(index, context, thoughtOnly = false) {
|
|
459
518
|
const { blocks, options, theme, width } = context;
|
|
460
519
|
const block = blocks[index];
|
|
461
520
|
// Search must reveal a match even when it falls outside a tool's normal preview.
|
|
462
|
-
const full = block.kind === 'tool' && (context.matches.has(index) || options.expandedTools?.has(block.callId) === true);
|
|
463
|
-
const
|
|
464
|
-
const thinkingOpen =
|
|
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,
|
|
521
|
+
const full = block.kind === 'tool' && (options.toolsExpanded === true || context.matches.has(index) || options.expandedTools?.has(block.callId) === true);
|
|
522
|
+
const raw = block.kind === 'tool' && (options.toolsExpanded === true || options.rawTools?.has(block.callId) === true || context.matches.has(index));
|
|
523
|
+
const thinkingOpen = options.hideThinking === false
|
|
524
|
+
|| (block.kind === 'assistant' && (context.openedThoughts.has(reasoningKey(block)) || context.matches.has(index)));
|
|
525
|
+
const signature = [width, options.colors, context.depth, context.themeName, terminalKey(options), options.mathMode, options.mermaidMode, thoughtOnly, full, raw, options.toolDetailsKey, block.kind === 'tool' && block.status === 'running' ? context.spinnerFrame : 0, thinkingOpen, options.hideThinking !== false].join('\0');
|
|
467
526
|
const cached = groupMemberCache.get(block);
|
|
468
527
|
let lines;
|
|
469
528
|
if (cached?.signature === signature)
|
|
470
529
|
lines = cached.lines;
|
|
471
530
|
else {
|
|
472
|
-
lines = block.kind === 'tool' ?
|
|
531
|
+
lines = block.kind === 'tool' ? toolDisclosureLines(block, theme, width, full, options.toolDetailsKey, context.spinnerFrame, raw)
|
|
473
532
|
: block.kind === 'assistant' ? thoughtOnly
|
|
474
|
-
? (
|
|
475
|
-
? openReasoningLines(block, theme, width, options.mathMode)
|
|
476
|
-
: [reasoningSummaryRow(block.reasoning, theme, width)])
|
|
533
|
+
? reasoningDisclosureLines(block, theme, width, thinkingOpen, options.mathMode)
|
|
477
534
|
: blockLines(block, theme, width, {
|
|
478
535
|
mathMode: options.mathMode ?? 'auto',
|
|
479
536
|
mermaidMode: options.mermaidMode ?? 'auto',
|
|
480
|
-
hideThinking: options.hideThinking
|
|
537
|
+
hideThinking: options.hideThinking !== false,
|
|
481
538
|
thinkingOpen,
|
|
539
|
+
processDisclosure: true,
|
|
482
540
|
})
|
|
483
541
|
: block.kind === 'notice' ? blockLines(block, theme, width) : [];
|
|
484
542
|
groupMemberCache.set(block, { signature, lines });
|
|
485
543
|
}
|
|
486
|
-
|
|
544
|
+
const painted = [...paintSearch(lines, index, context.occurrences, theme)];
|
|
545
|
+
if (options.selectedProcess === index && painted.length > 0) {
|
|
546
|
+
painted[0] = theme.fg('accent', '❯') + sliceCells(painted[0], 1, Math.max(0, width - 1));
|
|
547
|
+
}
|
|
548
|
+
return painted;
|
|
487
549
|
}
|
|
488
550
|
/**
|
|
489
551
|
* Paint the query occurrences of one block onto its rows.
|
|
@@ -570,11 +632,14 @@ function processGroupBody(group, context) {
|
|
|
570
632
|
const offsets = new Map();
|
|
571
633
|
const spans = [];
|
|
572
634
|
for (const index of groupMembers(group)) {
|
|
573
|
-
const
|
|
574
|
-
const rows = groupMemberLines(index, context, index === group.answer && group.tookThought
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
635
|
+
const block = context.blocks[index];
|
|
636
|
+
const rows = groupMemberLines(index, context, index === group.answer && group.tookThought);
|
|
637
|
+
const previous = context.blocks[index - 1];
|
|
638
|
+
const opened = (item, at) => item?.kind === 'tool'
|
|
639
|
+
? context.options.toolsExpanded === true || context.options.expandedTools?.has(item.callId) === true || context.matches.has(at)
|
|
640
|
+
: item?.kind === 'assistant' && item.reasoning !== '' && (context.options.hideThinking === false || context.openedThoughts.has(reasoningKey(item)));
|
|
641
|
+
const reply = block.kind === 'assistant' && block.text !== '' && index !== group.answer;
|
|
642
|
+
if (rows.length > 0 && lines.length > 0 && (opened(block, index) || opened(previous, index - 1) || reply || previous?.kind === 'assistant' && previous.text !== ''))
|
|
578
643
|
lines.push('');
|
|
579
644
|
offsets.set(index, lines.length);
|
|
580
645
|
if (context.blocks[index]?.kind === 'tool')
|
|
@@ -588,8 +653,8 @@ function processGroupBody(group, context) {
|
|
|
588
653
|
function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O') {
|
|
589
654
|
const label = elapsed === undefined ? 'Worked' : `Worked for ${elapsed}`;
|
|
590
655
|
const failure = group.failures === 0 ? '' : theme.fg('error', ` · ${group.failures} failed`);
|
|
591
|
-
const labelRoom = Math.max(1, width -
|
|
592
|
-
const summary =
|
|
656
|
+
const labelRoom = Math.max(1, width - 6 - visibleWidth(failure));
|
|
657
|
+
const summary = ` ${SYMBOL.folded} ${theme.fg('dim', truncateToWidth(label, labelRoom))}${failure}`;
|
|
593
658
|
const hint = ` · ${expandKey} to expand`;
|
|
594
659
|
const suffix = expandKey !== 'Disabled' && visibleWidth(summary) + visibleWidth(hint) <= width - 2
|
|
595
660
|
? theme.fg('dim', hint) : '';
|
|
@@ -600,15 +665,15 @@ function processGroupHeader(group, theme, width, elapsed, expandKey = 'Ctrl+O')
|
|
|
600
665
|
* to direct callers; turn inspection uses previews. omitReasoning excludes
|
|
601
666
|
* thinking already drawn by a run.
|
|
602
667
|
*/
|
|
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' } = {}) {
|
|
668
|
+
export function blockLines(block, theme, width, { toolsExpanded = false, omitReasoning = false, toolDetailsKey = 'Alt+O', mathMode = 'auto', mermaidMode = 'auto', hideThinking = false, thinkingOpen = false, processDisclosure = false, rawTool = false, expandedOutput = false, outputExpandKey = 'Ctrl+O' } = {}) {
|
|
604
669
|
if (block.kind === 'user')
|
|
605
670
|
return userBubble(block.text, theme, width);
|
|
606
671
|
if (block.kind === 'assistant') {
|
|
607
672
|
const lines = [];
|
|
608
673
|
if (block.reasoning !== '' && !omitReasoning) {
|
|
609
674
|
const collapsed = hideThinking && !thinkingOpen;
|
|
610
|
-
lines.push(...(collapsed
|
|
611
|
-
? [reasoningSummaryRow(block
|
|
675
|
+
lines.push(...(processDisclosure ? reasoningDisclosureLines(block, theme, width, !collapsed, mathMode) : collapsed
|
|
676
|
+
? [reasoningSummaryRow(block, theme, width)]
|
|
612
677
|
: openReasoningLines(block, theme, width, mathMode)));
|
|
613
678
|
if (block.text !== '')
|
|
614
679
|
lines.push('');
|
|
@@ -618,7 +683,7 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
618
683
|
lines.push(...assistantContentLines([theme.fg('dim', '…')], width, paddingX));
|
|
619
684
|
}
|
|
620
685
|
else if (block.text !== '') {
|
|
621
|
-
lines.push(...assistantMarkdown(block.text, theme, width, { ...(hasExplicitTextColor(theme) ? { color: 'text' } : {}), mathMode, mermaidMode }));
|
|
686
|
+
lines.push(...assistantMarkdown(block.text, theme, width, { ...(hasExplicitTextColor(theme) ? { color: 'text' } : {}), mathMode, mermaidMode }, block.streaming));
|
|
622
687
|
}
|
|
623
688
|
if (block.interrupted === true) {
|
|
624
689
|
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
@@ -627,7 +692,8 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
627
692
|
return lines;
|
|
628
693
|
}
|
|
629
694
|
if (block.kind === 'tool') {
|
|
630
|
-
return
|
|
695
|
+
return processDisclosure ? toolDisclosureLines(block, theme, width, toolsExpanded, toolDetailsKey, 0, rawTool)
|
|
696
|
+
: toolBlockLines(block, theme, width, toolsExpanded ? 'full' : 'preview', toolDetailsKey);
|
|
631
697
|
}
|
|
632
698
|
if (block.kind === 'workspace')
|
|
633
699
|
return workspaceBlockLines(block, theme, width, toolsExpanded);
|
|
@@ -657,6 +723,12 @@ export function blockLines(block, theme, width, { toolsExpanded = false, omitRea
|
|
|
657
723
|
return [renderTranscriptBoundary(block.text, theme, width)];
|
|
658
724
|
if (block.process !== undefined && block.framed !== true)
|
|
659
725
|
return [processNoticeLine(block, theme, width, '')];
|
|
726
|
+
if (block.detail !== undefined) {
|
|
727
|
+
const head = ' ' + theme.fg('dim', block.text);
|
|
728
|
+
return expandedOutput
|
|
729
|
+
? [truncateToWidth(head, width), '', ...renderMarkdown(block.detail, theme, Math.max(1, width - 4)).map(line => ' ' + line)]
|
|
730
|
+
: [truncateToWidth(head + theme.fg('dim', ' · ' + outputExpandKey + ': ' + theme.text('Expand')), width)];
|
|
731
|
+
}
|
|
660
732
|
if (block.framed !== true) {
|
|
661
733
|
const prefix = ' ';
|
|
662
734
|
const continuation = ' '.repeat(visibleWidth(prefix));
|
|
@@ -723,15 +795,9 @@ function renderTranscriptBoundary(label, theme, width) {
|
|
|
723
795
|
*/
|
|
724
796
|
const renderedBlockCache = new WeakMap();
|
|
725
797
|
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.
|
|
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');
|
|
798
|
+
const theme = createTheme(options.colors === true, options.colorDepth ?? 'truecolor', options.themeName ?? 'dark', options.terminal, options.language, options.compactLinks);
|
|
799
|
+
const key = [options.width, options.colors, options.colorDepth, options.themeName,
|
|
800
|
+
options.language, options.compactLinks, JSON.stringify(options.terminal), options.mathMode, options.mermaidMode].join('\0');
|
|
735
801
|
const cached = renderedBlockCache.get(state.blocks)?.get(key);
|
|
736
802
|
if (cached !== undefined)
|
|
737
803
|
return cached;
|
|
@@ -748,15 +814,10 @@ export function renderedBlocks(state, options) {
|
|
|
748
814
|
}
|
|
749
815
|
/** Plain rows one block contributes, without the search highlighting applied. */
|
|
750
816
|
function renderedRows(block, theme, options) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
: assistantMarkdown(block.text, theme, options.width, {
|
|
756
|
-
...(options.mathMode === undefined ? {} : { mathMode: options.mathMode }),
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
return blockSearchText(block).split('\n');
|
|
817
|
+
return blockLines(block, theme, options.width, {
|
|
818
|
+
processDisclosure: true, toolsExpanded: true, rawTool: true, thinkingOpen: true,
|
|
819
|
+
expandedOutput: true, mathMode: options.mathMode ?? 'auto', mermaidMode: options.mermaidMode ?? 'auto',
|
|
820
|
+
});
|
|
760
821
|
}
|
|
761
822
|
/** Plain text of one block, used by transcript search and match painting. */
|
|
762
823
|
export function blockSearchText(block) {
|
|
@@ -772,6 +833,8 @@ export function blockSearchText(block) {
|
|
|
772
833
|
}
|
|
773
834
|
if (block.kind === 'shell')
|
|
774
835
|
return `${block.command}\n${block.output}`;
|
|
836
|
+
if (block.kind === 'notice' && block.detail !== undefined)
|
|
837
|
+
return `${block.text}\n${block.detail}`;
|
|
775
838
|
return block.text;
|
|
776
839
|
}
|
|
777
840
|
function fitFrame(lines, width, stablePrefix = 0) {
|
|
@@ -796,6 +859,7 @@ const transcriptBodyCache = new WeakMap();
|
|
|
796
859
|
/** Settled immutable blocks keep their expensive Markdown/tool layout. */
|
|
797
860
|
const blockLinesCache = new WeakMap();
|
|
798
861
|
function cachedBlockLines(block, options, theme, themeName, depth, expanded, omitReasoning = false) {
|
|
862
|
+
const rawTool = block.kind === 'tool' && (options.toolsExpanded === true || options.rawTools?.has(block.callId) === true || options.transcriptSearch !== undefined && options.transcriptSearch.query.trim() !== '');
|
|
799
863
|
const cached = blockLinesCache.get(block);
|
|
800
864
|
if (cached !== undefined
|
|
801
865
|
&& cached.width === options.width
|
|
@@ -805,26 +869,29 @@ function cachedBlockLines(block, options, theme, themeName, depth, expanded, omi
|
|
|
805
869
|
&& cached.terminalKey === terminalKey(options)
|
|
806
870
|
&& cached.expanded === expanded
|
|
807
871
|
&& cached.omitReasoning === omitReasoning
|
|
808
|
-
&& cached.hideThinking === (options.hideThinking
|
|
872
|
+
&& cached.hideThinking === (options.hideThinking !== false)
|
|
809
873
|
&& cached.thinkingOpen === (block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true)
|
|
810
874
|
&& cached.mathMode === options.mathMode
|
|
811
875
|
&& cached.mermaidMode === options.mermaidMode
|
|
812
876
|
&& cached.toolDetailsKey === options.toolDetailsKey
|
|
877
|
+
&& cached.rawTool === rawTool
|
|
813
878
|
&& cached.toolDetails === (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true)
|
|
814
879
|
&& cached.expandedOutput === (options.expandedOutputs?.has(block) === true))
|
|
815
880
|
return cached.lines;
|
|
816
881
|
const toolDetails = block.kind === 'tool' && options.expandedTools?.has(block.callId) === true;
|
|
817
882
|
const lines = block.kind === 'tool' && toolDetails
|
|
818
|
-
?
|
|
883
|
+
? toolDisclosureLines(block, theme, options.width, true, options.toolDetailsKey, 0, rawTool)
|
|
819
884
|
: blockLines(block, theme, options.width, {
|
|
820
885
|
toolsExpanded: expanded,
|
|
886
|
+
rawTool,
|
|
887
|
+
processDisclosure: true,
|
|
821
888
|
omitReasoning,
|
|
822
889
|
toolDetailsKey: options.toolDetailsKey ?? 'Alt+O',
|
|
823
890
|
mathMode: options.mathMode ?? 'auto',
|
|
824
891
|
mermaidMode: options.mermaidMode ?? 'auto',
|
|
825
|
-
hideThinking: options.hideThinking
|
|
892
|
+
hideThinking: options.hideThinking !== false,
|
|
826
893
|
thinkingOpen: block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true,
|
|
827
|
-
expandedOutput: options.expandedOutputs?.has(block) === true,
|
|
894
|
+
expandedOutput: options.expandedOutputs?.has(block) === true || (block.kind === 'notice' && expanded),
|
|
828
895
|
outputExpandKey: options.turnDetailsKey ?? 'Ctrl+O',
|
|
829
896
|
});
|
|
830
897
|
blockLinesCache.set(block, {
|
|
@@ -832,6 +899,7 @@ function cachedBlockLines(block, options, theme, themeName, depth, expanded, omi
|
|
|
832
899
|
mermaidMode: options.mermaidMode,
|
|
833
900
|
toolDetailsKey: options.toolDetailsKey,
|
|
834
901
|
toolDetails,
|
|
902
|
+
rawTool,
|
|
835
903
|
expandedOutput: options.expandedOutputs?.has(block) === true,
|
|
836
904
|
width: options.width,
|
|
837
905
|
colors: options.colors,
|
|
@@ -840,7 +908,7 @@ function cachedBlockLines(block, options, theme, themeName, depth, expanded, omi
|
|
|
840
908
|
terminalKey: terminalKey(options),
|
|
841
909
|
expanded,
|
|
842
910
|
omitReasoning,
|
|
843
|
-
hideThinking: options.hideThinking
|
|
911
|
+
hideThinking: options.hideThinking !== false,
|
|
844
912
|
thinkingOpen: block.kind === 'assistant' && options.expandedReasoning?.has(reasoningKey(block)) === true,
|
|
845
913
|
lines,
|
|
846
914
|
});
|
|
@@ -1072,12 +1140,13 @@ export function turnGroups(state) {
|
|
|
1072
1140
|
function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
1073
1141
|
const toolsExpanded = options.toolsExpanded === true;
|
|
1074
1142
|
const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
|
|
1143
|
+
const rawTools = [...(options.rawTools ?? [])].sort().join('\0');
|
|
1075
1144
|
const expandedReasoning = [...(options.expandedReasoning ?? [])].sort().join('\0');
|
|
1076
|
-
const hideThinking = options.hideThinking
|
|
1145
|
+
const hideThinking = options.hideThinking !== false;
|
|
1077
1146
|
const openedGroups = [...(options.openedGroups ?? [])].sort().join('\0');
|
|
1078
1147
|
// A command output the reader opened paints different rows, so the body cache
|
|
1079
1148
|
// has to see it; without this the frame is reused and the key looks dead.
|
|
1080
|
-
const expandedOutputs =
|
|
1149
|
+
const expandedOutputs = state.blocks.flatMap((block, index) => options.expandedOutputs?.has(block) ? [index] : []).join(',');
|
|
1081
1150
|
const themeName = options.themeName ?? 'dark';
|
|
1082
1151
|
const depth = colorDepthOf(options);
|
|
1083
1152
|
const animatedSpinnerFrame = state.blocks.some(block => block.kind === 'tool' && block.status === 'running')
|
|
@@ -1109,6 +1178,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1109
1178
|
&& cached.mermaidMode === options.mermaidMode
|
|
1110
1179
|
&& cached.toolsExpanded === toolsExpanded
|
|
1111
1180
|
&& cached.expandedTools === expandedTools
|
|
1181
|
+
&& cached.rawTools === rawTools
|
|
1112
1182
|
&& cached.expandedReasoning === expandedReasoning
|
|
1113
1183
|
&& cached.hideThinking === hideThinking
|
|
1114
1184
|
&& cached.openedGroups === openedGroups
|
|
@@ -1118,9 +1188,11 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1118
1188
|
&& cached.turnDetailsKey === options.turnDetailsKey
|
|
1119
1189
|
&& cached.searchKey === searchKey
|
|
1120
1190
|
&& cached.status === state.status
|
|
1121
|
-
&& cached.focusBlock === options.focusBlock
|
|
1191
|
+
&& cached.focusBlock === options.focusBlock
|
|
1192
|
+
&& cached.selectedProcess === options.selectedProcess) {
|
|
1122
1193
|
return {
|
|
1123
1194
|
lines: cached.lines,
|
|
1195
|
+
disclosures: cached.disclosures,
|
|
1124
1196
|
fileRegions: cached.fileRegions,
|
|
1125
1197
|
blockStarts: cached.blockStarts,
|
|
1126
1198
|
blockDrawStarts: cached.blockDrawStarts,
|
|
@@ -1135,8 +1207,25 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1135
1207
|
const matches = new Set(occurrences.map(match => match.block));
|
|
1136
1208
|
const lines = [];
|
|
1137
1209
|
const fileRegions = [];
|
|
1138
|
-
const
|
|
1210
|
+
const disclosures = [];
|
|
1211
|
+
const disclosureByBlock = new Map();
|
|
1212
|
+
const disclosureAt = (index, row) => {
|
|
1139
1213
|
const block = state.blocks[index];
|
|
1214
|
+
if (block.kind !== 'tool' && !(block.kind === 'assistant' && block.reasoning.trim() !== ''))
|
|
1215
|
+
return;
|
|
1216
|
+
const column = block.kind === 'tool' && block.parentCallId !== undefined ? 4 : 2;
|
|
1217
|
+
if (options.width < column + 4)
|
|
1218
|
+
return;
|
|
1219
|
+
const target = { block: index, row, column: 0, width: column + 2,
|
|
1220
|
+
key: block.kind === 'tool' ? `call:${block.callId}` : `thought:${reasoningKey(block)}` };
|
|
1221
|
+
disclosures.push(target);
|
|
1222
|
+
disclosureByBlock.set(index, target);
|
|
1223
|
+
};
|
|
1224
|
+
const regionsAt = (index, start, length, expanded = false) => {
|
|
1225
|
+
const block = state.blocks[index];
|
|
1226
|
+
const target = disclosureByBlock.get(index);
|
|
1227
|
+
if (block.kind === 'tool' && target !== undefined && length > 1)
|
|
1228
|
+
target.detailEnd = start + length;
|
|
1140
1229
|
if (length === 0 || blockFileAction(block) === undefined)
|
|
1141
1230
|
return;
|
|
1142
1231
|
if (block.kind === 'workspace') {
|
|
@@ -1165,6 +1254,11 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1165
1254
|
const absorbed = new Set();
|
|
1166
1255
|
// Thoughts a search hit has to show in full, beyond what the reader opened.
|
|
1167
1256
|
const openedThoughts = new Set(options.expandedReasoning ?? []);
|
|
1257
|
+
for (const index of matches) {
|
|
1258
|
+
const block = state.blocks[index];
|
|
1259
|
+
if (block?.kind === 'assistant' && block.reasoning !== '')
|
|
1260
|
+
openedThoughts.add(reasoningKey(block));
|
|
1261
|
+
}
|
|
1168
1262
|
// Where each answer's thought is actually drawn, for its fold mark. The
|
|
1169
1263
|
// navigation offset below may point elsewhere; the mark must not follow it.
|
|
1170
1264
|
const drawnThoughts = new Map();
|
|
@@ -1181,21 +1275,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1181
1275
|
absorbed.add(group.answer);
|
|
1182
1276
|
if (group.turn !== undefined)
|
|
1183
1277
|
lastOfTurn.set(group.turn, index);
|
|
1184
|
-
const reasoningHit = group.tookThought && group.answer !== undefined
|
|
1185
|
-
&& search !== undefined && matches.has(group.answer)
|
|
1186
|
-
&& blockMatchesQuery(state.blocks[group.answer].reasoning, search.query);
|
|
1187
|
-
// A hit inside the thought the run took needs that thought laid out in
|
|
1188
|
-
// full. Opening only the run shows a one-line preview, so the matched text
|
|
1189
|
-
// stays out of sight while the transcript reports the hit. This is a render
|
|
1190
|
-
// decision, not the reader's own state, so it goes in a local set rather
|
|
1191
|
-
// than into the options the host owns.
|
|
1192
|
-
if (reasoningHit && group.answer !== undefined) {
|
|
1193
|
-
openedThoughts.add(reasoningKey(state.blocks[group.answer]));
|
|
1194
|
-
}
|
|
1195
1278
|
const asked = toolsExpanded
|
|
1196
1279
|
|| options.openedGroups?.has(group.key) === true
|
|
1197
1280
|
|| state.blocks.slice(group.start, group.until).some(block => block.kind === 'tool' && options.expandedTools?.has(block.callId))
|
|
1198
|
-
|| reasoningHit
|
|
1199
1281
|
|| [...matches].some(index => index >= group.start && index < group.until)
|
|
1200
1282
|
|| options.focusBlock !== undefined
|
|
1201
1283
|
&& options.focusBlock >= group.start && options.focusBlock < group.until;
|
|
@@ -1216,7 +1298,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1216
1298
|
blocks: state.blocks, theme, themeName, depth, width: options.width,
|
|
1217
1299
|
matches, occurrences, options: { ...options, expandedReasoning: openedThoughts },
|
|
1218
1300
|
openedThoughts,
|
|
1219
|
-
|
|
1301
|
+
spinnerFrame,
|
|
1220
1302
|
};
|
|
1221
1303
|
const compactProcessRow = (block) => {
|
|
1222
1304
|
if (block?.kind === 'notice')
|
|
@@ -1263,6 +1345,8 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1263
1345
|
liveFrom ??= headerRow;
|
|
1264
1346
|
// A live run has no header: it becomes a group when its turn ends.
|
|
1265
1347
|
if (!group.live && !shown) {
|
|
1348
|
+
if (options.width >= 6)
|
|
1349
|
+
disclosures.push({ row: headerRow, column: 0, width: 4, block: group.start, key: `run:${group.key}`, group: group.key });
|
|
1266
1350
|
lines.push(...processGroupHeader(group, theme, options.width, elapsedOf(groupIndex), options.turnDetailsKey));
|
|
1267
1351
|
}
|
|
1268
1352
|
const bodyRow = lines.length;
|
|
@@ -1272,18 +1356,26 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1272
1356
|
for (const line of body.lines)
|
|
1273
1357
|
lines.push(line);
|
|
1274
1358
|
offsets = body.offsets;
|
|
1359
|
+
for (const [index, offset] of offsets)
|
|
1360
|
+
disclosureAt(index, bodyRow + offset);
|
|
1275
1361
|
for (const span of body.spans)
|
|
1276
|
-
|
|
1362
|
+
regionsAt(span.index, bodyRow + span.start, span.end - span.start);
|
|
1277
1363
|
}
|
|
1278
1364
|
else {
|
|
1279
1365
|
for (let at = group.start; at < (group.answer ?? group.until); at += 1) {
|
|
1280
1366
|
const member = state.blocks[at];
|
|
1281
|
-
|
|
1367
|
+
const reply = member.kind === 'assistant' && member.text !== '';
|
|
1368
|
+
if (!reply && !(member.kind === 'tool' && member.status === 'error')
|
|
1282
1369
|
&& !(member.kind === 'notice' && member.level === 'error'))
|
|
1283
1370
|
continue;
|
|
1371
|
+
if (reply && lines.length > 0)
|
|
1372
|
+
lines.push('');
|
|
1284
1373
|
offsets.set(at, lines.length - bodyRow);
|
|
1285
|
-
const rows = member.kind === 'tool' ? toolRowLine(member, theme, options.width, spinnerFrame)
|
|
1286
|
-
|
|
1374
|
+
const rows = member.kind === 'tool' ? toolRowLine(member, theme, options.width, spinnerFrame)
|
|
1375
|
+
: reply ? cachedBlockLines(member, options, theme, themeName, depth, toolsExpanded, true) : groupMemberLines(at, context);
|
|
1376
|
+
if (member.kind === 'tool')
|
|
1377
|
+
disclosureAt(at, lines.length);
|
|
1378
|
+
regionsAt(at, lines.length, rows.length);
|
|
1287
1379
|
lines.push(...rows);
|
|
1288
1380
|
}
|
|
1289
1381
|
}
|
|
@@ -1302,8 +1394,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1302
1394
|
// has to report, so it is kept apart from the navigation offset below
|
|
1303
1395
|
// rather than being overwritten by it.
|
|
1304
1396
|
const thoughtHit = group.tookThought
|
|
1305
|
-
&& search !== undefined && matches.has(group.answer)
|
|
1306
|
-
&& !blockMatchesQuery(state.blocks[group.answer].text, search.query);
|
|
1397
|
+
&& search !== undefined && matches.has(group.answer);
|
|
1307
1398
|
const thoughtRow = offsets.get(group.answer);
|
|
1308
1399
|
drawnThoughts.set(group.answer, thoughtRow);
|
|
1309
1400
|
// The answer's own drawn start is its words, never the thought above
|
|
@@ -1314,11 +1405,14 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1314
1405
|
offsets.set(group.answer, lines.length - bodyRow);
|
|
1315
1406
|
const answer = state.blocks[group.answer];
|
|
1316
1407
|
const rendered = cachedBlockLines(answer, options, theme, themeName, depth, toolsExpanded, absorbed.has(group.answer));
|
|
1317
|
-
|
|
1408
|
+
const thoughtLines = thoughtHit
|
|
1409
|
+
? groupMemberLines(group.answer, context, true).length + 1 : 0;
|
|
1410
|
+
const answerHits = thoughtLines === 0 ? occurrences : occurrences.map(hit => hit.block === group.answer ? { ...hit, row: hit.row - thoughtLines } : hit);
|
|
1411
|
+
for (const line of paintSearch(rendered, group.answer, answerHits, theme))
|
|
1318
1412
|
lines.push(line);
|
|
1319
1413
|
if (thoughtHit && thoughtRow !== undefined)
|
|
1320
1414
|
blockStarts[group.answer] = bodyRow + thoughtRow;
|
|
1321
|
-
if (thoughtHit && thoughtRow !== undefined)
|
|
1415
|
+
if ((thoughtHit || options.selectedProcess === group.answer) && thoughtRow !== undefined)
|
|
1322
1416
|
offsets.set(group.answer, thoughtRow);
|
|
1323
1417
|
// One block, two rows: the run painted its thought above, this painted
|
|
1324
1418
|
// its words below. A search hit can be in either, and the offset has to
|
|
@@ -1340,8 +1434,9 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1340
1434
|
offsets.set(at, lines.length - bodyRow);
|
|
1341
1435
|
const rows = !group.live && !opened && trailing?.kind === 'tool'
|
|
1342
1436
|
? toolRowLine(trailing, theme, options.width, spinnerFrame)
|
|
1343
|
-
: groupMemberLines(at, context
|
|
1344
|
-
|
|
1437
|
+
: groupMemberLines(at, context);
|
|
1438
|
+
disclosureAt(at, lines.length);
|
|
1439
|
+
regionsAt(at, lines.length, rows.length);
|
|
1345
1440
|
for (const line of rows)
|
|
1346
1441
|
lines.push(line);
|
|
1347
1442
|
}
|
|
@@ -1363,7 +1458,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1363
1458
|
blockDrawStarts[at] = drawn === undefined ? headerRow : bodyRow + drawn;
|
|
1364
1459
|
}
|
|
1365
1460
|
if (member.kind === 'tool' && options.expandedTools?.has(member.callId) === true) {
|
|
1366
|
-
foldMarks.push({ key: `call:${member.callId}`, row });
|
|
1461
|
+
foldMarks.push({ key: `call:${member.callId}${options.rawTools?.has(member.callId) ? ':raw' : ''}`, row });
|
|
1367
1462
|
}
|
|
1368
1463
|
// The effective set, not the host's: a search can open a thought the
|
|
1369
1464
|
// reader never asked for, and the rows that adds still reshaped. The
|
|
@@ -1389,10 +1484,11 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1389
1484
|
}
|
|
1390
1485
|
}
|
|
1391
1486
|
blockStarts[index] = lines.length;
|
|
1487
|
+
disclosureAt(index, lines.length);
|
|
1392
1488
|
if (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true) {
|
|
1393
|
-
foldMarks.push({ key: `call:${block.callId}`, row: lines.length });
|
|
1489
|
+
foldMarks.push({ key: `call:${block.callId}${options.rawTools?.has(block.callId) ? ':raw' : ''}`, row: lines.length });
|
|
1394
1490
|
}
|
|
1395
|
-
if (block.kind === 'assistant' &&
|
|
1491
|
+
if (block.kind === 'assistant' && openedThoughts.has(reasoningKey(block))) {
|
|
1396
1492
|
foldMarks.push({ key: `thought:${reasoningKey(block)}`, row: lines.length });
|
|
1397
1493
|
}
|
|
1398
1494
|
const liveBlock = state.status !== 'idle' && isProcessBlock(block)
|
|
@@ -1402,17 +1498,34 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1402
1498
|
liveFrom ??= lines.length;
|
|
1403
1499
|
const expanded = (block.kind === 'workspace' && groups.some((group, at) => group.turn === block.turn && openGroups.has(at))) || matches.has(index) || toolsExpanded
|
|
1404
1500
|
|| (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
|
|
1405
|
-
const rendered = cachedBlockLines(block, options, theme, themeName, depth, expanded, absorbed.has(index));
|
|
1406
|
-
|
|
1501
|
+
const rendered = cachedBlockLines(block, context.options, theme, themeName, depth, expanded, absorbed.has(index));
|
|
1502
|
+
if (block.kind === 'commandOutput' && block.text.trimEnd().includes('\n')) {
|
|
1503
|
+
const foldable = /^ [▸▾] /u.test(stripAnsi(rendered[0] ?? ''));
|
|
1504
|
+
disclosures.push({ block: index, row: lines.length, column: 0, width: foldable ? Math.max(0, options.width - 8) : 0,
|
|
1505
|
+
key: `output:${index}`, detailEnd: lines.length + rendered.length,
|
|
1506
|
+
...(options.width >= 30 ? { actionColumn: options.width - 7 } : {}),
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
regionsAt(index, lines.length, rendered.length, expanded);
|
|
1407
1510
|
lines.push(...paintSearch(rendered, index, occurrences, theme));
|
|
1408
1511
|
blockDrawStarts[index] = blockStarts[index];
|
|
1409
1512
|
previous = block;
|
|
1410
1513
|
}
|
|
1411
1514
|
// Streaming text and a call settling change rows in place without changing
|
|
1412
1515
|
// this; opening, folding, or a run settling at turn end do.
|
|
1516
|
+
const selectedTool = disclosures.find(target => target.block === options.selectedProcess && target.group === undefined
|
|
1517
|
+
&& state.blocks[target.block]?.kind === 'tool');
|
|
1518
|
+
if (selectedTool !== undefined && options.width >= 20) {
|
|
1519
|
+
selectedTool.actionColumn = options.width - 7;
|
|
1520
|
+
const title = lines[selectedTool.row];
|
|
1521
|
+
const content = sliceCells(title, 0, visibleWidth(stripAnsi(title).trimEnd()));
|
|
1522
|
+
lines[selectedTool.row] = padToWidth(truncateToWidth(content, selectedTool.actionColumn - 1), selectedTool.actionColumn)
|
|
1523
|
+
+ theme.fg('accent', '[···]') + ' ';
|
|
1524
|
+
}
|
|
1413
1525
|
const foldShape = foldMarks.map(mark => mark.key).join('\u0001');
|
|
1414
1526
|
transcriptBodyCache.set(state.blocks, {
|
|
1415
1527
|
fileRegions,
|
|
1528
|
+
disclosures,
|
|
1416
1529
|
mathMode: options.mathMode,
|
|
1417
1530
|
mermaidMode: options.mermaidMode,
|
|
1418
1531
|
toolDetailsKey: options.toolDetailsKey,
|
|
@@ -1426,6 +1539,7 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1426
1539
|
spinnerFrame: animatedSpinnerFrame,
|
|
1427
1540
|
toolsExpanded,
|
|
1428
1541
|
expandedTools,
|
|
1542
|
+
rawTools,
|
|
1429
1543
|
expandedReasoning,
|
|
1430
1544
|
hideThinking,
|
|
1431
1545
|
openedGroups,
|
|
@@ -1434,13 +1548,14 @@ function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
|
1434
1548
|
turnSpans: state.turnSpans,
|
|
1435
1549
|
status: state.status,
|
|
1436
1550
|
focusBlock: options.focusBlock,
|
|
1551
|
+
selectedProcess: options.selectedProcess,
|
|
1437
1552
|
foldShape,
|
|
1438
1553
|
foldMarks,
|
|
1439
1554
|
liveFrom,
|
|
1440
1555
|
lines,
|
|
1441
1556
|
blockStarts,
|
|
1442
1557
|
});
|
|
1443
|
-
return { lines, blockStarts, blockDrawStarts, fileRegions, foldShape, foldMarks, liveFrom, occurrences };
|
|
1558
|
+
return { lines, blockStarts, blockDrawStarts, fileRegions, disclosures, foldShape, foldMarks, liveFrom, occurrences };
|
|
1444
1559
|
}
|
|
1445
1560
|
function commandSurfaceName(block) {
|
|
1446
1561
|
if (block?.kind === 'commandOutput')
|
|
@@ -1642,7 +1757,7 @@ export function renderAside(panel, theme, width, available = ASIDE_MAX_ROWS) {
|
|
|
1642
1757
|
const detail = panel.detail === undefined || panel.detail === ''
|
|
1643
1758
|
? []
|
|
1644
1759
|
: ['', theme.fg('dim', panel.detail)];
|
|
1645
|
-
const hint = ' ' + theme.fg('dim', formatOverlayHint([{ keys: '↑↓', action: 'scroll' }, { keys: '/copy', action: 'copy' }, { keys: 'Esc', action: 'close' }]));
|
|
1760
|
+
const hint = ' ' + theme.fg('dim', formatOverlayHint([{ keys: '↑↓', action: 'scroll' }, { keys: '/copy', action: 'copy' }, { keys: 'Esc', action: 'close' }], 'lowercase', theme.language));
|
|
1646
1761
|
// Two rules, the question, its blank line, and the hint are always drawn. What
|
|
1647
1762
|
// is left is the answer's window, so the pane never grows past its cap.
|
|
1648
1763
|
const cap = Math.max(6, Math.min(ASIDE_MAX_ROWS, available));
|
|
@@ -1701,12 +1816,14 @@ function subagentPhaseGlyph(phase, theme, spinnerFrame) {
|
|
|
1701
1816
|
return theme.fg('accent', SYMBOL.running);
|
|
1702
1817
|
return theme.fg('accent', SPINNER[spinnerFrame % SPINNER.length] ?? SYMBOL.running);
|
|
1703
1818
|
}
|
|
1819
|
+
if (phase === 'stopped')
|
|
1820
|
+
return theme.fg('warning', '■');
|
|
1704
1821
|
if (phase === 'error')
|
|
1705
1822
|
return theme.fg('error', SYMBOL.error);
|
|
1706
1823
|
return theme.fg('success', SYMBOL.success);
|
|
1707
1824
|
}
|
|
1708
1825
|
const SUBAGENT_PHASE_LABELS = {
|
|
1709
|
-
starting: 'Starting', running: 'Running', waiting: '
|
|
1826
|
+
starting: 'Starting', running: 'Running', waiting: 'Ready', completed: 'Done', stopped: 'Stopped', error: 'Failed',
|
|
1710
1827
|
};
|
|
1711
1828
|
/** How long a child has been (or was) running, as one label. */
|
|
1712
1829
|
export function formatAgentElapsed(agent, now = Date.now()) {
|
|
@@ -1720,46 +1837,63 @@ export function formatAgentElapsed(agent, now = Date.now()) {
|
|
|
1720
1837
|
return `${(ms / 1_000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
1721
1838
|
return `${Math.floor(ms / 60_000)}m${String(Math.floor((ms % 60_000) / 1_000)).padStart(2, '0')}s`;
|
|
1722
1839
|
}
|
|
1723
|
-
function subagentRowText(agent) {
|
|
1840
|
+
function subagentRowText(agent, theme) {
|
|
1724
1841
|
const indent = agent.depth > 1 ? ' '.repeat(agent.depth - 1) : '';
|
|
1725
|
-
return `${indent}${agent.label} · ${SUBAGENT_PHASE_LABELS[agent.phase]}`;
|
|
1842
|
+
return `${indent}${agent.label} · ${theme.text(agent.needsApproval ? 'Needs approval' : SUBAGENT_PHASE_LABELS[agent.phase])}`;
|
|
1843
|
+
}
|
|
1844
|
+
/** Choose one viewport for workflows and agents so neither can crowd out the composer. */
|
|
1845
|
+
export function rosterPreviewStart(roster) {
|
|
1846
|
+
const items = rosterItems(roster);
|
|
1847
|
+
const activeWorkflow = items.findIndex(item => item.kind === 'workflow' && item.run.status === 'open');
|
|
1848
|
+
if (activeWorkflow >= 0)
|
|
1849
|
+
return Math.max(0, Math.min(activeWorkflow, items.length - SUBAGENT_PREVIEW));
|
|
1850
|
+
return Math.max(0, Math.min((roster?.workflows?.length ?? 0) + subagentPreviewStart(roster?.agents ?? []), items.length - SUBAGENT_PREVIEW));
|
|
1726
1851
|
}
|
|
1727
1852
|
/** Compact, unframed descendant-subagent tree placed above Todos. */
|
|
1728
1853
|
export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspectedId, selected) {
|
|
1729
1854
|
const agents = roster?.agents ?? [];
|
|
1730
|
-
|
|
1855
|
+
const items = rosterItems(roster);
|
|
1856
|
+
if (items.length === 0 || width <= 0)
|
|
1731
1857
|
return [];
|
|
1732
1858
|
const running = agents.filter(agent => agent.phase === 'running' || agent.phase === 'starting').length;
|
|
1733
1859
|
const failed = agents.filter(agent => agent.phase === 'error').length;
|
|
1734
1860
|
const waiting = agents.filter(agent => agent.phase === 'waiting').length;
|
|
1735
1861
|
const done = agents.filter(agent => agent.phase === 'completed').length;
|
|
1862
|
+
const stopped = agents.filter(agent => agent.phase === 'stopped').length;
|
|
1863
|
+
const attention = agents.filter(agent => agent.needsApproval).length;
|
|
1736
1864
|
const counts = [
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1865
|
+
attention === 0 ? undefined : theme.text('{count} need approval', { count: attention }),
|
|
1866
|
+
stopped === 0 ? undefined : theme.text('{count} stopped', { count: stopped }),
|
|
1867
|
+
running === 0 ? undefined : theme.text('{count} running', { count: running }),
|
|
1868
|
+
waiting === 0 ? undefined : theme.text('{count} ready', { count: waiting }),
|
|
1869
|
+
done === 0 ? undefined : theme.text('{count} done', { count: done }),
|
|
1870
|
+
failed === 0 ? undefined : theme.text('{count} failed', { count: failed }),
|
|
1741
1871
|
].filter((part) => part !== undefined);
|
|
1742
1872
|
const pointing = selected !== undefined;
|
|
1743
|
-
const headerBody = theme.bold(theme.fg('accent', 'Agents'))
|
|
1873
|
+
const headerBody = theme.bold(theme.fg('accent', theme.text('Agents')))
|
|
1744
1874
|
+ (counts.length === 0 ? '' : theme.fg('dim', ` · ${counts.join(' · ')}`))
|
|
1745
1875
|
// Short enough to survive a narrow bar: the counts already use the width.
|
|
1746
|
-
+ theme.fg('dim', pointing ? ' · ↑↓ · Enter open · Esc' : ' · ↓ select');
|
|
1876
|
+
+ theme.fg('dim', pointing ? ' · ' + theme.text('↑↓ · Enter open · Esc') : ' · ' + theme.text('↓ select'));
|
|
1747
1877
|
const header = ' ' + (pointing ? theme.inverse(headerBody) : headerBody);
|
|
1748
|
-
|
|
1749
|
-
|
|
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}`}`));
|
|
1752
|
-
const start = subagentPreviewStart(agents);
|
|
1753
|
-
const end = Math.min(agents.length, start + SUBAGENT_PREVIEW);
|
|
1878
|
+
const start = selected === undefined ? rosterPreviewStart(roster) : Math.max(0, Math.min(selected, items.length - SUBAGENT_PREVIEW));
|
|
1879
|
+
const end = Math.min(items.length, start + SUBAGENT_PREVIEW);
|
|
1754
1880
|
const rows = [
|
|
1755
1881
|
...(start === 0 ? [] : [`… ${start} earlier`]),
|
|
1756
|
-
...
|
|
1757
|
-
...(end ===
|
|
1882
|
+
...items.slice(start, end),
|
|
1883
|
+
...(end === items.length ? [] : [`… ${items.length - end} more`]),
|
|
1758
1884
|
];
|
|
1759
|
-
const lines = [header, ...
|
|
1885
|
+
const lines = [header, ...rows.map((item, index) => {
|
|
1760
1886
|
const branch = ' ' + theme.fg('dim', index === rows.length - 1 ? '└─' : '├─') + ' ';
|
|
1761
|
-
if (typeof
|
|
1762
|
-
return branch + theme.fg('dim',
|
|
1887
|
+
if (typeof item === 'string')
|
|
1888
|
+
return branch + theme.fg('dim', item);
|
|
1889
|
+
const pointed = selected !== undefined && items[selected]?.key === item.key;
|
|
1890
|
+
if (item.kind === 'workflow') {
|
|
1891
|
+
const run = item.run;
|
|
1892
|
+
const body = (pointed ? SYMBOL.cursor + ' ' : '') + theme.text('Workflow') + ` · ${run.name} · ` + theme.text(workflowStatus(run.status))
|
|
1893
|
+
+ (run.phase === undefined ? '' : ` · ${run.phase}`);
|
|
1894
|
+
return branch + theme.fg(run.status === 'completed' ? 'success' : run.status === 'open' ? 'accent' : 'warning', pointed ? theme.bold(body) : body);
|
|
1895
|
+
}
|
|
1896
|
+
const row = item.agent;
|
|
1763
1897
|
const current = inspectedId === row.id;
|
|
1764
1898
|
const paint = row.phase === 'completed'
|
|
1765
1899
|
? (text) => theme.fg('dim', theme.strikethrough(text))
|
|
@@ -1768,9 +1902,8 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1768
1902
|
: row.phase === 'waiting'
|
|
1769
1903
|
? (text) => theme.fg('dim', text)
|
|
1770
1904
|
: (text) => text;
|
|
1771
|
-
const pointed = index === selected;
|
|
1772
1905
|
const marker = current || pointed ? theme.fg('accent', SYMBOL.cursor) + ' ' : '';
|
|
1773
|
-
const body = marker + subagentPhaseGlyph(row.phase, theme, spinnerFrame) + ' ' + paint(subagentRowText(row));
|
|
1906
|
+
const body = marker + subagentPhaseGlyph(row.phase, theme, spinnerFrame) + ' ' + paint(subagentRowText(row, theme));
|
|
1774
1907
|
return branch + (current || pointed ? theme.bold(body) : body);
|
|
1775
1908
|
})];
|
|
1776
1909
|
return lines.map(line => truncateToWidth(line, width));
|
|
@@ -1779,9 +1912,12 @@ export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspecte
|
|
|
1779
1912
|
export function renderInspectBanner(inspected, theme, width, spinnerFrame = 0) {
|
|
1780
1913
|
if (inspected === undefined || width <= 0)
|
|
1781
1914
|
return [];
|
|
1782
|
-
const guidance = inspected.
|
|
1915
|
+
const guidance = inspected.phase === 'running' || inspected.phase === 'starting'
|
|
1916
|
+
? theme.text('Ctrl+C stops this agent · Esc to return')
|
|
1917
|
+
: inspected.writable ? theme.text('Esc to return') : theme.text('Read-only · Esc to return');
|
|
1783
1918
|
const line = ' ' + theme.fg('accent', '←') + ' ' + subagentPhaseGlyph(inspected.phase, theme, spinnerFrame)
|
|
1784
1919
|
+ ' ' + theme.bold(inspected.label)
|
|
1920
|
+
+ theme.fg('dim', ' · ' + theme.text(inspected.needsApproval ? 'Needs approval' : SUBAGENT_PHASE_LABELS[inspected.phase]))
|
|
1785
1921
|
+ theme.fg('dim', ` · ${guidance}`);
|
|
1786
1922
|
return [truncateToWidth(line, width)];
|
|
1787
1923
|
}
|
|
@@ -1813,11 +1949,11 @@ function queuedMessageLabel(message) {
|
|
|
1813
1949
|
* failed while the user was reading elsewhere leaves an apparently idle screen.
|
|
1814
1950
|
* The row is bounded to a single line so it cannot squeeze the transcript.
|
|
1815
1951
|
*/
|
|
1816
|
-
export function renderTurnError(text, theme, width) {
|
|
1952
|
+
export function renderTurnError(text, theme, width, canContinue = false) {
|
|
1817
1953
|
if (text === '' || width <= 0)
|
|
1818
1954
|
return [];
|
|
1819
1955
|
const prefix = SYMBOL.error + ' ';
|
|
1820
|
-
const hint = ' · Alt+R
|
|
1956
|
+
const hint = canContinue ? ' · ' + theme.text('Alt+R continue') : '';
|
|
1821
1957
|
// Drop the retry hint before the message itself when the terminal is narrow.
|
|
1822
1958
|
const suffix = width - 2 - visibleWidth(prefix) - visibleWidth(hint) >= 8 ? hint : '';
|
|
1823
1959
|
const budget = Math.max(1, width - 2 - visibleWidth(prefix) - visibleWidth(suffix));
|
|
@@ -1832,23 +1968,26 @@ export function renderSteering(inbox, theme, width) {
|
|
|
1832
1968
|
const messages = inbox.filter(message => message.source.kind === 'user');
|
|
1833
1969
|
if (messages.length === 0 || width <= 0)
|
|
1834
1970
|
return [];
|
|
1835
|
-
const latest = queuedMessageLabel(messages
|
|
1971
|
+
const latest = queuedMessageLabel(messages[0]);
|
|
1836
1972
|
const count = messages.length > 1 ? ` · ${messages.length}` : '';
|
|
1837
1973
|
return [truncateToWidth(' ' + theme.fg('border', '│') + ' '
|
|
1838
|
-
+ theme.bold(theme.fg('accent', '
|
|
1974
|
+
+ theme.bold(theme.fg('accent', theme.text('Steer · next step') + count))
|
|
1839
1975
|
+ theme.fg('dim', ' · ') + theme.fg('text', latest), width)];
|
|
1840
1976
|
}
|
|
1841
1977
|
/** Compact, unframed pending-message view placed immediately above the composer. */
|
|
1842
|
-
export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
1978
|
+
export function renderQueuedSubmissions(submissions, theme, width, inbox = [], editable = true) {
|
|
1843
1979
|
const labels = [
|
|
1844
1980
|
...inbox.filter(message => message.source.kind === 'user').map(queuedMessageLabel),
|
|
1845
|
-
...submissions.map(queuedSubmissionLabel),
|
|
1981
|
+
...submissions.map(submission => theme.text(submission.recovered ? 'Recovered' : 'Sending') + ' · ' + queuedSubmissionLabel(submission)),
|
|
1846
1982
|
];
|
|
1847
1983
|
if (labels.length === 0 || width <= 0)
|
|
1848
1984
|
return [];
|
|
1849
1985
|
const start = Math.max(0, labels.length - QUEUED_SUBMISSION_PREVIEW);
|
|
1850
1986
|
const hidden = start;
|
|
1851
|
-
const
|
|
1987
|
+
const accepted = inbox.some(message => message.source.kind === 'user');
|
|
1988
|
+
const recovered = submissions.some(submission => submission.recovered);
|
|
1989
|
+
const queueLabel = theme.bold(theme.fg('accent', theme.text(accepted ? 'Queue' : recovered ? 'Recovered' : 'Sending')));
|
|
1990
|
+
const timing = theme.text(recovered ? 'review before sending' : accepted ? 'next turn' : 'not yet accepted');
|
|
1852
1991
|
const rail = ' ' + theme.fg('border', '│') + ' ';
|
|
1853
1992
|
const alignAction = (left, action, preserveAction = true) => {
|
|
1854
1993
|
const leftWidth = visibleWidth(left);
|
|
@@ -1861,15 +2000,12 @@ export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
|
1861
2000
|
const paintedLeft = truncateToWidth(left, width - actionWidth - 2);
|
|
1862
2001
|
return paintedLeft + ' ' + action;
|
|
1863
2002
|
};
|
|
1864
|
-
const editSingleHint = width >= 72 ? '↑/Alt+Up
|
|
1865
|
-
const editMultiHint = width >= 72 ? '
|
|
2003
|
+
const editSingleHint = !editable ? '' : width >= 72 ? theme.text('↑/Alt+Up recall · Alt+Q manage') : theme.text('Alt+Q manage');
|
|
2004
|
+
const editMultiHint = !editable ? '' : width >= 72 ? theme.text('↑ recall latest · Alt+Q manage') : theme.text('Alt+Q manage');
|
|
1866
2005
|
if (labels.length === 1) {
|
|
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))];
|
|
2006
|
+
return [alignAction(rail + queueLabel + theme.fg('dim', ' · ') + theme.fg('dim', timing + ' · ') + theme.fg('text', accepted ? labels[0] ?? '' : queuedSubmissionLabel(submissions[0])), theme.fg('dim', editSingleHint))];
|
|
1871
2007
|
}
|
|
1872
|
-
const summary = ` · ${labels.length} ·
|
|
2008
|
+
const summary = ` · ${labels.length} · ${timing}${hidden === 0 ? '' : ' · ' + theme.text('{count} earlier', { count: hidden })}`;
|
|
1873
2009
|
const visibleLabels = labels.slice(start);
|
|
1874
2010
|
const lines = [
|
|
1875
2011
|
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim', editMultiHint), false),
|
|
@@ -1886,7 +2022,7 @@ export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
|
1886
2022
|
* @returns the frame to hand to a renderer.
|
|
1887
2023
|
*/
|
|
1888
2024
|
export function renderView(state, options) {
|
|
1889
|
-
const theme = createTheme(options.colors === true, options.colorDepth ?? 'truecolor', options.themeName ?? 'dark', options.terminal);
|
|
2025
|
+
const theme = createTheme(options.colors === true, options.colorDepth ?? 'truecolor', options.themeName ?? 'dark', options.terminal, options.language, options.compactLinks);
|
|
1890
2026
|
const width = options.width;
|
|
1891
2027
|
const height = options.height;
|
|
1892
2028
|
const appName = options.appName ?? 'omdsh';
|
|
@@ -1894,6 +2030,10 @@ export function renderView(state, options) {
|
|
|
1894
2030
|
const pwd = options.pwd ?? '';
|
|
1895
2031
|
const motion = options.motion ?? 'full';
|
|
1896
2032
|
const spinnerFrame = motion === 'off' ? -1 : options.spinnerFrame ?? 0;
|
|
2033
|
+
if (options.fileBrowser !== undefined) {
|
|
2034
|
+
const browser = renderFileBrowser(options.fileBrowser, theme, width, height);
|
|
2035
|
+
return { ...browser, lines: fitFrame(browser.lines, width), documentRows: null };
|
|
2036
|
+
}
|
|
1897
2037
|
if (options.trajectory !== undefined) {
|
|
1898
2038
|
const trajectory = renderTrajectory(options.trajectory, theme, width, height);
|
|
1899
2039
|
return {
|
|
@@ -1914,7 +2054,7 @@ export function renderView(state, options) {
|
|
|
1914
2054
|
documentRows: null,
|
|
1915
2055
|
};
|
|
1916
2056
|
}
|
|
1917
|
-
if (options.promptSelector?.request.presentation === 'plan-review' || options.promptSelector?.request.presentation === 'document') {
|
|
2057
|
+
if (options.promptSelector?.renderDocument === undefined && (options.promptSelector?.request.presentation === 'plan-review' || options.promptSelector?.request.presentation === 'document')) {
|
|
1918
2058
|
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);
|
|
1919
2059
|
return {
|
|
1920
2060
|
...(review.image === undefined ? {} : { image: review.image }),
|
|
@@ -1924,6 +2064,7 @@ export function renderView(state, options) {
|
|
|
1924
2064
|
// A full-screen surface drew no transcript body.
|
|
1925
2065
|
documentRows: null,
|
|
1926
2066
|
...(review.document === undefined ? {} : { promptDocument: review.document }),
|
|
2067
|
+
...(review.actions === undefined ? {} : { promptActions: review.actions }),
|
|
1927
2068
|
};
|
|
1928
2069
|
}
|
|
1929
2070
|
const welcome = renderWelcome({
|
|
@@ -1946,19 +2087,17 @@ export function renderView(state, options) {
|
|
|
1946
2087
|
const activityLabelText = state.status === 'running'
|
|
1947
2088
|
? options.workingLabel ?? DEEP_DRIVING_LABEL
|
|
1948
2089
|
: state.status === 'compacting'
|
|
1949
|
-
? options.workingLabel ?? 'Compacting'
|
|
2090
|
+
? options.workingLabel ?? theme.text('Compacting')
|
|
1950
2091
|
: undefined;
|
|
1951
2092
|
const working = [];
|
|
1952
2093
|
const statusBar = resolveStatusBarConfig(options.statusBar, options.statusPreset);
|
|
1953
2094
|
const placeholder = options.inspected === undefined && state.status === 'idle'
|
|
1954
2095
|
&& options.input === '' && (options.inputImages ?? 0) === 0
|
|
1955
2096
|
? options.modelReady === false
|
|
1956
|
-
? 'Run /login to connect a model'
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
// to say the composer is ready for words.
|
|
1961
|
-
: 'Ask anything'
|
|
2097
|
+
? theme.text('Run /login to connect a model')
|
|
2098
|
+
: (options.sessionControls?.plan?.pending ? !options.sessionControls.plan.active : options.sessionControls?.plan?.active)
|
|
2099
|
+
? theme.text('Describe your task to generate a plan')
|
|
2100
|
+
: theme.text('Ask anything')
|
|
1962
2101
|
: null;
|
|
1963
2102
|
const inlineHint = options.transcriptSearch !== undefined
|
|
1964
2103
|
? transcriptSearchHint({
|
|
@@ -1969,11 +2108,17 @@ export function renderView(state, options) {
|
|
|
1969
2108
|
: options.inspected === undefined
|
|
1970
2109
|
? slashInlineHint(options.input, options.inputCursor, options.commands)
|
|
1971
2110
|
?? placeholder
|
|
1972
|
-
?? (state.status === 'running' && options.steerKey !== undefined
|
|
1973
|
-
?
|
|
2111
|
+
?? (state.status === 'running' && !options.input.startsWith('!') && options.steerKey !== undefined
|
|
2112
|
+
? (options.input === '' ? '' : ' ') + (options.busyEnter === 'steer'
|
|
2113
|
+
? theme.text('Enter: Steer · {key}: Queue', { key: options.queueKey ?? 'Alt+Enter' })
|
|
2114
|
+
: theme.text('Enter: Queue · {key}: Steer', { key: options.steerKey })) : null)
|
|
1974
2115
|
: options.inspected.writable
|
|
1975
|
-
? slashInlineHint(options.input, options.inputCursor, options.commands) ??
|
|
1976
|
-
|
|
2116
|
+
? slashInlineHint(options.input, options.inputCursor, options.commands) ?? (state.status === 'running'
|
|
2117
|
+
? options.busyEnter === 'steer'
|
|
2118
|
+
? theme.text('Enter: Steer · {key}: Queue', { key: options.queueKey ?? 'Alt+Enter' })
|
|
2119
|
+
: theme.text('Enter: Queue · {key}: Steer', { key: options.steerKey ?? 'Alt+S' })
|
|
2120
|
+
: theme.text('Enter to send'))
|
|
2121
|
+
: theme.text('Read-only · Esc to return');
|
|
1977
2122
|
const statusFooter = renderStatusFooter({
|
|
1978
2123
|
model: options.model,
|
|
1979
2124
|
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
|
|
@@ -1987,6 +2132,15 @@ export function renderView(state, options) {
|
|
|
1987
2132
|
width,
|
|
1988
2133
|
}, theme);
|
|
1989
2134
|
const permissionBadge = renderPermissionBadge(options.sessionControls?.permission, theme);
|
|
2135
|
+
const selectionLabels = options.selectionActions === undefined ? [] : [
|
|
2136
|
+
{ action: 'copy', label: '[' + uiText(options.language ?? 'en', 'Copy') + ']' },
|
|
2137
|
+
...(options.selectionActions === 'copy' ? [] : [{ action: 'quote', label: '[' + uiText(options.language ?? 'en', 'Quote') + ']' }]),
|
|
2138
|
+
];
|
|
2139
|
+
// Drop whole buttons on narrow terminals; never expose a clipped click target.
|
|
2140
|
+
while (selectionLabels.length > 0 && visibleWidth(selectionLabels.map(item => item.label).join(' ')) + 4 > width)
|
|
2141
|
+
selectionLabels.pop();
|
|
2142
|
+
const selectionNote = selectionLabels.map(item => item.label).join(' ');
|
|
2143
|
+
const selectionHint = selectionNote + ' · ' + uiText(options.language ?? 'en', 'Esc cancel');
|
|
1990
2144
|
const editorOpts = {
|
|
1991
2145
|
width,
|
|
1992
2146
|
input: options.input,
|
|
@@ -1997,7 +2151,7 @@ export function renderView(state, options) {
|
|
|
1997
2151
|
// says so the moment the character is typed — before the reader commits to
|
|
1998
2152
|
// running anything. Green is the one ink that means "this runs on your
|
|
1999
2153
|
// machine", which is also what a successful tool card uses.
|
|
2000
|
-
border: options.input.trimStart().startsWith('!')
|
|
2154
|
+
border: options.inspected === undefined && options.input.trimStart().startsWith('!')
|
|
2001
2155
|
? 'shellMode'
|
|
2002
2156
|
: options.inspected !== undefined && options.inspected.writable !== true
|
|
2003
2157
|
|| state.status === 'idle'
|
|
@@ -2008,7 +2162,8 @@ export function renderView(state, options) {
|
|
|
2008
2162
|
? { paintInput: (text, start) => paintComposerInputSlice(options.input, text, start, theme) }
|
|
2009
2163
|
: {}),
|
|
2010
2164
|
...(inlineHint !== null ? { inlineHint } : {}),
|
|
2011
|
-
...(options.transientStatus
|
|
2165
|
+
...(options.transientStatus !== undefined ? { note: options.transientStatus } : selectionNote === '' ? {}
|
|
2166
|
+
: { note: visibleWidth(selectionHint) + 4 <= width ? selectionHint : selectionNote }),
|
|
2012
2167
|
...(activityLabelText === undefined ? {} : {
|
|
2013
2168
|
activity: {
|
|
2014
2169
|
frame: motion === 'off' ? SYMBOL.running : SPINNER[Math.max(0, spinnerFrame) % SPINNER.length] ?? SYMBOL.running,
|
|
@@ -2029,9 +2184,17 @@ export function renderView(state, options) {
|
|
|
2029
2184
|
const treeRows = options.promptSelector === undefined || options.promptSelector.request.presentation !== 'tree'
|
|
2030
2185
|
? undefined
|
|
2031
2186
|
: renderPromptTreePage(options.promptSelector, theme, width, Math.max(10, Math.min(height - 8, Math.floor(height * 0.6))), options.input, options.inputCursor, appName);
|
|
2032
|
-
|
|
2187
|
+
let promptVisibleRows = Math.max(3, Math.min(10, height - working.length - 14));
|
|
2188
|
+
const documentPanel = options.promptSelector?.renderDocument === undefined ? undefined : renderPlanReviewPage(options.promptSelector, theme, width, Math.max(10, Math.min(height - working.length - 4, options.promptSelector.renderDocument(theme, Math.max(1, width - 6)).length + 8)), options.input, options.inputCursor, appName);
|
|
2189
|
+
let promptSelector = documentPanel ?? treeRows ?? (options.promptSelector === undefined
|
|
2033
2190
|
? undefined
|
|
2034
|
-
: renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor,
|
|
2191
|
+
: renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor, promptVisibleRows));
|
|
2192
|
+
// Details and live notices consume rows too. Window choices before they push
|
|
2193
|
+
// the navigation hints and fixed footer outside a short terminal.
|
|
2194
|
+
while (documentPanel === undefined && treeRows === undefined && options.promptSelector?.request.allowCustom === false
|
|
2195
|
+
&& promptSelector !== undefined && promptSelector.lines.length > height - working.length - 3 && promptVisibleRows > 3) {
|
|
2196
|
+
promptSelector = renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor, --promptVisibleRows);
|
|
2197
|
+
}
|
|
2035
2198
|
const settings = promptSelector !== undefined || options.settings === undefined
|
|
2036
2199
|
? undefined
|
|
2037
2200
|
: renderSettings(options.settings, theme, width);
|
|
@@ -2041,17 +2204,17 @@ export function renderView(state, options) {
|
|
|
2041
2204
|
const editor = promptSelector === undefined && settings === undefined && search === undefined
|
|
2042
2205
|
? renderEditor(editorOpts, theme)
|
|
2043
2206
|
: undefined;
|
|
2044
|
-
const queuedSubmissions = editor === undefined
|
|
2207
|
+
const queuedSubmissions = editor === undefined
|
|
2045
2208
|
? []
|
|
2046
2209
|
: [
|
|
2047
2210
|
...renderSteering(state.nextStepInbox, theme, width),
|
|
2048
|
-
...renderQueuedSubmissions(options.queuedSubmissions ?? [], theme, width, state.nextTurnInbox),
|
|
2211
|
+
...renderQueuedSubmissions(options.inspected === undefined || options.queuedSubmissionScope === options.inspected.id ? options.queuedSubmissions ?? [] : [], theme, width, state.nextTurnInbox, options.inspected === undefined || options.inspected.writable === true),
|
|
2049
2212
|
];
|
|
2050
2213
|
// Above the composer and below the queue: the failure explains why the queue
|
|
2051
2214
|
// is not moving, and the row survives until the next submission.
|
|
2052
2215
|
const turnError = editor === undefined || options.inspected !== undefined || state.turnError === undefined
|
|
2053
2216
|
? []
|
|
2054
|
-
: renderTurnError(state.turnError, theme, width);
|
|
2217
|
+
: renderTurnError(state.turnError, theme, width, options.canContinue === true);
|
|
2055
2218
|
const todos = editor === undefined || options.inspected !== undefined ? [] : renderTodos(state.todos, theme, width);
|
|
2056
2219
|
const aside = editor === undefined || options.inspected !== undefined || options.aside === undefined
|
|
2057
2220
|
? []
|
|
@@ -2082,9 +2245,12 @@ export function renderView(state, options) {
|
|
|
2082
2245
|
// share its header's row, so the offset list can be shorter than the block
|
|
2083
2246
|
// list, and clamping to it would aim the reader at a different block.
|
|
2084
2247
|
: Math.max(0, Math.min(options.focusBlock, state.blocks.length - 1));
|
|
2248
|
+
const focusRow = focusIndex === undefined ? undefined : transcript.blockStarts[focusIndex];
|
|
2249
|
+
const detailEnd = focusIndex === undefined ? undefined : transcript.disclosures.find(target => target.block === focusIndex)?.detailEnd;
|
|
2250
|
+
const focusOffset = Math.max(0, Math.min(options.focusBlockOffset ?? 0, (detailEnd ?? 0) - (focusRow ?? 0) - 1));
|
|
2085
2251
|
const focusStart = focusIndex === undefined ? undefined : options.focusBlockEdge === 'end'
|
|
2086
2252
|
? Math.max(0, (transcript.blockStarts[focusIndex + 1] ?? transcript.lines.length) - Math.max(1, budget - 2))
|
|
2087
|
-
:
|
|
2253
|
+
: focusRow === undefined ? undefined : focusRow + focusOffset;
|
|
2088
2254
|
const requestedStart = focusStart === undefined
|
|
2089
2255
|
? (options.scrollStart ?? Number.POSITIVE_INFINITY)
|
|
2090
2256
|
: transcriptStart + focusStart;
|
|
@@ -2102,12 +2268,12 @@ export function renderView(state, options) {
|
|
|
2102
2268
|
const lines = [...visible];
|
|
2103
2269
|
const showJump = !hasOverlay && windowed !== undefined && budget >= 3
|
|
2104
2270
|
&& (windowed.hiddenBelow > 0 || (options.openedGroups?.size ?? 0) > 0 || options.toolsExpanded === true);
|
|
2105
|
-
const jumpText = truncateToWidth(
|
|
2271
|
+
const jumpText = truncateToWidth('↓ Latest · End', width);
|
|
2106
2272
|
const jumpColumn = Math.max(0, Math.floor((width - visibleWidth(jumpText)) / 2));
|
|
2107
2273
|
const jumpToLatest = showJump ? { row: lines.length, column: jumpColumn, width: visibleWidth(jumpText) } : undefined;
|
|
2108
2274
|
if (visible.length > 0)
|
|
2109
2275
|
lines.push(showJump
|
|
2110
|
-
? ' '.repeat(jumpColumn) + theme.
|
|
2276
|
+
? ' '.repeat(jumpColumn) + theme.fg('muted', jumpText) : '');
|
|
2111
2277
|
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;
|
|
2112
2278
|
const fill = Math.max(0, height - lines.length - bottomRows);
|
|
2113
2279
|
/**
|
|
@@ -2203,21 +2369,51 @@ export function renderView(state, options) {
|
|
|
2203
2369
|
});
|
|
2204
2370
|
}
|
|
2205
2371
|
}
|
|
2372
|
+
// Tool context takes precedence in the existing single pinned row. Only
|
|
2373
|
+
// painted detail spans participate; folded calls and following replies do not.
|
|
2374
|
+
if (!hasOverlay && budget >= 3)
|
|
2375
|
+
for (const target of transcript.disclosures) {
|
|
2376
|
+
if (target.detailEnd === undefined)
|
|
2377
|
+
continue;
|
|
2378
|
+
const block = state.blocks[target.block];
|
|
2379
|
+
if (block?.kind !== 'tool')
|
|
2380
|
+
continue;
|
|
2381
|
+
const title = transcript.lines[target.row];
|
|
2382
|
+
const titleStart = block.parentCallId === undefined ? 4 : 6;
|
|
2383
|
+
const label = sliceCells(title, titleStart, (target.actionColumn ?? width) - titleStart).trimEnd();
|
|
2384
|
+
stickyHeaders.push({
|
|
2385
|
+
start: transcriptStart + topPad + target.row + 1,
|
|
2386
|
+
end: transcriptStart + topPad + target.detailEnd,
|
|
2387
|
+
text: paintSurface(theme, 'toolAltBg', 'toolOutput', padToWidth(truncateToWidth(' ' + label, width), width)),
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
const selectionActions = [];
|
|
2391
|
+
if (!hasOverlay && editor !== undefined && options.transientStatus === undefined) {
|
|
2392
|
+
const row = editorStart + inputLines.length - 1 + topPad;
|
|
2393
|
+
const painted = stripAnsi(trimmed[row] ?? '');
|
|
2394
|
+
for (const item of selectionLabels) {
|
|
2395
|
+
const at = painted.indexOf(item.label);
|
|
2396
|
+
if (at >= 0)
|
|
2397
|
+
selectionActions.push({ row, column: visibleWidth(painted.slice(0, at)), width: visibleWidth(item.label), action: item.action });
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2206
2400
|
return {
|
|
2207
2401
|
lines: trimmed,
|
|
2402
|
+
selectionActions,
|
|
2208
2403
|
stickyHeaders,
|
|
2209
2404
|
...(jumpToLatest === undefined ? {} : { jumpToLatest: { ...jumpToLatest, row: jumpToLatest.row + topPad } }),
|
|
2210
2405
|
// The tree's preview is a scrollable document too: without its coordinates
|
|
2211
2406
|
// the advertised Ctrl+↑↓ had nothing to move, because the card path never
|
|
2212
2407
|
// reported them.
|
|
2213
2408
|
...(promptSelector?.document === undefined ? {} : { promptDocument: promptSelector.document }),
|
|
2409
|
+
...(promptSelector?.choices === undefined ? {} : { promptChoices: promptSelector.choices.map(choice => ({ ...choice, row: choice.row + editorStart + topPad })) }),
|
|
2410
|
+
...(promptSelector?.actions === undefined ? {} : { promptActions: promptSelector.actions.map(action => ({ ...action, row: action.row + editorStart + topPad })) }),
|
|
2214
2411
|
documentRows: bodySource,
|
|
2215
2412
|
cursor: {
|
|
2216
2413
|
row: editorStart + caret.row + topPad,
|
|
2217
2414
|
column: Math.min(caret.column, width),
|
|
2218
2415
|
},
|
|
2219
|
-
cursorVisible:
|
|
2220
|
-
|| (options.inspected !== undefined && options.inspected.writable !== true)
|
|
2416
|
+
cursorVisible: (options.selectedProcess !== undefined && !hasOverlay) || (options.inspected !== undefined && options.inspected.writable !== true)
|
|
2221
2417
|
? false
|
|
2222
2418
|
: promptSelector?.cursorVisible ?? (settings === undefined),
|
|
2223
2419
|
liveStart,
|
|
@@ -2235,6 +2431,8 @@ export function renderView(state, options) {
|
|
|
2235
2431
|
hiddenBelow: 0,
|
|
2236
2432
|
}),
|
|
2237
2433
|
blockStarts: transcript.blockStarts,
|
|
2434
|
+
disclosures: transcript.disclosures.map(target => ({ ...target, row: transcriptStart + topPad + target.row,
|
|
2435
|
+
...(target.detailEnd === undefined ? {} : { detailEnd: transcriptStart + topPad + target.detailEnd }) })),
|
|
2238
2436
|
fileRegions: transcript.fileRegions.map(region => ({ ...region, start: transcriptStart + topPad + region.start, end: transcriptStart + topPad + region.end })),
|
|
2239
2437
|
blockDrawStarts: transcript.blockDrawStarts,
|
|
2240
2438
|
foldShape: transcript.foldShape,
|