lsd-pi 1.3.6 → 1.3.9
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 +82 -0
- 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/resources/extensions/mcp-client/index.js +72 -4
- package/dist/resources/extensions/slash-commands/plan.js +5 -5
- package/dist/resources/extensions/usage/index.js +34 -2
- package/dist/resources/extensions/voice/index.js +1 -0
- package/dist/resources/extensions/voice/push-to-talk.js +2 -0
- package/dist/welcome-screen.js +2 -2
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js +72 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +29 -2
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +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/tool-priority.js +1 -1
- package/packages/pi-coding-agent/dist/core/tool-priority.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/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +1 -0
- package/packages/pi-coding-agent/dist/main.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 +16 -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 +128 -13
- 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 +48 -4
- 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 +64 -15
- 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.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +2 -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 +73 -27
- 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/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.context-usage.test.ts +87 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -2
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +3 -0
- 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/tool-priority.ts +1 -1
- 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/main.ts +1 -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 +137 -14
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +60 -4
- 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 +73 -15
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +2 -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 +76 -27
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +4 -0
- 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
- package/src/resources/extensions/mcp-client/index.ts +83 -4
- package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +16 -0
- package/src/resources/extensions/slash-commands/plan.ts +6 -6
- package/src/resources/extensions/usage/index.ts +40 -2
- package/src/resources/extensions/voice/index.ts +1 -0
- package/src/resources/extensions/voice/push-to-talk.ts +3 -0
- package/src/resources/extensions/voice/tests/push-to-talk.test.ts +6 -0
|
@@ -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
|
|
@@ -126,6 +134,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
126
134
|
// When true, this component intentionally renders no lines
|
|
127
135
|
private hideComponent = false;
|
|
128
136
|
private manuallyHidden = false;
|
|
137
|
+
private indented = false;
|
|
129
138
|
private startTime = Date.now();
|
|
130
139
|
|
|
131
140
|
// Tool status spinner state
|
|
@@ -176,6 +185,40 @@ export class ToolExecutionComponent extends Container {
|
|
|
176
185
|
return isBuiltInName && !hasCustomRenderers;
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
private setPrimaryContent(useBox: boolean): void {
|
|
189
|
+
const hasBox = this.children.includes(this.contentBox);
|
|
190
|
+
const hasText = this.children.includes(this.contentText);
|
|
191
|
+
|
|
192
|
+
if (useBox) {
|
|
193
|
+
if (hasText) this.removeChild(this.contentText);
|
|
194
|
+
if (!hasBox) this.addChild(this.contentBox);
|
|
195
|
+
} else {
|
|
196
|
+
if (hasBox) this.removeChild(this.contentBox);
|
|
197
|
+
if (!hasText) this.addChild(this.contentText);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private getDiffTextToRender(): string | undefined {
|
|
202
|
+
if (this.toolName === "write") {
|
|
203
|
+
if (!this.isPartial && this.writeDiffPreview && !("error" in this.writeDiffPreview) && this.writeDiffPreview.diff) {
|
|
204
|
+
return this.writeDiffPreview.diff;
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this.toolName !== "edit" || this.result?.isError) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.result?.details?.diff) {
|
|
214
|
+
return this.result.details.diff;
|
|
215
|
+
}
|
|
216
|
+
if (this.editDiffPreview && !("error" in this.editDiffPreview) && this.editDiffPreview.diff) {
|
|
217
|
+
return this.editDiffPreview.diff;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
179
222
|
updateArgs(args: any): void {
|
|
180
223
|
this.args = args;
|
|
181
224
|
if (this.toolName === "write" && this.isPartial) {
|
|
@@ -287,10 +330,37 @@ export class ToolExecutionComponent extends Container {
|
|
|
287
330
|
if (rawPath !== null && fileContent !== null) {
|
|
288
331
|
this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
|
289
332
|
}
|
|
333
|
+
this.maybeComputeWriteDiff();
|
|
290
334
|
}
|
|
291
335
|
this.maybeComputeEditDiff();
|
|
292
336
|
}
|
|
293
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Compute write diff preview when we have complete args.
|
|
340
|
+
* This runs async and updates display when done.
|
|
341
|
+
*/
|
|
342
|
+
private maybeComputeWriteDiff(): void {
|
|
343
|
+
if (this.toolName !== "write") return;
|
|
344
|
+
|
|
345
|
+
const path = this.args?.path ?? this.args?.file_path;
|
|
346
|
+
const content = this.args?.content;
|
|
347
|
+
|
|
348
|
+
if (!path || content === undefined) return;
|
|
349
|
+
|
|
350
|
+
const argsKey = JSON.stringify({ path, content });
|
|
351
|
+
if (this.writeDiffArgsKey === argsKey) return;
|
|
352
|
+
|
|
353
|
+
this.writeDiffArgsKey = argsKey;
|
|
354
|
+
|
|
355
|
+
computeWriteDiff(path, content, this.cwd).then((result) => {
|
|
356
|
+
if (this.writeDiffArgsKey === argsKey) {
|
|
357
|
+
this.writeDiffPreview = result;
|
|
358
|
+
this.updateDisplay();
|
|
359
|
+
this.ui.requestRender();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
294
364
|
/**
|
|
295
365
|
* Compute edit diff preview when we have complete args.
|
|
296
366
|
* This runs async and updates display when done.
|
|
@@ -388,6 +458,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
388
458
|
this.updateDisplay();
|
|
389
459
|
}
|
|
390
460
|
|
|
461
|
+
setIndented(indented: boolean): void {
|
|
462
|
+
this.indented = indented;
|
|
463
|
+
this.updateDisplay();
|
|
464
|
+
}
|
|
465
|
+
|
|
391
466
|
isHidden(): boolean {
|
|
392
467
|
return this.hideComponent;
|
|
393
468
|
}
|
|
@@ -396,8 +471,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
396
471
|
return Date.now() - this.startTime;
|
|
397
472
|
}
|
|
398
473
|
|
|
399
|
-
shouldHideWhenCollapsed(): boolean {
|
|
400
|
-
return !this.isPartial && shouldCollapse(this.toolName, this.result?.isError ?? false);
|
|
474
|
+
shouldHideWhenCollapsed(collapseToolCalls = true): boolean {
|
|
475
|
+
return collapseToolCalls && !this.isPartial && shouldCollapse(this.toolName, this.result?.isError ?? false);
|
|
401
476
|
}
|
|
402
477
|
|
|
403
478
|
setRenderMode(mode: "minimal" | "normal"): void {
|
|
@@ -433,7 +508,18 @@ export class ToolExecutionComponent extends Container {
|
|
|
433
508
|
if (this.hideComponent) {
|
|
434
509
|
return [];
|
|
435
510
|
}
|
|
436
|
-
|
|
511
|
+
const lines = super.render(width);
|
|
512
|
+
if (this.indented) {
|
|
513
|
+
const gutter = theme.fg("dim", "│");
|
|
514
|
+
return lines.map((line, i) => {
|
|
515
|
+
if (i === lines.length - 1 && line === "") {
|
|
516
|
+
// Trailing empty spacer line → gutter only
|
|
517
|
+
return gutter;
|
|
518
|
+
}
|
|
519
|
+
return gutter + " " + theme.fg("dim", line);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return [...lines, ""];
|
|
437
523
|
}
|
|
438
524
|
|
|
439
525
|
private updateDisplay(): void {
|
|
@@ -469,15 +555,22 @@ export class ToolExecutionComponent extends Container {
|
|
|
469
555
|
}
|
|
470
556
|
|
|
471
557
|
const useBuiltInRenderer = this.shouldUseBuiltInRenderer();
|
|
558
|
+
const diffTextToRender = this.getDiffTextToRender();
|
|
472
559
|
let customRendererHasContent = false;
|
|
473
560
|
|
|
474
561
|
// Use built-in rendering for built-in tools (or overrides without custom renderers)
|
|
475
562
|
if (useBuiltInRenderer) {
|
|
563
|
+
const useDiffBox = !!diffTextToRender && !this.shouldHideCollapsedPreview();
|
|
564
|
+
this.setPrimaryContent(this.toolName === "bash" || useDiffBox);
|
|
476
565
|
if (this.toolName === "bash") {
|
|
477
566
|
// Bash uses Box with visual line truncation - no background
|
|
478
567
|
this.contentBox.setBgFn((text: string) => text);
|
|
479
568
|
this.contentBox.clear();
|
|
480
569
|
this.renderBashContent(statusIndicator);
|
|
570
|
+
} else if (useDiffBox && diffTextToRender) {
|
|
571
|
+
this.contentBox.setBgFn((text: string) => text);
|
|
572
|
+
this.contentBox.clear();
|
|
573
|
+
this.renderBuiltInDiffContent(statusIndicator, diffTextToRender);
|
|
481
574
|
} else {
|
|
482
575
|
// Other built-in tools: use Text directly with caching - no background
|
|
483
576
|
this.contentText.setCustomBgFn((text: string) => text);
|
|
@@ -619,6 +712,21 @@ export class ToolExecutionComponent extends Container {
|
|
|
619
712
|
this.hideComponent = this.manuallyHidden || computedHidden;
|
|
620
713
|
}
|
|
621
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Render built-in edit/write diff blocks with width-aware full-line backgrounds.
|
|
717
|
+
*/
|
|
718
|
+
private renderBuiltInDiffContent(statusIndicator: string, diffText: string): void {
|
|
719
|
+
const header = this.formatToolExecution(statusIndicator).split("\n\n", 1)[0] ?? "";
|
|
720
|
+
this.contentBox.addChild(new Text(header, 0, 0));
|
|
721
|
+
this.contentBox.addChild({
|
|
722
|
+
render: (width: number) => {
|
|
723
|
+
const contentWidth = Math.max(1, width - 2);
|
|
724
|
+
return ["", ...renderDiffLines(diffText, contentWidth).map((line) => ` ${line} `)];
|
|
725
|
+
},
|
|
726
|
+
invalidate: () => { },
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
622
730
|
/**
|
|
623
731
|
* Render bash content using visual line truncation (like bash-execution.ts)
|
|
624
732
|
*/
|
|
@@ -874,15 +982,38 @@ export class ToolExecutionComponent extends Container {
|
|
|
874
982
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
875
983
|
const fileContent = str(this.args?.content);
|
|
876
984
|
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
|
985
|
+
const firstChangedLine = this.writeDiffPreview && "firstChangedLine" in this.writeDiffPreview
|
|
986
|
+
? this.writeDiffPreview.firstChangedLine
|
|
987
|
+
: undefined;
|
|
877
988
|
|
|
878
989
|
let writePathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
|
879
990
|
if (rawPath && path) {
|
|
880
|
-
writePathDisplay = editorLink(rawPath, writePathDisplay, {
|
|
991
|
+
writePathDisplay = editorLink(rawPath, writePathDisplay, {
|
|
992
|
+
cwd: this.cwd,
|
|
993
|
+
line: firstChangedLine ?? undefined,
|
|
994
|
+
scheme: this.editorScheme,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (firstChangedLine) {
|
|
998
|
+
writePathDisplay += theme.fg("warning", `:${firstChangedLine}`);
|
|
881
999
|
}
|
|
882
1000
|
text = `${statusIndicator} ${theme.fg("toolTitle", theme.bold("write"))} ${writePathDisplay}`;
|
|
883
1001
|
|
|
884
1002
|
if (fileContent === null) {
|
|
885
1003
|
text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
|
|
1004
|
+
} else if (this.result?.isError) {
|
|
1005
|
+
const errorText = this.getTextOutput();
|
|
1006
|
+
if (errorText) {
|
|
1007
|
+
text += `\n\n${theme.fg("error", errorText)}`;
|
|
1008
|
+
}
|
|
1009
|
+
} else if (!this.isPartial && this.writeDiffPreview) {
|
|
1010
|
+
if ("error" in this.writeDiffPreview) {
|
|
1011
|
+
text += `\n\n${theme.fg("error", this.writeDiffPreview.error)}`;
|
|
1012
|
+
} else if (this.writeDiffPreview.diff) {
|
|
1013
|
+
text += hideCollapsedPreview
|
|
1014
|
+
? this.collapsedHintWithPrefix()
|
|
1015
|
+
: `\n\n${renderDiff(this.writeDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
|
|
1016
|
+
}
|
|
886
1017
|
} else if (fileContent) {
|
|
887
1018
|
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
|
888
1019
|
|
|
@@ -924,14 +1055,6 @@ export class ToolExecutionComponent extends Container {
|
|
|
924
1055
|
}
|
|
925
1056
|
}
|
|
926
1057
|
}
|
|
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
1058
|
} else if (this.toolName === "edit") {
|
|
936
1059
|
const rawPath = str(this.args?.file_path ?? this.args?.path);
|
|
937
1060
|
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
|
|
61
|
-
|
|
62
|
-
this.contentText.setText(`${indicator} ${title} ${details}`);
|
|
117
|
+
const details = theme.fg("text", groupedTools) + theme.fg("muted", ` · ${elapsed}s`);
|
|
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
|
});
|