lsd-pi 1.3.6 → 1.3.7
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/dist/cli.js +2 -1
- package/dist/lsd-settings-manager.d.ts +2 -0
- package/dist/lsd-settings-manager.js +5 -0
- package/dist/resource-loader.js +33 -3
- package/dist/resources/extensions/cache-timer/index.js +3 -2
- package/dist/welcome-screen.js +2 -2
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +12 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +16 -10
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +26 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +14 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +111 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +47 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +137 -6
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +40 -14
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +70 -25
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +18 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +14 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +13 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +119 -13
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +59 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +174 -6
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +50 -14
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +73 -25
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
- package/packages/pi-tui/dist/components/editor.js +3 -3
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -3
- package/pkg/dist/modes/interactive/theme/themes.js +4 -4
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cache-timer/index.ts +3 -2
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
} from "@gsd/pi-tui";
|
|
12
12
|
import stripAnsi from "strip-ansi";
|
|
13
13
|
import type { ToolDefinition } from "../../../core/extensions/types.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
computeEditDiff,
|
|
16
|
+
computeWriteDiff,
|
|
17
|
+
type EditDiffError,
|
|
18
|
+
type EditDiffResult,
|
|
19
|
+
} from "../../../core/tools/edit-diff.js";
|
|
15
20
|
import { allTools } from "../../../core/tools/index.js";
|
|
16
21
|
import { shouldCollapse } from "../../../core/tool-priority.js";
|
|
17
22
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
|
|
@@ -21,7 +26,7 @@ import { renderTerminalText } from "../../../utils/terminal-serializer.js";
|
|
|
21
26
|
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
|
|
22
27
|
import { type EditorScheme, editorLink } from "../utils/editor-link.js";
|
|
23
28
|
import { shortenPath } from "../utils/shorten-path.js";
|
|
24
|
-
import { renderDiff } from "./diff.js";
|
|
29
|
+
import { renderDiff, renderDiffLines } from "./diff.js";
|
|
25
30
|
import { keyHint } from "./keybinding-hints.js";
|
|
26
31
|
import { truncateToVisualLines } from "./visual-truncate.js";
|
|
27
32
|
|
|
@@ -119,6 +124,9 @@ export class ToolExecutionComponent extends Container {
|
|
|
119
124
|
// Cached edit diff preview (computed when args arrive, before tool executes)
|
|
120
125
|
private editDiffPreview?: EditDiffResult | EditDiffError;
|
|
121
126
|
private editDiffArgsKey?: string; // Track which args the preview is for
|
|
127
|
+
// Cached write diff preview (computed when args arrive, before tool executes)
|
|
128
|
+
private writeDiffPreview?: EditDiffResult | EditDiffError;
|
|
129
|
+
private writeDiffArgsKey?: string; // Track which args the preview is for
|
|
122
130
|
// Cached converted images for Kitty protocol (which requires PNG), keyed by index
|
|
123
131
|
private convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
|
124
132
|
// Incremental syntax highlighting cache for write tool call args
|
|
@@ -176,6 +184,40 @@ export class ToolExecutionComponent extends Container {
|
|
|
176
184
|
return isBuiltInName && !hasCustomRenderers;
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
private setPrimaryContent(useBox: boolean): void {
|
|
188
|
+
const hasBox = this.children.includes(this.contentBox);
|
|
189
|
+
const hasText = this.children.includes(this.contentText);
|
|
190
|
+
|
|
191
|
+
if (useBox) {
|
|
192
|
+
if (hasText) this.removeChild(this.contentText);
|
|
193
|
+
if (!hasBox) this.addChild(this.contentBox);
|
|
194
|
+
} else {
|
|
195
|
+
if (hasBox) this.removeChild(this.contentBox);
|
|
196
|
+
if (!hasText) this.addChild(this.contentText);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private getDiffTextToRender(): string | undefined {
|
|
201
|
+
if (this.toolName === "write") {
|
|
202
|
+
if (!this.isPartial && this.writeDiffPreview && !("error" in this.writeDiffPreview) && this.writeDiffPreview.diff) {
|
|
203
|
+
return this.writeDiffPreview.diff;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.toolName !== "edit" || this.result?.isError) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (this.result?.details?.diff) {
|
|
213
|
+
return this.result.details.diff;
|
|
214
|
+
}
|
|
215
|
+
if (this.editDiffPreview && !("error" in this.editDiffPreview) && this.editDiffPreview.diff) {
|
|
216
|
+
return this.editDiffPreview.diff;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
179
221
|
updateArgs(args: any): void {
|
|
180
222
|
this.args = args;
|
|
181
223
|
if (this.toolName === "write" && this.isPartial) {
|
|
@@ -287,10 +329,37 @@ export class ToolExecutionComponent extends Container {
|
|
|
287
329
|
if (rawPath !== null && fileContent !== null) {
|
|
288
330
|
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
289
331
|
}
|
|
332
|
+
this.maybeComputeWriteDiff();
|
|
290
333
|
}
|
|
291
334
|
this.maybeComputeEditDiff();
|
|
292
335
|
}
|
|
293
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Compute write diff preview when we have complete args.
|
|
339
|
+
* This runs async and updates display when done.
|
|
340
|
+
*/
|
|
341
|
+
private maybeComputeWriteDiff(): void {
|
|
342
|
+
if (this.toolName !== "write") return;
|
|
343
|
+
|
|
344
|
+
const path = this.args?.path ?? this.args?.file_path;
|
|
345
|
+
const content = this.args?.content;
|
|
346
|
+
|
|
347
|
+
if (!path || content === undefined) return;
|
|
348
|
+
|
|
349
|
+
const argsKey = JSON.stringify({ path, content });
|
|
350
|
+
if (this.writeDiffArgsKey === argsKey) return;
|
|
351
|
+
|
|
352
|
+
this.writeDiffArgsKey = argsKey;
|
|
353
|
+
|
|
354
|
+
computeWriteDiff(path, content, this.cwd).then((result) => {
|
|
355
|
+
if (this.writeDiffArgsKey === argsKey) {
|
|
356
|
+
this.writeDiffPreview = result;
|
|
357
|
+
this.updateDisplay();
|
|
358
|
+
this.ui.requestRender();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
294
363
|
/**
|
|
295
364
|
* Compute edit diff preview when we have complete args.
|
|
296
365
|
* This runs async and updates display when done.
|
|
@@ -396,8 +465,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
396
465
|
return Date.now() - this.startTime;
|
|
397
466
|
}
|
|
398
467
|
|
|
399
|
-
shouldHideWhenCollapsed(): boolean {
|
|
400
|
-
return !this.isPartial && shouldCollapse(this.toolName, this.result?.isError ?? false);
|
|
468
|
+
shouldHideWhenCollapsed(collapseToolCalls = true): boolean {
|
|
469
|
+
return collapseToolCalls && !this.isPartial && shouldCollapse(this.toolName, this.result?.isError ?? false);
|
|
401
470
|
}
|
|
402
471
|
|
|
403
472
|
setRenderMode(mode: "minimal" | "normal"): void {
|
|
@@ -469,15 +538,22 @@ export class ToolExecutionComponent extends Container {
|
|
|
469
538
|
}
|
|
470
539
|
|
|
471
540
|
const useBuiltInRenderer = this.shouldUseBuiltInRenderer();
|
|
541
|
+
const diffTextToRender = this.getDiffTextToRender();
|
|
472
542
|
let customRendererHasContent = false;
|
|
473
543
|
|
|
474
544
|
// Use built-in rendering for built-in tools (or overrides without custom renderers)
|
|
475
545
|
if (useBuiltInRenderer) {
|
|
546
|
+
const useDiffBox = !!diffTextToRender && !this.shouldHideCollapsedPreview();
|
|
547
|
+
this.setPrimaryContent(this.toolName === "bash" || useDiffBox);
|
|
476
548
|
if (this.toolName === "bash") {
|
|
477
549
|
// Bash uses Box with visual line truncation - no background
|
|
478
550
|
this.contentBox.setBgFn((text: string) => text);
|
|
479
551
|
this.contentBox.clear();
|
|
480
552
|
this.renderBashContent(statusIndicator);
|
|
553
|
+
} else if (useDiffBox && diffTextToRender) {
|
|
554
|
+
this.contentBox.setBgFn((text: string) => text);
|
|
555
|
+
this.contentBox.clear();
|
|
556
|
+
this.renderBuiltInDiffContent(statusIndicator, diffTextToRender);
|
|
481
557
|
} else {
|
|
482
558
|
// Other built-in tools: use Text directly with caching - no background
|
|
483
559
|
this.contentText.setCustomBgFn((text: string) => text);
|
|
@@ -619,6 +695,21 @@ export class ToolExecutionComponent extends Container {
|
|
|
619
695
|
this.hideComponent = this.manuallyHidden || computedHidden;
|
|
620
696
|
}
|
|
621
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Render built-in edit/write diff blocks with width-aware full-line backgrounds.
|
|
700
|
+
*/
|
|
701
|
+
private renderBuiltInDiffContent(statusIndicator: string, diffText: string): void {
|
|
702
|
+
const header = this.formatToolExecution(statusIndicator).split("\n\n", 1)[0] ?? "";
|
|
703
|
+
this.contentBox.addChild(new Text(header, 0, 0));
|
|
704
|
+
this.contentBox.addChild({
|
|
705
|
+
render: (width: number) => {
|
|
706
|
+
const contentWidth = Math.max(1, width - 2);
|
|
707
|
+
return ["", ...renderDiffLines(diffText, contentWidth).map((line) => ` ${line} `)];
|
|
708
|
+
},
|
|
709
|
+
invalidate: () => { },
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
622
713
|
/**
|
|
623
714
|
* Render bash content using visual line truncation (like bash-execution.ts)
|
|
624
715
|
*/
|
|
@@ -874,15 +965,38 @@ export class ToolExecutionComponent extends Container {
|
|
|
874
965
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
875
966
|
const fileContent = str(this.args?.content);
|
|
876
967
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
968
|
+
const firstChangedLine = this.writeDiffPreview && "firstChangedLine" in this.writeDiffPreview
|
|
969
|
+
? this.writeDiffPreview.firstChangedLine
|
|
970
|
+
: undefined;
|
|
877
971
|
|
|
878
972
|
let writePathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
|
879
973
|
if (rawPath && path) {
|
|
880
|
-
writePathDisplay = editorLink(rawPath, writePathDisplay, {
|
|
974
|
+
writePathDisplay = editorLink(rawPath, writePathDisplay, {
|
|
975
|
+
cwd: this.cwd,
|
|
976
|
+
line: firstChangedLine ?? undefined,
|
|
977
|
+
scheme: this.editorScheme,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (firstChangedLine) {
|
|
981
|
+
writePathDisplay += theme.fg("warning", `:${firstChangedLine}`);
|
|
881
982
|
}
|
|
882
983
|
text = `${statusIndicator} ${theme.fg("toolTitle", theme.bold("write"))} ${writePathDisplay}`;
|
|
883
984
|
|
|
884
985
|
if (fileContent === null) {
|
|
885
986
|
text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
|
|
987
|
+
} else if (this.result?.isError) {
|
|
988
|
+
const errorText = this.getTextOutput();
|
|
989
|
+
if (errorText) {
|
|
990
|
+
text += `\n\n${theme.fg("error", errorText)}`;
|
|
991
|
+
}
|
|
992
|
+
} else if (!this.isPartial && this.writeDiffPreview) {
|
|
993
|
+
if ("error" in this.writeDiffPreview) {
|
|
994
|
+
text += `\n\n${theme.fg("error", this.writeDiffPreview.error)}`;
|
|
995
|
+
} else if (this.writeDiffPreview.diff) {
|
|
996
|
+
text += hideCollapsedPreview
|
|
997
|
+
? this.collapsedHintWithPrefix()
|
|
998
|
+
: `\n\n${renderDiff(this.writeDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
|
|
999
|
+
}
|
|
886
1000
|
} else if (fileContent) {
|
|
887
1001
|
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
888
1002
|
|
|
@@ -924,14 +1038,6 @@ export class ToolExecutionComponent extends Container {
|
|
|
924
1038
|
}
|
|
925
1039
|
}
|
|
926
1040
|
}
|
|
927
|
-
|
|
928
|
-
// Show error if tool execution failed
|
|
929
|
-
if (this.result?.isError) {
|
|
930
|
-
const errorText = this.getTextOutput();
|
|
931
|
-
if (errorText) {
|
|
932
|
-
text += `\n\n${theme.fg("error", errorText)}`;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
1041
|
} else if (this.toolName === "edit") {
|
|
936
1042
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
937
1043
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
@@ -7,11 +7,68 @@ interface CollapsedTool {
|
|
|
7
7
|
elapsed: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// Tools that can be mixed together in one summary line
|
|
11
|
+
const MIXED_GROUPABLE_TOOLS = new Set([
|
|
12
|
+
"read", "find", "ls", "grep", "lsp",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
type SummaryDescriptor = {
|
|
16
|
+
action: string;
|
|
17
|
+
singular: string;
|
|
18
|
+
plural: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const TOOL_SUMMARY_DESCRIPTORS: Record<string, SummaryDescriptor> = {
|
|
22
|
+
read: { action: "reading", singular: "file", plural: "files" },
|
|
23
|
+
write: { action: "editing", singular: "file", plural: "files" },
|
|
24
|
+
edit: { action: "editing", singular: "file", plural: "files" },
|
|
25
|
+
grep: { action: "searching for", singular: "pattern", plural: "patterns" },
|
|
26
|
+
find: { action: "finding", singular: "path", plural: "paths" },
|
|
27
|
+
ls: { action: "listing", singular: "directory", plural: "directories" },
|
|
28
|
+
lsp: { action: "looking up", singular: "symbol", plural: "symbols" },
|
|
29
|
+
bash: { action: "running", singular: "command", plural: "commands" },
|
|
30
|
+
bg_shell: { action: "running", singular: "background command", plural: "background commands" },
|
|
31
|
+
fetch_page: { action: "reading", singular: "page", plural: "pages" },
|
|
32
|
+
resolve_library: { action: "searching for", singular: "library", plural: "libraries" },
|
|
33
|
+
get_library_docs: { action: "reading", singular: "doc", plural: "docs" },
|
|
34
|
+
web_search: { action: "searching web for", singular: "query", plural: "queries" },
|
|
35
|
+
"search-the-web": { action: "searching web for", singular: "query", plural: "queries" },
|
|
36
|
+
search_and_read: { action: "researching", singular: "topic", plural: "topics" },
|
|
37
|
+
google_search: { action: "searching web for", singular: "query", plural: "queries" },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function formatCount(count: number, singular: string, plural: string): string {
|
|
41
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function summarizeToolGroup(name: string, count: number): string {
|
|
45
|
+
if (name.startsWith("browser_")) {
|
|
46
|
+
return `using browser for ${formatCount(count, "step", "steps")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const descriptor = TOOL_SUMMARY_DESCRIPTORS[name];
|
|
50
|
+
if (!descriptor) {
|
|
51
|
+
return count > 1 ? `${name} ×${count}` : name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${descriptor.action} ${formatCount(count, descriptor.singular, descriptor.plural)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
10
57
|
export class ToolSummaryLine extends Container {
|
|
11
58
|
private tools: CollapsedTool[] = [];
|
|
12
59
|
private hidden = false;
|
|
13
60
|
private contentText: Text;
|
|
14
61
|
|
|
62
|
+
canGroupWith(toolName: string): boolean {
|
|
63
|
+
if (this.tools.length === 0) return true;
|
|
64
|
+
// Mixed-groupable tools can share a summary line regardless of order
|
|
65
|
+
if (MIXED_GROUPABLE_TOOLS.has(toolName) && this.tools.every((t) => MIXED_GROUPABLE_TOOLS.has(t.name))) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// Otherwise only same-tool grouping
|
|
69
|
+
return this.tools.every((tool) => tool.name === toolName);
|
|
70
|
+
}
|
|
71
|
+
|
|
15
72
|
constructor() {
|
|
16
73
|
super();
|
|
17
74
|
this.contentText = new Text("", 1, 0);
|
|
@@ -53,12 +110,11 @@ export class ToolSummaryLine extends Container {
|
|
|
53
110
|
}
|
|
54
111
|
|
|
55
112
|
const groupedTools = [...counts.entries()]
|
|
56
|
-
.map(([name, count]) => (
|
|
113
|
+
.map(([name, count]) => summarizeToolGroup(name, count))
|
|
57
114
|
.join(" · ");
|
|
58
115
|
const elapsed = (totalElapsed / 1000).toFixed(1);
|
|
59
116
|
const indicator = theme.fg("success", "●");
|
|
60
|
-
const title = theme.fg("toolTitle", theme.bold("collapsed tools"));
|
|
61
117
|
const details = theme.fg("muted", `${groupedTools} · ${elapsed}s`);
|
|
62
|
-
this.contentText.setText(`${indicator} ${
|
|
118
|
+
this.contentText.setText(`${indicator} ${details}`);
|
|
63
119
|
}
|
|
64
120
|
}
|
|
@@ -16,6 +16,15 @@ function assistantMessage(content: any[] = []): any {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function toolStartEvent(toolCallId: string, toolName: string, args: Record<string, unknown> = {}): any {
|
|
20
|
+
return {
|
|
21
|
+
type: "tool_execution_start",
|
|
22
|
+
toolCallId,
|
|
23
|
+
toolName,
|
|
24
|
+
args,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
function toolEndEvent(toolCallId: string, toolName: string): any {
|
|
20
29
|
return {
|
|
21
30
|
type: "tool_execution_end",
|
|
@@ -44,7 +53,7 @@ function createChatContainer() {
|
|
|
44
53
|
};
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
function createHost(): any {
|
|
56
|
+
function createHost(options: { collapseToolCalls?: boolean; collapsedToolCallsExpanded?: boolean; toolOutputExpanded?: boolean } = {}): any {
|
|
48
57
|
const chatContainer = createChatContainer();
|
|
49
58
|
return {
|
|
50
59
|
isInitialized: true,
|
|
@@ -61,10 +70,12 @@ function createHost(): any {
|
|
|
61
70
|
getShowImages: () => true,
|
|
62
71
|
getToolOutputMode: () => "normal",
|
|
63
72
|
getEditorScheme: () => "auto",
|
|
73
|
+
getCollapseToolCalls: () => options.collapseToolCalls ?? true,
|
|
64
74
|
},
|
|
65
75
|
pendingTools: new Map(),
|
|
66
76
|
collapsedToolSummaryLine: undefined,
|
|
67
|
-
|
|
77
|
+
collapsedToolCallsExpanded: options.collapsedToolCallsExpanded ?? options.toolOutputExpanded ?? false,
|
|
78
|
+
toolOutputExpanded: options.toolOutputExpanded ?? false,
|
|
68
79
|
hideThinkingBlock: false,
|
|
69
80
|
notificationSoundEnabled: false,
|
|
70
81
|
defaultEditor: { onEscape: undefined, bottomHint: "" },
|
|
@@ -124,7 +135,75 @@ function summaryLines(host: any): ToolSummaryLine[] {
|
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
127
|
-
it("
|
|
138
|
+
it("hides collapsible tool calls immediately to avoid blink before grouping", async () => {
|
|
139
|
+
const host = createHost();
|
|
140
|
+
|
|
141
|
+
await handleAgentEvent(host, {
|
|
142
|
+
type: "message_start",
|
|
143
|
+
message: assistantMessage(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await handleAgentEvent(host, toolStartEvent("tool-1", "read", { path: "README.md" }));
|
|
147
|
+
const pending = host.pendingTools.get("tool-1");
|
|
148
|
+
assert.ok(pending);
|
|
149
|
+
assert.equal(pending.isHidden(), true);
|
|
150
|
+
|
|
151
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
152
|
+
assert.equal(summaryLines(host).length, 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("hides streamed tool-call blocks immediately to avoid blink before grouping", async () => {
|
|
156
|
+
const host = createHost();
|
|
157
|
+
|
|
158
|
+
await handleAgentEvent(host, {
|
|
159
|
+
type: "message_start",
|
|
160
|
+
message: assistantMessage(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await handleAgentEvent(host, {
|
|
164
|
+
type: "message_update",
|
|
165
|
+
message: assistantMessage([
|
|
166
|
+
{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "README.md" } },
|
|
167
|
+
]),
|
|
168
|
+
} as any);
|
|
169
|
+
|
|
170
|
+
const pending = host.pendingTools.get("tool-1");
|
|
171
|
+
assert.ok(pending);
|
|
172
|
+
assert.equal(pending.isHidden(), true);
|
|
173
|
+
|
|
174
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
175
|
+
assert.equal(summaryLines(host).length, 1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("reveals initially hidden on-error tools when they fail", async () => {
|
|
179
|
+
const host = createHost();
|
|
180
|
+
|
|
181
|
+
await handleAgentEvent(host, {
|
|
182
|
+
type: "message_start",
|
|
183
|
+
message: assistantMessage(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await handleAgentEvent(host, toolStartEvent("tool-1", "bash", { command: "exit 1" }));
|
|
187
|
+
const pending = host.pendingTools.get("tool-1");
|
|
188
|
+
assert.ok(pending);
|
|
189
|
+
assert.equal(pending.isHidden(), true);
|
|
190
|
+
|
|
191
|
+
await handleAgentEvent(host, {
|
|
192
|
+
type: "tool_execution_end",
|
|
193
|
+
toolCallId: "tool-1",
|
|
194
|
+
toolName: "bash",
|
|
195
|
+
isError: true,
|
|
196
|
+
result: {
|
|
197
|
+
content: [{ type: "text", text: "failed" }],
|
|
198
|
+
details: {},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
assert.equal(summaryLines(host).length, 0);
|
|
203
|
+
assert.equal(pending.isHidden(), false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("groups consecutive identical collapsed tools", async () => {
|
|
128
207
|
const host = createHost();
|
|
129
208
|
|
|
130
209
|
await handleAgentEvent(host, {
|
|
@@ -146,7 +225,47 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
146
225
|
|
|
147
226
|
const summaries = summaryLines(host);
|
|
148
227
|
assert.equal(summaries.length, 1);
|
|
149
|
-
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("
|
|
228
|
+
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("reading 2 files · 1.0s"));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not group web fetches", async () => {
|
|
232
|
+
const host = createHost();
|
|
233
|
+
|
|
234
|
+
await handleAgentEvent(host, {
|
|
235
|
+
type: "message_start",
|
|
236
|
+
message: assistantMessage(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
addPendingTool(host, "tool-1", 200);
|
|
240
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "fetch_page"));
|
|
241
|
+
|
|
242
|
+
addPendingTool(host, "tool-2", 300);
|
|
243
|
+
await handleAgentEvent(host, toolEndEvent("tool-2", "fetch_page"));
|
|
244
|
+
|
|
245
|
+
const summaries = summaryLines(host);
|
|
246
|
+
assert.equal(summaries.length, 2);
|
|
247
|
+
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("reading 1 page · 0.2s"));
|
|
248
|
+
assert.ok(stripAnsi(summaries[1].render(160).join("\n")).includes("reading 1 page · 0.3s"));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("keeps different collapsed tool types in separate summaries", async () => {
|
|
252
|
+
const host = createHost();
|
|
253
|
+
|
|
254
|
+
await handleAgentEvent(host, {
|
|
255
|
+
type: "message_start",
|
|
256
|
+
message: assistantMessage(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
addPendingTool(host, "tool-1", 200);
|
|
260
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
261
|
+
|
|
262
|
+
addPendingTool(host, "tool-2", 300);
|
|
263
|
+
await handleAgentEvent(host, toolEndEvent("tool-2", "find"));
|
|
264
|
+
|
|
265
|
+
const summaries = summaryLines(host);
|
|
266
|
+
assert.equal(summaries.length, 2);
|
|
267
|
+
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("reading 1 file · 0.2s"));
|
|
268
|
+
assert.ok(stripAnsi(summaries[1].render(160).join("\n")).includes("finding 1 path · 0.3s"));
|
|
150
269
|
});
|
|
151
270
|
|
|
152
271
|
it("resets grouping after visible tool result", async () => {
|
|
@@ -173,7 +292,7 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
173
292
|
assert.ok(stripAnsi(summaries[1].render(160).join("\n")).includes("0.4s"));
|
|
174
293
|
});
|
|
175
294
|
|
|
176
|
-
it("
|
|
295
|
+
it("groups identical collapsed tools across empty assistant message boundaries", async () => {
|
|
177
296
|
const host = createHost();
|
|
178
297
|
|
|
179
298
|
await handleAgentEvent(host, {
|
|
@@ -194,7 +313,7 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
194
313
|
|
|
195
314
|
const summaries = summaryLines(host);
|
|
196
315
|
assert.equal(summaries.length, 1);
|
|
197
|
-
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("
|
|
316
|
+
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("reading 2 files · 0.7s"));
|
|
198
317
|
});
|
|
199
318
|
|
|
200
319
|
it("starts new collapsed group after visible assistant content", async () => {
|
|
@@ -225,4 +344,53 @@ describe("chat-controller collapsed tool summary lifecycle", () => {
|
|
|
225
344
|
assert.ok(stripAnsi(summaries[0].render(160).join("\n")).includes("0.3s"));
|
|
226
345
|
assert.ok(stripAnsi(summaries[1].render(160).join("\n")).includes("0.4s"));
|
|
227
346
|
});
|
|
347
|
+
|
|
348
|
+
it("keeps collapsed tool calls visible in intermediate mode", async () => {
|
|
349
|
+
const host = createHost({ collapseToolCalls: true, collapsedToolCallsExpanded: true });
|
|
350
|
+
|
|
351
|
+
await handleAgentEvent(host, {
|
|
352
|
+
type: "message_start",
|
|
353
|
+
message: assistantMessage(),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const tool = addPendingTool(host, "tool-1", 250);
|
|
357
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
358
|
+
|
|
359
|
+
const summaries = summaryLines(host);
|
|
360
|
+
assert.equal(tool.hidden, false);
|
|
361
|
+
assert.equal(summaries.length, 1);
|
|
362
|
+
assert.deepEqual(summaries[0].render(160), []);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("keeps collapsed tools visible when verbose mode is active", async () => {
|
|
366
|
+
const host = createHost({ collapseToolCalls: true, toolOutputExpanded: true });
|
|
367
|
+
|
|
368
|
+
await handleAgentEvent(host, {
|
|
369
|
+
type: "message_start",
|
|
370
|
+
message: assistantMessage(),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const tool = addPendingTool(host, "tool-1", 250);
|
|
374
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
375
|
+
|
|
376
|
+
const summaries = summaryLines(host);
|
|
377
|
+
assert.equal(tool.hidden, false);
|
|
378
|
+
assert.equal(summaries.length, 1);
|
|
379
|
+
assert.deepEqual(summaries[0].render(160), []);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("does not collapse tool calls when setting is disabled", async () => {
|
|
383
|
+
const host = createHost({ collapseToolCalls: false });
|
|
384
|
+
|
|
385
|
+
await handleAgentEvent(host, {
|
|
386
|
+
type: "message_start",
|
|
387
|
+
message: assistantMessage(),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const tool = addPendingTool(host, "tool-1", 250);
|
|
391
|
+
await handleAgentEvent(host, toolEndEvent("tool-1", "read"));
|
|
392
|
+
|
|
393
|
+
assert.equal(tool.hidden, false);
|
|
394
|
+
assert.equal(summaryLines(host).length, 0);
|
|
395
|
+
});
|
|
228
396
|
});
|
|
@@ -9,6 +9,15 @@ import { ToolSummaryLine } from "../components/tool-summary-line.js";
|
|
|
9
9
|
import { shouldCollapse } from "../../../core/tool-priority.js";
|
|
10
10
|
import { appKey } from "../components/keybinding-hints.js";
|
|
11
11
|
|
|
12
|
+
const GROUPABLE_COLLAPSED_TOOLS = new Set([
|
|
13
|
+
"read",
|
|
14
|
+
"find",
|
|
15
|
+
"ls",
|
|
16
|
+
"grep",
|
|
17
|
+
"lsp",
|
|
18
|
+
|
|
19
|
+
]);
|
|
20
|
+
|
|
12
21
|
export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
13
22
|
init: () => Promise<void>;
|
|
14
23
|
getMarkdownThemeWithSettings: () => any;
|
|
@@ -42,6 +51,24 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
42
51
|
host.collapsedToolSummaryLine = undefined;
|
|
43
52
|
};
|
|
44
53
|
|
|
54
|
+
// Tools that always render as their own visible row, never folded into a summary line
|
|
55
|
+
const ALWAYS_DIRECT_TOOLS = new Set([
|
|
56
|
+
"bash", "bg_shell",
|
|
57
|
+
"web_search", "search-the-web", "google_search", "search_and_read",
|
|
58
|
+
"fetch_page", "resolve_library", "get_library_docs",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const shouldStartToolHidden = (toolName: string): boolean => {
|
|
62
|
+
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
63
|
+
if (!collapseToolCalls || host.collapsedToolCallsExpanded) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (ALWAYS_DIRECT_TOOLS.has(toolName)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return shouldCollapse(toolName, false);
|
|
70
|
+
};
|
|
71
|
+
|
|
45
72
|
const hasVisibleRender = (child: { render?: (width: number) => string[] } | undefined): boolean => {
|
|
46
73
|
if (!child?.render) return true;
|
|
47
74
|
try {
|
|
@@ -51,14 +78,20 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
51
78
|
}
|
|
52
79
|
};
|
|
53
80
|
|
|
81
|
+
const canGroupCollapsedTool = (toolName: string): boolean => GROUPABLE_COLLAPSED_TOOLS.has(toolName);
|
|
82
|
+
|
|
54
83
|
const findAdjacentCollapsedToolSummary = (
|
|
84
|
+
toolName: string,
|
|
55
85
|
anchor?: { render: (width: number) => string[] },
|
|
56
86
|
): ToolSummaryLine | undefined => {
|
|
87
|
+
if (!canGroupCollapsedTool(toolName)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
57
90
|
const anchorIndex = anchor ? host.chatContainer.children.indexOf(anchor) : host.chatContainer.children.length;
|
|
58
91
|
for (let i = anchorIndex - 1; i >= 0; i--) {
|
|
59
92
|
const child = host.chatContainer.children[i];
|
|
60
93
|
if (child instanceof ToolSummaryLine) {
|
|
61
|
-
return child;
|
|
94
|
+
return child.canGroupWith(toolName) ? child : undefined;
|
|
62
95
|
}
|
|
63
96
|
if (child instanceof ToolExecutionComponent && child.isHidden()) {
|
|
64
97
|
continue;
|
|
@@ -71,17 +104,15 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
71
104
|
return undefined;
|
|
72
105
|
};
|
|
73
106
|
|
|
74
|
-
const appendCollapsedToolSummary = (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
if (!summary || !host.chatContainer.children.includes(summary)) {
|
|
107
|
+
const appendCollapsedToolSummary = (
|
|
108
|
+
toolName: string,
|
|
109
|
+
elapsed: number,
|
|
110
|
+
anchor?: { render: (width: number) => string[] },
|
|
111
|
+
): ToolSummaryLine => {
|
|
112
|
+
let summary = findAdjacentCollapsedToolSummary(toolName, anchor);
|
|
113
|
+
if (!summary) {
|
|
83
114
|
summary = new ToolSummaryLine();
|
|
84
|
-
summary.setHidden(host.
|
|
115
|
+
summary.setHidden(host.collapsedToolCallsExpanded);
|
|
85
116
|
if (anchor) {
|
|
86
117
|
const anchorIndex = host.chatContainer.children.indexOf(anchor);
|
|
87
118
|
if (anchorIndex >= 0) {
|
|
@@ -92,9 +123,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
92
123
|
} else {
|
|
93
124
|
host.chatContainer.addChild(summary);
|
|
94
125
|
}
|
|
95
|
-
host.collapsedToolSummaryLine = summary;
|
|
96
126
|
}
|
|
127
|
+
host.collapsedToolSummaryLine = summary;
|
|
97
128
|
summary.addTool(toolName, elapsed);
|
|
129
|
+
return summary;
|
|
98
130
|
};
|
|
99
131
|
|
|
100
132
|
switch (event.type) {
|
|
@@ -221,6 +253,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
221
253
|
host.ui,
|
|
222
254
|
);
|
|
223
255
|
component.setExpanded(host.toolOutputExpanded);
|
|
256
|
+
component.setHidden(shouldStartToolHidden(content.name));
|
|
224
257
|
host.chatContainer.addChild(component);
|
|
225
258
|
host.pendingTools.set(content.id, component);
|
|
226
259
|
} else {
|
|
@@ -240,6 +273,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
240
273
|
host.ui,
|
|
241
274
|
);
|
|
242
275
|
component.setExpanded(host.toolOutputExpanded);
|
|
276
|
+
component.setHidden(shouldStartToolHidden(content.name));
|
|
243
277
|
host.chatContainer.addChild(component);
|
|
244
278
|
host.pendingTools.set(content.id, component);
|
|
245
279
|
}
|
|
@@ -322,6 +356,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
322
356
|
host.ui,
|
|
323
357
|
);
|
|
324
358
|
component.setExpanded(host.toolOutputExpanded);
|
|
359
|
+
component.setHidden(shouldStartToolHidden(event.toolName));
|
|
325
360
|
host.chatContainer.addChild(component);
|
|
326
361
|
host.pendingTools.set(event.toolCallId, component);
|
|
327
362
|
host.ui.requestRender();
|
|
@@ -371,9 +406,10 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
371
406
|
const component = host.pendingTools.get(event.toolCallId);
|
|
372
407
|
if (component) {
|
|
373
408
|
component.updateResult({ ...event.result, isError: event.isError });
|
|
374
|
-
|
|
409
|
+
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
410
|
+
if (collapseToolCalls && shouldCollapse(event.toolName, event.isError) && !ALWAYS_DIRECT_TOOLS.has(event.toolName)) {
|
|
375
411
|
appendCollapsedToolSummary(event.toolName, component.getElapsed(), component);
|
|
376
|
-
component.setHidden(
|
|
412
|
+
component.setHidden(!host.collapsedToolCallsExpanded);
|
|
377
413
|
} else {
|
|
378
414
|
component.setHidden(false);
|
|
379
415
|
resetCollapsedToolSummary();
|