mini-coder 0.5.11 → 0.5.13
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/BENCHMARK.md +107 -0
- package/PROGRESS.md +4 -0
- package/README.md +66 -21
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-baseline.sh +15 -0
- package/benchmark-loop.sh +19 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +152 -38
- package/src/index.ts +107 -143
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +10 -12
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +218 -22
- package/src/shared.ts +39 -0
- package/src/skills.ts +12 -3
- package/src/submit.ts +20 -25
- package/src/text.ts +71 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +93 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +1001 -0
- package/src/tools.ts +190 -995
- package/src/ui/agent.ts +206 -110
- package/src/ui/commands.test.ts +489 -17
- package/src/ui/commands.ts +231 -55
- package/src/ui/conversation.test.ts +585 -0
- package/src/ui/conversation.ts +878 -455
- package/src/ui/help.ts +27 -11
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +422 -185
- package/src/plugins.ts +0 -183
package/src/ui/conversation.ts
CHANGED
|
@@ -8,12 +8,18 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { homedir } from "node:os";
|
|
11
12
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
basename,
|
|
14
|
+
extname,
|
|
15
|
+
isAbsolute,
|
|
16
|
+
join,
|
|
17
|
+
normalize,
|
|
18
|
+
relative,
|
|
19
|
+
} from "node:path";
|
|
20
|
+
import { SyntaxHighlight } from "@cel-tui/components";
|
|
15
21
|
import { HStack, measureContentHeight, Text, VStack } from "@cel-tui/core";
|
|
16
|
-
import type {
|
|
22
|
+
import type { Node } from "@cel-tui/types";
|
|
17
23
|
import type {
|
|
18
24
|
AssistantMessage,
|
|
19
25
|
TextContent,
|
|
@@ -22,8 +28,17 @@ import type {
|
|
|
22
28
|
} from "@mariozechner/pi-ai";
|
|
23
29
|
import type { AppState } from "../index.ts";
|
|
24
30
|
import type { UiMessage } from "../session.ts";
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
31
|
+
import { readBoolean, readFiniteNumber, readString } from "../shared.ts";
|
|
32
|
+
import { getSyntaxHighlightTheme, type Theme } from "../theme.ts";
|
|
33
|
+
import {
|
|
34
|
+
parseGrepResult,
|
|
35
|
+
parseLegacyShellResult,
|
|
36
|
+
parseReadContinuationHint,
|
|
37
|
+
parseReadResult,
|
|
38
|
+
parseShellResultDetails,
|
|
39
|
+
parseTodoSnapshot,
|
|
40
|
+
type TodoItem,
|
|
41
|
+
} from "../tools.ts";
|
|
27
42
|
import { APP_NAME, DEV_VERSION_LABEL } from "../version.ts";
|
|
28
43
|
|
|
29
44
|
/** Single blank-line gap used between conversation-level blocks. */
|
|
@@ -35,32 +50,9 @@ const UI_TOOL_PREVIEW_ROWS = 8;
|
|
|
35
50
|
/** Default width used when preview measurements do not receive one explicitly. */
|
|
36
51
|
const DEFAULT_TOOL_PREVIEW_WIDTH = 80;
|
|
37
52
|
|
|
38
|
-
/** Horizontal columns consumed by assistant-markdown padding. */
|
|
39
|
-
const MARKDOWN_BLOCK_CHROME_WIDTH = 2;
|
|
40
|
-
|
|
41
53
|
/** Horizontal columns consumed by tool-block padding and the left border. */
|
|
42
54
|
const TOOL_BLOCK_CHROME_WIDTH = 4;
|
|
43
55
|
|
|
44
|
-
/** ANSI16 fallback hex values for syntax-highlighter theme overrides. */
|
|
45
|
-
const ANSI_COLOR_HEX: Readonly<Record<Color, string>> = {
|
|
46
|
-
color00: "#000000",
|
|
47
|
-
color01: "#cd3131",
|
|
48
|
-
color02: "#0dbc79",
|
|
49
|
-
color03: "#e5e510",
|
|
50
|
-
color04: "#2472c8",
|
|
51
|
-
color05: "#bc3fbc",
|
|
52
|
-
color06: "#11a8cd",
|
|
53
|
-
color07: "#e5e5e5",
|
|
54
|
-
color08: "#666666",
|
|
55
|
-
color09: "#f14c4c",
|
|
56
|
-
color10: "#23d18b",
|
|
57
|
-
color11: "#f5f543",
|
|
58
|
-
color12: "#3b8eea",
|
|
59
|
-
color13: "#d670d6",
|
|
60
|
-
color14: "#29b8db",
|
|
61
|
-
color15: "#ffffff",
|
|
62
|
-
};
|
|
63
|
-
|
|
64
56
|
/** A pending tool result shown in the streaming tail. */
|
|
65
57
|
export interface PendingToolResult {
|
|
66
58
|
/** Tool call id from the assistant message. */
|
|
@@ -69,6 +61,8 @@ export interface PendingToolResult {
|
|
|
69
61
|
toolName: string;
|
|
70
62
|
/** Progressive or final tool-result content captured so far. */
|
|
71
63
|
content: ToolResultMessage["content"];
|
|
64
|
+
/** Optional structured details preserved on the tool result. */
|
|
65
|
+
details?: ToolResultMessage["details"];
|
|
72
66
|
/** Whether the tool result was an error. */
|
|
73
67
|
isError: boolean;
|
|
74
68
|
}
|
|
@@ -81,6 +75,8 @@ interface ConversationRenderOpts {
|
|
|
81
75
|
verbose: boolean;
|
|
82
76
|
/** Active UI theme. */
|
|
83
77
|
theme: Theme;
|
|
78
|
+
/** Session working directory used for path display. */
|
|
79
|
+
cwd?: string;
|
|
84
80
|
/** Available terminal width for width-aware tool previews. */
|
|
85
81
|
previewWidth?: number;
|
|
86
82
|
}
|
|
@@ -107,7 +103,7 @@ type ConversationMessage = AppState["messages"][number];
|
|
|
107
103
|
|
|
108
104
|
type ConversationLogState = Pick<
|
|
109
105
|
AppState,
|
|
110
|
-
"messages" | "showReasoning" | "verbose" | "theme"
|
|
106
|
+
"messages" | "showReasoning" | "verbose" | "theme" | "cwd"
|
|
111
107
|
> & {
|
|
112
108
|
versionLabel?: AppState["versionLabel"];
|
|
113
109
|
};
|
|
@@ -123,15 +119,14 @@ interface ToolCallArgsCache {
|
|
|
123
119
|
entries: Map<string, ToolCallRenderInfo>;
|
|
124
120
|
}
|
|
125
121
|
|
|
126
|
-
interface
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
count: number;
|
|
122
|
+
interface ConversationMessageRenderCacheEntry {
|
|
123
|
+
node: Node | null;
|
|
124
|
+
height: number;
|
|
130
125
|
showReasoning: boolean;
|
|
131
126
|
verbose: boolean;
|
|
132
127
|
previewWidth: number;
|
|
128
|
+
cwd: string | null;
|
|
133
129
|
theme: Theme | null;
|
|
134
|
-
nodes: Node[];
|
|
135
130
|
}
|
|
136
131
|
|
|
137
132
|
type ToolRenderDirection = "->" | "<-";
|
|
@@ -140,12 +135,13 @@ type ToolRenderLineKind =
|
|
|
140
135
|
| "command"
|
|
141
136
|
| "path"
|
|
142
137
|
| "text"
|
|
138
|
+
| "context"
|
|
143
139
|
| "diffAdded"
|
|
144
140
|
| "diffRemoved"
|
|
145
141
|
| "summary"
|
|
146
142
|
| "error";
|
|
147
143
|
|
|
148
|
-
type SyntaxThemeVariant = "markdown" | "shell";
|
|
144
|
+
type SyntaxThemeVariant = "markdown" | "code" | "shell";
|
|
149
145
|
|
|
150
146
|
interface HighlightedBodySpec {
|
|
151
147
|
/** Full raw source content to syntax-highlight. */
|
|
@@ -161,12 +157,16 @@ interface ToolBlockSpec {
|
|
|
161
157
|
toolName: string;
|
|
162
158
|
/** Direction shown in the header pill. */
|
|
163
159
|
direction: ToolRenderDirection;
|
|
160
|
+
/** Optional compact text appended after the header pill. */
|
|
161
|
+
headerSuffix?: string;
|
|
164
162
|
/** Logical body lines for the tool block. */
|
|
165
163
|
bodyLines: readonly ToolRenderLine[];
|
|
166
164
|
/** Optional syntax-highlighted body rendered from the full unsplit source. */
|
|
167
165
|
highlightedBody?: HighlightedBodySpec;
|
|
168
166
|
/** Optional custom body node rendered as-is. */
|
|
169
167
|
bodyNode?: Node;
|
|
168
|
+
/** Optional footer lines rendered after the body. */
|
|
169
|
+
footerLines?: readonly ToolRenderLine[];
|
|
170
170
|
/** Whether `/verbose` preview rules apply to the body. */
|
|
171
171
|
previewBody: boolean;
|
|
172
172
|
}
|
|
@@ -179,9 +179,36 @@ export interface ToolRenderLine {
|
|
|
179
179
|
text: string;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** A rendered conversation item plus its measured height. */
|
|
183
|
+
export interface ConversationLogItem {
|
|
184
|
+
/** Rendered conversation node. */
|
|
185
|
+
node: Node;
|
|
186
|
+
/** Intrinsic item height in rows, excluding conversation gaps. */
|
|
187
|
+
height: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Rendered items plus cached cumulative intrinsic heights. */
|
|
191
|
+
export interface ConversationLayoutSnapshot {
|
|
192
|
+
/** Rendered items in on-screen order. */
|
|
193
|
+
items: readonly ConversationLogItem[];
|
|
194
|
+
/** Cumulative intrinsic heights, excluding inter-item gaps. */
|
|
195
|
+
prefixHeights: readonly number[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface ConversationLayoutCache {
|
|
199
|
+
messages: readonly ConversationMessage[] | null;
|
|
200
|
+
count: number;
|
|
201
|
+
showReasoning: boolean;
|
|
202
|
+
verbose: boolean;
|
|
203
|
+
previewWidth: number;
|
|
204
|
+
cwd: string | null;
|
|
205
|
+
theme: Theme | null;
|
|
206
|
+
items: ConversationLogItem[];
|
|
207
|
+
prefixHeights: number[];
|
|
208
|
+
}
|
|
209
|
+
|
|
182
210
|
const EMPTY_STREAMING_CONTENT: AssistantMessage["content"] = [];
|
|
183
211
|
const EMPTY_PENDING_TOOL_RESULTS: readonly PendingToolResult[] = [];
|
|
184
|
-
const EMPTY_RENDER_NODES: Node[] = [];
|
|
185
212
|
const EMPTY_TOOL_RESULT_ARGS: Record<string, unknown> = Object.freeze({});
|
|
186
213
|
|
|
187
214
|
const toolCallArgsCache: ToolCallArgsCache = {
|
|
@@ -190,15 +217,21 @@ const toolCallArgsCache: ToolCallArgsCache = {
|
|
|
190
217
|
entries: new Map(),
|
|
191
218
|
};
|
|
192
219
|
|
|
193
|
-
|
|
220
|
+
let conversationMessageRenderCache = new WeakMap<
|
|
221
|
+
ConversationMessage,
|
|
222
|
+
ConversationMessageRenderCacheEntry
|
|
223
|
+
>();
|
|
224
|
+
|
|
225
|
+
const conversationLayoutCache: ConversationLayoutCache = {
|
|
194
226
|
messages: null,
|
|
195
|
-
startIndex: 0,
|
|
196
227
|
count: 0,
|
|
197
228
|
showReasoning: false,
|
|
198
229
|
verbose: false,
|
|
199
230
|
previewWidth: DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
231
|
+
cwd: null,
|
|
200
232
|
theme: null,
|
|
201
|
-
|
|
233
|
+
items: [],
|
|
234
|
+
prefixHeights: [0],
|
|
202
235
|
};
|
|
203
236
|
|
|
204
237
|
/** Reset cached committed conversation renders. */
|
|
@@ -206,14 +239,16 @@ export function resetConversationRenderCache(): void {
|
|
|
206
239
|
toolCallArgsCache.messages = null;
|
|
207
240
|
toolCallArgsCache.count = 0;
|
|
208
241
|
toolCallArgsCache.entries = new Map();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
242
|
+
conversationMessageRenderCache = new WeakMap();
|
|
243
|
+
conversationLayoutCache.messages = null;
|
|
244
|
+
conversationLayoutCache.count = 0;
|
|
245
|
+
conversationLayoutCache.showReasoning = false;
|
|
246
|
+
conversationLayoutCache.verbose = false;
|
|
247
|
+
conversationLayoutCache.previewWidth = DEFAULT_TOOL_PREVIEW_WIDTH;
|
|
248
|
+
conversationLayoutCache.cwd = null;
|
|
249
|
+
conversationLayoutCache.theme = null;
|
|
250
|
+
conversationLayoutCache.items = [];
|
|
251
|
+
conversationLayoutCache.prefixHeights = [0];
|
|
217
252
|
}
|
|
218
253
|
|
|
219
254
|
/** Render a user message with a subtle background. */
|
|
@@ -232,29 +267,24 @@ function renderUserMessage(msg: UserMessage, theme: Theme): Node {
|
|
|
232
267
|
}
|
|
233
268
|
|
|
234
269
|
/** Render a syntax-highlighted raw markdown block. */
|
|
235
|
-
function renderMarkdownTextBlock(
|
|
236
|
-
content: string,
|
|
237
|
-
theme: Theme,
|
|
238
|
-
previewWidth?: number,
|
|
239
|
-
): Node | null {
|
|
270
|
+
function renderMarkdownTextBlock(content: string, theme: Theme): Node | null {
|
|
240
271
|
if (content === "") {
|
|
241
272
|
return null;
|
|
242
273
|
}
|
|
243
274
|
|
|
244
|
-
const
|
|
275
|
+
const highlighted = getHighlightedBodyNode(
|
|
245
276
|
{
|
|
246
277
|
text: content,
|
|
247
278
|
language: "markdown",
|
|
248
279
|
themeVariant: "markdown",
|
|
249
280
|
},
|
|
250
281
|
theme,
|
|
251
|
-
getMarkdownBodyWidth(previewWidth),
|
|
252
282
|
);
|
|
253
|
-
if (
|
|
283
|
+
if (!highlighted) {
|
|
254
284
|
return null;
|
|
255
285
|
}
|
|
256
286
|
|
|
257
|
-
return HStack({ padding: { x: 1 } }, [VStack({ flex: 1 },
|
|
287
|
+
return HStack({ padding: { x: 1 } }, [VStack({ flex: 1 }, [highlighted])]);
|
|
258
288
|
}
|
|
259
289
|
|
|
260
290
|
function getAssistantErrorMessage(
|
|
@@ -324,7 +354,7 @@ function renderAssistantContentBlock(
|
|
|
324
354
|
opts: ConversationRenderOpts,
|
|
325
355
|
): Node | null {
|
|
326
356
|
if (block.type === "text" && block.text) {
|
|
327
|
-
return renderMarkdownTextBlock(block.text, opts.theme
|
|
357
|
+
return renderMarkdownTextBlock(block.text, opts.theme);
|
|
328
358
|
}
|
|
329
359
|
if (block.type === "thinking" && block.thinking) {
|
|
330
360
|
return renderThinkingBlock(block.thinking, opts);
|
|
@@ -377,21 +407,10 @@ function getPreviewWidth(previewWidth?: number): number {
|
|
|
377
407
|
return Math.max(1, Math.floor(previewWidth!));
|
|
378
408
|
}
|
|
379
409
|
|
|
380
|
-
function getMarkdownBodyWidth(previewWidth?: number): number {
|
|
381
|
-
return Math.max(
|
|
382
|
-
1,
|
|
383
|
-
getPreviewWidth(previewWidth) - MARKDOWN_BLOCK_CHROME_WIDTH,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
410
|
function getToolBodyWidth(previewWidth?: number): number {
|
|
388
411
|
return Math.max(1, getPreviewWidth(previewWidth) - TOOL_BLOCK_CHROME_WIDTH);
|
|
389
412
|
}
|
|
390
413
|
|
|
391
|
-
function getHighlightWrapChunkSize(bodyWidth: number): number {
|
|
392
|
-
return Math.max(1, Math.min(HIGHLIGHT_WRAP_MAX_CHUNK_GRAPHEMES, bodyWidth));
|
|
393
|
-
}
|
|
394
|
-
|
|
395
414
|
/** Split multi-line tool text into logical render lines. */
|
|
396
415
|
function splitToolTextLines(
|
|
397
416
|
text: string,
|
|
@@ -409,254 +428,228 @@ function splitToolTextLines(
|
|
|
409
428
|
return lines.map((line) => ({ kind, text: line }));
|
|
410
429
|
}
|
|
411
430
|
|
|
431
|
+
function normalizeDisplayLineEndings(text: string): string {
|
|
432
|
+
return text.replace(/\r\n/g, "\n");
|
|
433
|
+
}
|
|
434
|
+
|
|
412
435
|
/** Read a string argument from a tool-call argument map. */
|
|
413
436
|
function getToolArgString(args: Record<string, unknown>, key: string): string {
|
|
414
|
-
|
|
415
|
-
return typeof value === "string" ? value : "";
|
|
437
|
+
return readString(args, key) ?? "";
|
|
416
438
|
}
|
|
417
439
|
|
|
418
|
-
|
|
419
|
-
const todoCount = Array.isArray(args.todos) ? args.todos.length : 0;
|
|
420
|
-
const todoLabel = todoCount === 1 ? "todo" : "todos";
|
|
421
|
-
return `Updating todos... ${todoCount} ${todoLabel} updated`;
|
|
422
|
-
}
|
|
440
|
+
const HOME_DIR = homedir();
|
|
423
441
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
442
|
+
function resolveToolPath(path: string, cwd?: string): string {
|
|
443
|
+
if (path === "") {
|
|
444
|
+
return "";
|
|
445
|
+
}
|
|
446
|
+
if (!cwd || isAbsolute(path)) {
|
|
447
|
+
return path;
|
|
448
|
+
}
|
|
449
|
+
return join(cwd, path);
|
|
430
450
|
}
|
|
431
451
|
|
|
432
|
-
function
|
|
433
|
-
|
|
452
|
+
function abbreviateHomePath(path: string): string {
|
|
453
|
+
if (path === HOME_DIR) {
|
|
454
|
+
return "~";
|
|
455
|
+
}
|
|
456
|
+
if (path.startsWith(`${HOME_DIR}/`)) {
|
|
457
|
+
return `~/${path.slice(HOME_DIR.length + 1)}`;
|
|
458
|
+
}
|
|
459
|
+
return path;
|
|
434
460
|
}
|
|
435
461
|
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
case "readImage":
|
|
440
|
-
case "todoWrite":
|
|
441
|
-
case "todoRead":
|
|
442
|
-
return theme.accentText;
|
|
443
|
-
case "edit":
|
|
444
|
-
return theme.secondaryAccentText;
|
|
445
|
-
default:
|
|
446
|
-
return theme.secondaryAccentText ?? theme.toolText;
|
|
462
|
+
function normalizeDisplayPath(path: string): string {
|
|
463
|
+
if (path === "") {
|
|
464
|
+
return path;
|
|
447
465
|
}
|
|
466
|
+
return normalize(path).replace(/\\/g, "/");
|
|
448
467
|
}
|
|
449
468
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
granularity: "grapheme",
|
|
457
|
-
});
|
|
458
|
-
const HIGHLIGHT_WRAP_MAX_CHUNK_GRAPHEMES = 32;
|
|
459
|
-
const syntaxThemeCache: Record<
|
|
460
|
-
SyntaxThemeVariant,
|
|
461
|
-
WeakMap<Theme, SyntaxThemeRegistration>
|
|
462
|
-
> = {
|
|
463
|
-
markdown: new WeakMap(),
|
|
464
|
-
shell: new WeakMap(),
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
function colorToHex(color: Color | undefined): string | undefined {
|
|
468
|
-
return color ? ANSI_COLOR_HEX[color] : undefined;
|
|
469
|
+
function formatResolvedToolPath(path: string, cwd?: string): string {
|
|
470
|
+
const resolved = resolveToolPath(path, cwd);
|
|
471
|
+
if (resolved === "") {
|
|
472
|
+
return path;
|
|
473
|
+
}
|
|
474
|
+
return abbreviateHomePath(normalizeDisplayPath(resolved));
|
|
469
475
|
}
|
|
470
476
|
|
|
471
|
-
function
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
fontStyle?: string,
|
|
476
|
-
): void {
|
|
477
|
-
const foregroundHex = colorToHex(foreground);
|
|
478
|
-
if (!foregroundHex && !fontStyle) {
|
|
479
|
-
return;
|
|
477
|
+
function formatRelativeToolPath(path: string, cwd?: string): string {
|
|
478
|
+
const resolved = resolveToolPath(path, cwd);
|
|
479
|
+
if (resolved === "" || !cwd || !isAbsolute(resolved)) {
|
|
480
|
+
return normalizeDisplayPath(path);
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
483
|
+
const relativePath = relative(cwd, resolved);
|
|
484
|
+
if (relativePath === "") {
|
|
485
|
+
return ".";
|
|
486
|
+
}
|
|
487
|
+
if (relativePath.startsWith("..")) {
|
|
488
|
+
return formatResolvedToolPath(resolved);
|
|
489
|
+
}
|
|
490
|
+
return normalizeDisplayPath(relativePath);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function getReadLanguageCandidates(path: string): string[] {
|
|
494
|
+
const fileName = basename(path).toLowerCase();
|
|
495
|
+
const extension = extname(fileName).toLowerCase();
|
|
496
|
+
|
|
497
|
+
switch (extension) {
|
|
498
|
+
case ".ts":
|
|
499
|
+
return ["typescript"];
|
|
500
|
+
case ".tsx":
|
|
501
|
+
return ["tsx", "typescript"];
|
|
502
|
+
case ".js":
|
|
503
|
+
case ".mjs":
|
|
504
|
+
case ".cjs":
|
|
505
|
+
return ["javascript"];
|
|
506
|
+
case ".jsx":
|
|
507
|
+
return ["jsx", "javascript"];
|
|
508
|
+
case ".json":
|
|
509
|
+
return ["json"];
|
|
510
|
+
case ".md":
|
|
511
|
+
return ["markdown"];
|
|
512
|
+
case ".yml":
|
|
513
|
+
case ".yaml":
|
|
514
|
+
return ["yaml"];
|
|
515
|
+
case ".toml":
|
|
516
|
+
return ["toml"];
|
|
517
|
+
case ".sh":
|
|
518
|
+
case ".bash":
|
|
519
|
+
return ["bash"];
|
|
520
|
+
case ".css":
|
|
521
|
+
return ["css"];
|
|
522
|
+
case ".html":
|
|
523
|
+
case ".htm":
|
|
524
|
+
return ["html"];
|
|
525
|
+
case ".xml":
|
|
526
|
+
return ["xml"];
|
|
527
|
+
case ".py":
|
|
528
|
+
return ["python"];
|
|
529
|
+
case ".rs":
|
|
530
|
+
return ["rust"];
|
|
531
|
+
case ".go":
|
|
532
|
+
return ["go"];
|
|
533
|
+
case ".java":
|
|
534
|
+
return ["java"];
|
|
535
|
+
case ".c":
|
|
536
|
+
return ["c"];
|
|
537
|
+
case ".h":
|
|
538
|
+
return ["c"];
|
|
539
|
+
case ".cpp":
|
|
540
|
+
case ".cc":
|
|
541
|
+
case ".cxx":
|
|
542
|
+
case ".hpp":
|
|
543
|
+
return ["cpp"];
|
|
544
|
+
default:
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (fileName === "dockerfile") {
|
|
549
|
+
return ["dockerfile"];
|
|
550
|
+
}
|
|
551
|
+
if (fileName === "makefile") {
|
|
552
|
+
return ["makefile"];
|
|
553
|
+
}
|
|
554
|
+
if (fileName === "agents.md" || fileName === "readme.md") {
|
|
555
|
+
return ["markdown"];
|
|
556
|
+
}
|
|
557
|
+
return [];
|
|
489
558
|
}
|
|
490
559
|
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
pushSyntaxTokenColor(tokenColors, "comment", theme.mutedText);
|
|
496
|
-
pushSyntaxTokenColor(
|
|
497
|
-
tokenColors,
|
|
498
|
-
["keyword", "storage"],
|
|
499
|
-
theme.secondaryAccentText,
|
|
500
|
-
);
|
|
501
|
-
pushSyntaxTokenColor(
|
|
502
|
-
tokenColors,
|
|
503
|
-
[
|
|
504
|
-
"entity.name.function",
|
|
505
|
-
"function",
|
|
506
|
-
"meta.function-call",
|
|
507
|
-
"support.function",
|
|
508
|
-
"title",
|
|
509
|
-
],
|
|
510
|
-
theme.accentText,
|
|
511
|
-
);
|
|
512
|
-
pushSyntaxTokenColor(
|
|
513
|
-
tokenColors,
|
|
514
|
-
[
|
|
515
|
-
"built_in",
|
|
516
|
-
"class",
|
|
517
|
-
"entity.name.type",
|
|
518
|
-
"entity.other.inherited-class",
|
|
519
|
-
"support.class",
|
|
520
|
-
"support.type",
|
|
521
|
-
"type",
|
|
522
|
-
],
|
|
523
|
-
theme.accentText,
|
|
524
|
-
);
|
|
525
|
-
pushSyntaxTokenColor(
|
|
526
|
-
tokenColors,
|
|
527
|
-
["constant.language", "constant.numeric", "literal", "symbol"],
|
|
528
|
-
theme.secondaryAccentText ?? theme.accentText,
|
|
529
|
-
);
|
|
530
|
-
pushSyntaxTokenColor(
|
|
531
|
-
tokenColors,
|
|
532
|
-
["markup.inline", "string"],
|
|
533
|
-
theme.diffAdded,
|
|
534
|
-
);
|
|
535
|
-
pushSyntaxTokenColor(tokenColors, "regexp", theme.diffRemoved);
|
|
536
|
-
pushSyntaxTokenColor(
|
|
537
|
-
tokenColors,
|
|
538
|
-
["attr", "attribute", "entity.other.attribute-name", "property"],
|
|
539
|
-
theme.accentText,
|
|
540
|
-
);
|
|
541
|
-
pushSyntaxTokenColor(
|
|
542
|
-
tokenColors,
|
|
543
|
-
["entity.name.tag", "name", "tag"],
|
|
544
|
-
theme.accentText,
|
|
545
|
-
);
|
|
560
|
+
function formatTodoWriteCallSummary(args: Record<string, unknown>): string {
|
|
561
|
+
const todoCount = Array.isArray(args.todos) ? args.todos.length : 0;
|
|
562
|
+
const todoLabel = todoCount === 1 ? "todo" : "todos";
|
|
563
|
+
return `Updating todos... ${todoCount} ${todoLabel} updated`;
|
|
546
564
|
}
|
|
547
565
|
|
|
548
|
-
function
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
tokenColors,
|
|
556
|
-
"bullet",
|
|
557
|
-
theme.secondaryAccentText,
|
|
558
|
-
"bold",
|
|
566
|
+
function parseShellToolContent(
|
|
567
|
+
content: ToolResultMessage["content"],
|
|
568
|
+
details?: ToolResultMessage["details"],
|
|
569
|
+
) {
|
|
570
|
+
return (
|
|
571
|
+
parseShellResultDetails(details) ??
|
|
572
|
+
parseLegacyShellResult(getToolContentText(content))
|
|
559
573
|
);
|
|
560
|
-
pushSyntaxTokenColor(tokenColors, ["code", "string"], theme.diffAdded);
|
|
561
|
-
pushSyntaxTokenColor(tokenColors, "link", theme.accentText, "underline");
|
|
562
|
-
pushSyntaxTokenColor(tokenColors, "strong", undefined, "bold");
|
|
563
|
-
pushSyntaxTokenColor(tokenColors, "emphasis", undefined, "italic");
|
|
564
574
|
}
|
|
565
575
|
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
):
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
pushShellSyntaxTokenColors(tokenColors, theme);
|
|
579
|
-
} else {
|
|
580
|
-
pushMarkdownSyntaxTokenColors(tokenColors, theme);
|
|
576
|
+
function buildShellResultLines(
|
|
577
|
+
content: ToolResultMessage["content"],
|
|
578
|
+
details?: ToolResultMessage["details"],
|
|
579
|
+
): { bodyLines: ToolRenderLine[]; footerLines?: ToolRenderLine[] } {
|
|
580
|
+
const result = parseShellToolContent(content, details);
|
|
581
|
+
if (!result) {
|
|
582
|
+
return {
|
|
583
|
+
bodyLines: splitToolTextLines(
|
|
584
|
+
normalizeDisplayLineEndings(getToolContentText(content)),
|
|
585
|
+
"text",
|
|
586
|
+
),
|
|
587
|
+
};
|
|
581
588
|
}
|
|
582
589
|
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
cache.set(theme, syntaxTheme);
|
|
587
|
-
return syntaxTheme;
|
|
588
|
-
}
|
|
590
|
+
const bodyLines: ToolRenderLine[] = [];
|
|
591
|
+
const stdout = normalizeDisplayLineEndings(result.stdout);
|
|
592
|
+
const stderr = normalizeDisplayLineEndings(result.stderr);
|
|
589
593
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
chunkSize: number,
|
|
593
|
-
): Node[] {
|
|
594
|
-
if (node.content === "") {
|
|
595
|
-
return [Text("", node.props)];
|
|
594
|
+
if (stdout !== "") {
|
|
595
|
+
bodyLines.push(...splitToolTextLines(stdout, "text"));
|
|
596
596
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
return [Text(node.content, node.props)];
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const children: Node[] = [];
|
|
604
|
-
for (const part of parts) {
|
|
605
|
-
if (/^\s+$/u.test(part)) {
|
|
606
|
-
children.push(Text(part, node.props));
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
let chunk = "";
|
|
611
|
-
let chunkGraphemes = 0;
|
|
612
|
-
for (const { segment } of graphemeSegmenter.segment(part)) {
|
|
613
|
-
chunk += segment;
|
|
614
|
-
chunkGraphemes += 1;
|
|
615
|
-
|
|
616
|
-
if (chunkGraphemes === chunkSize) {
|
|
617
|
-
children.push(Text(chunk, node.props));
|
|
618
|
-
chunk = "";
|
|
619
|
-
chunkGraphemes = 0;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (chunk !== "") {
|
|
624
|
-
children.push(Text(chunk, node.props));
|
|
597
|
+
if (stderr !== "") {
|
|
598
|
+
if (bodyLines.length > 0) {
|
|
599
|
+
bodyLines.push({ kind: "text", text: "" });
|
|
625
600
|
}
|
|
601
|
+
bodyLines.push(...splitToolTextLines(stderr, "text"));
|
|
602
|
+
}
|
|
603
|
+
if (bodyLines.length === 0) {
|
|
604
|
+
bodyLines.push({ kind: "summary", text: "(no output)" });
|
|
626
605
|
}
|
|
627
606
|
|
|
628
|
-
return
|
|
607
|
+
return {
|
|
608
|
+
bodyLines,
|
|
609
|
+
footerLines: [
|
|
610
|
+
{
|
|
611
|
+
kind: "text",
|
|
612
|
+
text: `exit ${result.exitCode}`,
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
};
|
|
629
616
|
}
|
|
630
617
|
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
618
|
+
function getToolHeaderName(toolName: string): string {
|
|
619
|
+
return toolName.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
620
|
+
}
|
|
635
621
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
622
|
+
function getToolHeaderColor(toolName: string, theme: Theme) {
|
|
623
|
+
switch (toolName) {
|
|
624
|
+
case "shell":
|
|
625
|
+
case "read":
|
|
626
|
+
case "grep":
|
|
627
|
+
case "readImage":
|
|
628
|
+
case "todoWrite":
|
|
629
|
+
case "todoRead":
|
|
630
|
+
return theme.accentText;
|
|
631
|
+
case "edit":
|
|
632
|
+
return theme.secondaryAccentText;
|
|
633
|
+
default:
|
|
634
|
+
return theme.secondaryAccentText ?? theme.toolText;
|
|
635
|
+
}
|
|
643
636
|
}
|
|
644
637
|
|
|
645
|
-
function
|
|
638
|
+
function getHighlightedBodyNode(
|
|
646
639
|
spec: HighlightedBodySpec,
|
|
647
640
|
theme: Theme,
|
|
648
|
-
|
|
649
|
-
): Node[] {
|
|
641
|
+
): Node | null {
|
|
650
642
|
if (spec.text === "") {
|
|
651
|
-
return
|
|
643
|
+
return null;
|
|
652
644
|
}
|
|
653
645
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
646
|
+
try {
|
|
647
|
+
return SyntaxHighlight(spec.text, spec.language, {
|
|
648
|
+
theme: getSyntaxHighlightTheme(theme, spec.themeVariant),
|
|
649
|
+
});
|
|
650
|
+
} catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
660
653
|
}
|
|
661
654
|
|
|
662
655
|
/** Render a single styled text node for a tool line. */
|
|
@@ -673,6 +666,11 @@ function renderToolLine(line: ToolRenderLine, theme: Theme): Node {
|
|
|
673
666
|
return Text(line.text, { fgColor: theme.diffAdded, wrap: "word" });
|
|
674
667
|
case "diffRemoved":
|
|
675
668
|
return Text(line.text, { fgColor: theme.diffRemoved, wrap: "word" });
|
|
669
|
+
case "context":
|
|
670
|
+
return Text(line.text, {
|
|
671
|
+
fgColor: theme.mutedText,
|
|
672
|
+
wrap: "word",
|
|
673
|
+
});
|
|
676
674
|
case "summary":
|
|
677
675
|
return Text(line.text, {
|
|
678
676
|
fgColor: theme.toolText,
|
|
@@ -682,7 +680,10 @@ function renderToolLine(line: ToolRenderLine, theme: Theme): Node {
|
|
|
682
680
|
case "error":
|
|
683
681
|
return Text(line.text, { fgColor: theme.error, wrap: "word" });
|
|
684
682
|
case "text":
|
|
685
|
-
return Text(line.text, {
|
|
683
|
+
return Text(line.text, {
|
|
684
|
+
fgColor: theme.toolText,
|
|
685
|
+
wrap: "word",
|
|
686
|
+
});
|
|
686
687
|
}
|
|
687
688
|
}
|
|
688
689
|
|
|
@@ -736,36 +737,58 @@ function renderTodoChecklist(todos: readonly TodoItem[], theme: Theme): Node {
|
|
|
736
737
|
);
|
|
737
738
|
}
|
|
738
739
|
|
|
739
|
-
function
|
|
740
|
-
|
|
741
|
-
|
|
740
|
+
function measureToolNodeHeight(node: Node, width: number): number {
|
|
741
|
+
return measureContentHeight(VStack({}, [node]), { width });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function renderFullToolBody(spec: ToolBlockSpec, theme: Theme): Node | null {
|
|
745
|
+
if (spec.highlightedBody) {
|
|
746
|
+
const highlighted = getHighlightedBodyNode(spec.highlightedBody, theme);
|
|
747
|
+
if (highlighted) {
|
|
748
|
+
return highlighted;
|
|
749
|
+
}
|
|
742
750
|
}
|
|
743
751
|
|
|
744
|
-
|
|
745
|
-
|
|
752
|
+
if (spec.bodyLines.length === 0) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
746
755
|
|
|
747
|
-
|
|
748
|
-
return measureContentHeight(VStack({}, [line]), { width });
|
|
756
|
+
return VStack({}, renderToolLines(spec.bodyLines, theme));
|
|
749
757
|
}
|
|
750
758
|
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
):
|
|
756
|
-
|
|
757
|
-
|
|
759
|
+
function renderToolBodyLines(
|
|
760
|
+
spec: ToolBlockSpec,
|
|
761
|
+
lines: readonly ToolRenderLine[],
|
|
762
|
+
theme: Theme,
|
|
763
|
+
): Node | null {
|
|
764
|
+
if (lines.length === 0) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
758
767
|
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
768
|
+
if (spec.highlightedBody) {
|
|
769
|
+
const highlighted = getHighlightedBodyNode(
|
|
770
|
+
{
|
|
771
|
+
...spec.highlightedBody,
|
|
772
|
+
text: lines.map((line) => line.text).join("\n"),
|
|
773
|
+
},
|
|
774
|
+
theme,
|
|
775
|
+
);
|
|
776
|
+
if (highlighted) {
|
|
777
|
+
return highlighted;
|
|
763
778
|
}
|
|
764
|
-
remainingRows -= lineHeight;
|
|
765
|
-
visibleLineCount += 1;
|
|
766
779
|
}
|
|
767
780
|
|
|
768
|
-
return
|
|
781
|
+
return VStack({}, renderToolLines(lines, theme));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function wrapToolPreviewBody(body: Node): Node {
|
|
785
|
+
return VStack(
|
|
786
|
+
{
|
|
787
|
+
height: UI_TOOL_PREVIEW_ROWS,
|
|
788
|
+
justifyContent: "end",
|
|
789
|
+
},
|
|
790
|
+
body.type === "vstack" ? [...body.children] : [body],
|
|
791
|
+
);
|
|
769
792
|
}
|
|
770
793
|
|
|
771
794
|
function renderToolHeaderPill(
|
|
@@ -781,49 +804,64 @@ function renderToolHeaderPill(
|
|
|
781
804
|
]);
|
|
782
805
|
}
|
|
783
806
|
|
|
784
|
-
function
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (
|
|
790
|
-
|
|
807
|
+
function renderToolHeaderRow(spec: ToolBlockSpec, theme: Theme): Node {
|
|
808
|
+
const children: Node[] = [
|
|
809
|
+
renderToolHeaderPill(spec.toolName, spec.direction, theme),
|
|
810
|
+
];
|
|
811
|
+
|
|
812
|
+
if (spec.headerSuffix) {
|
|
813
|
+
children.push(
|
|
814
|
+
Text(` ${spec.headerSuffix}`, {
|
|
815
|
+
fgColor: theme.toolText,
|
|
816
|
+
wrap: "word",
|
|
817
|
+
}),
|
|
818
|
+
);
|
|
791
819
|
}
|
|
792
820
|
|
|
793
|
-
|
|
794
|
-
|
|
821
|
+
return HStack({}, children);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function renderCompactToolBody(
|
|
825
|
+
spec: ToolBlockSpec,
|
|
826
|
+
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme">,
|
|
827
|
+
): { body: Node | null; summary?: ToolRenderLine } {
|
|
828
|
+
if (spec.bodyLines.length === 0) {
|
|
829
|
+
return { body: null };
|
|
795
830
|
}
|
|
796
831
|
|
|
797
832
|
const bodyWidth = getToolBodyWidth(opts.previewWidth);
|
|
798
|
-
|
|
799
|
-
if (totalHeight <= UI_TOOL_PREVIEW_ROWS) {
|
|
800
|
-
return { body: VStack({}, [...lines]) };
|
|
801
|
-
}
|
|
833
|
+
let previewStart = Math.max(0, spec.bodyLines.length - UI_TOOL_PREVIEW_ROWS);
|
|
802
834
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const hiddenLineCount = Math.max(0, lines.length - visibleLineCount);
|
|
835
|
+
for (;;) {
|
|
836
|
+
const previewLines = spec.bodyLines.slice(previewStart);
|
|
837
|
+
const previewBody = renderToolBodyLines(spec, previewLines, opts.theme);
|
|
838
|
+
if (!previewBody) {
|
|
839
|
+
return { body: null };
|
|
840
|
+
}
|
|
810
841
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
842
|
+
const previewHeight = measureToolNodeHeight(previewBody, bodyWidth);
|
|
843
|
+
if (
|
|
844
|
+
previewHeight <= UI_TOOL_PREVIEW_ROWS ||
|
|
845
|
+
previewStart >= spec.bodyLines.length - 1
|
|
846
|
+
) {
|
|
847
|
+
const hiddenLineCount = previewStart;
|
|
848
|
+
return {
|
|
849
|
+
body:
|
|
850
|
+
hiddenLineCount > 0 || previewHeight > UI_TOOL_PREVIEW_ROWS
|
|
851
|
+
? wrapToolPreviewBody(previewBody)
|
|
852
|
+
: previewBody,
|
|
853
|
+
summary:
|
|
854
|
+
hiddenLineCount > 0
|
|
855
|
+
? {
|
|
856
|
+
kind: "summary",
|
|
857
|
+
text: `And ${hiddenLineCount} lines more`,
|
|
858
|
+
}
|
|
859
|
+
: undefined,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
previewStart += 1;
|
|
864
|
+
}
|
|
827
865
|
}
|
|
828
866
|
|
|
829
867
|
function renderToolBody(
|
|
@@ -834,23 +872,13 @@ function renderToolBody(
|
|
|
834
872
|
return { body: spec.bodyNode };
|
|
835
873
|
}
|
|
836
874
|
|
|
837
|
-
if (spec.
|
|
838
|
-
return
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
opts.theme,
|
|
842
|
-
getToolBodyWidth(opts.previewWidth),
|
|
843
|
-
),
|
|
844
|
-
spec.previewBody,
|
|
845
|
-
opts,
|
|
846
|
-
);
|
|
875
|
+
if (opts.verbose || !spec.previewBody) {
|
|
876
|
+
return {
|
|
877
|
+
body: renderFullToolBody(spec, opts.theme),
|
|
878
|
+
};
|
|
847
879
|
}
|
|
848
880
|
|
|
849
|
-
return
|
|
850
|
-
renderToolLines(spec.bodyLines, opts.theme),
|
|
851
|
-
spec.previewBody,
|
|
852
|
-
opts,
|
|
853
|
-
);
|
|
881
|
+
return renderCompactToolBody(spec, opts);
|
|
854
882
|
}
|
|
855
883
|
|
|
856
884
|
/** Render a tool block with a left border and compact header pill. */
|
|
@@ -859,11 +887,7 @@ function renderToolBlock(
|
|
|
859
887
|
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
860
888
|
): Node {
|
|
861
889
|
const body = renderToolBody(spec, opts);
|
|
862
|
-
const children: Node[] = [
|
|
863
|
-
HStack({}, [
|
|
864
|
-
renderToolHeaderPill(spec.toolName, spec.direction, opts.theme),
|
|
865
|
-
]),
|
|
866
|
-
];
|
|
890
|
+
const children: Node[] = [renderToolHeaderRow(spec, opts.theme)];
|
|
867
891
|
|
|
868
892
|
if (body.body) {
|
|
869
893
|
children.push(body.body);
|
|
@@ -871,18 +895,41 @@ function renderToolBlock(
|
|
|
871
895
|
if (body.summary) {
|
|
872
896
|
children.push(renderToolLine(body.summary, opts.theme));
|
|
873
897
|
}
|
|
898
|
+
if (spec.footerLines) {
|
|
899
|
+
children.push(...renderToolLines(spec.footerLines, opts.theme));
|
|
900
|
+
}
|
|
874
901
|
|
|
875
902
|
return HStack({ padding: { x: 1 } }, [
|
|
876
903
|
Text("│ ", { fgColor: opts.theme.toolBorder }),
|
|
877
|
-
VStack({ flex: 1
|
|
904
|
+
VStack({ flex: 1 }, children),
|
|
878
905
|
]);
|
|
879
906
|
}
|
|
880
907
|
|
|
881
|
-
function
|
|
908
|
+
function getToolTextBlocks(content: ToolResultMessage["content"]): string[] {
|
|
882
909
|
return content
|
|
883
910
|
.filter((entry): entry is TextContent => entry.type === "text")
|
|
884
|
-
.map((entry) => entry.text)
|
|
885
|
-
|
|
911
|
+
.map((entry) => entry.text);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function getToolContentText(content: ToolResultMessage["content"]): string {
|
|
915
|
+
return getToolTextBlocks(content).join("\n");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function parseReadToolContent(
|
|
919
|
+
content: ToolResultMessage["content"],
|
|
920
|
+
): ReturnType<typeof parseReadResult> {
|
|
921
|
+
const textBlocks = getToolTextBlocks(content);
|
|
922
|
+
if (textBlocks.length > 1) {
|
|
923
|
+
const continuation = parseReadContinuationHint(textBlocks.at(-1) ?? "");
|
|
924
|
+
if (continuation) {
|
|
925
|
+
return {
|
|
926
|
+
body: textBlocks.slice(0, -1).join("\n"),
|
|
927
|
+
continuation,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return parseReadResult(textBlocks.join("\n"));
|
|
886
933
|
}
|
|
887
934
|
|
|
888
935
|
function buildShellToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
@@ -903,6 +950,70 @@ function buildShellToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
|
903
950
|
};
|
|
904
951
|
}
|
|
905
952
|
|
|
953
|
+
function buildReadToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
954
|
+
const lines: ToolRenderLine[] = [];
|
|
955
|
+
const filePath = getToolArgString(args, "path");
|
|
956
|
+
const offset = readFiniteNumber(args, "offset");
|
|
957
|
+
const limit = readFiniteNumber(args, "limit");
|
|
958
|
+
|
|
959
|
+
if (filePath) {
|
|
960
|
+
lines.push({ kind: "path", text: filePath });
|
|
961
|
+
}
|
|
962
|
+
if (offset !== null) {
|
|
963
|
+
lines.push({ kind: "text", text: `offset: ${offset}` });
|
|
964
|
+
}
|
|
965
|
+
if (limit !== null) {
|
|
966
|
+
lines.push({ kind: "text", text: `limit: ${limit}` });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
toolName: "read",
|
|
971
|
+
direction: "->",
|
|
972
|
+
bodyLines: lines,
|
|
973
|
+
previewBody: true,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function buildGrepToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
978
|
+
const lines: ToolRenderLine[] = [];
|
|
979
|
+
const pattern = getToolArgString(args, "pattern");
|
|
980
|
+
const path = getToolArgString(args, "path");
|
|
981
|
+
const glob = getToolArgString(args, "glob");
|
|
982
|
+
const context = readFiniteNumber(args, "context");
|
|
983
|
+
const limit = readFiniteNumber(args, "limit");
|
|
984
|
+
const ignoreCase = readBoolean(args, "ignoreCase");
|
|
985
|
+
const literal = readBoolean(args, "literal");
|
|
986
|
+
|
|
987
|
+
if (pattern) {
|
|
988
|
+
lines.push({ kind: "command", text: pattern });
|
|
989
|
+
}
|
|
990
|
+
if (path) {
|
|
991
|
+
lines.push({ kind: "text", text: `path: ${path}` });
|
|
992
|
+
}
|
|
993
|
+
if (glob) {
|
|
994
|
+
lines.push({ kind: "text", text: `glob: ${glob}` });
|
|
995
|
+
}
|
|
996
|
+
if (ignoreCase) {
|
|
997
|
+
lines.push({ kind: "text", text: "ignoreCase: true" });
|
|
998
|
+
}
|
|
999
|
+
if (literal) {
|
|
1000
|
+
lines.push({ kind: "text", text: "literal: true" });
|
|
1001
|
+
}
|
|
1002
|
+
if (context !== null) {
|
|
1003
|
+
lines.push({ kind: "text", text: `context: ${context}` });
|
|
1004
|
+
}
|
|
1005
|
+
if (limit !== null) {
|
|
1006
|
+
lines.push({ kind: "text", text: `limit: ${limit}` });
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
return {
|
|
1010
|
+
toolName: "grep",
|
|
1011
|
+
direction: "->",
|
|
1012
|
+
bodyLines: lines,
|
|
1013
|
+
previewBody: true,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
906
1017
|
function buildEditToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
907
1018
|
const lines: ToolRenderLine[] = [];
|
|
908
1019
|
const filePath = getToolArgString(args, "path");
|
|
@@ -947,6 +1058,10 @@ function buildReadImageToolCallSpec(
|
|
|
947
1058
|
};
|
|
948
1059
|
}
|
|
949
1060
|
|
|
1061
|
+
function isMcpToolName(toolName: string): boolean {
|
|
1062
|
+
return toolName.includes("__");
|
|
1063
|
+
}
|
|
1064
|
+
|
|
950
1065
|
function buildGenericToolCallSpec(
|
|
951
1066
|
toolName: string,
|
|
952
1067
|
args: Record<string, unknown>,
|
|
@@ -956,7 +1071,7 @@ function buildGenericToolCallSpec(
|
|
|
956
1071
|
toolName,
|
|
957
1072
|
direction: "->",
|
|
958
1073
|
bodyLines: text && text !== "{}" ? splitToolTextLines(text, "text") : [],
|
|
959
|
-
previewBody:
|
|
1074
|
+
previewBody: isMcpToolName(toolName),
|
|
960
1075
|
};
|
|
961
1076
|
}
|
|
962
1077
|
|
|
@@ -967,6 +1082,12 @@ function buildToolCallSpec(
|
|
|
967
1082
|
if (toolName === "shell") {
|
|
968
1083
|
return buildShellToolCallSpec(args);
|
|
969
1084
|
}
|
|
1085
|
+
if (toolName === "read") {
|
|
1086
|
+
return buildReadToolCallSpec(args);
|
|
1087
|
+
}
|
|
1088
|
+
if (toolName === "grep") {
|
|
1089
|
+
return buildGrepToolCallSpec(args);
|
|
1090
|
+
}
|
|
970
1091
|
if (toolName === "edit") {
|
|
971
1092
|
return buildEditToolCallSpec(args);
|
|
972
1093
|
}
|
|
@@ -991,14 +1112,119 @@ function renderToolCall(
|
|
|
991
1112
|
|
|
992
1113
|
function buildShellToolResultSpec(
|
|
993
1114
|
content: ToolResultMessage["content"],
|
|
1115
|
+
details?: ToolResultMessage["details"],
|
|
994
1116
|
): ToolBlockSpec {
|
|
1117
|
+
const shellResult = buildShellResultLines(content, details);
|
|
995
1118
|
return {
|
|
996
1119
|
toolName: "shell",
|
|
997
1120
|
direction: "<-",
|
|
998
|
-
bodyLines:
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1121
|
+
bodyLines: shellResult.bodyLines,
|
|
1122
|
+
...(shellResult.footerLines
|
|
1123
|
+
? { footerLines: shellResult.footerLines }
|
|
1124
|
+
: {}),
|
|
1125
|
+
previewBody: true,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function buildReadToolResultSpec(
|
|
1130
|
+
args: Record<string, unknown>,
|
|
1131
|
+
content: ToolResultMessage["content"],
|
|
1132
|
+
isError: boolean,
|
|
1133
|
+
cwd?: string,
|
|
1134
|
+
): ToolBlockSpec {
|
|
1135
|
+
const filePath = getToolArgString(args, "path");
|
|
1136
|
+
const resolvedPath = formatResolvedToolPath(filePath, cwd);
|
|
1137
|
+
const resultText = getToolContentText(content);
|
|
1138
|
+
|
|
1139
|
+
if (isError) {
|
|
1140
|
+
return {
|
|
1141
|
+
toolName: "read",
|
|
1142
|
+
direction: "<-",
|
|
1143
|
+
...(resolvedPath ? { headerSuffix: resolvedPath } : {}),
|
|
1144
|
+
bodyLines: splitToolTextLines(
|
|
1145
|
+
normalizeDisplayLineEndings(resultText),
|
|
1146
|
+
"error",
|
|
1147
|
+
),
|
|
1148
|
+
previewBody: true,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const parsed = parseReadToolContent(content);
|
|
1153
|
+
const bodyText = parsed.body;
|
|
1154
|
+
const displayBodyText = normalizeDisplayLineEndings(bodyText);
|
|
1155
|
+
const language = getReadLanguageCandidates(resolveToolPath(filePath, cwd))[0];
|
|
1156
|
+
return {
|
|
1157
|
+
toolName: "read",
|
|
1158
|
+
direction: "<-",
|
|
1159
|
+
...(resolvedPath ? { headerSuffix: resolvedPath } : {}),
|
|
1160
|
+
bodyLines:
|
|
1161
|
+
displayBodyText === ""
|
|
1162
|
+
? [{ kind: "summary", text: "Empty file." }]
|
|
1163
|
+
: splitToolTextLines(displayBodyText, "text"),
|
|
1164
|
+
...(bodyText !== "" && language
|
|
1165
|
+
? {
|
|
1166
|
+
highlightedBody: {
|
|
1167
|
+
text: bodyText,
|
|
1168
|
+
language,
|
|
1169
|
+
themeVariant: "code" as const,
|
|
1170
|
+
},
|
|
1171
|
+
}
|
|
1172
|
+
: {}),
|
|
1173
|
+
previewBody: true,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function buildGrepToolResultSpec(
|
|
1178
|
+
content: ToolResultMessage["content"],
|
|
1179
|
+
isError: boolean,
|
|
1180
|
+
cwd?: string,
|
|
1181
|
+
): ToolBlockSpec {
|
|
1182
|
+
const resultText = getToolContentText(content);
|
|
1183
|
+
if (isError) {
|
|
1184
|
+
return {
|
|
1185
|
+
toolName: "grep",
|
|
1186
|
+
direction: "<-",
|
|
1187
|
+
bodyLines: splitToolTextLines(resultText, "error"),
|
|
1188
|
+
previewBody: true,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const result = parseGrepResult(resultText);
|
|
1193
|
+
if (!result) {
|
|
1194
|
+
return {
|
|
1195
|
+
toolName: "grep",
|
|
1196
|
+
direction: "<-",
|
|
1197
|
+
bodyLines: splitToolTextLines(resultText, "text"),
|
|
1198
|
+
previewBody: true,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const lines: ToolRenderLine[] = [];
|
|
1203
|
+
for (const file of result.files) {
|
|
1204
|
+
lines.push({
|
|
1205
|
+
kind: "path",
|
|
1206
|
+
text: formatRelativeToolPath(file.path, cwd),
|
|
1207
|
+
});
|
|
1208
|
+
for (const line of file.lines) {
|
|
1209
|
+
const text = line.text.replace(/\r?\n$/, "");
|
|
1210
|
+
lines.push({
|
|
1211
|
+
kind: line.kind === "context" ? "context" : "text",
|
|
1212
|
+
text: ` ${line.lineNumber}: ${text}`,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (lines.length === 0) {
|
|
1218
|
+
lines.push({ kind: "summary", text: "No matches found." });
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return {
|
|
1222
|
+
toolName: "grep",
|
|
1223
|
+
direction: "<-",
|
|
1224
|
+
bodyLines: lines,
|
|
1225
|
+
...(result.hint
|
|
1226
|
+
? { footerLines: [{ kind: "summary", text: result.hint }] }
|
|
1227
|
+
: {}),
|
|
1002
1228
|
previewBody: true,
|
|
1003
1229
|
};
|
|
1004
1230
|
}
|
|
@@ -1083,7 +1309,7 @@ function buildGenericToolResultSpec(
|
|
|
1083
1309
|
toolName,
|
|
1084
1310
|
direction: "<-",
|
|
1085
1311
|
bodyLines: splitToolTextLines(output, isError ? "error" : "text"),
|
|
1086
|
-
previewBody:
|
|
1312
|
+
previewBody: isMcpToolName(toolName),
|
|
1087
1313
|
};
|
|
1088
1314
|
}
|
|
1089
1315
|
|
|
@@ -1092,10 +1318,26 @@ function renderToolResultContent(
|
|
|
1092
1318
|
args: Record<string, unknown>,
|
|
1093
1319
|
content: ToolResultMessage["content"],
|
|
1094
1320
|
isError: boolean,
|
|
1095
|
-
opts: Pick<
|
|
1321
|
+
opts: Pick<
|
|
1322
|
+
ConversationRenderOpts,
|
|
1323
|
+
"previewWidth" | "verbose" | "theme" | "cwd"
|
|
1324
|
+
>,
|
|
1325
|
+
details?: ToolResultMessage["details"],
|
|
1096
1326
|
): Node {
|
|
1097
1327
|
if (toolName === "shell") {
|
|
1098
|
-
return renderToolBlock(buildShellToolResultSpec(content), opts);
|
|
1328
|
+
return renderToolBlock(buildShellToolResultSpec(content, details), opts);
|
|
1329
|
+
}
|
|
1330
|
+
if (toolName === "read") {
|
|
1331
|
+
return renderToolBlock(
|
|
1332
|
+
buildReadToolResultSpec(args, content, isError, opts.cwd),
|
|
1333
|
+
opts,
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (toolName === "grep") {
|
|
1337
|
+
return renderToolBlock(
|
|
1338
|
+
buildGrepToolResultSpec(content, isError, opts.cwd),
|
|
1339
|
+
opts,
|
|
1340
|
+
);
|
|
1099
1341
|
}
|
|
1100
1342
|
if (toolName === "edit") {
|
|
1101
1343
|
return renderToolBlock(
|
|
@@ -1130,6 +1372,7 @@ function renderToolResultContent(
|
|
|
1130
1372
|
* @param resultText - Text content from the tool result message.
|
|
1131
1373
|
* @param isError - Whether the tool execution failed.
|
|
1132
1374
|
* @param opts - Shared conversation render options.
|
|
1375
|
+
* @param details - Optional structured tool-result details.
|
|
1133
1376
|
* @returns The rendered tool block node.
|
|
1134
1377
|
*/
|
|
1135
1378
|
export function renderToolResult(
|
|
@@ -1138,12 +1381,20 @@ export function renderToolResult(
|
|
|
1138
1381
|
resultText: string,
|
|
1139
1382
|
isError: boolean,
|
|
1140
1383
|
opts: ConversationRenderOpts,
|
|
1384
|
+
details?: ToolResultMessage["details"],
|
|
1141
1385
|
): Node {
|
|
1142
1386
|
const content: ToolResultMessage["content"] = resultText
|
|
1143
1387
|
? [{ type: "text", text: resultText }]
|
|
1144
1388
|
: [];
|
|
1145
1389
|
|
|
1146
|
-
return renderToolResultContent(
|
|
1390
|
+
return renderToolResultContent(
|
|
1391
|
+
toolName,
|
|
1392
|
+
args,
|
|
1393
|
+
content,
|
|
1394
|
+
isError,
|
|
1395
|
+
opts,
|
|
1396
|
+
details,
|
|
1397
|
+
);
|
|
1147
1398
|
}
|
|
1148
1399
|
|
|
1149
1400
|
function renderUiTodoMessage(
|
|
@@ -1170,11 +1421,7 @@ function renderUiMessage(
|
|
|
1170
1421
|
}
|
|
1171
1422
|
|
|
1172
1423
|
if (msg.format === "markdown") {
|
|
1173
|
-
const markdown = renderMarkdownTextBlock(
|
|
1174
|
-
msg.content,
|
|
1175
|
-
opts.theme,
|
|
1176
|
-
opts.previewWidth,
|
|
1177
|
-
);
|
|
1424
|
+
const markdown = renderMarkdownTextBlock(msg.content, opts.theme);
|
|
1178
1425
|
if (markdown) {
|
|
1179
1426
|
return markdown;
|
|
1180
1427
|
}
|
|
@@ -1206,13 +1453,6 @@ function renderEmptyConversationBanner(
|
|
|
1206
1453
|
]);
|
|
1207
1454
|
}
|
|
1208
1455
|
|
|
1209
|
-
function pushConversationNode(nodes: Node[], node: Node | null): void {
|
|
1210
|
-
if (!node) {
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
nodes.push(node);
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
1456
|
function rememberToolCallArgs(
|
|
1217
1457
|
message: AssistantMessage,
|
|
1218
1458
|
toolCallArgs: Map<string, ToolCallRenderInfo>,
|
|
@@ -1234,8 +1474,16 @@ function renderToolResultMessage(
|
|
|
1234
1474
|
content: ToolResultMessage["content"],
|
|
1235
1475
|
isError: boolean,
|
|
1236
1476
|
renderOpts: ConversationRenderOpts,
|
|
1477
|
+
details?: ToolResultMessage["details"],
|
|
1237
1478
|
): Node {
|
|
1238
|
-
return renderToolResultContent(
|
|
1479
|
+
return renderToolResultContent(
|
|
1480
|
+
toolName,
|
|
1481
|
+
args,
|
|
1482
|
+
content,
|
|
1483
|
+
isError,
|
|
1484
|
+
renderOpts,
|
|
1485
|
+
details,
|
|
1486
|
+
);
|
|
1239
1487
|
}
|
|
1240
1488
|
|
|
1241
1489
|
function renderConversationMessage(
|
|
@@ -1262,6 +1510,7 @@ function renderConversationMessage(
|
|
|
1262
1510
|
message.content,
|
|
1263
1511
|
message.isError,
|
|
1264
1512
|
renderOpts,
|
|
1513
|
+
message.details,
|
|
1265
1514
|
);
|
|
1266
1515
|
}
|
|
1267
1516
|
|
|
@@ -1285,58 +1534,150 @@ function cacheToolCallArgs(messages: readonly ConversationMessage[]): void {
|
|
|
1285
1534
|
toolCallArgsCache.count = messages.length;
|
|
1286
1535
|
}
|
|
1287
1536
|
|
|
1288
|
-
function
|
|
1289
|
-
|
|
1290
|
-
|
|
1537
|
+
function canReuseConversationMessageRender(
|
|
1538
|
+
cached: ConversationMessageRenderCacheEntry | undefined,
|
|
1539
|
+
renderOpts: ConversationRenderOpts,
|
|
1291
1540
|
previewWidth: number,
|
|
1292
|
-
):
|
|
1541
|
+
): cached is ConversationMessageRenderCacheEntry {
|
|
1293
1542
|
return (
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
conversationRenderCache.count <= state.messages.length
|
|
1543
|
+
cached !== undefined &&
|
|
1544
|
+
cached.showReasoning === renderOpts.showReasoning &&
|
|
1545
|
+
cached.verbose === renderOpts.verbose &&
|
|
1546
|
+
cached.previewWidth === previewWidth &&
|
|
1547
|
+
cached.cwd === renderOpts.cwd &&
|
|
1548
|
+
cached.theme === renderOpts.theme
|
|
1301
1549
|
);
|
|
1302
1550
|
}
|
|
1303
1551
|
|
|
1304
|
-
function
|
|
1552
|
+
function canReuseCommittedConversationLayout(
|
|
1305
1553
|
state: ConversationLogState,
|
|
1306
1554
|
renderOpts: ConversationRenderOpts,
|
|
1307
|
-
|
|
1555
|
+
previewWidth: number,
|
|
1556
|
+
): boolean {
|
|
1557
|
+
return (
|
|
1558
|
+
conversationLayoutCache.messages === state.messages &&
|
|
1559
|
+
conversationLayoutCache.count <= state.messages.length &&
|
|
1560
|
+
conversationLayoutCache.showReasoning === renderOpts.showReasoning &&
|
|
1561
|
+
conversationLayoutCache.verbose === renderOpts.verbose &&
|
|
1562
|
+
conversationLayoutCache.previewWidth === previewWidth &&
|
|
1563
|
+
conversationLayoutCache.cwd === renderOpts.cwd &&
|
|
1564
|
+
conversationLayoutCache.theme === renderOpts.theme
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function appendConversationLogItem(
|
|
1569
|
+
items: ConversationLogItem[],
|
|
1570
|
+
prefixHeights: number[],
|
|
1571
|
+
item: ConversationLogItem | null,
|
|
1308
1572
|
): void {
|
|
1573
|
+
if (!item) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
items.push(item);
|
|
1578
|
+
prefixHeights.push(prefixHeights[prefixHeights.length - 1]! + item.height);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function measureConversationItemHeight(
|
|
1582
|
+
node: Node,
|
|
1583
|
+
previewWidth: number,
|
|
1584
|
+
): number {
|
|
1585
|
+
return measureContentHeight(node, {
|
|
1586
|
+
width: getPreviewWidth(previewWidth),
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function createConversationLogItem(
|
|
1591
|
+
node: Node | null,
|
|
1592
|
+
previewWidth: number,
|
|
1593
|
+
): ConversationLogItem | null {
|
|
1594
|
+
if (!node) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
return {
|
|
1599
|
+
node,
|
|
1600
|
+
height: measureConversationItemHeight(node, previewWidth),
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function getCommittedConversationLogItem(
|
|
1605
|
+
message: ConversationMessage,
|
|
1606
|
+
renderOpts: ConversationRenderOpts,
|
|
1607
|
+
): ConversationLogItem | null {
|
|
1309
1608
|
const previewWidth = getPreviewWidth(renderOpts.previewWidth);
|
|
1310
|
-
|
|
1609
|
+
const cached = conversationMessageRenderCache.get(message);
|
|
1610
|
+
if (canReuseConversationMessageRender(cached, renderOpts, previewWidth)) {
|
|
1611
|
+
if (!cached.node) {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
node: cached.node,
|
|
1616
|
+
height: cached.height,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const node = renderConversationMessage(
|
|
1621
|
+
message,
|
|
1622
|
+
renderOpts,
|
|
1623
|
+
toolCallArgsCache.entries,
|
|
1624
|
+
renderOpts.theme,
|
|
1625
|
+
);
|
|
1626
|
+
const nextEntry: ConversationMessageRenderCacheEntry = {
|
|
1627
|
+
node,
|
|
1628
|
+
height: node ? measureConversationItemHeight(node, previewWidth) : 0,
|
|
1629
|
+
showReasoning: renderOpts.showReasoning,
|
|
1630
|
+
verbose: renderOpts.verbose,
|
|
1631
|
+
previewWidth,
|
|
1632
|
+
cwd: renderOpts.cwd ?? null,
|
|
1633
|
+
theme: renderOpts.theme,
|
|
1634
|
+
};
|
|
1635
|
+
conversationMessageRenderCache.set(message, nextEntry);
|
|
1636
|
+
|
|
1637
|
+
if (!node) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
return {
|
|
1642
|
+
node,
|
|
1643
|
+
height: nextEntry.height,
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1311
1646
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1647
|
+
function getCommittedConversationLayoutSnapshot(
|
|
1648
|
+
state: ConversationLogState,
|
|
1649
|
+
renderOpts: ConversationRenderOpts,
|
|
1650
|
+
): ConversationLayoutSnapshot {
|
|
1651
|
+
const previewWidth = getPreviewWidth(renderOpts.previewWidth);
|
|
1652
|
+
if (!canReuseCommittedConversationLayout(state, renderOpts, previewWidth)) {
|
|
1653
|
+
conversationLayoutCache.messages = state.messages;
|
|
1654
|
+
conversationLayoutCache.count = 0;
|
|
1655
|
+
conversationLayoutCache.showReasoning = renderOpts.showReasoning;
|
|
1656
|
+
conversationLayoutCache.verbose = renderOpts.verbose;
|
|
1657
|
+
conversationLayoutCache.previewWidth = previewWidth;
|
|
1658
|
+
conversationLayoutCache.cwd = renderOpts.cwd ?? null;
|
|
1659
|
+
conversationLayoutCache.theme = renderOpts.theme;
|
|
1660
|
+
conversationLayoutCache.items = [];
|
|
1661
|
+
conversationLayoutCache.prefixHeights = [0];
|
|
1321
1662
|
}
|
|
1322
1663
|
|
|
1323
1664
|
for (
|
|
1324
|
-
let index =
|
|
1665
|
+
let index = conversationLayoutCache.count;
|
|
1325
1666
|
index < state.messages.length;
|
|
1326
1667
|
index++
|
|
1327
1668
|
) {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
renderOpts,
|
|
1333
|
-
toolCallArgsCache.entries,
|
|
1334
|
-
state.theme,
|
|
1335
|
-
),
|
|
1669
|
+
appendConversationLogItem(
|
|
1670
|
+
conversationLayoutCache.items,
|
|
1671
|
+
conversationLayoutCache.prefixHeights,
|
|
1672
|
+
getCommittedConversationLogItem(state.messages[index]!, renderOpts),
|
|
1336
1673
|
);
|
|
1337
1674
|
}
|
|
1338
1675
|
|
|
1339
|
-
|
|
1676
|
+
conversationLayoutCache.count = state.messages.length;
|
|
1677
|
+
return {
|
|
1678
|
+
items: conversationLayoutCache.items,
|
|
1679
|
+
prefixHeights: conversationLayoutCache.prefixHeights,
|
|
1680
|
+
};
|
|
1340
1681
|
}
|
|
1341
1682
|
|
|
1342
1683
|
function hasStreamingTail(streaming: StreamingConversationState): boolean {
|
|
@@ -1347,67 +1688,149 @@ function hasStreamingTail(streaming: StreamingConversationState): boolean {
|
|
|
1347
1688
|
);
|
|
1348
1689
|
}
|
|
1349
1690
|
|
|
1691
|
+
function appendStreamingConversationLogItems(
|
|
1692
|
+
items: ConversationLogItem[],
|
|
1693
|
+
prefixHeights: number[],
|
|
1694
|
+
streaming: StreamingConversationState,
|
|
1695
|
+
renderOpts: ConversationRenderOpts,
|
|
1696
|
+
previewWidth: number,
|
|
1697
|
+
): void {
|
|
1698
|
+
appendConversationLogItem(
|
|
1699
|
+
items,
|
|
1700
|
+
prefixHeights,
|
|
1701
|
+
createConversationLogItem(
|
|
1702
|
+
renderAssistantMessage(
|
|
1703
|
+
{
|
|
1704
|
+
content: streaming.content,
|
|
1705
|
+
},
|
|
1706
|
+
renderOpts,
|
|
1707
|
+
),
|
|
1708
|
+
previewWidth,
|
|
1709
|
+
),
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
for (const pendingToolResult of streaming.pendingToolResults) {
|
|
1713
|
+
const info = toolCallArgsCache.entries.get(pendingToolResult.toolCallId);
|
|
1714
|
+
appendConversationLogItem(
|
|
1715
|
+
items,
|
|
1716
|
+
prefixHeights,
|
|
1717
|
+
createConversationLogItem(
|
|
1718
|
+
renderToolResultMessage(
|
|
1719
|
+
info?.name ?? pendingToolResult.toolName,
|
|
1720
|
+
info?.args ?? EMPTY_TOOL_RESULT_ARGS,
|
|
1721
|
+
pendingToolResult.content,
|
|
1722
|
+
pendingToolResult.isError,
|
|
1723
|
+
renderOpts,
|
|
1724
|
+
pendingToolResult.details,
|
|
1725
|
+
),
|
|
1726
|
+
previewWidth,
|
|
1727
|
+
),
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function buildEmptyConversationBannerSnapshot(
|
|
1733
|
+
state: ConversationLogState,
|
|
1734
|
+
previewWidth: number,
|
|
1735
|
+
): ConversationLayoutSnapshot {
|
|
1736
|
+
const banner = renderEmptyConversationBanner(
|
|
1737
|
+
state.theme,
|
|
1738
|
+
state.versionLabel ?? DEV_VERSION_LABEL,
|
|
1739
|
+
);
|
|
1740
|
+
const item = {
|
|
1741
|
+
node: banner,
|
|
1742
|
+
height: measureConversationItemHeight(banner, previewWidth),
|
|
1743
|
+
};
|
|
1744
|
+
return {
|
|
1745
|
+
items: [item],
|
|
1746
|
+
prefixHeights: [0, item.height],
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1350
1750
|
/**
|
|
1351
|
-
* Build
|
|
1751
|
+
* Build rendered conversation items plus cached cumulative intrinsic heights.
|
|
1352
1752
|
*
|
|
1353
1753
|
* @param state - Conversation rendering state.
|
|
1354
1754
|
* @param streaming - Current in-progress assistant tail, if any.
|
|
1355
|
-
* @param startIndex - Index of the first committed message to render.
|
|
1356
1755
|
* @param previewWidth - Available terminal width for width-aware tool previews.
|
|
1357
|
-
* @returns
|
|
1756
|
+
* @returns Rendered items plus cumulative heights in on-screen order.
|
|
1358
1757
|
*/
|
|
1359
|
-
export function
|
|
1758
|
+
export function buildConversationLayoutSnapshot(
|
|
1360
1759
|
state: ConversationLogState,
|
|
1361
1760
|
streaming: StreamingConversationState,
|
|
1362
|
-
startIndex = 0,
|
|
1363
1761
|
previewWidth = DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
1364
|
-
):
|
|
1762
|
+
): ConversationLayoutSnapshot {
|
|
1365
1763
|
const renderOpts: ConversationRenderOpts = {
|
|
1366
1764
|
showReasoning: state.showReasoning,
|
|
1367
1765
|
verbose: state.verbose,
|
|
1368
1766
|
theme: state.theme,
|
|
1767
|
+
cwd: state.cwd,
|
|
1369
1768
|
previewWidth,
|
|
1370
1769
|
};
|
|
1371
1770
|
|
|
1372
|
-
|
|
1373
|
-
return [
|
|
1374
|
-
renderEmptyConversationBanner(
|
|
1375
|
-
state.theme,
|
|
1376
|
-
state.versionLabel ?? DEV_VERSION_LABEL,
|
|
1377
|
-
),
|
|
1378
|
-
];
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
cacheCommittedConversation(state, renderOpts, startIndex);
|
|
1771
|
+
cacheToolCallArgs(state.messages);
|
|
1382
1772
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1773
|
+
const committedSnapshot = getCommittedConversationLayoutSnapshot(
|
|
1774
|
+
state,
|
|
1775
|
+
renderOpts,
|
|
1776
|
+
);
|
|
1777
|
+
const streamingTailVisible = hasStreamingTail(streaming);
|
|
1778
|
+
if (!streamingTailVisible) {
|
|
1779
|
+
if (committedSnapshot.items.length > 0 || state.messages.length > 0) {
|
|
1780
|
+
return committedSnapshot;
|
|
1781
|
+
}
|
|
1782
|
+
return buildEmptyConversationBannerSnapshot(state, previewWidth);
|
|
1385
1783
|
}
|
|
1386
1784
|
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
),
|
|
1785
|
+
const items = [...committedSnapshot.items];
|
|
1786
|
+
const prefixHeights = [...committedSnapshot.prefixHeights];
|
|
1787
|
+
appendStreamingConversationLogItems(
|
|
1788
|
+
items,
|
|
1789
|
+
prefixHeights,
|
|
1790
|
+
streaming,
|
|
1791
|
+
renderOpts,
|
|
1792
|
+
previewWidth,
|
|
1396
1793
|
);
|
|
1794
|
+
return {
|
|
1795
|
+
items,
|
|
1796
|
+
prefixHeights,
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1397
1799
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1800
|
+
/**
|
|
1801
|
+
* Build the rendered conversation items with cached per-message nodes/heights.
|
|
1802
|
+
*
|
|
1803
|
+
* @param state - Conversation rendering state.
|
|
1804
|
+
* @param streaming - Current in-progress assistant tail, if any.
|
|
1805
|
+
* @param previewWidth - Available terminal width for width-aware tool previews.
|
|
1806
|
+
* @returns Rendered items in on-screen order.
|
|
1807
|
+
*/
|
|
1808
|
+
export function buildConversationLogItems(
|
|
1809
|
+
state: ConversationLogState,
|
|
1810
|
+
streaming: StreamingConversationState,
|
|
1811
|
+
previewWidth = DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
1812
|
+
): readonly ConversationLogItem[] {
|
|
1813
|
+
return buildConversationLayoutSnapshot(state, streaming, previewWidth).items;
|
|
1814
|
+
}
|
|
1411
1815
|
|
|
1412
|
-
|
|
1816
|
+
/**
|
|
1817
|
+
* Build the full conversation log as an array of nodes.
|
|
1818
|
+
*
|
|
1819
|
+
* @param state - Conversation rendering state.
|
|
1820
|
+
* @param streaming - Current in-progress assistant tail, if any.
|
|
1821
|
+
* @param startIndex - Index of the first rendered item to include.
|
|
1822
|
+
* @param previewWidth - Available terminal width for width-aware tool previews.
|
|
1823
|
+
* @param endIndex - Exclusive rendered-item end index.
|
|
1824
|
+
* @returns The rendered conversation log nodes.
|
|
1825
|
+
*/
|
|
1826
|
+
export function buildConversationLogNodes(
|
|
1827
|
+
state: ConversationLogState,
|
|
1828
|
+
streaming: StreamingConversationState,
|
|
1829
|
+
startIndex = 0,
|
|
1830
|
+
previewWidth = DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
1831
|
+
endIndex = Number.POSITIVE_INFINITY,
|
|
1832
|
+
): Node[] {
|
|
1833
|
+
return buildConversationLogItems(state, streaming, previewWidth)
|
|
1834
|
+
.slice(startIndex, endIndex)
|
|
1835
|
+
.map((item) => item.node);
|
|
1413
1836
|
}
|