mini-coder 0.5.14 → 0.6.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/README.md +26 -109
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/nono-mini-coder.json +42 -0
- package/package.json +17 -22
- package/src/agent.ts +243 -1403
- package/src/args.ts +289 -0
- package/src/headless.ts +41 -359
- package/src/index.ts +29 -1016
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +219 -284
- package/src/session.ts +55 -1306
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-read.ts +80 -293
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +271 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +228 -0
- package/src/types.ts +164 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -5
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/assistant-output.ts +0 -73
- package/src/cli.ts +0 -134
- package/src/delegation.ts +0 -238
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -385
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -376
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-delegate.ts +0 -125
- package/src/tool-grep.ts +0 -606
- package/src/tool-shell.ts +0 -1051
- package/src/tools.ts +0 -1179
- package/src/ui/agent.ts +0 -320
- package/src/ui/commands.test.ts +0 -957
- package/src/ui/commands.ts +0 -848
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
package/src/ui/conversation.ts
DELETED
|
@@ -1,1836 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conversation-log rendering for the terminal UI.
|
|
3
|
-
*
|
|
4
|
-
* Renders user, assistant, tool, and UI-only messages into cel-tui nodes.
|
|
5
|
-
* Streaming state is passed in explicitly so the top-level UI module can keep
|
|
6
|
-
* ownership of module-scoped runtime state while this module stays pure.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import {
|
|
13
|
-
basename,
|
|
14
|
-
extname,
|
|
15
|
-
isAbsolute,
|
|
16
|
-
join,
|
|
17
|
-
normalize,
|
|
18
|
-
relative,
|
|
19
|
-
} from "node:path";
|
|
20
|
-
import { SyntaxHighlight } from "@cel-tui/components";
|
|
21
|
-
import { HStack, measureContentHeight, Text, VStack } from "@cel-tui/core";
|
|
22
|
-
import type { Node } from "@cel-tui/types";
|
|
23
|
-
import type {
|
|
24
|
-
AssistantMessage,
|
|
25
|
-
TextContent,
|
|
26
|
-
ToolResultMessage,
|
|
27
|
-
UserMessage,
|
|
28
|
-
} from "@mariozechner/pi-ai";
|
|
29
|
-
import type { AppState } from "../index.ts";
|
|
30
|
-
import type { UiMessage } from "../session.ts";
|
|
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";
|
|
42
|
-
import { APP_NAME, DEV_VERSION_LABEL } from "../version.ts";
|
|
43
|
-
|
|
44
|
-
/** Single blank-line gap used between conversation-level blocks. */
|
|
45
|
-
export const CONVERSATION_GAP = 1;
|
|
46
|
-
|
|
47
|
-
/** Fixed rendered height for non-verbose tool previews. */
|
|
48
|
-
const UI_TOOL_PREVIEW_ROWS = 8;
|
|
49
|
-
|
|
50
|
-
/** Default width used when preview measurements do not receive one explicitly. */
|
|
51
|
-
const DEFAULT_TOOL_PREVIEW_WIDTH = 80;
|
|
52
|
-
|
|
53
|
-
/** Horizontal columns consumed by tool-block padding and the left border. */
|
|
54
|
-
const TOOL_BLOCK_CHROME_WIDTH = 4;
|
|
55
|
-
|
|
56
|
-
/** A pending tool result shown in the streaming tail. */
|
|
57
|
-
export interface PendingToolResult {
|
|
58
|
-
/** Tool call id from the assistant message. */
|
|
59
|
-
toolCallId: string;
|
|
60
|
-
/** Tool name. */
|
|
61
|
-
toolName: string;
|
|
62
|
-
/** Progressive or final tool-result content captured so far. */
|
|
63
|
-
content: ToolResultMessage["content"];
|
|
64
|
-
/** Optional structured details preserved on the tool result. */
|
|
65
|
-
details?: ToolResultMessage["details"];
|
|
66
|
-
/** Whether the tool result was an error. */
|
|
67
|
-
isError: boolean;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Render options shared by completed and in-progress assistant content. */
|
|
71
|
-
interface ConversationRenderOpts {
|
|
72
|
-
/** Whether reasoning blocks are visible. */
|
|
73
|
-
showReasoning: boolean;
|
|
74
|
-
/** Whether tool output should be truncated in the UI. */
|
|
75
|
-
verbose: boolean;
|
|
76
|
-
/** Active UI theme. */
|
|
77
|
-
theme: Theme;
|
|
78
|
-
/** Session working directory used for path display. */
|
|
79
|
-
cwd?: string;
|
|
80
|
-
/** Available terminal width for width-aware tool previews. */
|
|
81
|
-
previewWidth?: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** A single streaming assistant tail appended after persisted messages. */
|
|
85
|
-
export interface StreamingConversationState {
|
|
86
|
-
/** Whether a response is currently streaming. */
|
|
87
|
-
isStreaming: boolean;
|
|
88
|
-
/** Assistant content accumulated so far. */
|
|
89
|
-
content: AssistantMessage["content"];
|
|
90
|
-
/** Tool results observed during the current streaming turn. */
|
|
91
|
-
pendingToolResults: readonly PendingToolResult[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Display-only assistant state used while a response is still streaming. */
|
|
95
|
-
interface AssistantRenderState {
|
|
96
|
-
/** Assistant content blocks accumulated so far. */
|
|
97
|
-
content: AssistantMessage["content"];
|
|
98
|
-
/** Optional error text appended below the assistant content. */
|
|
99
|
-
errorMessage?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type ConversationMessage = AppState["messages"][number];
|
|
103
|
-
|
|
104
|
-
type ConversationLogState = Pick<
|
|
105
|
-
AppState,
|
|
106
|
-
"messages" | "showReasoning" | "verbose" | "theme" | "cwd"
|
|
107
|
-
> & {
|
|
108
|
-
versionLabel?: AppState["versionLabel"];
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
type ToolCallRenderInfo = {
|
|
112
|
-
name: string;
|
|
113
|
-
args: Record<string, unknown>;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
interface ToolCallArgsCache {
|
|
117
|
-
messages: readonly ConversationMessage[] | null;
|
|
118
|
-
count: number;
|
|
119
|
-
entries: Map<string, ToolCallRenderInfo>;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface ConversationMessageRenderCacheEntry {
|
|
123
|
-
node: Node | null;
|
|
124
|
-
height: number;
|
|
125
|
-
showReasoning: boolean;
|
|
126
|
-
verbose: boolean;
|
|
127
|
-
previewWidth: number;
|
|
128
|
-
cwd: string | null;
|
|
129
|
-
theme: Theme | null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
type ToolRenderDirection = "->" | "<-";
|
|
133
|
-
|
|
134
|
-
type ToolRenderLineKind =
|
|
135
|
-
| "command"
|
|
136
|
-
| "path"
|
|
137
|
-
| "text"
|
|
138
|
-
| "context"
|
|
139
|
-
| "diffAdded"
|
|
140
|
-
| "diffRemoved"
|
|
141
|
-
| "summary"
|
|
142
|
-
| "error";
|
|
143
|
-
|
|
144
|
-
type SyntaxThemeVariant = "markdown" | "code" | "shell";
|
|
145
|
-
|
|
146
|
-
interface HighlightedBodySpec {
|
|
147
|
-
/** Full raw source content to syntax-highlight. */
|
|
148
|
-
text: string;
|
|
149
|
-
/** Registered lextide language id. */
|
|
150
|
-
language: string;
|
|
151
|
-
/** Semantic theme variant for this highlighted content. */
|
|
152
|
-
themeVariant: SyntaxThemeVariant;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
interface ToolBlockSpec {
|
|
156
|
-
/** Tool name shown in the header pill. */
|
|
157
|
-
toolName: string;
|
|
158
|
-
/** Direction shown in the header pill. */
|
|
159
|
-
direction: ToolRenderDirection;
|
|
160
|
-
/** Optional compact text appended after the header pill. */
|
|
161
|
-
headerSuffix?: string;
|
|
162
|
-
/** Logical body lines for the tool block. */
|
|
163
|
-
bodyLines: readonly ToolRenderLine[];
|
|
164
|
-
/** Optional syntax-highlighted body rendered from the full unsplit source. */
|
|
165
|
-
highlightedBody?: HighlightedBodySpec;
|
|
166
|
-
/** Optional custom body node rendered as-is. */
|
|
167
|
-
bodyNode?: Node;
|
|
168
|
-
/** Optional footer lines rendered after the body. */
|
|
169
|
-
footerLines?: readonly ToolRenderLine[];
|
|
170
|
-
/** Whether `/verbose` preview rules apply to the body. */
|
|
171
|
-
previewBody: boolean;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** A single logical line in a rendered tool block. */
|
|
175
|
-
export interface ToolRenderLine {
|
|
176
|
-
/** Semantic line kind for styling. */
|
|
177
|
-
kind: ToolRenderLineKind;
|
|
178
|
-
/** Text content for this line. */
|
|
179
|
-
text: string;
|
|
180
|
-
}
|
|
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
|
-
|
|
210
|
-
const EMPTY_STREAMING_CONTENT: AssistantMessage["content"] = [];
|
|
211
|
-
const EMPTY_PENDING_TOOL_RESULTS: readonly PendingToolResult[] = [];
|
|
212
|
-
const EMPTY_TOOL_RESULT_ARGS: Record<string, unknown> = Object.freeze({});
|
|
213
|
-
|
|
214
|
-
const toolCallArgsCache: ToolCallArgsCache = {
|
|
215
|
-
messages: null,
|
|
216
|
-
count: 0,
|
|
217
|
-
entries: new Map(),
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
let conversationMessageRenderCache = new WeakMap<
|
|
221
|
-
ConversationMessage,
|
|
222
|
-
ConversationMessageRenderCacheEntry
|
|
223
|
-
>();
|
|
224
|
-
|
|
225
|
-
const conversationLayoutCache: ConversationLayoutCache = {
|
|
226
|
-
messages: null,
|
|
227
|
-
count: 0,
|
|
228
|
-
showReasoning: false,
|
|
229
|
-
verbose: false,
|
|
230
|
-
previewWidth: DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
231
|
-
cwd: null,
|
|
232
|
-
theme: null,
|
|
233
|
-
items: [],
|
|
234
|
-
prefixHeights: [0],
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
/** Reset cached committed conversation renders. */
|
|
238
|
-
export function resetConversationRenderCache(): void {
|
|
239
|
-
toolCallArgsCache.messages = null;
|
|
240
|
-
toolCallArgsCache.count = 0;
|
|
241
|
-
toolCallArgsCache.entries = new Map();
|
|
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];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Render a user message with a subtle background. */
|
|
255
|
-
function renderUserMessage(msg: UserMessage, theme: Theme): Node {
|
|
256
|
-
const text =
|
|
257
|
-
typeof msg.content === "string"
|
|
258
|
-
? msg.content
|
|
259
|
-
: msg.content
|
|
260
|
-
.filter((content): content is TextContent => content.type === "text")
|
|
261
|
-
.map((content) => content.text)
|
|
262
|
-
.join("");
|
|
263
|
-
|
|
264
|
-
return VStack({ bgColor: theme.userMsgBg, padding: { x: 1 } }, [
|
|
265
|
-
Text(text, { wrap: "word" }),
|
|
266
|
-
]);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Render a syntax-highlighted raw markdown block. */
|
|
270
|
-
function renderMarkdownTextBlock(content: string, theme: Theme): Node | null {
|
|
271
|
-
if (content === "") {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const highlighted = getHighlightedBodyNode(
|
|
276
|
-
{
|
|
277
|
-
text: content,
|
|
278
|
-
language: "markdown",
|
|
279
|
-
themeVariant: "markdown",
|
|
280
|
-
},
|
|
281
|
-
theme,
|
|
282
|
-
);
|
|
283
|
-
if (!highlighted) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return HStack({ padding: { x: 1 } }, [VStack({ flex: 1 }, [highlighted])]);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function getAssistantErrorMessage(
|
|
291
|
-
assistant: AssistantMessage | AssistantRenderState,
|
|
292
|
-
): string | undefined {
|
|
293
|
-
if ("role" in assistant && assistant.stopReason === "error") {
|
|
294
|
-
return assistant.errorMessage;
|
|
295
|
-
}
|
|
296
|
-
if ("role" in assistant) {
|
|
297
|
-
return undefined;
|
|
298
|
-
}
|
|
299
|
-
return assistant.errorMessage;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function renderThinkingBlock(
|
|
303
|
-
thinking: string,
|
|
304
|
-
opts: ConversationRenderOpts,
|
|
305
|
-
): Node {
|
|
306
|
-
if (opts.showReasoning) {
|
|
307
|
-
return VStack({ padding: { x: 1 } }, [
|
|
308
|
-
Text(thinking, {
|
|
309
|
-
wrap: "word",
|
|
310
|
-
fgColor: opts.theme.mutedText,
|
|
311
|
-
italic: true,
|
|
312
|
-
}),
|
|
313
|
-
]);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const lineCount = thinking.split("\n").length;
|
|
317
|
-
const unit = lineCount === 1 ? "line" : "lines";
|
|
318
|
-
return VStack({ padding: { x: 1 } }, [
|
|
319
|
-
Text(`Thinking... ${lineCount} ${unit}.`, {
|
|
320
|
-
fgColor: opts.theme.mutedText,
|
|
321
|
-
italic: true,
|
|
322
|
-
}),
|
|
323
|
-
]);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function coalesceAdjacentTextBlocks(
|
|
327
|
-
content: AssistantMessage["content"],
|
|
328
|
-
): AssistantMessage["content"] {
|
|
329
|
-
const coalesced: AssistantMessage["content"] = [];
|
|
330
|
-
|
|
331
|
-
for (const block of content) {
|
|
332
|
-
if (block.type !== "text") {
|
|
333
|
-
coalesced.push(block);
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const previous = coalesced.at(-1);
|
|
338
|
-
if (previous?.type === "text") {
|
|
339
|
-
coalesced[coalesced.length - 1] = {
|
|
340
|
-
...previous,
|
|
341
|
-
text: previous.text + block.text,
|
|
342
|
-
};
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
coalesced.push({ ...block });
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return coalesced;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function renderAssistantContentBlock(
|
|
353
|
-
block: AssistantMessage["content"][number],
|
|
354
|
-
opts: ConversationRenderOpts,
|
|
355
|
-
): Node | null {
|
|
356
|
-
if (block.type === "text" && block.text) {
|
|
357
|
-
return renderMarkdownTextBlock(block.text, opts.theme);
|
|
358
|
-
}
|
|
359
|
-
if (block.type === "thinking" && block.thinking) {
|
|
360
|
-
return renderThinkingBlock(block.thinking, opts);
|
|
361
|
-
}
|
|
362
|
-
if (block.type === "toolCall") {
|
|
363
|
-
return renderToolCall(block, opts);
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Render a completed or in-progress assistant response.
|
|
370
|
-
*
|
|
371
|
-
* @param assistant - Completed assistant message or in-progress render state.
|
|
372
|
-
* @param opts - Shared conversation render options.
|
|
373
|
-
* @returns The rendered assistant node, or `null` when there is nothing to show.
|
|
374
|
-
*/
|
|
375
|
-
export function renderAssistantMessage(
|
|
376
|
-
assistant: AssistantMessage | AssistantRenderState,
|
|
377
|
-
opts: ConversationRenderOpts,
|
|
378
|
-
): Node | null {
|
|
379
|
-
const children = coalesceAdjacentTextBlocks(assistant.content)
|
|
380
|
-
.map((block) => {
|
|
381
|
-
return renderAssistantContentBlock(block, opts);
|
|
382
|
-
})
|
|
383
|
-
.filter((child): child is Node => child !== null);
|
|
384
|
-
|
|
385
|
-
const errorMessage = getAssistantErrorMessage(assistant);
|
|
386
|
-
if (errorMessage) {
|
|
387
|
-
children.push(
|
|
388
|
-
VStack({ padding: { x: 1 } }, [
|
|
389
|
-
Text(`Error: ${errorMessage}`, {
|
|
390
|
-
fgColor: opts.theme.error,
|
|
391
|
-
}),
|
|
392
|
-
]),
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (children.length === 0) {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return VStack({ gap: CONVERSATION_GAP }, children);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function getPreviewWidth(previewWidth?: number): number {
|
|
404
|
-
if (!Number.isFinite(previewWidth)) {
|
|
405
|
-
return DEFAULT_TOOL_PREVIEW_WIDTH;
|
|
406
|
-
}
|
|
407
|
-
return Math.max(1, Math.floor(previewWidth!));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function getToolBodyWidth(previewWidth?: number): number {
|
|
411
|
-
return Math.max(1, getPreviewWidth(previewWidth) - TOOL_BLOCK_CHROME_WIDTH);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** Split multi-line tool text into logical render lines. */
|
|
415
|
-
function splitToolTextLines(
|
|
416
|
-
text: string,
|
|
417
|
-
kind: Exclude<ToolRenderLineKind, "summary">,
|
|
418
|
-
): ToolRenderLine[] {
|
|
419
|
-
if (text === "") {
|
|
420
|
-
return [];
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const lines = text.split("\n");
|
|
424
|
-
if (lines.at(-1) === "") {
|
|
425
|
-
lines.pop();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return lines.map((line) => ({ kind, text: line }));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function normalizeDisplayLineEndings(text: string): string {
|
|
432
|
-
return text.replace(/\r\n/g, "\n");
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** Read a string argument from a tool-call argument map. */
|
|
436
|
-
function getToolArgString(args: Record<string, unknown>, key: string): string {
|
|
437
|
-
return readString(args, key) ?? "";
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const HOME_DIR = homedir();
|
|
441
|
-
|
|
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);
|
|
450
|
-
}
|
|
451
|
-
|
|
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;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function normalizeDisplayPath(path: string): string {
|
|
463
|
-
if (path === "") {
|
|
464
|
-
return path;
|
|
465
|
-
}
|
|
466
|
-
return normalize(path).replace(/\\/g, "/");
|
|
467
|
-
}
|
|
468
|
-
|
|
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));
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function formatRelativeToolPath(path: string, cwd?: string): string {
|
|
478
|
-
const resolved = resolveToolPath(path, cwd);
|
|
479
|
-
if (resolved === "" || !cwd || !isAbsolute(resolved)) {
|
|
480
|
-
return normalizeDisplayPath(path);
|
|
481
|
-
}
|
|
482
|
-
|
|
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 [];
|
|
558
|
-
}
|
|
559
|
-
|
|
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`;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function parseShellToolContent(
|
|
567
|
-
content: ToolResultMessage["content"],
|
|
568
|
-
details?: ToolResultMessage["details"],
|
|
569
|
-
) {
|
|
570
|
-
return (
|
|
571
|
-
parseShellResultDetails(details) ??
|
|
572
|
-
parseLegacyShellResult(getToolContentText(content))
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
|
|
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
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const bodyLines: ToolRenderLine[] = [];
|
|
591
|
-
const stdout = normalizeDisplayLineEndings(result.stdout);
|
|
592
|
-
const stderr = normalizeDisplayLineEndings(result.stderr);
|
|
593
|
-
|
|
594
|
-
if (stdout !== "") {
|
|
595
|
-
bodyLines.push(...splitToolTextLines(stdout, "text"));
|
|
596
|
-
}
|
|
597
|
-
if (stderr !== "") {
|
|
598
|
-
if (bodyLines.length > 0) {
|
|
599
|
-
bodyLines.push({ kind: "text", text: "" });
|
|
600
|
-
}
|
|
601
|
-
bodyLines.push(...splitToolTextLines(stderr, "text"));
|
|
602
|
-
}
|
|
603
|
-
if (bodyLines.length === 0) {
|
|
604
|
-
bodyLines.push({ kind: "summary", text: "(no output)" });
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
bodyLines,
|
|
609
|
-
footerLines: [
|
|
610
|
-
{
|
|
611
|
-
kind: "text",
|
|
612
|
-
text: `exit ${result.exitCode}`,
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function getToolHeaderName(toolName: string): string {
|
|
619
|
-
return toolName.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
620
|
-
}
|
|
621
|
-
|
|
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
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function getHighlightedBodyNode(
|
|
639
|
-
spec: HighlightedBodySpec,
|
|
640
|
-
theme: Theme,
|
|
641
|
-
): Node | null {
|
|
642
|
-
if (spec.text === "") {
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
try {
|
|
647
|
-
return SyntaxHighlight(spec.text, spec.language, {
|
|
648
|
-
theme: getSyntaxHighlightTheme(theme, spec.themeVariant),
|
|
649
|
-
});
|
|
650
|
-
} catch {
|
|
651
|
-
return null;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/** Render a single styled text node for a tool line. */
|
|
656
|
-
function renderToolLine(line: ToolRenderLine, theme: Theme): Node {
|
|
657
|
-
switch (line.kind) {
|
|
658
|
-
case "command":
|
|
659
|
-
case "path":
|
|
660
|
-
return Text(line.text, {
|
|
661
|
-
fgColor: theme.accentText,
|
|
662
|
-
bold: true,
|
|
663
|
-
wrap: "word",
|
|
664
|
-
});
|
|
665
|
-
case "diffAdded":
|
|
666
|
-
return Text(line.text, { fgColor: theme.diffAdded, wrap: "word" });
|
|
667
|
-
case "diffRemoved":
|
|
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
|
-
});
|
|
674
|
-
case "summary":
|
|
675
|
-
return Text(line.text, {
|
|
676
|
-
fgColor: theme.toolText,
|
|
677
|
-
italic: true,
|
|
678
|
-
wrap: "word",
|
|
679
|
-
});
|
|
680
|
-
case "error":
|
|
681
|
-
return Text(line.text, { fgColor: theme.error, wrap: "word" });
|
|
682
|
-
case "text":
|
|
683
|
-
return Text(line.text, {
|
|
684
|
-
fgColor: theme.toolText,
|
|
685
|
-
wrap: "word",
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/** Render styled text nodes for tool lines. */
|
|
691
|
-
function renderToolLines(
|
|
692
|
-
lines: readonly ToolRenderLine[],
|
|
693
|
-
theme: Theme,
|
|
694
|
-
): Node[] {
|
|
695
|
-
return lines.map((line) => renderToolLine(line, theme));
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function getTodoMarker(todo: TodoItem): "[ ]" | "[~]" | "[x]" {
|
|
699
|
-
switch (todo.status) {
|
|
700
|
-
case "in_progress":
|
|
701
|
-
return "[~]";
|
|
702
|
-
case "completed":
|
|
703
|
-
return "[x]";
|
|
704
|
-
default:
|
|
705
|
-
return "[ ]";
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function getTodoColor(todo: TodoItem, theme: Theme): Theme["toolText"] {
|
|
710
|
-
switch (todo.status) {
|
|
711
|
-
case "in_progress":
|
|
712
|
-
return theme.accentText;
|
|
713
|
-
case "completed":
|
|
714
|
-
return theme.diffAdded;
|
|
715
|
-
default:
|
|
716
|
-
return theme.toolText;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function renderTodoChecklist(todos: readonly TodoItem[], theme: Theme): Node {
|
|
721
|
-
if (todos.length === 0) {
|
|
722
|
-
return Text("No todo items.", {
|
|
723
|
-
fgColor: theme.mutedText,
|
|
724
|
-
italic: true,
|
|
725
|
-
wrap: "word",
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
return VStack(
|
|
730
|
-
{},
|
|
731
|
-
todos.map((todo) =>
|
|
732
|
-
Text(`${getTodoMarker(todo)} ${todo.content}`, {
|
|
733
|
-
fgColor: getTodoColor(todo, theme),
|
|
734
|
-
wrap: "word",
|
|
735
|
-
}),
|
|
736
|
-
),
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
|
|
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
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (spec.bodyLines.length === 0) {
|
|
753
|
-
return null;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
return VStack({}, renderToolLines(spec.bodyLines, theme));
|
|
757
|
-
}
|
|
758
|
-
|
|
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
|
-
}
|
|
767
|
-
|
|
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;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
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
|
-
);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function renderToolHeaderPill(
|
|
795
|
-
toolName: string,
|
|
796
|
-
direction: ToolRenderDirection,
|
|
797
|
-
theme: Theme,
|
|
798
|
-
): Node {
|
|
799
|
-
return HStack({ bgColor: theme.toolBorder, padding: { x: 1 } }, [
|
|
800
|
-
Text(`${getToolHeaderName(toolName)} ${direction}`, {
|
|
801
|
-
fgColor: getToolHeaderColor(toolName, theme),
|
|
802
|
-
bold: true,
|
|
803
|
-
}),
|
|
804
|
-
]);
|
|
805
|
-
}
|
|
806
|
-
|
|
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
|
-
);
|
|
819
|
-
}
|
|
820
|
-
|
|
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 };
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
const bodyWidth = getToolBodyWidth(opts.previewWidth);
|
|
833
|
-
let previewStart = Math.max(0, spec.bodyLines.length - UI_TOOL_PREVIEW_ROWS);
|
|
834
|
-
|
|
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
|
-
}
|
|
841
|
-
|
|
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
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function renderToolBody(
|
|
868
|
-
spec: ToolBlockSpec,
|
|
869
|
-
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
870
|
-
): { body: Node | null; summary?: ToolRenderLine } {
|
|
871
|
-
if (spec.bodyNode) {
|
|
872
|
-
return { body: spec.bodyNode };
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
if (opts.verbose || !spec.previewBody) {
|
|
876
|
-
return {
|
|
877
|
-
body: renderFullToolBody(spec, opts.theme),
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
return renderCompactToolBody(spec, opts);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/** Render a tool block with a left border and compact header pill. */
|
|
885
|
-
function renderToolBlock(
|
|
886
|
-
spec: ToolBlockSpec,
|
|
887
|
-
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
888
|
-
): Node {
|
|
889
|
-
const body = renderToolBody(spec, opts);
|
|
890
|
-
const children: Node[] = [renderToolHeaderRow(spec, opts.theme)];
|
|
891
|
-
|
|
892
|
-
if (body.body) {
|
|
893
|
-
children.push(body.body);
|
|
894
|
-
}
|
|
895
|
-
if (body.summary) {
|
|
896
|
-
children.push(renderToolLine(body.summary, opts.theme));
|
|
897
|
-
}
|
|
898
|
-
if (spec.footerLines) {
|
|
899
|
-
children.push(...renderToolLines(spec.footerLines, opts.theme));
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
return HStack({ padding: { x: 1 } }, [
|
|
903
|
-
Text("│ ", { fgColor: opts.theme.toolBorder }),
|
|
904
|
-
VStack({ flex: 1 }, children),
|
|
905
|
-
]);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function getToolTextBlocks(content: ToolResultMessage["content"]): string[] {
|
|
909
|
-
return content
|
|
910
|
-
.filter((entry): entry is TextContent => entry.type === "text")
|
|
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"));
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function buildShellToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
936
|
-
const command = getToolArgString(args, "command");
|
|
937
|
-
return {
|
|
938
|
-
toolName: "shell",
|
|
939
|
-
direction: "->",
|
|
940
|
-
bodyLines: splitToolTextLines(command, "command"),
|
|
941
|
-
highlightedBody:
|
|
942
|
-
command === ""
|
|
943
|
-
? undefined
|
|
944
|
-
: {
|
|
945
|
-
text: command,
|
|
946
|
-
language: "bash",
|
|
947
|
-
themeVariant: "shell",
|
|
948
|
-
},
|
|
949
|
-
previewBody: true,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
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
|
-
|
|
1017
|
-
function buildEditToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
1018
|
-
const lines: ToolRenderLine[] = [];
|
|
1019
|
-
const filePath = getToolArgString(args, "path");
|
|
1020
|
-
if (filePath) {
|
|
1021
|
-
lines.push({ kind: "path", text: filePath });
|
|
1022
|
-
}
|
|
1023
|
-
lines.push(
|
|
1024
|
-
...splitToolTextLines(getToolArgString(args, "oldText"), "diffRemoved"),
|
|
1025
|
-
);
|
|
1026
|
-
lines.push(
|
|
1027
|
-
...splitToolTextLines(getToolArgString(args, "newText"), "diffAdded"),
|
|
1028
|
-
);
|
|
1029
|
-
|
|
1030
|
-
return {
|
|
1031
|
-
toolName: "edit",
|
|
1032
|
-
direction: "->",
|
|
1033
|
-
bodyLines: lines,
|
|
1034
|
-
previewBody: true,
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function buildTodoWriteToolCallSpec(
|
|
1039
|
-
args: Record<string, unknown>,
|
|
1040
|
-
): ToolBlockSpec {
|
|
1041
|
-
return {
|
|
1042
|
-
toolName: "todoWrite",
|
|
1043
|
-
direction: "->",
|
|
1044
|
-
bodyLines: [{ kind: "text", text: formatTodoWriteCallSummary(args) }],
|
|
1045
|
-
previewBody: false,
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
function buildReadImageToolCallSpec(
|
|
1050
|
-
args: Record<string, unknown>,
|
|
1051
|
-
): ToolBlockSpec {
|
|
1052
|
-
const filePath = getToolArgString(args, "path");
|
|
1053
|
-
return {
|
|
1054
|
-
toolName: "readImage",
|
|
1055
|
-
direction: "->",
|
|
1056
|
-
bodyLines: filePath ? [{ kind: "path", text: filePath }] : [],
|
|
1057
|
-
previewBody: false,
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
function isMcpToolName(toolName: string): boolean {
|
|
1062
|
-
return toolName.includes("__");
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function buildGenericToolCallSpec(
|
|
1066
|
-
toolName: string,
|
|
1067
|
-
args: Record<string, unknown>,
|
|
1068
|
-
): ToolBlockSpec {
|
|
1069
|
-
const text = JSON.stringify(args, null, 2);
|
|
1070
|
-
return {
|
|
1071
|
-
toolName,
|
|
1072
|
-
direction: "->",
|
|
1073
|
-
bodyLines: text && text !== "{}" ? splitToolTextLines(text, "text") : [],
|
|
1074
|
-
previewBody: isMcpToolName(toolName),
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function buildToolCallSpec(
|
|
1079
|
-
toolName: string,
|
|
1080
|
-
args: Record<string, unknown>,
|
|
1081
|
-
): ToolBlockSpec {
|
|
1082
|
-
if (toolName === "shell") {
|
|
1083
|
-
return buildShellToolCallSpec(args);
|
|
1084
|
-
}
|
|
1085
|
-
if (toolName === "read") {
|
|
1086
|
-
return buildReadToolCallSpec(args);
|
|
1087
|
-
}
|
|
1088
|
-
if (toolName === "grep") {
|
|
1089
|
-
return buildGrepToolCallSpec(args);
|
|
1090
|
-
}
|
|
1091
|
-
if (toolName === "edit") {
|
|
1092
|
-
return buildEditToolCallSpec(args);
|
|
1093
|
-
}
|
|
1094
|
-
if (toolName === "todoWrite") {
|
|
1095
|
-
return buildTodoWriteToolCallSpec(args);
|
|
1096
|
-
}
|
|
1097
|
-
if (toolName === "readImage") {
|
|
1098
|
-
return buildReadImageToolCallSpec(args);
|
|
1099
|
-
}
|
|
1100
|
-
return buildGenericToolCallSpec(toolName, args);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function renderToolCall(
|
|
1104
|
-
toolCall: Extract<AssistantMessage["content"][number], { type: "toolCall" }>,
|
|
1105
|
-
opts: Pick<ConversationRenderOpts, "previewWidth" | "verbose" | "theme">,
|
|
1106
|
-
): Node {
|
|
1107
|
-
return renderToolBlock(
|
|
1108
|
-
buildToolCallSpec(toolCall.name, toolCall.arguments),
|
|
1109
|
-
opts,
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
function buildShellToolResultSpec(
|
|
1114
|
-
content: ToolResultMessage["content"],
|
|
1115
|
-
details?: ToolResultMessage["details"],
|
|
1116
|
-
): ToolBlockSpec {
|
|
1117
|
-
const shellResult = buildShellResultLines(content, details);
|
|
1118
|
-
return {
|
|
1119
|
-
toolName: "shell",
|
|
1120
|
-
direction: "<-",
|
|
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
|
-
: {}),
|
|
1228
|
-
previewBody: true,
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function buildEditToolResultSpec(
|
|
1233
|
-
args: Record<string, unknown>,
|
|
1234
|
-
isError: boolean,
|
|
1235
|
-
resultText: string,
|
|
1236
|
-
): ToolBlockSpec {
|
|
1237
|
-
if (isError) {
|
|
1238
|
-
return {
|
|
1239
|
-
toolName: "edit",
|
|
1240
|
-
direction: "<-",
|
|
1241
|
-
bodyLines: splitToolTextLines(resultText, "error"),
|
|
1242
|
-
previewBody: true,
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const filePath = getToolArgString(args, "path");
|
|
1247
|
-
return {
|
|
1248
|
-
toolName: "edit",
|
|
1249
|
-
direction: "<-",
|
|
1250
|
-
bodyLines: filePath ? [{ kind: "path", text: `~ ${filePath}` }] : [],
|
|
1251
|
-
previewBody: false,
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function buildTodoToolResultSpec(
|
|
1256
|
-
toolName: string,
|
|
1257
|
-
content: ToolResultMessage["content"],
|
|
1258
|
-
isError: boolean,
|
|
1259
|
-
theme: Theme,
|
|
1260
|
-
): ToolBlockSpec {
|
|
1261
|
-
if (isError) {
|
|
1262
|
-
return {
|
|
1263
|
-
toolName,
|
|
1264
|
-
direction: "<-",
|
|
1265
|
-
bodyLines: splitToolTextLines(getToolContentText(content), "error"),
|
|
1266
|
-
previewBody: false,
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
const todos = parseTodoSnapshot(getToolContentText(content)) ?? [];
|
|
1271
|
-
return {
|
|
1272
|
-
toolName,
|
|
1273
|
-
direction: "<-",
|
|
1274
|
-
bodyLines: [],
|
|
1275
|
-
bodyNode: renderTodoChecklist(todos, theme),
|
|
1276
|
-
previewBody: false,
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
function buildReadImageToolResultSpec(
|
|
1281
|
-
args: Record<string, unknown>,
|
|
1282
|
-
content: ToolResultMessage["content"],
|
|
1283
|
-
isError: boolean,
|
|
1284
|
-
): ToolBlockSpec {
|
|
1285
|
-
if (isError) {
|
|
1286
|
-
return {
|
|
1287
|
-
toolName: "readImage",
|
|
1288
|
-
direction: "<-",
|
|
1289
|
-
bodyLines: splitToolTextLines(getToolContentText(content), "error"),
|
|
1290
|
-
previewBody: false,
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const filePath = getToolArgString(args, "path");
|
|
1295
|
-
return {
|
|
1296
|
-
toolName: "readImage",
|
|
1297
|
-
direction: "<-",
|
|
1298
|
-
bodyLines: filePath ? [{ kind: "path", text: filePath }] : [],
|
|
1299
|
-
previewBody: false,
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function buildGenericToolResultSpec(
|
|
1304
|
-
toolName: string,
|
|
1305
|
-
output: string,
|
|
1306
|
-
isError: boolean,
|
|
1307
|
-
): ToolBlockSpec {
|
|
1308
|
-
return {
|
|
1309
|
-
toolName,
|
|
1310
|
-
direction: "<-",
|
|
1311
|
-
bodyLines: splitToolTextLines(output, isError ? "error" : "text"),
|
|
1312
|
-
previewBody: isMcpToolName(toolName),
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
function renderToolResultContent(
|
|
1317
|
-
toolName: string,
|
|
1318
|
-
args: Record<string, unknown>,
|
|
1319
|
-
content: ToolResultMessage["content"],
|
|
1320
|
-
isError: boolean,
|
|
1321
|
-
opts: Pick<
|
|
1322
|
-
ConversationRenderOpts,
|
|
1323
|
-
"previewWidth" | "verbose" | "theme" | "cwd"
|
|
1324
|
-
>,
|
|
1325
|
-
details?: ToolResultMessage["details"],
|
|
1326
|
-
): Node {
|
|
1327
|
-
if (toolName === "shell") {
|
|
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
|
-
);
|
|
1341
|
-
}
|
|
1342
|
-
if (toolName === "edit") {
|
|
1343
|
-
return renderToolBlock(
|
|
1344
|
-
buildEditToolResultSpec(args, isError, getToolContentText(content)),
|
|
1345
|
-
opts,
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
if (toolName === "todoWrite" || toolName === "todoRead") {
|
|
1349
|
-
return renderToolBlock(
|
|
1350
|
-
buildTodoToolResultSpec(toolName, content, isError, opts.theme),
|
|
1351
|
-
opts,
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
if (toolName === "readImage") {
|
|
1355
|
-
return renderToolBlock(
|
|
1356
|
-
buildReadImageToolResultSpec(args, content, isError),
|
|
1357
|
-
opts,
|
|
1358
|
-
);
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
return renderToolBlock(
|
|
1362
|
-
buildGenericToolResultSpec(toolName, getToolContentText(content), isError),
|
|
1363
|
-
opts,
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
/**
|
|
1368
|
-
* Render a tool result, dispatching by tool name.
|
|
1369
|
-
*
|
|
1370
|
-
* @param toolName - Tool name to render.
|
|
1371
|
-
* @param args - Tool call arguments used for compact result rendering.
|
|
1372
|
-
* @param resultText - Text content from the tool result message.
|
|
1373
|
-
* @param isError - Whether the tool execution failed.
|
|
1374
|
-
* @param opts - Shared conversation render options.
|
|
1375
|
-
* @param details - Optional structured tool-result details.
|
|
1376
|
-
* @returns The rendered tool block node.
|
|
1377
|
-
*/
|
|
1378
|
-
export function renderToolResult(
|
|
1379
|
-
toolName: string,
|
|
1380
|
-
args: Record<string, unknown>,
|
|
1381
|
-
resultText: string,
|
|
1382
|
-
isError: boolean,
|
|
1383
|
-
opts: ConversationRenderOpts,
|
|
1384
|
-
details?: ToolResultMessage["details"],
|
|
1385
|
-
): Node {
|
|
1386
|
-
const content: ToolResultMessage["content"] = resultText
|
|
1387
|
-
? [{ type: "text", text: resultText }]
|
|
1388
|
-
: [];
|
|
1389
|
-
|
|
1390
|
-
return renderToolResultContent(
|
|
1391
|
-
toolName,
|
|
1392
|
-
args,
|
|
1393
|
-
content,
|
|
1394
|
-
isError,
|
|
1395
|
-
opts,
|
|
1396
|
-
details,
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
function renderUiTodoMessage(
|
|
1401
|
-
msg: Extract<UiMessage, { kind: "todo" }>,
|
|
1402
|
-
theme: Theme,
|
|
1403
|
-
): Node {
|
|
1404
|
-
return VStack({ padding: { x: 1 } }, [
|
|
1405
|
-
Text("todo", {
|
|
1406
|
-
fgColor: theme.secondaryAccentText ?? theme.toolText,
|
|
1407
|
-
bold: true,
|
|
1408
|
-
wrap: "word",
|
|
1409
|
-
}),
|
|
1410
|
-
renderTodoChecklist(msg.todos, theme),
|
|
1411
|
-
]);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
/** Render an internal UI message in the conversation log. */
|
|
1415
|
-
function renderUiMessage(
|
|
1416
|
-
msg: UiMessage,
|
|
1417
|
-
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme">,
|
|
1418
|
-
): Node {
|
|
1419
|
-
if (msg.kind === "todo") {
|
|
1420
|
-
return renderUiTodoMessage(msg, opts.theme);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
if (msg.format === "markdown") {
|
|
1424
|
-
const markdown = renderMarkdownTextBlock(msg.content, opts.theme);
|
|
1425
|
-
if (markdown) {
|
|
1426
|
-
return markdown;
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
return VStack({ padding: { x: 1 } }, [
|
|
1431
|
-
Text(msg.content, {
|
|
1432
|
-
fgColor: opts.theme.mutedText,
|
|
1433
|
-
italic: true,
|
|
1434
|
-
wrap: "word",
|
|
1435
|
-
}),
|
|
1436
|
-
]);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
function renderEmptyConversationBanner(
|
|
1440
|
-
theme: Theme,
|
|
1441
|
-
versionLabel: string,
|
|
1442
|
-
): Node {
|
|
1443
|
-
return HStack({ justifyContent: "center" }, [
|
|
1444
|
-
VStack({ padding: { x: 1 } }, [
|
|
1445
|
-
Text(APP_NAME, {
|
|
1446
|
-
bold: true,
|
|
1447
|
-
fgColor: theme.accentText,
|
|
1448
|
-
}),
|
|
1449
|
-
Text(versionLabel, {
|
|
1450
|
-
fgColor: theme.mutedText,
|
|
1451
|
-
}),
|
|
1452
|
-
]),
|
|
1453
|
-
]);
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
function rememberToolCallArgs(
|
|
1457
|
-
message: AssistantMessage,
|
|
1458
|
-
toolCallArgs: Map<string, ToolCallRenderInfo>,
|
|
1459
|
-
): void {
|
|
1460
|
-
for (const block of message.content) {
|
|
1461
|
-
if (block.type !== "toolCall") {
|
|
1462
|
-
continue;
|
|
1463
|
-
}
|
|
1464
|
-
toolCallArgs.set(block.id, {
|
|
1465
|
-
name: block.name,
|
|
1466
|
-
args: block.arguments,
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
function renderToolResultMessage(
|
|
1472
|
-
toolName: string,
|
|
1473
|
-
args: Record<string, unknown>,
|
|
1474
|
-
content: ToolResultMessage["content"],
|
|
1475
|
-
isError: boolean,
|
|
1476
|
-
renderOpts: ConversationRenderOpts,
|
|
1477
|
-
details?: ToolResultMessage["details"],
|
|
1478
|
-
): Node {
|
|
1479
|
-
return renderToolResultContent(
|
|
1480
|
-
toolName,
|
|
1481
|
-
args,
|
|
1482
|
-
content,
|
|
1483
|
-
isError,
|
|
1484
|
-
renderOpts,
|
|
1485
|
-
details,
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
function renderConversationMessage(
|
|
1490
|
-
message: ConversationMessage,
|
|
1491
|
-
renderOpts: ConversationRenderOpts,
|
|
1492
|
-
toolCallArgs: Map<string, ToolCallRenderInfo>,
|
|
1493
|
-
theme: Theme,
|
|
1494
|
-
): Node | null {
|
|
1495
|
-
if (message.role === "ui") {
|
|
1496
|
-
return renderUiMessage(message, renderOpts);
|
|
1497
|
-
}
|
|
1498
|
-
if (message.role === "user") {
|
|
1499
|
-
return renderUserMessage(message, theme);
|
|
1500
|
-
}
|
|
1501
|
-
if (message.role === "assistant") {
|
|
1502
|
-
rememberToolCallArgs(message, toolCallArgs);
|
|
1503
|
-
return renderAssistantMessage(message, renderOpts);
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const info = toolCallArgs.get(message.toolCallId);
|
|
1507
|
-
return renderToolResultMessage(
|
|
1508
|
-
info?.name ?? message.toolName,
|
|
1509
|
-
info?.args ?? EMPTY_TOOL_RESULT_ARGS,
|
|
1510
|
-
message.content,
|
|
1511
|
-
message.isError,
|
|
1512
|
-
renderOpts,
|
|
1513
|
-
message.details,
|
|
1514
|
-
);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function cacheToolCallArgs(messages: readonly ConversationMessage[]): void {
|
|
1518
|
-
if (
|
|
1519
|
-
toolCallArgsCache.messages !== messages ||
|
|
1520
|
-
toolCallArgsCache.count > messages.length
|
|
1521
|
-
) {
|
|
1522
|
-
toolCallArgsCache.messages = messages;
|
|
1523
|
-
toolCallArgsCache.count = 0;
|
|
1524
|
-
toolCallArgsCache.entries = new Map();
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
for (let index = toolCallArgsCache.count; index < messages.length; index++) {
|
|
1528
|
-
const message = messages[index];
|
|
1529
|
-
if (message?.role === "assistant") {
|
|
1530
|
-
rememberToolCallArgs(message, toolCallArgsCache.entries);
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
toolCallArgsCache.count = messages.length;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function canReuseConversationMessageRender(
|
|
1538
|
-
cached: ConversationMessageRenderCacheEntry | undefined,
|
|
1539
|
-
renderOpts: ConversationRenderOpts,
|
|
1540
|
-
previewWidth: number,
|
|
1541
|
-
): cached is ConversationMessageRenderCacheEntry {
|
|
1542
|
-
return (
|
|
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
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
function canReuseCommittedConversationLayout(
|
|
1553
|
-
state: ConversationLogState,
|
|
1554
|
-
renderOpts: ConversationRenderOpts,
|
|
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,
|
|
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 {
|
|
1608
|
-
const previewWidth = getPreviewWidth(renderOpts.previewWidth);
|
|
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
|
-
}
|
|
1646
|
-
|
|
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];
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
for (
|
|
1665
|
-
let index = conversationLayoutCache.count;
|
|
1666
|
-
index < state.messages.length;
|
|
1667
|
-
index++
|
|
1668
|
-
) {
|
|
1669
|
-
appendConversationLogItem(
|
|
1670
|
-
conversationLayoutCache.items,
|
|
1671
|
-
conversationLayoutCache.prefixHeights,
|
|
1672
|
-
getCommittedConversationLogItem(state.messages[index]!, renderOpts),
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
conversationLayoutCache.count = state.messages.length;
|
|
1677
|
-
return {
|
|
1678
|
-
items: conversationLayoutCache.items,
|
|
1679
|
-
prefixHeights: conversationLayoutCache.prefixHeights,
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
function hasStreamingTail(streaming: StreamingConversationState): boolean {
|
|
1684
|
-
return (
|
|
1685
|
-
(streaming.content !== EMPTY_STREAMING_CONTENT ||
|
|
1686
|
-
streaming.pendingToolResults !== EMPTY_PENDING_TOOL_RESULTS) &&
|
|
1687
|
-
(streaming.content.length > 0 || streaming.pendingToolResults.length > 0)
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
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
|
-
|
|
1750
|
-
/**
|
|
1751
|
-
* Build rendered conversation items plus cached cumulative intrinsic heights.
|
|
1752
|
-
*
|
|
1753
|
-
* @param state - Conversation rendering state.
|
|
1754
|
-
* @param streaming - Current in-progress assistant tail, if any.
|
|
1755
|
-
* @param previewWidth - Available terminal width for width-aware tool previews.
|
|
1756
|
-
* @returns Rendered items plus cumulative heights in on-screen order.
|
|
1757
|
-
*/
|
|
1758
|
-
export function buildConversationLayoutSnapshot(
|
|
1759
|
-
state: ConversationLogState,
|
|
1760
|
-
streaming: StreamingConversationState,
|
|
1761
|
-
previewWidth = DEFAULT_TOOL_PREVIEW_WIDTH,
|
|
1762
|
-
): ConversationLayoutSnapshot {
|
|
1763
|
-
const renderOpts: ConversationRenderOpts = {
|
|
1764
|
-
showReasoning: state.showReasoning,
|
|
1765
|
-
verbose: state.verbose,
|
|
1766
|
-
theme: state.theme,
|
|
1767
|
-
cwd: state.cwd,
|
|
1768
|
-
previewWidth,
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
cacheToolCallArgs(state.messages);
|
|
1772
|
-
|
|
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);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
const items = [...committedSnapshot.items];
|
|
1786
|
-
const prefixHeights = [...committedSnapshot.prefixHeights];
|
|
1787
|
-
appendStreamingConversationLogItems(
|
|
1788
|
-
items,
|
|
1789
|
-
prefixHeights,
|
|
1790
|
-
streaming,
|
|
1791
|
-
renderOpts,
|
|
1792
|
-
previewWidth,
|
|
1793
|
-
);
|
|
1794
|
-
return {
|
|
1795
|
-
items,
|
|
1796
|
-
prefixHeights,
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
|
|
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
|
-
}
|
|
1815
|
-
|
|
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);
|
|
1836
|
-
}
|