@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.2
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 +53 -0
- package/dist/cli.js +3610 -3566
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/git.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli.ts +27 -5
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +11 -0
- package/src/cursor.ts +1 -1
- package/src/discovery/helpers.ts +8 -0
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/irc/bus.ts +5 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/tool-execution.ts +45 -12
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +16 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +163 -49
- package/src/session/session-history-format.ts +6 -2
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +16 -8
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +18 -36
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +9 -2
- package/src/tools/renderers.ts +11 -0
- package/src/tools/ssh.ts +17 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/git.ts +8 -0
package/src/tools/bash.ts
CHANGED
|
@@ -1151,22 +1151,30 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1151
1151
|
renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
1152
1152
|
const renderArgs = toBashRenderArgs(args, config);
|
|
1153
1153
|
const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
|
|
1154
|
-
const header =
|
|
1155
|
-
config.showHeader === false
|
|
1156
|
-
? undefined
|
|
1157
|
-
: renderStatusLine({ icon: "pending", title: config.resolveTitle(args, options) }, uiTheme);
|
|
1158
1154
|
const outputBlock = new CachedOutputBlock();
|
|
1159
1155
|
return markFramedBlockComponent({
|
|
1160
|
-
render: (width: number): readonly string[] =>
|
|
1161
|
-
|
|
1156
|
+
render: (width: number): readonly string[] => {
|
|
1157
|
+
const header =
|
|
1158
|
+
config.showHeader === false
|
|
1159
|
+
? undefined
|
|
1160
|
+
: renderStatusLine(
|
|
1161
|
+
{
|
|
1162
|
+
icon: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
1163
|
+
spinnerFrame: options.spinnerFrame,
|
|
1164
|
+
title: config.resolveTitle(args, options),
|
|
1165
|
+
},
|
|
1166
|
+
uiTheme,
|
|
1167
|
+
);
|
|
1168
|
+
return outputBlock.render(
|
|
1162
1169
|
{
|
|
1163
1170
|
header,
|
|
1164
|
-
state: "pending",
|
|
1171
|
+
state: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
1165
1172
|
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
|
|
1166
1173
|
width,
|
|
1167
1174
|
},
|
|
1168
1175
|
uiTheme,
|
|
1169
|
-
)
|
|
1176
|
+
);
|
|
1177
|
+
},
|
|
1170
1178
|
invalidate: () => {
|
|
1171
1179
|
outputBlock.invalidate();
|
|
1172
1180
|
},
|
|
@@ -187,6 +187,8 @@ function extractTextOutput(content: Array<{ type: string; text?: string }> | und
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
export const browserToolRenderer = {
|
|
190
|
+
animatedPendingPreview: (args: unknown) => (args as BrowserRenderArgs).action === "run",
|
|
191
|
+
animatedPartialResult: (args: unknown) => (args as BrowserRenderArgs).action === "run",
|
|
190
192
|
renderCall(args: BrowserRenderArgs, options: RenderResultOptions, theme: Theme): Component {
|
|
191
193
|
const action = args.action;
|
|
192
194
|
if (action === "run") {
|
package/src/tools/debug.ts
CHANGED
|
@@ -583,6 +583,7 @@ function summarizeDebugCall(args: DebugRenderArgs): string {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
export const debugToolRenderer = {
|
|
586
|
+
animatedPartialResult: true,
|
|
586
587
|
renderCall(args: DebugRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
|
|
587
588
|
const text = renderStatusLine({ icon: "pending", title: "Debug", description: summarizeDebugCall(args) }, theme);
|
|
588
589
|
return new Text(text, 0, 0);
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -489,6 +489,8 @@ function formatCellOutputLines(
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
export const evalToolRenderer = {
|
|
492
|
+
animatedPendingPreview: true,
|
|
493
|
+
animatedPartialResult: true,
|
|
492
494
|
renderCall(args: EvalRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
493
495
|
const cells = getRenderCells(args);
|
|
494
496
|
|
|
@@ -502,7 +504,7 @@ export const evalToolRenderer = {
|
|
|
502
504
|
|
|
503
505
|
return markFramedBlockComponent({
|
|
504
506
|
render: (width: number): readonly string[] => {
|
|
505
|
-
const key = `${options.expanded ? 1 : 0}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
507
|
+
const key = `${options.expanded ? 1 : 0}|${options.spinnerFrame ?? "-"}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
506
508
|
if (cached && cached.key === key && cached.width === width) {
|
|
507
509
|
return cached.result;
|
|
508
510
|
}
|
|
@@ -518,7 +520,8 @@ export const evalToolRenderer = {
|
|
|
518
520
|
index: i,
|
|
519
521
|
total: cells.length,
|
|
520
522
|
title: cell.title,
|
|
521
|
-
status: "pending",
|
|
523
|
+
status: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
524
|
+
spinnerFrame: options.spinnerFrame,
|
|
522
525
|
width,
|
|
523
526
|
// Viewport-sized tail window following the newest streamed code
|
|
524
527
|
// line; renderResult keeps the same cap so the cell never snaps
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -416,6 +416,9 @@ function renderWatchCall(args: GithubToolRenderArgs, options: RenderResultOption
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
export const githubToolRenderer = {
|
|
419
|
+
// No animatedPendingPreview: renderCall materializes plain Text components
|
|
420
|
+
// once per display rebuild (no render closure), so a live spinner interval
|
|
421
|
+
// would request 30fps repaints while the visible glyph stays frozen.
|
|
419
422
|
renderCall(args: GithubToolRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
420
423
|
const op = typeof args.op === "string" && args.op.trim().length > 0 ? args.op.trim() : undefined;
|
|
421
424
|
if (op === "run_watch") {
|
package/src/tools/glob.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
partitionExistingPaths,
|
|
27
27
|
resolveExplicitFindPatterns,
|
|
28
28
|
resolveToCwd,
|
|
29
|
+
toPathList,
|
|
29
30
|
} from "./path-utils";
|
|
30
31
|
import {
|
|
31
32
|
createCachedComponent,
|
|
@@ -38,11 +39,9 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
|
38
39
|
import { toolResult } from "./tool-result";
|
|
39
40
|
|
|
40
41
|
const findSchema = type({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.atLeastLength(1)
|
|
45
|
-
.describe("globs including search paths"),
|
|
42
|
+
"path?": type("string").describe(
|
|
43
|
+
'glob, file, or directory to search — a single path or a semicolon-delimited list ("src/**/*.ts; test/**/*.ts"). Omitted -> searches the workspace root (".")',
|
|
44
|
+
),
|
|
46
45
|
"hidden?": type("boolean").describe("include hidden files"),
|
|
47
46
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
48
47
|
"limit?": type("number").describe("max results"),
|
|
@@ -110,19 +109,19 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
110
109
|
readonly examples: readonly ToolExample<typeof findSchema.infer>[] = [
|
|
111
110
|
{
|
|
112
111
|
caption: "Glob files",
|
|
113
|
-
call: {
|
|
112
|
+
call: { path: "src/**/*.ts" },
|
|
114
113
|
},
|
|
115
114
|
{
|
|
116
|
-
caption: "Multiple targets —
|
|
117
|
-
call: {
|
|
115
|
+
caption: "Multiple targets — semicolon-delimited list",
|
|
116
|
+
call: { path: "src/**/*.ts; test/**/*.ts" },
|
|
118
117
|
},
|
|
119
118
|
{
|
|
120
119
|
caption: "Glob gitignored files like .env",
|
|
121
|
-
call: {
|
|
120
|
+
call: { path: ".env*", gitignore: false },
|
|
122
121
|
},
|
|
123
122
|
{
|
|
124
123
|
caption: "Glob directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
|
|
125
|
-
call: {
|
|
124
|
+
call: { path: "**/tests" },
|
|
126
125
|
},
|
|
127
126
|
];
|
|
128
127
|
readonly strict = true;
|
|
@@ -144,13 +143,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
144
143
|
onUpdate?: AgentToolUpdateCallback<GlobToolDetails>,
|
|
145
144
|
_context?: AgentToolContext,
|
|
146
145
|
): Promise<AgentToolResult<GlobToolDetails>> {
|
|
147
|
-
const {
|
|
146
|
+
const { path: pathInput, limit, hidden, gitignore } = params;
|
|
148
147
|
|
|
149
148
|
return untilAborted(signal, async () => {
|
|
150
149
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
150
|
+
const scopedPaths = toPathList(pathInput);
|
|
151
|
+
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
151
152
|
const rawPatternInputs = this.#customOps
|
|
152
|
-
?
|
|
153
|
-
: await expandDelimitedPathEntries(
|
|
153
|
+
? effectivePaths
|
|
154
|
+
: await expandDelimitedPathEntries(effectivePaths, this.session.cwd, { splitter: parseFindPattern });
|
|
154
155
|
const rawPatterns = rawPatternInputs.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
|
|
155
156
|
const internalRouter = InternalUrlRouter.instance();
|
|
156
157
|
const normalizedPatterns: string[] = [];
|
|
@@ -180,7 +181,7 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
180
181
|
normalizedPatterns.push(resource.sourcePath);
|
|
181
182
|
}
|
|
182
183
|
if (normalizedPatterns.some(pattern => pattern.length === 0)) {
|
|
183
|
-
throw new ToolError("`
|
|
184
|
+
throw new ToolError("`path` must contain non-empty globs or paths");
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
@@ -468,12 +469,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
468
469
|
// =============================================================================
|
|
469
470
|
|
|
470
471
|
interface GlobRenderArgs {
|
|
472
|
+
path?: string | string[];
|
|
473
|
+
/** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
|
|
471
474
|
paths?: string | string[];
|
|
472
475
|
limit?: number;
|
|
473
476
|
}
|
|
474
477
|
|
|
475
|
-
function formatGlobRenderPaths(
|
|
476
|
-
|
|
478
|
+
function formatGlobRenderPaths(args: GlobRenderArgs | undefined): string | undefined {
|
|
479
|
+
const list = toPathList(args?.path ?? args?.paths);
|
|
480
|
+
return list.length > 0 ? list.join(", ") : undefined;
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
@@ -493,7 +497,7 @@ export const globToolRenderer = {
|
|
|
493
497
|
icon: "pending",
|
|
494
498
|
title: "Glob",
|
|
495
499
|
titleColor: "toolTitle",
|
|
496
|
-
description: formatGlobRenderPaths(args
|
|
500
|
+
description: formatGlobRenderPaths(args) || "*",
|
|
497
501
|
meta,
|
|
498
502
|
},
|
|
499
503
|
uiTheme,
|
|
@@ -533,7 +537,7 @@ export const globToolRenderer = {
|
|
|
533
537
|
iconOverride: globStatusIcon(uiTheme),
|
|
534
538
|
title: "Glob",
|
|
535
539
|
titleColor: "toolTitle",
|
|
536
|
-
description: formatGlobRenderPaths(args
|
|
540
|
+
description: formatGlobRenderPaths(args),
|
|
537
541
|
meta: [formatCount("file", lines.length)],
|
|
538
542
|
},
|
|
539
543
|
uiTheme,
|
|
@@ -573,7 +577,7 @@ export const globToolRenderer = {
|
|
|
573
577
|
icon: "warning",
|
|
574
578
|
title: "Glob",
|
|
575
579
|
titleColor: "toolTitle",
|
|
576
|
-
description: formatGlobRenderPaths(args
|
|
580
|
+
description: formatGlobRenderPaths(args),
|
|
577
581
|
meta: ["0 files"],
|
|
578
582
|
},
|
|
579
583
|
uiTheme,
|
|
@@ -590,7 +594,7 @@ export const globToolRenderer = {
|
|
|
590
594
|
...(truncated ? { icon: "warning" as const } : { iconOverride: globStatusIcon(uiTheme) }),
|
|
591
595
|
title: "Glob",
|
|
592
596
|
titleColor: "toolTitle",
|
|
593
|
-
description: formatGlobRenderPaths(args
|
|
597
|
+
description: formatGlobRenderPaths(args),
|
|
594
598
|
meta,
|
|
595
599
|
},
|
|
596
600
|
uiTheme,
|
package/src/tools/grep.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
selectorLineRanges,
|
|
55
55
|
splitInternalUrlSel,
|
|
56
56
|
splitPathAndSel,
|
|
57
|
+
toPathList,
|
|
57
58
|
} from "./path-utils";
|
|
58
59
|
import {
|
|
59
60
|
createCachedComponent,
|
|
@@ -69,15 +70,13 @@ import { ToolError } from "./tool-errors";
|
|
|
69
70
|
import { toolResult } from "./tool-result";
|
|
70
71
|
|
|
71
72
|
const searchPathEntry = type("string").describe(
|
|
72
|
-
'file, directory, glob, internal URL, or "<file>:<lines>" selector (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
|
|
73
|
+
'file, directory, glob, internal URL, or "<file>:<lines>" selector to search (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
|
|
73
74
|
);
|
|
74
75
|
const searchSchema = type({
|
|
75
76
|
pattern: type("string").describe("regex pattern"),
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
|
|
80
|
-
),
|
|
77
|
+
"path?": searchPathEntry.describe(
|
|
78
|
+
'file, directory, glob, internal URL, or "<file>:<lines>" selector to search; pass several as a semicolon-delimited list ("src; tests"). Omitted -> searches the workspace root (".")',
|
|
79
|
+
),
|
|
81
80
|
"case?": type("boolean").describe("case-sensitive search"),
|
|
82
81
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
83
82
|
"skip?": type("number")
|
|
@@ -86,27 +85,6 @@ const searchSchema = type({
|
|
|
86
85
|
});
|
|
87
86
|
|
|
88
87
|
export type GrepToolInput = typeof searchSchema.infer;
|
|
89
|
-
function parseStringEncodedPathArray(input: string): string[] | null {
|
|
90
|
-
const trimmed = input.trim();
|
|
91
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
92
|
-
|
|
93
|
-
let parsed: unknown;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(trimmed);
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!Array.isArray(parsed) || parsed.some(entry => typeof entry !== "string")) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
return parsed;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function toPathList(input: string | string[] | undefined): string[] {
|
|
107
|
-
if (typeof input === "string") return parseStringEncodedPathArray(input) ?? [input];
|
|
108
|
-
return input ?? [];
|
|
109
|
-
}
|
|
110
88
|
|
|
111
89
|
/** Maximum number of distinct files surfaced in a single response. The
|
|
112
90
|
* agent paginates further pages via `skip`. */
|
|
@@ -184,7 +162,7 @@ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
|
|
|
184
162
|
// `:conflicts:1-1`) instead of silently widening the search or dropping a chunk.
|
|
185
163
|
if (!isReadSelectorGrammar(internalSplit.sel)) {
|
|
186
164
|
throw new ToolError(
|
|
187
|
-
`
|
|
165
|
+
`path entry "${entry}" has an invalid selector ":${internalSplit.sel}" — use ":N-M" line ranges, ":raw"/":conflicts", a range plus ":raw", or percent-encode a literal ":" as %3A`,
|
|
188
166
|
);
|
|
189
167
|
}
|
|
190
168
|
specs.push({ original: entry, clean: internalSplit.path, ranges: selectorLineRanges(internalSplit.sel) });
|
|
@@ -197,7 +175,7 @@ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
|
|
|
197
175
|
const parsed = parseLineRanges(split.sel);
|
|
198
176
|
if (!parsed) {
|
|
199
177
|
throw new ToolError(
|
|
200
|
-
`
|
|
178
|
+
`path entry "${entry}" — only line-range selectors like ":50-100" are supported (no ":raw"/":conflicts")`,
|
|
201
179
|
);
|
|
202
180
|
}
|
|
203
181
|
if (hasGlobPathChars(split.path)) {
|
|
@@ -858,8 +836,10 @@ type SearchParams = typeof searchSchema.infer;
|
|
|
858
836
|
|
|
859
837
|
export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails> {
|
|
860
838
|
readonly name = "grep";
|
|
861
|
-
readonly approval = (args: unknown): ToolTier =>
|
|
862
|
-
|
|
839
|
+
readonly approval = (args: unknown): ToolTier => {
|
|
840
|
+
const a = args as { path?: string | string[]; paths?: string | string[] };
|
|
841
|
+
return toPathList(a.path ?? a.paths).some(pathTargetsSsh) ? "exec" : "read";
|
|
842
|
+
};
|
|
863
843
|
readonly label = "Grep";
|
|
864
844
|
readonly loadMode = "discoverable";
|
|
865
845
|
readonly summary = "Grep file contents using ripgrep (fast regex search)";
|
|
@@ -882,7 +862,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
882
862
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
883
863
|
_toolContext?: AgentToolContext,
|
|
884
864
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
885
|
-
const { pattern,
|
|
865
|
+
const { pattern, path: rawPath, case: caseSensitive, gitignore, skip } = params;
|
|
886
866
|
|
|
887
867
|
return untilAborted(signal, async () => {
|
|
888
868
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -897,7 +877,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
897
877
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
898
878
|
throw new ToolError("Skip must be a non-negative number");
|
|
899
879
|
}
|
|
900
|
-
const scopedPaths = toPathList(
|
|
880
|
+
const scopedPaths = toPathList(rawPath);
|
|
901
881
|
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
902
882
|
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
903
883
|
const pathSpecs = parsePathSpecs(rawEntries);
|
|
@@ -972,7 +952,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
972
952
|
trackImmutableSources: true,
|
|
973
953
|
surfaceExactFilePaths: true,
|
|
974
954
|
fanOutFileTargets: true,
|
|
975
|
-
multipathStatHint: " (`
|
|
955
|
+
multipathStatHint: " (`path` list entries must each exist relative to cwd)",
|
|
976
956
|
settings: this.session.settings,
|
|
977
957
|
signal,
|
|
978
958
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
@@ -1047,7 +1027,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1047
1027
|
? ` (archive members were not searchable: ${archiveUnreadable.join(", ")})`
|
|
1048
1028
|
: "";
|
|
1049
1029
|
throw new ToolError(
|
|
1050
|
-
`Path not found: ${missingPaths.join(", ")};
|
|
1030
|
+
`Path not found: ${missingPaths.join(", ")}; list each target in the semicolon-delimited \`path\`${archiveHint}`,
|
|
1051
1031
|
);
|
|
1052
1032
|
}
|
|
1053
1033
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
@@ -1505,6 +1485,8 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1505
1485
|
|
|
1506
1486
|
interface GrepRenderArgs {
|
|
1507
1487
|
pattern: string;
|
|
1488
|
+
path?: string | string[];
|
|
1489
|
+
/** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
|
|
1508
1490
|
paths?: string | string[];
|
|
1509
1491
|
case?: boolean;
|
|
1510
1492
|
gitignore?: boolean;
|
|
@@ -1674,7 +1656,7 @@ function grepStatusIcon(uiTheme: Theme): string {
|
|
|
1674
1656
|
export const grepToolRenderer = {
|
|
1675
1657
|
inline: true,
|
|
1676
1658
|
renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
1677
|
-
const paths = toPathList(args.paths);
|
|
1659
|
+
const paths = toPathList(args.path ?? args.paths);
|
|
1678
1660
|
const meta: string[] = [];
|
|
1679
1661
|
if (paths.length) meta.push(`in ${paths.join(", ")}`);
|
|
1680
1662
|
if (args.case === false) meta.push("case:insensitive");
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -510,6 +510,39 @@ export function normalizePathLikeInput(input: string): string {
|
|
|
510
510
|
return stripOuterDoubleQuotes(input.trim());
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Parse a JSON-encoded array of path strings (e.g. `'["a.ts","b.ts"]'`).
|
|
515
|
+
* Returns `null` when the input is not a bracketed JSON string array, so the
|
|
516
|
+
* caller can fall back to treating the input as a single literal path.
|
|
517
|
+
*/
|
|
518
|
+
function parseStringEncodedPathArray(input: string): string[] | null {
|
|
519
|
+
const trimmed = input.trim();
|
|
520
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
521
|
+
|
|
522
|
+
let parsed: unknown;
|
|
523
|
+
try {
|
|
524
|
+
parsed = JSON.parse(trimmed);
|
|
525
|
+
} catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!Array.isArray(parsed) || parsed.some(entry => typeof entry !== "string")) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return parsed;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Normalize a path argument that may arrive as a single string, a JSON-encoded
|
|
537
|
+
* string array (`'["a.ts"]'`), or an actual array into a flat `string[]`.
|
|
538
|
+
* Delimited single strings (`"a.ts b.ts"`) are left for
|
|
539
|
+
* {@link expandDelimitedPathEntries} to split.
|
|
540
|
+
*/
|
|
541
|
+
export function toPathList(input: string | string[] | undefined): string[] {
|
|
542
|
+
if (typeof input === "string") return parseStringEncodedPathArray(input) ?? [input];
|
|
543
|
+
return input ?? [];
|
|
544
|
+
}
|
|
545
|
+
|
|
513
546
|
const GLOB_PATH_CHARS = ["*", "?", "[", "{"] as const;
|
|
514
547
|
|
|
515
548
|
export function hasGlobPathChars(filePath: string): boolean {
|
|
@@ -592,12 +625,21 @@ async function delimitedPathPartResolves(entry: string, cwd: string, splitter: P
|
|
|
592
625
|
}
|
|
593
626
|
}
|
|
594
627
|
|
|
628
|
+
/**
|
|
629
|
+
* How many split parts must resolve to an existing path for the split to win.
|
|
630
|
+
* Semicolon is the documented list delimiter, so it splits unconditionally
|
|
631
|
+
* (`"none"`) — an all-missing list must still fan out so multi-path missing
|
|
632
|
+
* semantics can name every entry. Comma is legacy recovery (`"some"`), and
|
|
633
|
+
* whitespace/mixed are aggressive heuristics gated on every part existing.
|
|
634
|
+
*/
|
|
635
|
+
type DelimitedResolveRequirement = "all" | "some" | "none";
|
|
636
|
+
|
|
595
637
|
async function tryDelimitedPathSplit(
|
|
596
638
|
entry: string,
|
|
597
639
|
cwd: string,
|
|
598
640
|
splitter: PathEntrySplitter,
|
|
599
641
|
mode: DelimitedPathSplitMode,
|
|
600
|
-
|
|
642
|
+
requirement: DelimitedResolveRequirement,
|
|
601
643
|
): Promise<string[] | null> {
|
|
602
644
|
const rawParts = splitTopLevelDelimitedPath(entry, mode);
|
|
603
645
|
if (rawParts.length < 2) return null;
|
|
@@ -606,9 +648,12 @@ async function tryDelimitedPathSplit(
|
|
|
606
648
|
if (parts.length === 0) return null;
|
|
607
649
|
if (parts.length < 2 && rawParts.length === parts.length) return null;
|
|
608
650
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
651
|
+
if (requirement !== "none") {
|
|
652
|
+
const resolved = await Promise.all(parts.map(part => delimitedPathPartResolves(part, cwd, splitter)));
|
|
653
|
+
const valid = requirement === "all" ? resolved.every(Boolean) : resolved.some(Boolean);
|
|
654
|
+
if (!valid) return null;
|
|
655
|
+
}
|
|
656
|
+
return parts;
|
|
612
657
|
}
|
|
613
658
|
|
|
614
659
|
/**
|
|
@@ -632,10 +677,10 @@ export async function splitDelimitedPathEntry(
|
|
|
632
677
|
}
|
|
633
678
|
|
|
634
679
|
return (
|
|
635
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon",
|
|
636
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "comma",
|
|
637
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "whitespace",
|
|
638
|
-
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "mixed",
|
|
680
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon", "none")) ??
|
|
681
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "comma", "some")) ??
|
|
682
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "whitespace", "all")) ??
|
|
683
|
+
(await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "mixed", "all"))
|
|
639
684
|
);
|
|
640
685
|
}
|
|
641
686
|
|
|
@@ -1094,11 +1139,11 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
1094
1139
|
const { rawPaths: inputs, cwd, internalUrlAction } = opts;
|
|
1095
1140
|
const normalizedRawPaths = inputs.map(normalizePathLikeInput);
|
|
1096
1141
|
if (normalizedRawPaths.some(rawPath => rawPath.length === 0)) {
|
|
1097
|
-
throw new ToolError("
|
|
1142
|
+
throw new ToolError("Search scope entries must be non-empty paths or globs");
|
|
1098
1143
|
}
|
|
1099
1144
|
const rawPaths = await expandDelimitedPathEntries(normalizedRawPaths, cwd);
|
|
1100
1145
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
1101
|
-
throw new ToolError("
|
|
1146
|
+
throw new ToolError("Search scope entries must be non-empty paths or globs");
|
|
1102
1147
|
}
|
|
1103
1148
|
// Strict external-URL schemes. `file://` is intentionally absent: it has
|
|
1104
1149
|
// local-path semantics (expandPath strips it downstream), so it flows through
|
package/src/tools/read.ts
CHANGED
|
@@ -316,7 +316,14 @@ function formatMergedBraceLine(
|
|
|
316
316
|
|
|
317
317
|
function countTextLines(text: string): number {
|
|
318
318
|
if (text.length === 0) return 0;
|
|
319
|
-
|
|
319
|
+
// Count newlines directly instead of allocating an array via split("\n").
|
|
320
|
+
// Called on every read of file content; the result is identical (N newlines
|
|
321
|
+
// ⇒ N+1 lines for non-empty text).
|
|
322
|
+
let lines = 1;
|
|
323
|
+
for (let i = 0; i < text.length; i++) {
|
|
324
|
+
if (text.charCodeAt(i) === 10) lines++;
|
|
325
|
+
}
|
|
326
|
+
return lines;
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
function contiguousLineNumbers(startLine: number, count: number): number[] {
|
|
@@ -1317,7 +1324,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1317
1324
|
startLine: startNum,
|
|
1318
1325
|
lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
|
|
1319
1326
|
};
|
|
1320
|
-
if (shouldAddHashLines) seenLines = contiguousLineNumbers(startNum,
|
|
1327
|
+
if (shouldAddHashLines) seenLines = contiguousLineNumbers(startNum, lineCount);
|
|
1321
1328
|
const formatted = formatTextWithMode(content, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
1322
1329
|
if (!hashContext || emittedHashlineHeader) return formatted;
|
|
1323
1330
|
emittedHashlineHeader = true;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -63,6 +63,17 @@ export type ToolRenderer = {
|
|
|
63
63
|
* with the final render and may commit like any settled stream.
|
|
64
64
|
*/
|
|
65
65
|
provisionalPartialResult?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the renderer's pending-call path visibly consumes
|
|
68
|
+
* `options.spinnerFrame`. Used to avoid scheduling repaint ticks for live
|
|
69
|
+
* partial calls whose bytes cannot change between spinner frames.
|
|
70
|
+
*/
|
|
71
|
+
animatedPendingPreview?: boolean | ((args: unknown) => boolean);
|
|
72
|
+
/**
|
|
73
|
+
* Whether the renderer's partial-result path visibly consumes
|
|
74
|
+
* `options.spinnerFrame`.
|
|
75
|
+
*/
|
|
76
|
+
animatedPartialResult?: boolean | ((args: unknown) => boolean);
|
|
66
77
|
};
|
|
67
78
|
|
|
68
79
|
export const toolRenderers: Record<string, ToolRenderer> = {
|
package/src/tools/ssh.ts
CHANGED
|
@@ -261,23 +261,33 @@ function formatSshCommandLines(command: string, uiTheme: Theme): string[] {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
export const sshToolRenderer = {
|
|
264
|
-
|
|
264
|
+
animatedPendingPreview: true,
|
|
265
|
+
renderCall(args: SshRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
265
266
|
const host = args.host || "…";
|
|
266
267
|
const command = args.command ?? "";
|
|
267
|
-
const header = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}]` }, uiTheme);
|
|
268
268
|
const cmdLines = formatSshCommandLines(command, uiTheme);
|
|
269
269
|
const outputBlock = new CachedOutputBlock();
|
|
270
270
|
return markFramedBlockComponent({
|
|
271
|
-
render: (width: number): readonly string[] =>
|
|
272
|
-
|
|
271
|
+
render: (width: number): readonly string[] => {
|
|
272
|
+
const header = renderStatusLine(
|
|
273
|
+
{
|
|
274
|
+
icon: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
275
|
+
spinnerFrame: options.spinnerFrame,
|
|
276
|
+
title: "SSH",
|
|
277
|
+
description: `[${host}]`,
|
|
278
|
+
},
|
|
279
|
+
uiTheme,
|
|
280
|
+
);
|
|
281
|
+
return outputBlock.render(
|
|
273
282
|
{
|
|
274
283
|
header,
|
|
275
|
-
state: "pending",
|
|
276
|
-
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded:
|
|
284
|
+
state: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
285
|
+
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
|
|
277
286
|
width,
|
|
278
287
|
},
|
|
279
288
|
uiTheme,
|
|
280
|
-
)
|
|
289
|
+
);
|
|
290
|
+
},
|
|
281
291
|
invalidate: () => {
|
|
282
292
|
outputBlock.invalidate();
|
|
283
293
|
},
|
package/src/tts/index.ts
CHANGED