@oh-my-pi/pi-coding-agent 5.1.1 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/package.json +5 -5
- package/src/core/tools/bash.ts +6 -7
- package/src/core/tools/find.ts +20 -14
- package/src/core/tools/grep.ts +15 -13
- package/src/core/tools/ls.ts +14 -8
- package/src/core/tools/lsp/index.ts +39 -7
- package/src/core/tools/read.ts +27 -6
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/task/index.ts +30 -4
- package/src/core/tools/task/parallel.ts +35 -15
- package/src/modes/interactive/components/tool-execution.ts +2 -1
- package/src/modes/interactive/interactive-mode.ts +2 -3
- package/src/prompts/tools/read.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.2.1] - 2026-01-14
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
- Fixed stale diagnostic results by tracking diagnostic versions before file sync operations
|
|
9
|
+
- Fixed race condition where LSP diagnostics could return outdated results after file modifications
|
|
10
|
+
|
|
11
|
+
## [5.2.0] - 2026-01-14
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added `withLines` parameter to read tool for optional line number output (default: true, cat -n format)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Changed find/grep/ls tool output to render inline without background box for cleaner visual flow
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed task tool abort to return partial results instead of failing (completed tasks preserved, cancelled tasks shown as skipped)
|
|
24
|
+
- Fixed TUI crash when bash output metadata lines exceed terminal width on narrow terminals
|
|
25
|
+
- Fixed find tool not matching `**/filename` patterns (was incorrectly using `--full-path` for glob depth wildcards)
|
|
26
|
+
|
|
5
27
|
## [5.1.1] - 2026-01-14
|
|
6
28
|
|
|
7
29
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "5.
|
|
43
|
-
"@oh-my-pi/pi-ai": "5.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "5.
|
|
45
|
-
"@oh-my-pi/pi-tui": "5.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "5.2.1",
|
|
43
|
+
"@oh-my-pi/pi-ai": "5.2.1",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "5.2.1",
|
|
45
|
+
"@oh-my-pi/pi-tui": "5.2.1",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
48
48
|
"@sinclair/typebox": "^0.34.46",
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, resolve, sep } from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { Text, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
6
|
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate";
|
|
7
7
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
@@ -293,16 +293,15 @@ export const bashToolRenderer = {
|
|
|
293
293
|
const outputLines: string[] = [];
|
|
294
294
|
if (cachedSkipped && cachedSkipped > 0) {
|
|
295
295
|
outputLines.push("");
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
`${uiTheme.format.ellipsis} (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
|
|
300
|
-
),
|
|
296
|
+
const skippedLine = uiTheme.fg(
|
|
297
|
+
"dim",
|
|
298
|
+
`${uiTheme.format.ellipsis} (${cachedSkipped} earlier lines, showing ${cachedLines.length} of ${cachedSkipped + cachedLines.length}) (ctrl+o to expand)`,
|
|
301
299
|
);
|
|
300
|
+
outputLines.push(truncateToWidth(skippedLine, width, uiTheme.fg("dim", uiTheme.format.ellipsis)));
|
|
302
301
|
}
|
|
303
302
|
outputLines.push(...cachedLines);
|
|
304
303
|
if (warningLine) {
|
|
305
|
-
outputLines.push(warningLine);
|
|
304
|
+
outputLines.push(truncateToWidth(warningLine, width, uiTheme.fg("warning", uiTheme.format.ellipsis)));
|
|
306
305
|
}
|
|
307
306
|
return outputLines;
|
|
308
307
|
},
|
package/src/core/tools/find.ts
CHANGED
|
@@ -218,7 +218,11 @@ export function createFindTool(session: ToolSession, options?: FindToolOptions):
|
|
|
218
218
|
// When pattern contains path separators (e.g. "reports/**"), use --full-path
|
|
219
219
|
// so fd matches against the full path, not just the filename.
|
|
220
220
|
// Also prepend **/ to anchor the pattern at any depth in the search path.
|
|
221
|
-
|
|
221
|
+
// Note: "**/foo.rs" is a glob construct (filename at any depth), not a path.
|
|
222
|
+
// Only patterns with real path components like "foo/bar" or "foo/**/bar" need --full-path.
|
|
223
|
+
const patternWithoutLeadingStarStar = pattern.replace(/^\*\*\//, "");
|
|
224
|
+
const hasPathSeparator =
|
|
225
|
+
patternWithoutLeadingStarStar.includes("/") || patternWithoutLeadingStarStar.includes("\\");
|
|
222
226
|
const effectivePattern = hasPathSeparator && !pattern.startsWith("**/") ? `**/${pattern}` : pattern;
|
|
223
227
|
const args: string[] = [
|
|
224
228
|
"--glob", // Use glob pattern
|
|
@@ -418,10 +422,11 @@ interface FindRenderArgs {
|
|
|
418
422
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
419
423
|
|
|
420
424
|
export const findToolRenderer = {
|
|
425
|
+
inline: true,
|
|
421
426
|
renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
|
|
422
427
|
const ui = createToolUIKit(uiTheme);
|
|
423
428
|
const label = ui.title("Find");
|
|
424
|
-
let text = `${label} ${uiTheme.fg("accent", args.pattern || "*")}`;
|
|
429
|
+
let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.pattern || "*")}`;
|
|
425
430
|
|
|
426
431
|
const meta: string[] = [];
|
|
427
432
|
if (args.path) meta.push(`in ${args.path}`);
|
|
@@ -436,15 +441,16 @@ export const findToolRenderer = {
|
|
|
436
441
|
},
|
|
437
442
|
|
|
438
443
|
renderResult(
|
|
439
|
-
result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails },
|
|
444
|
+
result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
|
|
440
445
|
{ expanded }: RenderResultOptions,
|
|
441
446
|
uiTheme: Theme,
|
|
442
447
|
): Component {
|
|
443
448
|
const ui = createToolUIKit(uiTheme);
|
|
444
449
|
const details = result.details;
|
|
445
450
|
|
|
446
|
-
if (details?.error) {
|
|
447
|
-
|
|
451
|
+
if (result.isError || details?.error) {
|
|
452
|
+
const errorText = details?.error || result.content?.find((c) => c.type === "text")?.text || "Unknown error";
|
|
453
|
+
return new Text(` ${ui.errorMessage(errorText)}`, 0, 0);
|
|
448
454
|
}
|
|
449
455
|
|
|
450
456
|
const hasDetailedData = details?.fileCount !== undefined;
|
|
@@ -452,7 +458,7 @@ export const findToolRenderer = {
|
|
|
452
458
|
|
|
453
459
|
if (!hasDetailedData) {
|
|
454
460
|
if (!textContent || textContent.includes("No files matching") || textContent.trim() === "") {
|
|
455
|
-
return new Text(ui.emptyMessage("No files found")
|
|
461
|
+
return new Text(` ${ui.emptyMessage("No files found")}`, 0, 0);
|
|
456
462
|
}
|
|
457
463
|
|
|
458
464
|
const lines = textContent.split("\n").filter((l) => l.trim());
|
|
@@ -464,15 +470,15 @@ export const findToolRenderer = {
|
|
|
464
470
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
465
471
|
const summary = ui.count("file", lines.length);
|
|
466
472
|
const expandHint = ui.expandHint(expanded, hasMore);
|
|
467
|
-
let text =
|
|
473
|
+
let text = ` ${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
468
474
|
|
|
469
475
|
for (let i = 0; i < displayLines.length; i++) {
|
|
470
476
|
const isLast = i === displayLines.length - 1 && remaining === 0;
|
|
471
477
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
472
|
-
text += `\n
|
|
478
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", displayLines[i])}`;
|
|
473
479
|
}
|
|
474
480
|
if (remaining > 0) {
|
|
475
|
-
text += `\n
|
|
481
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "file"))}`;
|
|
476
482
|
}
|
|
477
483
|
return new Text(text, 0, 0);
|
|
478
484
|
}
|
|
@@ -482,7 +488,7 @@ export const findToolRenderer = {
|
|
|
482
488
|
const files = details?.files ?? [];
|
|
483
489
|
|
|
484
490
|
if (fileCount === 0) {
|
|
485
|
-
return new Text(ui.emptyMessage("No files found")
|
|
491
|
+
return new Text(` ${ui.emptyMessage("No files found")}`, 0, 0);
|
|
486
492
|
}
|
|
487
493
|
|
|
488
494
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
@@ -492,7 +498,7 @@ export const findToolRenderer = {
|
|
|
492
498
|
const hasMoreFiles = files.length > maxFiles;
|
|
493
499
|
const expandHint = ui.expandHint(expanded, hasMoreFiles);
|
|
494
500
|
|
|
495
|
-
let text =
|
|
501
|
+
let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
|
|
496
502
|
|
|
497
503
|
const truncationReasons: string[] = [];
|
|
498
504
|
if (details?.resultLimitReached) {
|
|
@@ -515,12 +521,12 @@ export const findToolRenderer = {
|
|
|
515
521
|
const entryIcon = isDir
|
|
516
522
|
? uiTheme.fg("accent", uiTheme.icon.folder)
|
|
517
523
|
: uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
518
|
-
text += `\n
|
|
524
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry)}`;
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
if (hasMoreFiles) {
|
|
522
528
|
const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
523
|
-
text += `\n
|
|
529
|
+
text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
|
|
524
530
|
"muted",
|
|
525
531
|
ui.moreItems(files.length - maxFiles, "file"),
|
|
526
532
|
)}`;
|
|
@@ -528,7 +534,7 @@ export const findToolRenderer = {
|
|
|
528
534
|
}
|
|
529
535
|
|
|
530
536
|
if (hasTruncation) {
|
|
531
|
-
text += `\n
|
|
537
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
|
|
532
538
|
}
|
|
533
539
|
|
|
534
540
|
return new Text(text, 0, 0);
|
package/src/core/tools/grep.ts
CHANGED
|
@@ -621,10 +621,11 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
|
621
621
|
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
622
622
|
|
|
623
623
|
export const grepToolRenderer = {
|
|
624
|
+
inline: true,
|
|
624
625
|
renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
|
|
625
626
|
const ui = createToolUIKit(uiTheme);
|
|
626
627
|
const label = ui.title("Grep");
|
|
627
|
-
let text = `${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
|
|
628
|
+
let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
|
|
628
629
|
|
|
629
630
|
const meta: string[] = [];
|
|
630
631
|
if (args.path) meta.push(`in ${args.path}`);
|
|
@@ -647,15 +648,16 @@ export const grepToolRenderer = {
|
|
|
647
648
|
},
|
|
648
649
|
|
|
649
650
|
renderResult(
|
|
650
|
-
result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails },
|
|
651
|
+
result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
|
|
651
652
|
{ expanded }: RenderResultOptions,
|
|
652
653
|
uiTheme: Theme,
|
|
653
654
|
): Component {
|
|
654
655
|
const ui = createToolUIKit(uiTheme);
|
|
655
656
|
const details = result.details;
|
|
656
657
|
|
|
657
|
-
if (details?.error) {
|
|
658
|
-
|
|
658
|
+
if (result.isError || details?.error) {
|
|
659
|
+
const errorText = details?.error || result.content?.find((c) => c.type === "text")?.text || "Unknown error";
|
|
660
|
+
return new Text(` ${ui.errorMessage(errorText)}`, 0, 0);
|
|
659
661
|
}
|
|
660
662
|
|
|
661
663
|
const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
|
|
@@ -663,7 +665,7 @@ export const grepToolRenderer = {
|
|
|
663
665
|
if (!hasDetailedData) {
|
|
664
666
|
const textContent = result.content?.find((c) => c.type === "text")?.text;
|
|
665
667
|
if (!textContent || textContent === "No matches found") {
|
|
666
|
-
return new Text(ui.emptyMessage("No matches found")
|
|
668
|
+
return new Text(` ${ui.emptyMessage("No matches found")}`, 0, 0);
|
|
667
669
|
}
|
|
668
670
|
|
|
669
671
|
const lines = textContent.split("\n").filter((line) => line.trim() !== "");
|
|
@@ -675,16 +677,16 @@ export const grepToolRenderer = {
|
|
|
675
677
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
676
678
|
const summary = ui.count("item", lines.length);
|
|
677
679
|
const expandHint = ui.expandHint(expanded, hasMore);
|
|
678
|
-
let text =
|
|
680
|
+
let text = ` ${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
679
681
|
|
|
680
682
|
for (let i = 0; i < displayLines.length; i++) {
|
|
681
683
|
const isLast = i === displayLines.length - 1 && remaining === 0;
|
|
682
684
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
683
|
-
text += `\n
|
|
685
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", displayLines[i])}`;
|
|
684
686
|
}
|
|
685
687
|
|
|
686
688
|
if (remaining > 0) {
|
|
687
|
-
text += `\n
|
|
689
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "item"))}`;
|
|
688
690
|
}
|
|
689
691
|
|
|
690
692
|
return new Text(text, 0, 0);
|
|
@@ -697,7 +699,7 @@ export const grepToolRenderer = {
|
|
|
697
699
|
const files = details?.files ?? [];
|
|
698
700
|
|
|
699
701
|
if (matchCount === 0) {
|
|
700
|
-
return new Text(ui.emptyMessage("No matches found")
|
|
702
|
+
return new Text(` ${ui.emptyMessage("No matches found")}`, 0, 0);
|
|
701
703
|
}
|
|
702
704
|
|
|
703
705
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
@@ -715,7 +717,7 @@ export const grepToolRenderer = {
|
|
|
715
717
|
const hasMoreFiles = fileEntries.length > maxFiles;
|
|
716
718
|
const expandHint = ui.expandHint(expanded, hasMoreFiles);
|
|
717
719
|
|
|
718
|
-
let text =
|
|
720
|
+
let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
|
|
719
721
|
|
|
720
722
|
const truncationReasons: string[] = [];
|
|
721
723
|
if (details?.matchLimitReached) {
|
|
@@ -748,12 +750,12 @@ export const grepToolRenderer = {
|
|
|
748
750
|
entry.count !== undefined
|
|
749
751
|
? ` ${uiTheme.fg("dim", `(${entry.count} match${entry.count !== 1 ? "es" : ""})`)}`
|
|
750
752
|
: "";
|
|
751
|
-
text += `\n
|
|
753
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry.path)}${countLabel}`;
|
|
752
754
|
}
|
|
753
755
|
|
|
754
756
|
if (hasMoreFiles) {
|
|
755
757
|
const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
756
|
-
text += `\n
|
|
758
|
+
text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
|
|
757
759
|
"muted",
|
|
758
760
|
ui.moreItems(fileEntries.length - maxFiles, "file"),
|
|
759
761
|
)}`;
|
|
@@ -761,7 +763,7 @@ export const grepToolRenderer = {
|
|
|
761
763
|
}
|
|
762
764
|
|
|
763
765
|
if (hasTruncation) {
|
|
764
|
-
text += `\n
|
|
766
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
|
|
765
767
|
}
|
|
766
768
|
|
|
767
769
|
return new Text(text, 0, 0);
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatBytes,
|
|
14
14
|
formatCount,
|
|
15
15
|
formatEmptyMessage,
|
|
16
|
+
formatErrorMessage,
|
|
16
17
|
formatExpandHint,
|
|
17
18
|
formatMeta,
|
|
18
19
|
formatMoreItems,
|
|
@@ -205,9 +206,10 @@ interface LsRenderArgs {
|
|
|
205
206
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
206
207
|
|
|
207
208
|
export const lsToolRenderer = {
|
|
209
|
+
inline: true,
|
|
208
210
|
renderCall(args: LsRenderArgs, uiTheme: Theme): Component {
|
|
209
211
|
const label = uiTheme.fg("toolTitle", uiTheme.bold("Ls"));
|
|
210
|
-
let text = `${label} ${uiTheme.fg("accent", args.path || ".")}`;
|
|
212
|
+
let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.path || ".")}`;
|
|
211
213
|
|
|
212
214
|
const meta: string[] = [];
|
|
213
215
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
@@ -217,18 +219,22 @@ export const lsToolRenderer = {
|
|
|
217
219
|
},
|
|
218
220
|
|
|
219
221
|
renderResult(
|
|
220
|
-
result: { content: Array<{ type: string; text?: string }>; details?: LsToolDetails },
|
|
222
|
+
result: { content: Array<{ type: string; text?: string }>; details?: LsToolDetails; isError?: boolean },
|
|
221
223
|
{ expanded }: RenderResultOptions,
|
|
222
224
|
uiTheme: Theme,
|
|
223
225
|
): Component {
|
|
224
226
|
const details = result.details;
|
|
225
227
|
const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
226
228
|
|
|
229
|
+
if (result.isError) {
|
|
230
|
+
return new Text(` ${formatErrorMessage(textContent, uiTheme)}`, 0, 0);
|
|
231
|
+
}
|
|
232
|
+
|
|
227
233
|
if (
|
|
228
234
|
(!textContent || textContent.trim() === "" || textContent.trim() === "(empty directory)") &&
|
|
229
235
|
(!details?.entries || details.entries.length === 0)
|
|
230
236
|
) {
|
|
231
|
-
return new Text(formatEmptyMessage("Empty directory", uiTheme)
|
|
237
|
+
return new Text(` ${formatEmptyMessage("Empty directory", uiTheme)}`, 0, 0);
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
let entries: string[] = details?.entries ? [...details.entries] : [];
|
|
@@ -238,7 +244,7 @@ export const lsToolRenderer = {
|
|
|
238
244
|
}
|
|
239
245
|
|
|
240
246
|
if (entries.length === 0) {
|
|
241
|
-
return new Text(formatEmptyMessage("Empty directory", uiTheme)
|
|
247
|
+
return new Text(` ${formatEmptyMessage("Empty directory", uiTheme)}`, 0, 0);
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
let dirCount = details?.dirCount;
|
|
@@ -267,7 +273,7 @@ export const lsToolRenderer = {
|
|
|
267
273
|
const hasMoreEntries = entries.length > maxEntries;
|
|
268
274
|
const expandHint = formatExpandHint(uiTheme, expanded, hasMoreEntries);
|
|
269
275
|
|
|
270
|
-
let text =
|
|
276
|
+
let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${expandHint}`;
|
|
271
277
|
|
|
272
278
|
const truncationReasons: string[] = [];
|
|
273
279
|
if (details?.entryLimitReached) {
|
|
@@ -290,19 +296,19 @@ export const lsToolRenderer = {
|
|
|
290
296
|
? uiTheme.fg("accent", uiTheme.icon.folder)
|
|
291
297
|
: uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
292
298
|
const entryColor = isDir ? "accent" : "toolOutput";
|
|
293
|
-
text += `\n
|
|
299
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg(entryColor, entry)}`;
|
|
294
300
|
}
|
|
295
301
|
|
|
296
302
|
if (hasMoreEntries) {
|
|
297
303
|
const moreEntriesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
298
|
-
text += `\n
|
|
304
|
+
text += `\n ${uiTheme.fg("dim", moreEntriesBranch)} ${uiTheme.fg(
|
|
299
305
|
"muted",
|
|
300
306
|
formatMoreItems(entries.length - maxEntries, "entry", uiTheme),
|
|
301
307
|
)}`;
|
|
302
308
|
}
|
|
303
309
|
|
|
304
310
|
if (hasTruncation) {
|
|
305
|
-
text += `\n
|
|
311
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
306
312
|
"warning",
|
|
307
313
|
`truncated: ${truncationReasons.join(", ")}`,
|
|
308
314
|
)}`;
|
|
@@ -331,12 +331,14 @@ async function waitForDiagnostics(
|
|
|
331
331
|
uri: string,
|
|
332
332
|
timeoutMs = 3000,
|
|
333
333
|
signal?: AbortSignal,
|
|
334
|
+
minVersion?: number,
|
|
334
335
|
): Promise<Diagnostic[]> {
|
|
335
336
|
const start = Date.now();
|
|
336
337
|
while (Date.now() - start < timeoutMs) {
|
|
337
338
|
signal?.throwIfAborted();
|
|
338
339
|
const diagnostics = client.diagnostics.get(uri);
|
|
339
|
-
|
|
340
|
+
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
341
|
+
if (diagnostics !== undefined && versionOk) return diagnostics;
|
|
340
342
|
await sleep(100);
|
|
341
343
|
}
|
|
342
344
|
return client.diagnostics.get(uri) ?? [];
|
|
@@ -462,12 +464,35 @@ export interface FileDiagnosticsResult {
|
|
|
462
464
|
formatter?: FileFormatResult;
|
|
463
465
|
}
|
|
464
466
|
|
|
467
|
+
/** Captured diagnostic versions per server (before sync) */
|
|
468
|
+
type DiagnosticVersions = Map<string, number>;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Capture current diagnostic versions for all LSP servers.
|
|
472
|
+
* Call this BEFORE syncing content to detect stale diagnostics later.
|
|
473
|
+
*/
|
|
474
|
+
async function captureDiagnosticVersions(
|
|
475
|
+
cwd: string,
|
|
476
|
+
servers: Array<[string, ServerConfig]>,
|
|
477
|
+
): Promise<DiagnosticVersions> {
|
|
478
|
+
const versions = new Map<string, number>();
|
|
479
|
+
await Promise.allSettled(
|
|
480
|
+
servers.map(async ([serverName, serverConfig]) => {
|
|
481
|
+
if (serverConfig.createClient) return;
|
|
482
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
483
|
+
versions.set(serverName, client.diagnosticsVersion);
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
return versions;
|
|
487
|
+
}
|
|
488
|
+
|
|
465
489
|
/**
|
|
466
490
|
* Get diagnostics for a file using LSP or custom linter client.
|
|
467
491
|
*
|
|
468
492
|
* @param absolutePath - Absolute path to the file
|
|
469
493
|
* @param cwd - Working directory for LSP config resolution
|
|
470
494
|
* @param servers - Servers to query diagnostics for
|
|
495
|
+
* @param minVersions - Minimum diagnostic versions per server (to detect stale results)
|
|
471
496
|
* @returns Diagnostic results or undefined if no servers
|
|
472
497
|
*/
|
|
473
498
|
async function getDiagnosticsForFile(
|
|
@@ -475,6 +500,7 @@ async function getDiagnosticsForFile(
|
|
|
475
500
|
cwd: string,
|
|
476
501
|
servers: Array<[string, ServerConfig]>,
|
|
477
502
|
signal?: AbortSignal,
|
|
503
|
+
minVersions?: DiagnosticVersions,
|
|
478
504
|
): Promise<FileDiagnosticsResult | undefined> {
|
|
479
505
|
if (servers.length === 0) {
|
|
480
506
|
return undefined;
|
|
@@ -499,8 +525,9 @@ async function getDiagnosticsForFile(
|
|
|
499
525
|
// Default: use LSP
|
|
500
526
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
501
527
|
signal?.throwIfAborted();
|
|
502
|
-
// Content already synced + didSave sent,
|
|
503
|
-
const
|
|
528
|
+
// Content already synced + didSave sent, wait for fresh diagnostics
|
|
529
|
+
const minVersion = minVersions?.get(serverName);
|
|
530
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, signal, minVersion);
|
|
504
531
|
return { serverName, diagnostics };
|
|
505
532
|
}),
|
|
506
533
|
);
|
|
@@ -675,6 +702,9 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
675
702
|
const getWritePromise = once(() => writeContent(finalContent));
|
|
676
703
|
const useCustomFormatter = enableFormat && customLinterServers.length > 0;
|
|
677
704
|
|
|
705
|
+
// Capture diagnostic versions BEFORE syncing to detect stale diagnostics
|
|
706
|
+
const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers) : undefined;
|
|
707
|
+
|
|
678
708
|
let formatter: FileFormatResult | undefined;
|
|
679
709
|
let diagnostics: FileDiagnosticsResult | undefined;
|
|
680
710
|
try {
|
|
@@ -710,9 +740,9 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
710
740
|
// 5. Notify saved to LSP servers
|
|
711
741
|
await notifyFileSaved(dst, cwd, lspServers, operationSignal);
|
|
712
742
|
|
|
713
|
-
// 6. Get diagnostics from all servers
|
|
743
|
+
// 6. Get diagnostics from all servers (wait for fresh results)
|
|
714
744
|
if (enableDiagnostics) {
|
|
715
|
-
diagnostics = await getDiagnosticsForFile(dst, cwd, servers, operationSignal);
|
|
745
|
+
diagnostics = await getDiagnosticsForFile(dst, cwd, servers, operationSignal, minVersions);
|
|
716
746
|
}
|
|
717
747
|
});
|
|
718
748
|
} catch {
|
|
@@ -829,8 +859,9 @@ export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema,
|
|
|
829
859
|
continue;
|
|
830
860
|
}
|
|
831
861
|
const client = await getOrCreateClient(serverConfig, session.cwd);
|
|
862
|
+
const minVersion = client.diagnosticsVersion;
|
|
832
863
|
await refreshFile(client, resolved);
|
|
833
|
-
const diagnostics = await waitForDiagnostics(client, uri);
|
|
864
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, minVersion);
|
|
834
865
|
allDiagnostics.push(...diagnostics);
|
|
835
866
|
} catch {
|
|
836
867
|
// Server failed, continue with others
|
|
@@ -1091,8 +1122,9 @@ export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema,
|
|
|
1091
1122
|
};
|
|
1092
1123
|
}
|
|
1093
1124
|
|
|
1125
|
+
const actionsMinVersion = client.diagnosticsVersion;
|
|
1094
1126
|
await refreshFile(client, targetFile);
|
|
1095
|
-
const diagnostics = await waitForDiagnostics(client, uri);
|
|
1127
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, actionsMinVersion);
|
|
1096
1128
|
const endLine = (end_line ?? line ?? 1) - 1;
|
|
1097
1129
|
const endCharacter = (end_character ?? column ?? 1) - 1;
|
|
1098
1130
|
const range = { start: position, end: { line: endLine, character: endCharacter } };
|
package/src/core/tools/read.ts
CHANGED
|
@@ -417,6 +417,7 @@ const readSchema = Type.Object({
|
|
|
417
417
|
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
418
418
|
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
419
419
|
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
420
|
+
lines: Type.Optional(Type.Boolean({ description: "Prepend line numbers to output (default: true)" })),
|
|
420
421
|
});
|
|
421
422
|
|
|
422
423
|
export interface ReadToolDetails {
|
|
@@ -436,7 +437,7 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
436
437
|
parameters: readSchema,
|
|
437
438
|
execute: async (
|
|
438
439
|
toolCallId: string,
|
|
439
|
-
{ path: readPath, offset, limit }: { path: string; offset?: number; limit?: number },
|
|
440
|
+
{ path: readPath, offset, limit, lines }: { path: string; offset?: number; limit?: number; lines?: boolean },
|
|
440
441
|
signal?: AbortSignal,
|
|
441
442
|
) => {
|
|
442
443
|
const absolutePath = resolveReadPath(readPath, session.cwd);
|
|
@@ -599,6 +600,20 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
599
600
|
// Apply truncation (respects both line and byte limits)
|
|
600
601
|
const truncation = truncateHead(selectedContent);
|
|
601
602
|
|
|
603
|
+
// Add line numbers if requested (default: true)
|
|
604
|
+
const shouldAddLineNumbers = lines !== false;
|
|
605
|
+
const prependLineNumbers = (text: string, startNum: number): string => {
|
|
606
|
+
const lines = text.split("\n");
|
|
607
|
+
const lastLineNum = startNum + lines.length - 1;
|
|
608
|
+
const padWidth = String(lastLineNum).length;
|
|
609
|
+
return lines
|
|
610
|
+
.map((line, i) => {
|
|
611
|
+
const lineNum = String(startNum + i).padStart(padWidth, " ");
|
|
612
|
+
return `${lineNum}\t${line}`;
|
|
613
|
+
})
|
|
614
|
+
.join("\n");
|
|
615
|
+
};
|
|
616
|
+
|
|
602
617
|
let outputText: string;
|
|
603
618
|
|
|
604
619
|
if (truncation.firstLineExceedsLimit) {
|
|
@@ -607,8 +622,8 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
607
622
|
const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
|
|
608
623
|
const shownSize = formatSize(snippet.bytes);
|
|
609
624
|
|
|
610
|
-
outputText = snippet.text;
|
|
611
|
-
if (
|
|
625
|
+
outputText = shouldAddLineNumbers ? prependLineNumbers(snippet.text, startLineDisplay) : snippet.text;
|
|
626
|
+
if (snippet.text.length > 0) {
|
|
612
627
|
outputText += `\n\n[Line ${startLineDisplay} is ${formatSize(
|
|
613
628
|
firstLineBytes,
|
|
614
629
|
)}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Showing first ${shownSize} of the line.]`;
|
|
@@ -623,7 +638,9 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
623
638
|
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
624
639
|
const nextOffset = endLineDisplay + 1;
|
|
625
640
|
|
|
626
|
-
outputText =
|
|
641
|
+
outputText = shouldAddLineNumbers
|
|
642
|
+
? prependLineNumbers(truncation.content, startLineDisplay)
|
|
643
|
+
: truncation.content;
|
|
627
644
|
|
|
628
645
|
if (truncation.truncatedBy === "lines") {
|
|
629
646
|
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
@@ -638,11 +655,15 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
638
655
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
639
656
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
640
657
|
|
|
641
|
-
outputText =
|
|
658
|
+
outputText = shouldAddLineNumbers
|
|
659
|
+
? prependLineNumbers(truncation.content, startLineDisplay)
|
|
660
|
+
: truncation.content;
|
|
642
661
|
outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
|
|
643
662
|
} else {
|
|
644
663
|
// No truncation, no user limit exceeded
|
|
645
|
-
outputText =
|
|
664
|
+
outputText = shouldAddLineNumbers
|
|
665
|
+
? prependLineNumbers(truncation.content, startLineDisplay)
|
|
666
|
+
: truncation.content;
|
|
646
667
|
}
|
|
647
668
|
|
|
648
669
|
content = [{ type: "text", text: outputText }];
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
MAX_AGENTS_IN_DESCRIPTION,
|
|
30
30
|
MAX_CONCURRENCY,
|
|
31
31
|
MAX_PARALLEL_TASKS,
|
|
32
|
+
type SingleResult,
|
|
32
33
|
type TaskToolDetails,
|
|
33
34
|
taskSchema,
|
|
34
35
|
} from "./types";
|
|
@@ -337,7 +338,7 @@ export async function createTaskTool(
|
|
|
337
338
|
emitProgress();
|
|
338
339
|
|
|
339
340
|
// Execute in parallel with concurrency limit
|
|
340
|
-
const results = await mapWithConcurrencyLimit(
|
|
341
|
+
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
|
341
342
|
tasksWithContext,
|
|
342
343
|
MAX_CONCURRENCY,
|
|
343
344
|
async (task, index) => {
|
|
@@ -371,6 +372,29 @@ export async function createTaskTool(
|
|
|
371
372
|
signal,
|
|
372
373
|
);
|
|
373
374
|
|
|
375
|
+
// Fill in skipped tasks (undefined entries from abort) with placeholder results
|
|
376
|
+
const results: SingleResult[] = partialResults.map((result, index) => {
|
|
377
|
+
if (result !== undefined) return result;
|
|
378
|
+
const task = tasksWithContext[index];
|
|
379
|
+
return {
|
|
380
|
+
index,
|
|
381
|
+
taskId: task.taskId,
|
|
382
|
+
agent: agentName,
|
|
383
|
+
agentSource: agent.source,
|
|
384
|
+
task: task.task,
|
|
385
|
+
description: task.description,
|
|
386
|
+
exitCode: 1,
|
|
387
|
+
output: "",
|
|
388
|
+
stderr: "Skipped (cancelled before start)",
|
|
389
|
+
truncated: false,
|
|
390
|
+
durationMs: 0,
|
|
391
|
+
tokens: 0,
|
|
392
|
+
modelOverride,
|
|
393
|
+
error: "Skipped",
|
|
394
|
+
aborted: true,
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
|
|
374
398
|
// Aggregate usage from executor results (already accumulated incrementally)
|
|
375
399
|
const aggregatedUsage = createUsageTotals();
|
|
376
400
|
let hasAggregatedUsage = false;
|
|
@@ -391,10 +415,11 @@ export async function createTaskTool(
|
|
|
391
415
|
|
|
392
416
|
// Build final output - match plugin format
|
|
393
417
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
418
|
+
const cancelledCount = results.filter((r) => r.aborted).length;
|
|
394
419
|
const totalDuration = Date.now() - startTime;
|
|
395
420
|
|
|
396
421
|
const summaries = results.map((r) => {
|
|
397
|
-
const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
|
|
422
|
+
const status = r.aborted ? "cancelled" : r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
|
|
398
423
|
const output = r.output.trim() || r.stderr.trim() || "(no output)";
|
|
399
424
|
const preview = output.split("\n").slice(0, 5).join("\n");
|
|
400
425
|
const meta = r.outputMeta
|
|
@@ -403,13 +428,14 @@ export async function createTaskTool(
|
|
|
403
428
|
return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
|
|
404
429
|
});
|
|
405
430
|
|
|
406
|
-
const outputIds = results.map((r) => r.taskId);
|
|
431
|
+
const outputIds = results.filter((r) => !r.aborted || r.output.trim()).map((r) => r.taskId);
|
|
407
432
|
const outputHint =
|
|
408
433
|
outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
|
|
409
434
|
const schemaNote = schemaOverridden
|
|
410
435
|
? `\n\nNote: Agent '${agentName}' has a fixed output schema; your 'output' parameter was ignored.\nRequired schema: ${JSON.stringify(agent.output)}`
|
|
411
436
|
: "";
|
|
412
|
-
const
|
|
437
|
+
const cancelledNote = aborted && cancelledCount > 0 ? ` (${cancelledCount} cancelled)` : "";
|
|
438
|
+
const summary = `${successCount}/${results.length} succeeded${cancelledNote} [${formatDuration(
|
|
413
439
|
totalDuration,
|
|
414
440
|
)}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}${schemaNote}`;
|
|
415
441
|
|
|
@@ -4,31 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
import { MAX_CONCURRENCY } from "./types";
|
|
6
6
|
|
|
7
|
+
/** Result of parallel execution */
|
|
8
|
+
export interface ParallelResult<R> {
|
|
9
|
+
/** Results array - undefined entries indicate tasks that were skipped due to abort */
|
|
10
|
+
results: (R | undefined)[];
|
|
11
|
+
/** Whether execution was aborted before all tasks completed */
|
|
12
|
+
aborted: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
/**
|
|
8
16
|
* Execute items with a concurrency limit using a worker pool pattern.
|
|
9
17
|
* Results are returned in the same order as input items.
|
|
10
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* On abort: returns partial results with `aborted: true`. Completed tasks are preserved,
|
|
20
|
+
* in-progress tasks will complete with their abort handling, skipped tasks are `undefined`.
|
|
21
|
+
*
|
|
22
|
+
* On error: fails fast - does not wait for other workers to complete.
|
|
11
23
|
*
|
|
12
24
|
* @param items - Items to process
|
|
13
25
|
* @param concurrency - Maximum concurrent operations
|
|
14
26
|
* @param fn - Async function to execute for each item
|
|
15
|
-
* @param signal - Optional abort signal to stop scheduling work
|
|
27
|
+
* @param signal - Optional abort signal to stop scheduling new work
|
|
16
28
|
*/
|
|
17
29
|
export async function mapWithConcurrencyLimit<T, R>(
|
|
18
30
|
items: T[],
|
|
19
31
|
concurrency: number,
|
|
20
32
|
fn: (item: T, index: number) => Promise<R>,
|
|
21
33
|
signal?: AbortSignal,
|
|
22
|
-
): Promise<R
|
|
34
|
+
): Promise<ParallelResult<R>> {
|
|
23
35
|
const limit = Math.max(1, Math.min(concurrency, items.length, MAX_CONCURRENCY));
|
|
24
|
-
const results: R[] = new Array(items.length);
|
|
36
|
+
const results: (R | undefined)[] = new Array(items.length);
|
|
25
37
|
let nextIndex = 0;
|
|
26
38
|
|
|
27
39
|
// Create internal abort controller to cancel workers on any rejection
|
|
28
40
|
const abortController = new AbortController();
|
|
29
41
|
const workerSignal = signal ? AbortSignal.any([signal, abortController.signal]) : abortController.signal;
|
|
30
42
|
|
|
31
|
-
// Promise that rejects on first error - used to fail fast
|
|
43
|
+
// Promise that rejects on first error - used to fail fast (not for abort)
|
|
32
44
|
let rejectFirst: (error: unknown) => void;
|
|
33
45
|
const firstErrorPromise = new Promise<never>((_, reject) => {
|
|
34
46
|
rejectFirst = reject;
|
|
@@ -36,15 +48,20 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
36
48
|
|
|
37
49
|
const worker = async (): Promise<void> => {
|
|
38
50
|
while (true) {
|
|
39
|
-
|
|
51
|
+
// On abort, stop picking up new work - but don't throw
|
|
52
|
+
if (workerSignal.aborted) return;
|
|
40
53
|
const index = nextIndex++;
|
|
41
54
|
if (index >= items.length) return;
|
|
42
55
|
try {
|
|
43
56
|
results[index] = await fn(items[index], index);
|
|
44
57
|
} catch (error) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
// On abort, the fn itself handles it and returns a result
|
|
59
|
+
// Only propagate non-abort errors
|
|
60
|
+
if (!workerSignal.aborted) {
|
|
61
|
+
abortController.abort();
|
|
62
|
+
rejectFirst(error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
48
65
|
}
|
|
49
66
|
}
|
|
50
67
|
};
|
|
@@ -53,13 +70,16 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
53
70
|
const workers = Array(limit)
|
|
54
71
|
.fill(null)
|
|
55
72
|
.map(() => worker());
|
|
56
|
-
await Promise.race([Promise.all(workers), firstErrorPromise]);
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
try {
|
|
75
|
+
await Promise.race([Promise.all(workers), firstErrorPromise]);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// If aborted, don't rethrow - return partial results
|
|
78
|
+
if (signal?.aborted) {
|
|
79
|
+
return { results, aborted: true };
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
return results;
|
|
84
|
+
return { results, aborted: signal?.aborted ?? false };
|
|
65
85
|
}
|
|
@@ -390,7 +390,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
390
390
|
} else if (this.toolName in toolRenderers) {
|
|
391
391
|
// Built-in tools with renderers
|
|
392
392
|
const renderer = toolRenderers[this.toolName];
|
|
393
|
-
|
|
393
|
+
// Inline renderers skip background styling
|
|
394
|
+
this.contentBox.setBgFn(renderer.inline ? undefined : bgFn);
|
|
394
395
|
this.contentBox.clear();
|
|
395
396
|
|
|
396
397
|
const shouldRenderCall = !this.result || !renderer.mergeCallAndResult;
|
|
@@ -345,9 +345,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
345
345
|
this.ui.start();
|
|
346
346
|
this.isInitialized = true;
|
|
347
347
|
|
|
348
|
-
// Set terminal title
|
|
349
|
-
|
|
350
|
-
this.ui.terminal.setTitle(`pi - ${cwdBasename}`);
|
|
348
|
+
// Set initial terminal title (will be updated when session title is generated)
|
|
349
|
+
this.ui.terminal.setTitle("π");
|
|
351
350
|
|
|
352
351
|
// Initialize hooks with TUI-based UI context
|
|
353
352
|
await this.initHooksAndCustomTools();
|
|
@@ -5,7 +5,7 @@ Usage:
|
|
|
5
5
|
- By default, it reads up to {{DEFAULT_MAX_LINES}} lines starting from the beginning of the file
|
|
6
6
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
7
7
|
- Any lines longer than 500 characters will be truncated
|
|
8
|
-
-
|
|
8
|
+
- By default, results include line numbers (cat -n format). Use `lines: false` to omit them
|
|
9
9
|
- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.
|
|
10
10
|
- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.
|
|
11
11
|
- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.
|