mini-coder 0.5.12 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARK.md +15 -316
- package/PROGRESS.md +1 -2
- package/README.md +53 -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 +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +97 -24
- package/src/index.ts +78 -93
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +2 -12
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +17 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-shell.ts +138 -6
- package/src/tools.ts +4 -0
- package/src/ui/agent.ts +7 -0
- package/src/ui/commands.test.ts +475 -4
- package/src/ui/commands.ts +210 -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,7 +17,8 @@ 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,
|
|
23
24
|
forkSession,
|
|
@@ -30,9 +31,15 @@ import {
|
|
|
30
31
|
undoLastTurn,
|
|
31
32
|
} from "../session.ts";
|
|
32
33
|
import { updateSettings } from "../settings.ts";
|
|
34
|
+
import { clearQueuedUserMessages } from "../submit.ts";
|
|
33
35
|
import { collapseWhitespace, truncateText } from "../text.ts";
|
|
34
36
|
import { getTodoItems } from "../tools.ts";
|
|
35
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
buildHelpText,
|
|
39
|
+
COMMAND_DESCRIPTIONS,
|
|
40
|
+
SKILL_REFERENCE_DESCRIPTION,
|
|
41
|
+
SKILL_REFERENCE_LABEL,
|
|
42
|
+
} from "./help.ts";
|
|
36
43
|
import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
|
|
37
44
|
import type { UiRenderPriority } from "./runtime.ts";
|
|
38
45
|
import { abbreviatePath } from "./status.ts";
|
|
@@ -74,6 +81,22 @@ interface UiCommandRuntime {
|
|
|
74
81
|
openInBrowser: (url: string) => void;
|
|
75
82
|
}
|
|
76
83
|
|
|
84
|
+
interface UiCommandDeps {
|
|
85
|
+
/** Connect a configured MCP server on demand. */
|
|
86
|
+
connectMcpServer: (
|
|
87
|
+
server: AppState["mcpServers"][number],
|
|
88
|
+
) => Promise<string[]>;
|
|
89
|
+
/** Disconnect an MCP server and clear its imported tools. */
|
|
90
|
+
disconnectMcpServer: (
|
|
91
|
+
server: AppState["mcpServers"][number],
|
|
92
|
+
) => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const defaultUiCommandDeps: UiCommandDeps = {
|
|
96
|
+
connectMcpServer,
|
|
97
|
+
disconnectMcpServer,
|
|
98
|
+
};
|
|
99
|
+
|
|
77
100
|
/** Public command actions consumed by `ui.ts` and unit tests. */
|
|
78
101
|
interface UiCommandController {
|
|
79
102
|
/** Apply a model selection and persist it to settings. */
|
|
@@ -212,14 +235,117 @@ interface OverlayItem {
|
|
|
212
235
|
filterText: string;
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
function formatSkillLabel(skill: AppState["skills"][number]): string {
|
|
239
|
+
return skill.description
|
|
240
|
+
? `${skill.name} · ${skill.description}`
|
|
241
|
+
: skill.name;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function formatToolCount(count: number): string {
|
|
245
|
+
return `${count} tool${count === 1 ? "" : "s"}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatMcpServerLabel(server: AppState["mcpServers"][number]): string {
|
|
249
|
+
return `${server.name} · ${server.enabled ? "on" : "off"} · ${formatToolCount(server.tools.length)}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatMcpToggleMessage(
|
|
253
|
+
server: AppState["mcpServers"][number],
|
|
254
|
+
toolCount: number,
|
|
255
|
+
): string {
|
|
256
|
+
const action = server.enabled ? "Enabled" : "Disabled";
|
|
257
|
+
const delta = `${server.enabled ? "+" : "-"}${toolCount}`;
|
|
258
|
+
const toolSuffix = toolCount === 1 ? "tool" : "tools";
|
|
259
|
+
return `${action} MCP server "${server.name}" (${delta} ${toolSuffix}).`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isRepoLocalMcpServer(state: AppState, serverName: string): boolean {
|
|
263
|
+
return (
|
|
264
|
+
state.repoSettings.mcp?.servers?.some(
|
|
265
|
+
(entry) => entry.name === serverName,
|
|
266
|
+
) ?? false
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function persistMcpServerSettings(
|
|
271
|
+
state: AppState,
|
|
272
|
+
server: AppState["mcpServers"][number],
|
|
273
|
+
): void {
|
|
274
|
+
if (isRepoLocalMcpServer(state, server.name)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
state.settings = updateSettings(state.settingsPath, {
|
|
279
|
+
mcp: {
|
|
280
|
+
servers: [
|
|
281
|
+
{
|
|
282
|
+
name: server.name,
|
|
283
|
+
url: server.url,
|
|
284
|
+
enabled: server.enabled,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function toggleMcpServer(
|
|
292
|
+
server: AppState["mcpServers"][number],
|
|
293
|
+
state: AppState,
|
|
294
|
+
runtime: UiCommandRuntime,
|
|
295
|
+
deps: UiCommandDeps,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
try {
|
|
298
|
+
if (server.enabled) {
|
|
299
|
+
const toolCount = server.tools.length;
|
|
300
|
+
await deps.disconnectMcpServer(server);
|
|
301
|
+
server.enabled = false;
|
|
302
|
+
persistMcpServerSettings(state, server);
|
|
303
|
+
runtime.appendInfoMessage(
|
|
304
|
+
formatMcpToggleMessage(server, toolCount),
|
|
305
|
+
state,
|
|
306
|
+
);
|
|
307
|
+
runtime.requestRender("normal");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const warnings = await deps.connectMcpServer(server);
|
|
312
|
+
if (!server.connected) {
|
|
313
|
+
for (const warning of warnings) {
|
|
314
|
+
runtime.appendInfoMessage(warning, state);
|
|
315
|
+
}
|
|
316
|
+
runtime.requestRender("normal");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
server.enabled = true;
|
|
321
|
+
persistMcpServerSettings(state, server);
|
|
322
|
+
runtime.appendInfoMessage(
|
|
323
|
+
formatMcpToggleMessage(server, server.tools.length),
|
|
324
|
+
state,
|
|
325
|
+
);
|
|
326
|
+
for (const warning of warnings) {
|
|
327
|
+
runtime.appendInfoMessage(warning, state);
|
|
328
|
+
}
|
|
329
|
+
runtime.requestRender("normal");
|
|
330
|
+
} catch (error) {
|
|
331
|
+
runtime.appendInfoMessage(
|
|
332
|
+
`MCP server "${server.name}": ${getErrorMessage(error)}`,
|
|
333
|
+
state,
|
|
334
|
+
);
|
|
335
|
+
runtime.requestRender("normal");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
215
339
|
/**
|
|
216
340
|
* Create the UI command controller bound to the current UI runtime hooks.
|
|
217
341
|
*
|
|
218
342
|
* @param runtime - State mutation and rendering hooks owned by `ui.ts`.
|
|
343
|
+
* @param deps - Internal MCP command dependencies, mainly for tests.
|
|
219
344
|
* @returns Command actions for slash commands and overlays.
|
|
220
345
|
*/
|
|
221
346
|
export function createCommandController(
|
|
222
347
|
runtime: UiCommandRuntime,
|
|
348
|
+
deps: UiCommandDeps = defaultUiCommandDeps,
|
|
223
349
|
): UiCommandController {
|
|
224
350
|
const openSelectOverlay = (
|
|
225
351
|
state: AppState,
|
|
@@ -235,20 +361,52 @@ export function createCommandController(
|
|
|
235
361
|
focused: true,
|
|
236
362
|
highlightColor: state.theme.accentText,
|
|
237
363
|
onSelect,
|
|
364
|
+
onKeyPress: (key) => {
|
|
365
|
+
if (key === "escape") {
|
|
366
|
+
runtime.dismissOverlay();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
},
|
|
238
371
|
onBlur: runtime.dismissOverlay,
|
|
239
372
|
});
|
|
240
373
|
|
|
241
374
|
runtime.openOverlay({ select, title });
|
|
242
375
|
};
|
|
243
376
|
|
|
244
|
-
const
|
|
245
|
-
const items =
|
|
246
|
-
label:
|
|
247
|
-
value:
|
|
248
|
-
filterText:
|
|
377
|
+
const handleSkillCommand = (state: AppState): void => {
|
|
378
|
+
const items = state.skills.map((skill) => ({
|
|
379
|
+
label: formatSkillLabel(skill),
|
|
380
|
+
value: skill.name,
|
|
381
|
+
filterText: `${skill.name} ${skill.description ?? ""}`,
|
|
249
382
|
}));
|
|
250
383
|
|
|
251
|
-
|
|
384
|
+
openSelectOverlay(
|
|
385
|
+
state,
|
|
386
|
+
"Select a skill",
|
|
387
|
+
items,
|
|
388
|
+
"type to filter skills...",
|
|
389
|
+
(skillName) => {
|
|
390
|
+
runtime.setInputValue(`/${SKILL_COMMAND}:${skillName}`);
|
|
391
|
+
runtime.dismissOverlay();
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const showCommandAutocomplete = (state: AppState): void => {
|
|
397
|
+
const items: OverlayItem[] = [
|
|
398
|
+
...COMMANDS.map((command) => ({
|
|
399
|
+
label: `/${command} ${COMMAND_DESCRIPTIONS[command] ?? ""}`,
|
|
400
|
+
value: command,
|
|
401
|
+
filterText: command,
|
|
402
|
+
})),
|
|
403
|
+
{
|
|
404
|
+
label: `${SKILL_REFERENCE_LABEL} ${SKILL_REFERENCE_DESCRIPTION}`,
|
|
405
|
+
value: SKILL_COMMAND,
|
|
406
|
+
filterText: `${SKILL_COMMAND} ${SKILL_REFERENCE_LABEL}`,
|
|
407
|
+
},
|
|
408
|
+
];
|
|
409
|
+
|
|
252
410
|
openSelectOverlay(
|
|
253
411
|
state,
|
|
254
412
|
"Commands",
|
|
@@ -256,6 +414,12 @@ export function createCommandController(
|
|
|
256
414
|
"type to filter commands...",
|
|
257
415
|
(value) => {
|
|
258
416
|
runtime.dismissOverlay();
|
|
417
|
+
if (value === SKILL_COMMAND) {
|
|
418
|
+
handleSkillCommand(state);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
runtime.setInputValue("");
|
|
259
423
|
handleCommand(value, state);
|
|
260
424
|
},
|
|
261
425
|
);
|
|
@@ -376,6 +540,7 @@ export function createCommandController(
|
|
|
376
540
|
items,
|
|
377
541
|
"type to filter sessions...",
|
|
378
542
|
(sessionId) => {
|
|
543
|
+
clearQueuedUserMessages(state);
|
|
379
544
|
if (sessionId !== currentSessionId) {
|
|
380
545
|
const picked = sessions.find((session) => session.id === sessionId);
|
|
381
546
|
if (picked) {
|
|
@@ -394,6 +559,7 @@ export function createCommandController(
|
|
|
394
559
|
if (state.running) {
|
|
395
560
|
return;
|
|
396
561
|
}
|
|
562
|
+
clearQueuedUserMessages(state);
|
|
397
563
|
state.session = null;
|
|
398
564
|
clearConversationState(state);
|
|
399
565
|
await runtime.reloadPromptContext(state);
|
|
@@ -412,6 +578,8 @@ export function createCommandController(
|
|
|
412
578
|
};
|
|
413
579
|
|
|
414
580
|
const handleUndoCommand = async (state: AppState): Promise<void> => {
|
|
581
|
+
clearQueuedUserMessages(state);
|
|
582
|
+
|
|
415
583
|
if (state.running && state.abortController) {
|
|
416
584
|
state.abortController.abort();
|
|
417
585
|
}
|
|
@@ -445,6 +613,34 @@ export function createCommandController(
|
|
|
445
613
|
});
|
|
446
614
|
};
|
|
447
615
|
|
|
616
|
+
const handleMcpCommand = (state: AppState): void => {
|
|
617
|
+
if (state.mcpServers.length === 0) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const items = state.mcpServers.map((server) => ({
|
|
622
|
+
label: formatMcpServerLabel(server),
|
|
623
|
+
value: server.name,
|
|
624
|
+
filterText: `${server.name} ${server.url} ${server.enabled ? "on" : "off"}`,
|
|
625
|
+
}));
|
|
626
|
+
|
|
627
|
+
openSelectOverlay(
|
|
628
|
+
state,
|
|
629
|
+
"Toggle MCP servers",
|
|
630
|
+
items,
|
|
631
|
+
"type to filter MCP servers...",
|
|
632
|
+
(serverName) => {
|
|
633
|
+
const server = state.mcpServers.find(
|
|
634
|
+
(entry) => entry.name === serverName,
|
|
635
|
+
);
|
|
636
|
+
runtime.dismissOverlay();
|
|
637
|
+
if (server) {
|
|
638
|
+
void toggleMcpServer(server, state, runtime, deps);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
};
|
|
643
|
+
|
|
448
644
|
const performLogin = async (
|
|
449
645
|
provider: OAuthProviderInterface,
|
|
450
646
|
state: AppState,
|
|
@@ -611,12 +807,18 @@ export function createCommandController(
|
|
|
611
807
|
case "verbose":
|
|
612
808
|
handleVerboseCommand(state);
|
|
613
809
|
return true;
|
|
810
|
+
case "mcp":
|
|
811
|
+
handleMcpCommand(state);
|
|
812
|
+
return true;
|
|
614
813
|
case "todo":
|
|
615
814
|
handleTodoCommand(state);
|
|
616
815
|
return true;
|
|
617
816
|
case "help":
|
|
618
817
|
handleHelpCommand(state);
|
|
619
818
|
return true;
|
|
819
|
+
case SKILL_COMMAND:
|
|
820
|
+
handleSkillCommand(state);
|
|
821
|
+
return true;
|
|
620
822
|
default:
|
|
621
823
|
return false;
|
|
622
824
|
}
|
|
@@ -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
|
});
|