mini-coder 0.5.12 → 0.5.14
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 +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
package/src/ui/commands.ts
CHANGED
|
@@ -17,9 +17,12 @@ import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
|
|
17
17
|
import { getErrorMessage } from "../errors.ts";
|
|
18
18
|
import type { AppState } from "../index.ts";
|
|
19
19
|
import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
|
|
20
|
-
import { COMMANDS } from "../input.ts";
|
|
20
|
+
import { COMMANDS, SKILL_COMMAND } from "../input.ts";
|
|
21
|
+
import { connectMcpServer, disconnectMcpServer } from "../mcp.ts";
|
|
21
22
|
import {
|
|
22
23
|
clearConversationState,
|
|
24
|
+
computeSessionContextTokens,
|
|
25
|
+
computeSessionStats,
|
|
23
26
|
forkSession,
|
|
24
27
|
listPromptHistory,
|
|
25
28
|
listSessions,
|
|
@@ -30,9 +33,15 @@ import {
|
|
|
30
33
|
undoLastTurn,
|
|
31
34
|
} from "../session.ts";
|
|
32
35
|
import { updateSettings } from "../settings.ts";
|
|
36
|
+
import { clearQueuedUserMessages } from "../submit.ts";
|
|
33
37
|
import { collapseWhitespace, truncateText } from "../text.ts";
|
|
34
38
|
import { getTodoItems } from "../tools.ts";
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
buildHelpText,
|
|
41
|
+
COMMAND_DESCRIPTIONS,
|
|
42
|
+
SKILL_REFERENCE_DESCRIPTION,
|
|
43
|
+
SKILL_REFERENCE_LABEL,
|
|
44
|
+
} from "./help.ts";
|
|
36
45
|
import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
|
|
37
46
|
import type { UiRenderPriority } from "./runtime.ts";
|
|
38
47
|
import { abbreviatePath } from "./status.ts";
|
|
@@ -74,6 +83,22 @@ interface UiCommandRuntime {
|
|
|
74
83
|
openInBrowser: (url: string) => void;
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
interface UiCommandDeps {
|
|
87
|
+
/** Connect a configured MCP server on demand. */
|
|
88
|
+
connectMcpServer: (
|
|
89
|
+
server: AppState["mcpServers"][number],
|
|
90
|
+
) => Promise<string[]>;
|
|
91
|
+
/** Disconnect an MCP server and clear its imported tools. */
|
|
92
|
+
disconnectMcpServer: (
|
|
93
|
+
server: AppState["mcpServers"][number],
|
|
94
|
+
) => Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const defaultUiCommandDeps: UiCommandDeps = {
|
|
98
|
+
connectMcpServer,
|
|
99
|
+
disconnectMcpServer,
|
|
100
|
+
};
|
|
101
|
+
|
|
77
102
|
/** Public command actions consumed by `ui.ts` and unit tests. */
|
|
78
103
|
interface UiCommandController {
|
|
79
104
|
/** Apply a model selection and persist it to settings. */
|
|
@@ -212,14 +237,117 @@ interface OverlayItem {
|
|
|
212
237
|
filterText: string;
|
|
213
238
|
}
|
|
214
239
|
|
|
240
|
+
function formatSkillLabel(skill: AppState["skills"][number]): string {
|
|
241
|
+
return skill.description
|
|
242
|
+
? `${skill.name} · ${skill.description}`
|
|
243
|
+
: skill.name;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatToolCount(count: number): string {
|
|
247
|
+
return `${count} tool${count === 1 ? "" : "s"}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatMcpServerLabel(server: AppState["mcpServers"][number]): string {
|
|
251
|
+
return `${server.name} · ${server.enabled ? "on" : "off"} · ${formatToolCount(server.tools.length)}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function formatMcpToggleMessage(
|
|
255
|
+
server: AppState["mcpServers"][number],
|
|
256
|
+
toolCount: number,
|
|
257
|
+
): string {
|
|
258
|
+
const action = server.enabled ? "Enabled" : "Disabled";
|
|
259
|
+
const delta = `${server.enabled ? "+" : "-"}${toolCount}`;
|
|
260
|
+
const toolSuffix = toolCount === 1 ? "tool" : "tools";
|
|
261
|
+
return `${action} MCP server "${server.name}" (${delta} ${toolSuffix}).`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isRepoLocalMcpServer(state: AppState, serverName: string): boolean {
|
|
265
|
+
return (
|
|
266
|
+
state.repoSettings.mcp?.servers?.some(
|
|
267
|
+
(entry) => entry.name === serverName,
|
|
268
|
+
) ?? false
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function persistMcpServerSettings(
|
|
273
|
+
state: AppState,
|
|
274
|
+
server: AppState["mcpServers"][number],
|
|
275
|
+
): void {
|
|
276
|
+
if (isRepoLocalMcpServer(state, server.name)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
state.settings = updateSettings(state.settingsPath, {
|
|
281
|
+
mcp: {
|
|
282
|
+
servers: [
|
|
283
|
+
{
|
|
284
|
+
name: server.name,
|
|
285
|
+
url: server.url,
|
|
286
|
+
enabled: server.enabled,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function toggleMcpServer(
|
|
294
|
+
server: AppState["mcpServers"][number],
|
|
295
|
+
state: AppState,
|
|
296
|
+
runtime: UiCommandRuntime,
|
|
297
|
+
deps: UiCommandDeps,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
try {
|
|
300
|
+
if (server.enabled) {
|
|
301
|
+
const toolCount = server.tools.length;
|
|
302
|
+
await deps.disconnectMcpServer(server);
|
|
303
|
+
server.enabled = false;
|
|
304
|
+
persistMcpServerSettings(state, server);
|
|
305
|
+
runtime.appendInfoMessage(
|
|
306
|
+
formatMcpToggleMessage(server, toolCount),
|
|
307
|
+
state,
|
|
308
|
+
);
|
|
309
|
+
runtime.requestRender("normal");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const warnings = await deps.connectMcpServer(server);
|
|
314
|
+
if (!server.connected) {
|
|
315
|
+
for (const warning of warnings) {
|
|
316
|
+
runtime.appendInfoMessage(warning, state);
|
|
317
|
+
}
|
|
318
|
+
runtime.requestRender("normal");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
server.enabled = true;
|
|
323
|
+
persistMcpServerSettings(state, server);
|
|
324
|
+
runtime.appendInfoMessage(
|
|
325
|
+
formatMcpToggleMessage(server, server.tools.length),
|
|
326
|
+
state,
|
|
327
|
+
);
|
|
328
|
+
for (const warning of warnings) {
|
|
329
|
+
runtime.appendInfoMessage(warning, state);
|
|
330
|
+
}
|
|
331
|
+
runtime.requestRender("normal");
|
|
332
|
+
} catch (error) {
|
|
333
|
+
runtime.appendInfoMessage(
|
|
334
|
+
`MCP server "${server.name}": ${getErrorMessage(error)}`,
|
|
335
|
+
state,
|
|
336
|
+
);
|
|
337
|
+
runtime.requestRender("normal");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
215
341
|
/**
|
|
216
342
|
* Create the UI command controller bound to the current UI runtime hooks.
|
|
217
343
|
*
|
|
218
344
|
* @param runtime - State mutation and rendering hooks owned by `ui.ts`.
|
|
345
|
+
* @param deps - Internal MCP command dependencies, mainly for tests.
|
|
219
346
|
* @returns Command actions for slash commands and overlays.
|
|
220
347
|
*/
|
|
221
348
|
export function createCommandController(
|
|
222
349
|
runtime: UiCommandRuntime,
|
|
350
|
+
deps: UiCommandDeps = defaultUiCommandDeps,
|
|
223
351
|
): UiCommandController {
|
|
224
352
|
const openSelectOverlay = (
|
|
225
353
|
state: AppState,
|
|
@@ -235,20 +363,52 @@ export function createCommandController(
|
|
|
235
363
|
focused: true,
|
|
236
364
|
highlightColor: state.theme.accentText,
|
|
237
365
|
onSelect,
|
|
366
|
+
onKeyPress: (key) => {
|
|
367
|
+
if (key === "escape") {
|
|
368
|
+
runtime.dismissOverlay();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
},
|
|
238
373
|
onBlur: runtime.dismissOverlay,
|
|
239
374
|
});
|
|
240
375
|
|
|
241
376
|
runtime.openOverlay({ select, title });
|
|
242
377
|
};
|
|
243
378
|
|
|
244
|
-
const
|
|
245
|
-
const items =
|
|
246
|
-
label:
|
|
247
|
-
value:
|
|
248
|
-
filterText:
|
|
379
|
+
const handleSkillCommand = (state: AppState): void => {
|
|
380
|
+
const items = state.skills.map((skill) => ({
|
|
381
|
+
label: formatSkillLabel(skill),
|
|
382
|
+
value: skill.name,
|
|
383
|
+
filterText: `${skill.name} ${skill.description ?? ""}`,
|
|
249
384
|
}));
|
|
250
385
|
|
|
251
|
-
|
|
386
|
+
openSelectOverlay(
|
|
387
|
+
state,
|
|
388
|
+
"Select a skill",
|
|
389
|
+
items,
|
|
390
|
+
"type to filter skills...",
|
|
391
|
+
(skillName) => {
|
|
392
|
+
runtime.setInputValue(`/${SKILL_COMMAND}:${skillName}`);
|
|
393
|
+
runtime.dismissOverlay();
|
|
394
|
+
},
|
|
395
|
+
);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const showCommandAutocomplete = (state: AppState): void => {
|
|
399
|
+
const items: OverlayItem[] = [
|
|
400
|
+
...COMMANDS.map((command) => ({
|
|
401
|
+
label: `/${command} ${COMMAND_DESCRIPTIONS[command] ?? ""}`,
|
|
402
|
+
value: command,
|
|
403
|
+
filterText: command,
|
|
404
|
+
})),
|
|
405
|
+
{
|
|
406
|
+
label: `${SKILL_REFERENCE_LABEL} ${SKILL_REFERENCE_DESCRIPTION}`,
|
|
407
|
+
value: SKILL_COMMAND,
|
|
408
|
+
filterText: `${SKILL_COMMAND} ${SKILL_REFERENCE_LABEL}`,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
|
|
252
412
|
openSelectOverlay(
|
|
253
413
|
state,
|
|
254
414
|
"Commands",
|
|
@@ -256,6 +416,12 @@ export function createCommandController(
|
|
|
256
416
|
"type to filter commands...",
|
|
257
417
|
(value) => {
|
|
258
418
|
runtime.dismissOverlay();
|
|
419
|
+
if (value === SKILL_COMMAND) {
|
|
420
|
+
handleSkillCommand(state);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
runtime.setInputValue("");
|
|
259
425
|
handleCommand(value, state);
|
|
260
426
|
},
|
|
261
427
|
);
|
|
@@ -376,11 +542,17 @@ export function createCommandController(
|
|
|
376
542
|
items,
|
|
377
543
|
"type to filter sessions...",
|
|
378
544
|
(sessionId) => {
|
|
545
|
+
clearQueuedUserMessages(state);
|
|
379
546
|
if (sessionId !== currentSessionId) {
|
|
380
547
|
const picked = sessions.find((session) => session.id === sessionId);
|
|
381
548
|
if (picked) {
|
|
382
549
|
state.session = picked;
|
|
383
550
|
replaceConversationState(state, loadMessages(state.db, picked.id));
|
|
551
|
+
state.stats = computeSessionStats(state.db, picked.id);
|
|
552
|
+
state.contextTokens = computeSessionContextTokens(
|
|
553
|
+
state.db,
|
|
554
|
+
picked.id,
|
|
555
|
+
);
|
|
384
556
|
runtime.scrollConversationToBottom();
|
|
385
557
|
}
|
|
386
558
|
}
|
|
@@ -394,6 +566,7 @@ export function createCommandController(
|
|
|
394
566
|
if (state.running) {
|
|
395
567
|
return;
|
|
396
568
|
}
|
|
569
|
+
clearQueuedUserMessages(state);
|
|
397
570
|
state.session = null;
|
|
398
571
|
clearConversationState(state);
|
|
399
572
|
await runtime.reloadPromptContext(state);
|
|
@@ -408,10 +581,14 @@ export function createCommandController(
|
|
|
408
581
|
const forked = forkSession(state.db, state.session.id);
|
|
409
582
|
state.session = forked;
|
|
410
583
|
replaceConversationState(state, loadMessages(state.db, forked.id));
|
|
584
|
+
state.stats = computeSessionStats(state.db, forked.id);
|
|
585
|
+
state.contextTokens = computeSessionContextTokens(state.db, forked.id);
|
|
411
586
|
runtime.appendInfoMessage("Forked session.", state);
|
|
412
587
|
};
|
|
413
588
|
|
|
414
589
|
const handleUndoCommand = async (state: AppState): Promise<void> => {
|
|
590
|
+
clearQueuedUserMessages(state);
|
|
591
|
+
|
|
415
592
|
if (state.running && state.abortController) {
|
|
416
593
|
state.abortController.abort();
|
|
417
594
|
}
|
|
@@ -426,6 +603,11 @@ export function createCommandController(
|
|
|
426
603
|
const removed = undoLastTurn(state.db, state.session.id);
|
|
427
604
|
if (removed) {
|
|
428
605
|
replaceConversationState(state, loadMessages(state.db, state.session.id));
|
|
606
|
+
state.stats = computeSessionStats(state.db, state.session.id);
|
|
607
|
+
state.contextTokens = computeSessionContextTokens(
|
|
608
|
+
state.db,
|
|
609
|
+
state.session.id,
|
|
610
|
+
);
|
|
429
611
|
runtime.scrollConversationToBottom();
|
|
430
612
|
runtime.requestRender("normal");
|
|
431
613
|
}
|
|
@@ -445,6 +627,34 @@ export function createCommandController(
|
|
|
445
627
|
});
|
|
446
628
|
};
|
|
447
629
|
|
|
630
|
+
const handleMcpCommand = (state: AppState): void => {
|
|
631
|
+
if (state.mcpServers.length === 0) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const items = state.mcpServers.map((server) => ({
|
|
636
|
+
label: formatMcpServerLabel(server),
|
|
637
|
+
value: server.name,
|
|
638
|
+
filterText: `${server.name} ${server.url} ${server.enabled ? "on" : "off"}`,
|
|
639
|
+
}));
|
|
640
|
+
|
|
641
|
+
openSelectOverlay(
|
|
642
|
+
state,
|
|
643
|
+
"Toggle MCP servers",
|
|
644
|
+
items,
|
|
645
|
+
"type to filter MCP servers...",
|
|
646
|
+
(serverName) => {
|
|
647
|
+
const server = state.mcpServers.find(
|
|
648
|
+
(entry) => entry.name === serverName,
|
|
649
|
+
);
|
|
650
|
+
runtime.dismissOverlay();
|
|
651
|
+
if (server) {
|
|
652
|
+
void toggleMcpServer(server, state, runtime, deps);
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
);
|
|
656
|
+
};
|
|
657
|
+
|
|
448
658
|
const performLogin = async (
|
|
449
659
|
provider: OAuthProviderInterface,
|
|
450
660
|
state: AppState,
|
|
@@ -611,12 +821,18 @@ export function createCommandController(
|
|
|
611
821
|
case "verbose":
|
|
612
822
|
handleVerboseCommand(state);
|
|
613
823
|
return true;
|
|
824
|
+
case "mcp":
|
|
825
|
+
handleMcpCommand(state);
|
|
826
|
+
return true;
|
|
614
827
|
case "todo":
|
|
615
828
|
handleTodoCommand(state);
|
|
616
829
|
return true;
|
|
617
830
|
case "help":
|
|
618
831
|
handleHelpCommand(state);
|
|
619
832
|
return true;
|
|
833
|
+
case SKILL_COMMAND:
|
|
834
|
+
handleSkillCommand(state);
|
|
835
|
+
return true;
|
|
620
836
|
default:
|
|
621
837
|
return false;
|
|
622
838
|
}
|
|
@@ -55,17 +55,6 @@ function collectTextNodes(node: Node | null): TextNode[] {
|
|
|
55
55
|
return node.children.flatMap((child) => collectTextNodes(child));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function findTextNode(node: Node | null, content: string): TextNode {
|
|
59
|
-
const textNode = collectTextNodes(node).find(
|
|
60
|
-
(text) => text.content === content,
|
|
61
|
-
);
|
|
62
|
-
expect(textNode).toBeDefined();
|
|
63
|
-
if (!textNode) {
|
|
64
|
-
throw new Error(`Missing text node: ${content}`);
|
|
65
|
-
}
|
|
66
|
-
return textNode;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
58
|
function makeAssistantToolCallMessage(): AssistantMessage {
|
|
70
59
|
return {
|
|
71
60
|
role: "assistant",
|
|
@@ -146,7 +135,7 @@ describe("ui/conversation", () => {
|
|
|
146
135
|
expect(lines.join("\n")).not.toContain('"path"');
|
|
147
136
|
});
|
|
148
137
|
|
|
149
|
-
test("shell tool-call previews
|
|
138
|
+
test("shell tool-call previews render the command instead of raw JSON", () => {
|
|
150
139
|
const node = renderAssistantMessage(
|
|
151
140
|
{
|
|
152
141
|
content: [
|
|
@@ -169,9 +158,220 @@ describe("ui/conversation", () => {
|
|
|
169
158
|
},
|
|
170
159
|
);
|
|
171
160
|
|
|
172
|
-
|
|
173
|
-
|
|
161
|
+
const lines = collectRenderedLines(node);
|
|
162
|
+
expect(lines).toContain("│ shell ->");
|
|
163
|
+
expect(lines.join("\n")).toContain('if true; then echo "$HOME"; fi');
|
|
164
|
+
expect(lines.join("\n")).not.toContain('"command"');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("compact shell tool-call previews only render the visible tail slice", () => {
|
|
168
|
+
const command = Array.from(
|
|
169
|
+
{ length: 14 },
|
|
170
|
+
(_, index) => `echo line ${index + 1}`,
|
|
171
|
+
).join("\n");
|
|
172
|
+
|
|
173
|
+
const compactLines = collectRenderedLines(
|
|
174
|
+
renderAssistantMessage(
|
|
175
|
+
{
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "toolCall",
|
|
179
|
+
id: "call-shell",
|
|
180
|
+
name: "shell",
|
|
181
|
+
arguments: { command },
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
showReasoning: true,
|
|
187
|
+
verbose: false,
|
|
188
|
+
theme: DEFAULT_THEME,
|
|
189
|
+
cwd: "/tmp/project",
|
|
190
|
+
previewWidth: 80,
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
const verboseLines = collectRenderedLines(
|
|
195
|
+
renderAssistantMessage(
|
|
196
|
+
{
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "toolCall",
|
|
200
|
+
id: "call-shell",
|
|
201
|
+
name: "shell",
|
|
202
|
+
arguments: { command },
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
showReasoning: true,
|
|
208
|
+
verbose: true,
|
|
209
|
+
theme: DEFAULT_THEME,
|
|
210
|
+
cwd: "/tmp/project",
|
|
211
|
+
previewWidth: 80,
|
|
212
|
+
},
|
|
213
|
+
),
|
|
174
214
|
);
|
|
215
|
+
|
|
216
|
+
expect(compactLines).toContain("│ shell ->");
|
|
217
|
+
expect(compactLines).toContain("│ echo line 14");
|
|
218
|
+
expect(compactLines).toContain("│ And 6 lines more");
|
|
219
|
+
expect(compactLines).not.toContain("│ echo line 1");
|
|
220
|
+
expect(verboseLines).toContain("│ echo line 1");
|
|
221
|
+
expect(compactLines.length).toBeLessThan(verboseLines.length);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("MCP tool-call previews honor verbose mode", () => {
|
|
225
|
+
const args = {
|
|
226
|
+
query: "routing",
|
|
227
|
+
section: "guides",
|
|
228
|
+
limit: 10,
|
|
229
|
+
offset: 0,
|
|
230
|
+
filter1: "alpha",
|
|
231
|
+
filter2: "beta",
|
|
232
|
+
filter3: "gamma",
|
|
233
|
+
filter4: "delta",
|
|
234
|
+
filter5: "epsilon",
|
|
235
|
+
filter6: "zeta",
|
|
236
|
+
filter7: "eta",
|
|
237
|
+
filter8: "theta",
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const compactLines = collectRenderedLines(
|
|
241
|
+
renderAssistantMessage(
|
|
242
|
+
{
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "toolCall",
|
|
246
|
+
id: "call-mcp",
|
|
247
|
+
name: "docs__search",
|
|
248
|
+
arguments: args,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
showReasoning: true,
|
|
254
|
+
verbose: false,
|
|
255
|
+
theme: DEFAULT_THEME,
|
|
256
|
+
cwd: "/tmp/project",
|
|
257
|
+
previewWidth: 80,
|
|
258
|
+
},
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
const verboseLines = collectRenderedLines(
|
|
262
|
+
renderAssistantMessage(
|
|
263
|
+
{
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "toolCall",
|
|
267
|
+
id: "call-mcp",
|
|
268
|
+
name: "docs__search",
|
|
269
|
+
arguments: args,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
showReasoning: true,
|
|
275
|
+
verbose: true,
|
|
276
|
+
theme: DEFAULT_THEME,
|
|
277
|
+
cwd: "/tmp/project",
|
|
278
|
+
previewWidth: 80,
|
|
279
|
+
},
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(compactLines).toContain("│ docs__search ->");
|
|
284
|
+
expect(compactLines.some((line) => line.includes("And "))).toBe(true);
|
|
285
|
+
expect(compactLines.join("\n")).not.toContain('"query": "routing"');
|
|
286
|
+
expect(verboseLines.join("\n")).toContain('"query": "routing"');
|
|
287
|
+
expect(compactLines.length).toBeLessThan(verboseLines.length);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("shell tool results render structured stdout/stderr details without stderr labels or error styling", () => {
|
|
291
|
+
const stdout = Array.from(
|
|
292
|
+
{ length: 12 },
|
|
293
|
+
(_, index) => `line ${index + 1}`,
|
|
294
|
+
).join("\n");
|
|
295
|
+
const node = renderToolResult(
|
|
296
|
+
"shell",
|
|
297
|
+
{ command: "run-tests" },
|
|
298
|
+
"Exit code: 1\nlegacy text should be ignored when details exist",
|
|
299
|
+
true,
|
|
300
|
+
{
|
|
301
|
+
showReasoning: true,
|
|
302
|
+
verbose: false,
|
|
303
|
+
theme: DEFAULT_THEME,
|
|
304
|
+
cwd: "/tmp/project",
|
|
305
|
+
previewWidth: 48,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
stdout,
|
|
309
|
+
stderr: "boom",
|
|
310
|
+
exitCode: 1,
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
const lines = collectRenderedLines(node);
|
|
314
|
+
const textNodes = collectTextNodes(node);
|
|
315
|
+
|
|
316
|
+
expect(lines).toContain("│ shell <-");
|
|
317
|
+
expect(lines).toContain("│ boom");
|
|
318
|
+
expect(lines).toContain("│ exit 1");
|
|
319
|
+
expect(lines).toContain("│ And 6 lines more");
|
|
320
|
+
expect(lines.join("\n")).not.toContain("stderr:");
|
|
321
|
+
expect(lines.join("\n")).not.toContain("legacy text should be ignored");
|
|
322
|
+
expect(
|
|
323
|
+
textNodes.some(
|
|
324
|
+
(textNode) =>
|
|
325
|
+
textNode.content === "boom" &&
|
|
326
|
+
textNode.props.fgColor === DEFAULT_THEME.toolText,
|
|
327
|
+
),
|
|
328
|
+
).toBe(true);
|
|
329
|
+
expect(
|
|
330
|
+
textNodes.some(
|
|
331
|
+
(textNode) =>
|
|
332
|
+
textNode.content === "exit 1" &&
|
|
333
|
+
textNode.props.fgColor === DEFAULT_THEME.toolText,
|
|
334
|
+
),
|
|
335
|
+
).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("shell tool results still render legacy flattened results from persisted history without stderr labels", () => {
|
|
339
|
+
const node = renderToolResult(
|
|
340
|
+
"shell",
|
|
341
|
+
{ command: "run-tests" },
|
|
342
|
+
"Exit code: 1\nout\n\n[stderr]\nerr",
|
|
343
|
+
true,
|
|
344
|
+
{
|
|
345
|
+
showReasoning: true,
|
|
346
|
+
verbose: true,
|
|
347
|
+
theme: DEFAULT_THEME,
|
|
348
|
+
cwd: "/tmp/project",
|
|
349
|
+
previewWidth: 80,
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
const lines = collectRenderedLines(node);
|
|
353
|
+
const textNodes = collectTextNodes(node);
|
|
354
|
+
|
|
355
|
+
expect(lines).toContain("│ out");
|
|
356
|
+
expect(lines).toContain("│ err");
|
|
357
|
+
expect(lines).toContain("│ exit 1");
|
|
358
|
+
expect(lines.join("\n")).not.toContain("stderr:");
|
|
359
|
+
expect(lines.join("\n")).not.toContain("[stderr]");
|
|
360
|
+
expect(lines.join("\n")).not.toContain("Exit code:");
|
|
361
|
+
expect(
|
|
362
|
+
textNodes.some(
|
|
363
|
+
(textNode) =>
|
|
364
|
+
textNode.content === "err" &&
|
|
365
|
+
textNode.props.fgColor === DEFAULT_THEME.toolText,
|
|
366
|
+
),
|
|
367
|
+
).toBe(true);
|
|
368
|
+
expect(
|
|
369
|
+
textNodes.some(
|
|
370
|
+
(textNode) =>
|
|
371
|
+
textNode.content === "exit 1" &&
|
|
372
|
+
textNode.props.fgColor === DEFAULT_THEME.toolText,
|
|
373
|
+
),
|
|
374
|
+
).toBe(true);
|
|
175
375
|
});
|
|
176
376
|
|
|
177
377
|
test("read tool results include the resolved path, hide model paging hints, and render fewer body lines when verbose is off", () => {
|
|
@@ -292,17 +492,12 @@ describe("ui/conversation", () => {
|
|
|
292
492
|
},
|
|
293
493
|
);
|
|
294
494
|
|
|
295
|
-
expect(findTextNode(node, "42").props.fgColor).toBe(
|
|
296
|
-
DEFAULT_THEME.secondaryAccentText,
|
|
297
|
-
);
|
|
298
|
-
expect(
|
|
299
|
-
findTextNode(node, "supercalifragilisticexpialidociousIdentifier")
|
|
300
|
-
.content,
|
|
301
|
-
).toBe("supercalifragilisticexpialidociousIdentifier");
|
|
302
495
|
expect(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
496
|
+
collectTextNodes(node).some(
|
|
497
|
+
(textNode) =>
|
|
498
|
+
textNode.content === "supercalifragilisticexpialidociousIdentifier",
|
|
499
|
+
),
|
|
500
|
+
).toBe(true);
|
|
306
501
|
});
|
|
307
502
|
|
|
308
503
|
test("grep tool results render grouped files and lines instead of raw JSON", () => {
|
|
@@ -353,8 +548,38 @@ describe("ui/conversation", () => {
|
|
|
353
548
|
expect(lines).toContain("│ 858: spec: ToolBlockSpec,");
|
|
354
549
|
expect(lines.join("\n")).not.toContain('"files"');
|
|
355
550
|
expect(lines.join("\n")).not.toContain('"kind"');
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("MCP tool results honor verbose mode", () => {
|
|
554
|
+
const output = Array.from(
|
|
555
|
+
{ length: 14 },
|
|
556
|
+
(_, index) => `result ${index + 1}`,
|
|
557
|
+
).join("\n");
|
|
558
|
+
|
|
559
|
+
const compactLines = collectRenderedLines(
|
|
560
|
+
renderToolResult("docs__search", { query: "routing" }, output, false, {
|
|
561
|
+
showReasoning: true,
|
|
562
|
+
verbose: false,
|
|
563
|
+
theme: DEFAULT_THEME,
|
|
564
|
+
cwd: "/tmp/project",
|
|
565
|
+
previewWidth: 80,
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
const verboseLines = collectRenderedLines(
|
|
569
|
+
renderToolResult("docs__search", { query: "routing" }, output, false, {
|
|
570
|
+
showReasoning: true,
|
|
571
|
+
verbose: true,
|
|
572
|
+
theme: DEFAULT_THEME,
|
|
573
|
+
cwd: "/tmp/project",
|
|
574
|
+
previewWidth: 80,
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
expect(compactLines).toContain("│ docs__search <-");
|
|
579
|
+
expect(compactLines).toContain("│ result 14");
|
|
580
|
+
expect(compactLines).toContain("│ And 6 lines more");
|
|
581
|
+
expect(compactLines).not.toContain("│ result 1");
|
|
582
|
+
expect(verboseLines).toContain("│ result 1");
|
|
583
|
+
expect(compactLines.length).toBeLessThan(verboseLines.length);
|
|
359
584
|
});
|
|
360
585
|
});
|