@oh-my-pi/pi-coding-agent 15.10.2 → 15.10.3
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 +46 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +5 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/commit/agentic/agent.ts +1 -0
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/js/shared/prelude.txt +26 -10
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/lsp/index.ts +128 -52
- package/src/main.ts +54 -14
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/controllers/event-controller.ts +6 -1
- package/src/modes/controllers/input-controller.ts +10 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/tools/eval.md +3 -1
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/sdk.ts +59 -1
- package/src/session/agent-session.ts +5 -3
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +2 -2
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +1 -0
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +0 -7
- package/src/tools/eval-render.ts +4 -23
- package/src/tools/find.ts +148 -106
- package/src/tools/index.ts +32 -0
- package/src/tools/path-utils.ts +19 -22
- package/src/tools/read.ts +16 -8
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +3 -12
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
package/src/tools/find.ts
CHANGED
|
@@ -117,6 +117,12 @@ export interface FindToolOptions {
|
|
|
117
117
|
operations?: FindOperations;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
interface FindTarget {
|
|
121
|
+
searchPath: string;
|
|
122
|
+
globPattern: string;
|
|
123
|
+
hasGlob: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
121
127
|
readonly name = "find";
|
|
122
128
|
readonly approval = "read" as const;
|
|
@@ -193,15 +199,31 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
const multiPattern = await resolveExplicitFindPatterns(effectivePatterns, this.session.cwd);
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
const isSingle = !multiPattern;
|
|
203
|
+
const targets: FindTarget[] = multiPattern
|
|
204
|
+
? multiPattern.targets.map(target => ({
|
|
205
|
+
searchPath: resolveToCwd(target.basePath, this.session.cwd),
|
|
206
|
+
globPattern: target.globPattern,
|
|
207
|
+
hasGlob: target.hasGlob,
|
|
208
|
+
}))
|
|
209
|
+
: [
|
|
210
|
+
(() => {
|
|
211
|
+
const parsed = parseFindPattern(effectivePatterns[0] ?? ".");
|
|
212
|
+
return {
|
|
213
|
+
searchPath: resolveToCwd(parsed.basePath, this.session.cwd),
|
|
214
|
+
globPattern: parsed.globPattern,
|
|
215
|
+
hasGlob: parsed.hasGlob,
|
|
216
|
+
};
|
|
217
|
+
})(),
|
|
218
|
+
];
|
|
219
|
+
const scopePath = multiPattern?.scopePath ?? formatScopePath(targets[0].searchPath);
|
|
220
|
+
|
|
221
|
+
for (const target of targets) {
|
|
222
|
+
if (target.searchPath === "/") {
|
|
223
|
+
throw new ToolError("Searching from root directory '/' is not allowed");
|
|
224
|
+
}
|
|
204
225
|
}
|
|
226
|
+
|
|
205
227
|
const requestedLimit = limit ?? DEFAULT_LIMIT;
|
|
206
228
|
if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) {
|
|
207
229
|
throw new ToolError("Limit must be a positive number");
|
|
@@ -213,9 +235,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
213
235
|
const timeoutMs = Math.min(MAX_GLOB_TIMEOUT_MS, Math.max(MIN_GLOB_TIMEOUT_MS, requestedTimeoutMs));
|
|
214
236
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
215
237
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
216
|
-
const formatMatchPath = (matchPath: string, fileType?: natives.FileType): string => {
|
|
238
|
+
const formatMatchPath = (matchPath: string, base: string, fileType?: natives.FileType): string => {
|
|
217
239
|
const hadTrailingSlash = matchPath.endsWith("/") || matchPath.endsWith("\\");
|
|
218
|
-
const absolutePath = path.isAbsolute(matchPath) ? matchPath : path.resolve(
|
|
240
|
+
const absolutePath = path.isAbsolute(matchPath) ? matchPath : path.resolve(base, matchPath);
|
|
219
241
|
return formatPathRelativeToCwd(absolutePath, this.session.cwd, {
|
|
220
242
|
trailingSlash: fileType === natives.FileType.Dir || hadTrailingSlash,
|
|
221
243
|
});
|
|
@@ -276,45 +298,41 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
276
298
|
return resultBuilder.done();
|
|
277
299
|
};
|
|
278
300
|
|
|
301
|
+
// Walk each user path as its own root and run the globs concurrently.
|
|
302
|
+
// Collapsing multiple paths to a shared base would force the walker to
|
|
303
|
+
// traverse and stat every unrelated sibling under that ancestor; per-path
|
|
304
|
+
// roots keep each scan bounded to exactly what the user asked for.
|
|
279
305
|
if (this.#customOps?.glob) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
306
|
+
const customOps = this.#customOps;
|
|
307
|
+
const perTarget = await Promise.all(
|
|
308
|
+
targets.map(async target => {
|
|
309
|
+
if (!(await customOps.exists(target.searchPath))) {
|
|
310
|
+
if (isSingle) throw new ToolError(`Path not found: ${scopePath}`);
|
|
311
|
+
return [] as string[];
|
|
312
|
+
}
|
|
313
|
+
if (!target.hasGlob && customOps.stat) {
|
|
314
|
+
const stat = await customOps.stat(target.searchPath);
|
|
315
|
+
if (stat.isFile()) return [formatScopePath(target.searchPath)];
|
|
316
|
+
}
|
|
317
|
+
const results = await customOps.glob(target.globPattern, target.searchPath, {
|
|
318
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
319
|
+
limit: effectiveLimit,
|
|
320
|
+
});
|
|
321
|
+
return results.map(matchPath => formatMatchPath(matchPath, target.searchPath));
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
const seen = new Set<string>();
|
|
325
|
+
const merged: string[] = [];
|
|
326
|
+
for (const group of perTarget) {
|
|
327
|
+
for (const entry of group) {
|
|
328
|
+
if (seen.has(entry)) continue;
|
|
329
|
+
seen.add(entry);
|
|
330
|
+
merged.push(entry);
|
|
288
331
|
}
|
|
289
332
|
}
|
|
290
|
-
|
|
291
|
-
const results = await this.#customOps.glob(globPattern, searchPath, {
|
|
292
|
-
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
293
|
-
limit: effectiveLimit,
|
|
294
|
-
});
|
|
295
|
-
const relativized = results.map(p => formatMatchPath(p));
|
|
296
|
-
|
|
297
|
-
return buildResult(relativized);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
let searchStat: fs.Stats;
|
|
301
|
-
try {
|
|
302
|
-
searchStat = await fs.promises.stat(searchPath);
|
|
303
|
-
} catch (err) {
|
|
304
|
-
if (isEnoent(err)) {
|
|
305
|
-
throw new ToolError(`Path not found: ${scopePath}`);
|
|
306
|
-
}
|
|
307
|
-
throw err;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!hasGlob && searchStat.isFile()) {
|
|
311
|
-
return buildResult([scopePath]);
|
|
312
|
-
}
|
|
313
|
-
if (!searchStat.isDirectory()) {
|
|
314
|
-
throw new ToolError(`Path is not a directory: ${searchPath}`);
|
|
333
|
+
return buildResult(merged);
|
|
315
334
|
}
|
|
316
335
|
|
|
317
|
-
let matches: natives.GlobMatch[];
|
|
318
336
|
const onUpdateMatches: string[] = [];
|
|
319
337
|
const onUpdateMtimes: number[] = [];
|
|
320
338
|
const updateIntervalMs = 200;
|
|
@@ -335,87 +353,111 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
335
353
|
details,
|
|
336
354
|
});
|
|
337
355
|
};
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
pattern: globPattern,
|
|
351
|
-
path: searchPath,
|
|
352
|
-
hidden: includeHidden,
|
|
353
|
-
maxResults: effectiveLimit,
|
|
354
|
-
sortByMtime: true,
|
|
355
|
-
gitignore: useGitignore,
|
|
356
|
-
// parseFindPattern explicitly prepends "**/" when the user's
|
|
357
|
-
// pattern begins with a glob (so `*.ts` becomes `**/*.ts`).
|
|
358
|
-
// Anything that arrives here without "**/" was scoped to a
|
|
359
|
-
// single directory by the user (e.g. `dir/*`); disable the
|
|
360
|
-
// native auto-recursion so `dir/*` does not silently match
|
|
361
|
-
// `dir/sub/nested.ts`.
|
|
362
|
-
recursive: false,
|
|
363
|
-
signal: combinedSignal,
|
|
364
|
-
},
|
|
365
|
-
onMatch,
|
|
366
|
-
),
|
|
367
|
-
);
|
|
356
|
+
const streamed = new Set<string>();
|
|
357
|
+
const makeOnMatch =
|
|
358
|
+
(base: string) =>
|
|
359
|
+
(err: Error | null, match: natives.GlobMatch | null): void => {
|
|
360
|
+
if (err || combinedSignal.aborted || !match?.path) return;
|
|
361
|
+
const relativePath = formatMatchPath(match.path, base, match.fileType);
|
|
362
|
+
if (streamed.has(relativePath)) return;
|
|
363
|
+
streamed.add(relativePath);
|
|
364
|
+
onUpdateMatches.push(relativePath);
|
|
365
|
+
onUpdateMtimes.push(match.mtime ?? 0);
|
|
366
|
+
emitUpdate();
|
|
367
|
+
};
|
|
368
368
|
|
|
369
369
|
let timedOut = false;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
370
|
+
const runTarget = async (target: FindTarget): Promise<Array<{ path: string; mtime: number }>> => {
|
|
371
|
+
throwIfAborted(signal);
|
|
372
|
+
let stat: fs.Stats;
|
|
373
|
+
try {
|
|
374
|
+
stat = await fs.promises.stat(target.searchPath);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
if (isEnoent(err)) {
|
|
377
|
+
if (isSingle) throw new ToolError(`Path not found: ${scopePath}`);
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
if (!target.hasGlob && stat.isFile()) {
|
|
383
|
+
return [{ path: formatScopePath(target.searchPath), mtime: stat.mtimeMs }];
|
|
384
|
+
}
|
|
385
|
+
if (!stat.isDirectory()) {
|
|
386
|
+
if (isSingle) throw new ToolError(`Path is not a directory: ${target.searchPath}`);
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const result = await untilAborted(combinedSignal, () =>
|
|
391
|
+
natives.glob(
|
|
392
|
+
{
|
|
393
|
+
pattern: target.globPattern,
|
|
394
|
+
path: target.searchPath,
|
|
395
|
+
hidden: includeHidden,
|
|
396
|
+
maxResults: effectiveLimit,
|
|
397
|
+
sortByMtime: true,
|
|
398
|
+
gitignore: useGitignore,
|
|
399
|
+
// parseFindPattern explicitly prepends "**/" when the user's
|
|
400
|
+
// pattern begins with a glob (so `*.ts` becomes `**/*.ts`).
|
|
401
|
+
// Anything that arrives here without "**/" was scoped to a
|
|
402
|
+
// single directory by the user (e.g. `dir/*`); disable the
|
|
403
|
+
// native auto-recursion so `dir/*` does not silently match
|
|
404
|
+
// `dir/sub/nested.ts`.
|
|
405
|
+
recursive: false,
|
|
406
|
+
signal: combinedSignal,
|
|
407
|
+
},
|
|
408
|
+
makeOnMatch(target.searchPath),
|
|
409
|
+
),
|
|
410
|
+
);
|
|
411
|
+
throwIfAborted(signal);
|
|
412
|
+
const out: Array<{ path: string; mtime: number }> = [];
|
|
413
|
+
for (const match of result.matches) {
|
|
414
|
+
if (!match.path) continue;
|
|
415
|
+
out.push({
|
|
416
|
+
path: formatMatchPath(match.path, target.searchPath, match.fileType),
|
|
417
|
+
mtime: match.mtime ?? 0,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
423
|
+
if (timeoutSignal.aborted && !signal?.aborted) {
|
|
424
|
+
timedOut = true;
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
382
427
|
throw new ToolAbortError();
|
|
383
428
|
}
|
|
384
|
-
} else {
|
|
385
429
|
throw error;
|
|
386
430
|
}
|
|
387
|
-
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const perTarget = await Promise.all(targets.map(runTarget));
|
|
388
434
|
|
|
389
435
|
if (timedOut) {
|
|
390
436
|
// Drain the partial matches accumulated during streaming and return them
|
|
391
437
|
// instead of throwing — empty results after a multi-second wait force the
|
|
392
438
|
// caller to retry blind, which is the worst possible outcome.
|
|
393
|
-
const
|
|
394
|
-
const partial: Array<{ p: string; m: number }> = [];
|
|
395
|
-
for (let i = 0; i < onUpdateMatches.length; i++) {
|
|
396
|
-
const entry = onUpdateMatches[i];
|
|
397
|
-
if (seen.has(entry)) continue;
|
|
398
|
-
seen.add(entry);
|
|
399
|
-
partial.push({ p: entry, m: onUpdateMtimes[i] ?? 0 });
|
|
400
|
-
}
|
|
439
|
+
const partial = onUpdateMatches.map((entry, index) => ({ p: entry, m: onUpdateMtimes[index] ?? 0 }));
|
|
401
440
|
partial.sort((a, b) => b.m - a.m);
|
|
402
|
-
const sortedPaths = partial.map(
|
|
441
|
+
const sortedPaths = partial.map(entry => entry.p);
|
|
403
442
|
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
|
|
404
443
|
const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — increase timeout or narrow pattern`;
|
|
405
444
|
return buildResult(sortedPaths, { notice, forceTruncated: true });
|
|
406
445
|
}
|
|
407
446
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
447
|
+
// Merge per-target results: native glob already ranks each target's own
|
|
448
|
+
// matches by mtime and caps them at the limit, so a global mtime re-sort
|
|
449
|
+
// plus dedup yields the correct top-N across all roots.
|
|
450
|
+
const seen = new Set<string>();
|
|
451
|
+
const merged: Array<{ path: string; mtime: number }> = [];
|
|
452
|
+
for (const group of perTarget) {
|
|
453
|
+
for (const entry of group) {
|
|
454
|
+
if (seen.has(entry.path)) continue;
|
|
455
|
+
seen.add(entry.path);
|
|
456
|
+
merged.push(entry);
|
|
413
457
|
}
|
|
414
|
-
|
|
415
|
-
relativized.push(formatMatchPath(match.path, match.fileType));
|
|
416
458
|
}
|
|
417
|
-
|
|
418
|
-
return buildResult(
|
|
459
|
+
merged.sort((a, b) => b.mtime - a.mtime);
|
|
460
|
+
return buildResult(merged.map(entry => entry.path));
|
|
419
461
|
});
|
|
420
462
|
}
|
|
421
463
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -117,6 +117,29 @@ export type {
|
|
|
117
117
|
DiscoverableToolSource,
|
|
118
118
|
} from "../tool-discovery/tool-index";
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A late LSP diagnostics result that arrived after the edit/write tool already
|
|
122
|
+
* returned. Surfaced to the model and the transcript via
|
|
123
|
+
* {@link ToolSession.queueDeferredDiagnostics}, batched through the session
|
|
124
|
+
* yield queue like background-job results.
|
|
125
|
+
*/
|
|
126
|
+
export interface DeferredDiagnosticsEntry {
|
|
127
|
+
/** Absolute path the diagnostics belong to (the renderer shortens it). */
|
|
128
|
+
path: string;
|
|
129
|
+
/** One-line severity summary, e.g. "2 errors". */
|
|
130
|
+
summary: string;
|
|
131
|
+
/** Formatted, ready-to-display diagnostic lines. */
|
|
132
|
+
messages: string[];
|
|
133
|
+
/** True when any message is error severity. */
|
|
134
|
+
errored: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Evaluated at injection time (in the dispatcher's stale check): drop the entry
|
|
137
|
+
* when a newer mutation to the same file has superseded it, so the model never
|
|
138
|
+
* sees diagnostics for stale content.
|
|
139
|
+
*/
|
|
140
|
+
isStale(): boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
/** Session context for tool factories */
|
|
121
144
|
export interface ToolSession {
|
|
122
145
|
/** Current working directory */
|
|
@@ -284,6 +307,15 @@ export interface ToolSession {
|
|
|
284
307
|
|
|
285
308
|
/** Queue a hidden message to be injected at the next agent turn. */
|
|
286
309
|
queueDeferredMessage?(message: CustomMessage): void;
|
|
310
|
+
/** Queue late LSP diagnostics (arrived after an edit/write returned) to be shown
|
|
311
|
+
* in the transcript and delivered to the model at the next yield, like background
|
|
312
|
+
* job results. */
|
|
313
|
+
queueDeferredDiagnostics?(entry: DeferredDiagnosticsEntry): void;
|
|
314
|
+
/** Bump and return the session-global mutation counter for `path`. Edit/write
|
|
315
|
+
* tools call this on every file mutation so stale late-diagnostics can be dropped. */
|
|
316
|
+
bumpFileMutationVersion?(path: string): number;
|
|
317
|
+
/** Read the current session-global mutation counter for `path` (0 if never mutated). */
|
|
318
|
+
getFileMutationVersion?(path: string): number;
|
|
287
319
|
/** Get the active OpenTelemetry config so subagent dispatch can forward
|
|
288
320
|
* the parent's tracer/hooks with the subagent's own identity stamped. */
|
|
289
321
|
getTelemetry?: () => AgentTelemetryConfig | undefined;
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -572,9 +572,14 @@ export interface ResolvedMultiSearchPath {
|
|
|
572
572
|
targets?: ResolvedSearchTarget[];
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
-
export interface
|
|
575
|
+
export interface ResolvedFindTarget {
|
|
576
576
|
basePath: string;
|
|
577
577
|
globPattern: string;
|
|
578
|
+
hasGlob: boolean;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export interface ResolvedMultiFindPattern {
|
|
582
|
+
targets: ResolvedFindTarget[];
|
|
578
583
|
scopePath: string;
|
|
579
584
|
}
|
|
580
585
|
|
|
@@ -782,30 +787,22 @@ async function resolveFindPatternItems(
|
|
|
782
787
|
return undefined;
|
|
783
788
|
}
|
|
784
789
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (item.parsedPattern.hasGlob) {
|
|
798
|
-
return joinRelativeGlob(relativeBasePath, item.parsedPattern.globPattern);
|
|
799
|
-
}
|
|
800
|
-
if (item.stat.isDirectory()) {
|
|
801
|
-
return joinRelativeGlob(relativeBasePath, "**/*");
|
|
802
|
-
}
|
|
803
|
-
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
790
|
+
// Each path becomes its own walk root. Collapsing to a shared common ancestor
|
|
791
|
+
// (and filtering with a brace-union glob) would force the walker to traverse
|
|
792
|
+
// and stat every unrelated sibling under that ancestor — two paths under
|
|
793
|
+
// $HOME would scan all of $HOME. The find tool fans these targets out in
|
|
794
|
+
// parallel instead, so every scan stays bounded to exactly one requested path.
|
|
795
|
+
const targets = patternItems.map(item => {
|
|
796
|
+
const parsedPattern = parseFindPattern(item);
|
|
797
|
+
return {
|
|
798
|
+
basePath: resolveToCwd(parsedPattern.basePath, cwd),
|
|
799
|
+
globPattern: parsedPattern.globPattern,
|
|
800
|
+
hasGlob: parsedPattern.hasGlob,
|
|
801
|
+
};
|
|
804
802
|
});
|
|
805
803
|
|
|
806
804
|
return {
|
|
807
|
-
|
|
808
|
-
globPattern: buildBraceUnion(combinedPatterns) ?? "**/*",
|
|
805
|
+
targets,
|
|
809
806
|
scopePath: toScopeDisplay(patternItems, cwd),
|
|
810
807
|
};
|
|
811
808
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -575,6 +575,8 @@ export interface ReadToolDetails {
|
|
|
575
575
|
summary?: { lines: number; elidedSpans: number; elidedLines: number };
|
|
576
576
|
/** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
|
|
577
577
|
conflictCount?: number;
|
|
578
|
+
/** Paths recovered from a delimited read argument; used only by the TUI to render one call as multiple read rows. */
|
|
579
|
+
displayReadTargets?: string[];
|
|
578
580
|
}
|
|
579
581
|
|
|
580
582
|
type ReadParams = ReadToolInput;
|
|
@@ -670,7 +672,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
670
672
|
readonly loadMode = "essential";
|
|
671
673
|
readonly description: string;
|
|
672
674
|
readonly parameters = readSchema;
|
|
673
|
-
readonly nonAbortable = true;
|
|
674
675
|
readonly strict = true;
|
|
675
676
|
|
|
676
677
|
readonly #autoResizeImages: boolean;
|
|
@@ -704,6 +705,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
704
705
|
const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
|
|
705
706
|
const notes = [notice];
|
|
706
707
|
const content: Array<TextContent | ImageContent> = [];
|
|
708
|
+
const displayReadTargets: string[] = [];
|
|
707
709
|
let pendingText = notice;
|
|
708
710
|
const flushText = () => {
|
|
709
711
|
if (pendingText.length === 0) return;
|
|
@@ -717,6 +719,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
717
719
|
for (const part of parts) {
|
|
718
720
|
try {
|
|
719
721
|
const result = await this.execute("read-delimited-part", { path: part }, signal);
|
|
722
|
+
displayReadTargets.push(result.details?.suffixResolution?.to ?? part);
|
|
720
723
|
for (const block of result.content) {
|
|
721
724
|
if (block.type === "text") {
|
|
722
725
|
appendText(block.text);
|
|
@@ -730,12 +733,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
730
733
|
const message = error instanceof Error ? error.message : String(error);
|
|
731
734
|
const errorNote = `Could not read ${part}: ${message}`;
|
|
732
735
|
notes.push(errorNote);
|
|
736
|
+
displayReadTargets.push(part);
|
|
733
737
|
appendText(`[${errorNote}]`);
|
|
734
738
|
}
|
|
735
739
|
}
|
|
736
740
|
flushText();
|
|
737
741
|
|
|
738
|
-
return toolResult<ReadToolDetails>({ notes }).content(content).done();
|
|
742
|
+
return toolResult<ReadToolDetails>({ notes, displayReadTargets }).content(content).done();
|
|
739
743
|
}
|
|
740
744
|
|
|
741
745
|
async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
|
|
@@ -1648,7 +1652,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1648
1652
|
throw new ToolError("Multi-range line selectors are not supported for directory listings.");
|
|
1649
1653
|
}
|
|
1650
1654
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1651
|
-
|
|
1655
|
+
// Directory listings are deterministic and fast; never abort them mid-scan
|
|
1656
|
+
// (an interrupt would otherwise surface a misleading "Operation aborted").
|
|
1657
|
+
const dirResult = await this.#readDirectory(absolutePath, offset, limit, undefined);
|
|
1652
1658
|
if (suffixResolution) {
|
|
1653
1659
|
dirResult.details ??= {};
|
|
1654
1660
|
dirResult.details.suffixResolution = suffixResolution;
|
|
@@ -1815,7 +1821,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1815
1821
|
parsed,
|
|
1816
1822
|
displayMode,
|
|
1817
1823
|
suffixResolution,
|
|
1818
|
-
|
|
1824
|
+
undefined, // plain-file read: deterministic and fast, never abort mid-read
|
|
1819
1825
|
);
|
|
1820
1826
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1821
1827
|
content = [{ type: "text", text: multiResult.outputText }];
|
|
@@ -1874,7 +1880,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1874
1880
|
maxLinesToCollect,
|
|
1875
1881
|
maxBytesForRead,
|
|
1876
1882
|
selectedLineLimit,
|
|
1877
|
-
|
|
1883
|
+
undefined, // plain-file read: deterministic and fast, never abort mid-read
|
|
1878
1884
|
);
|
|
1879
1885
|
|
|
1880
1886
|
const {
|
|
@@ -2368,11 +2374,13 @@ function formatReadPathLink(
|
|
|
2368
2374
|
const plainDisplayPath = options.suffixResolution
|
|
2369
2375
|
? shortenPath(options.suffixResolution.to)
|
|
2370
2376
|
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
2371
|
-
const
|
|
2377
|
+
const absoluteInputPath = path.isAbsolute(basePath) ? basePath : undefined;
|
|
2378
|
+
const target =
|
|
2379
|
+
options.resolvedPath ?? options.sourcePath ?? tryResolveInternalUrlSync(basePath) ?? absoluteInputPath;
|
|
2372
2380
|
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
2373
2381
|
const linkOptions = line !== undefined ? { line } : undefined;
|
|
2374
|
-
const
|
|
2375
|
-
return `${
|
|
2382
|
+
const linkedPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
2383
|
+
return `${linkedPath}${selectorSuffix}`;
|
|
2376
2384
|
}
|
|
2377
2385
|
|
|
2378
2386
|
export const readToolRenderer = {
|
|
@@ -338,6 +338,7 @@ export function formatDiagnostics(
|
|
|
338
338
|
expanded: boolean,
|
|
339
339
|
theme: Theme,
|
|
340
340
|
getLangIcon: (filePath: string) => string,
|
|
341
|
+
options?: { title?: string },
|
|
341
342
|
): string {
|
|
342
343
|
if (diag.messages.length === 0) return "";
|
|
343
344
|
|
|
@@ -369,7 +370,8 @@ export function formatDiagnostics(
|
|
|
369
370
|
? theme.styledSymbol("status.error", "error")
|
|
370
371
|
: theme.styledSymbol("status.warning", "warning");
|
|
371
372
|
const summary = sanitizeDiagnosticDisplayText(diag.summary);
|
|
372
|
-
|
|
373
|
+
const summaryTag = summary ? ` ${theme.fg("dim", `(${summary})`)}` : "";
|
|
374
|
+
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", options?.title ?? "Diagnostics")}${summaryTag}`;
|
|
373
375
|
|
|
374
376
|
const maxDiags = expanded ? diag.messages.length : 5;
|
|
375
377
|
let diagsShown = 0;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -40,21 +40,6 @@ export type ToolRenderer = {
|
|
|
40
40
|
args?: unknown,
|
|
41
41
|
) => Component;
|
|
42
42
|
mergeCallAndResult?: boolean;
|
|
43
|
-
/**
|
|
44
|
-
* While a tool's preview is still streaming, report whether the
|
|
45
|
-
* currently-rendered preview is append-only: its rows only grow at the bottom
|
|
46
|
-
* and never re-layout above the bottom live region (a full, top-anchored
|
|
47
|
-
* content/code preview). The transcript reports this up to the TUI so a
|
|
48
|
-
* streaming preview taller than the viewport commits its scrolled-off head to
|
|
49
|
-
* native scrollback instead of dropping it (see
|
|
50
|
-
* `ToolExecutionComponent.isTranscriptBlockAppendOnly`). `result` is the
|
|
51
|
-
* latest (possibly partial) tool result, or `undefined` before one exists —
|
|
52
|
-
* `eval`/`bash` use its presence to defer committing until the streamed input
|
|
53
|
-
* (code) has finalized. Omit (or return `false`) for previews that slide a
|
|
54
|
-
* tail window or later collapse to a compact result — committing their head
|
|
55
|
-
* would strand stale rows.
|
|
56
|
-
*/
|
|
57
|
-
isStreamingPreviewAppendOnly?: (args: unknown, options: RenderResultOptions, result?: unknown) => boolean;
|
|
58
43
|
/** Render without background box, inline in the response flow */
|
|
59
44
|
inline?: boolean;
|
|
60
45
|
};
|
package/src/tools/ssh.ts
CHANGED
package/src/tools/todo.ts
CHANGED
package/src/tools/write.ts
CHANGED
|
@@ -277,7 +277,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
277
277
|
readonly label = "Write";
|
|
278
278
|
readonly description: string;
|
|
279
279
|
readonly parameters = writeSchema;
|
|
280
|
-
readonly nonAbortable = true;
|
|
281
280
|
readonly strict = true;
|
|
282
281
|
readonly concurrency = "exclusive";
|
|
283
282
|
readonly loadMode = "discoverable";
|
|
@@ -582,6 +581,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
582
581
|
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
583
582
|
const diagnostics = await this.#writethrough(absolutePath, newContent, signal, undefined, batchRequest);
|
|
584
583
|
invalidateFsScanAfterWrite(absolutePath);
|
|
584
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
585
585
|
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
586
586
|
this.session.conflictHistory?.invalidate(entry.id);
|
|
587
587
|
|
|
@@ -707,6 +707,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
707
707
|
|
|
708
708
|
const diagnostics = await this.#writethrough(absolutePath, text, signal, undefined, batchRequest);
|
|
709
709
|
invalidateFsScanAfterWrite(absolutePath);
|
|
710
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
710
711
|
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
711
712
|
for (const entry of fileEntries) history.invalidate(entry.id);
|
|
712
713
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, text);
|
|
@@ -886,6 +887,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
886
887
|
|
|
887
888
|
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
|
|
888
889
|
invalidateFsScanAfterWrite(absolutePath);
|
|
890
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
889
891
|
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
890
892
|
|
|
891
893
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
@@ -1039,17 +1041,6 @@ export const writeToolRenderer = {
|
|
|
1039
1041
|
});
|
|
1040
1042
|
},
|
|
1041
1043
|
|
|
1042
|
-
// Only the expanded (Ctrl+O) preview is append-only: it renders the whole
|
|
1043
|
-
// content top-anchored, so streamed chunks only append rows at the bottom.
|
|
1044
|
-
// The collapsed preview slides a bounded tail window (`formatStreamingContent`
|
|
1045
|
-
// with `WRITE_STREAMING_PREVIEW_LINES`) whose visible rows re-layout as the
|
|
1046
|
-
// window moves — not append-only, but it never overflows the viewport, so its
|
|
1047
|
-
// head is never at risk of being dropped regardless. `write` has no partial
|
|
1048
|
-
// result (content streams as args), so `result` is ignored here.
|
|
1049
|
-
isStreamingPreviewAppendOnly(args: WriteRenderArgs, options: RenderResultOptions, _result?: unknown): boolean {
|
|
1050
|
-
return Boolean(options?.expanded && args.content);
|
|
1051
|
-
},
|
|
1052
|
-
|
|
1053
1044
|
renderResult(
|
|
1054
1045
|
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
|
|
1055
1046
|
options: RenderResultOptions,
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -32,8 +32,6 @@ export interface CodeCellOptions {
|
|
|
32
32
|
*/
|
|
33
33
|
codeTail?: boolean;
|
|
34
34
|
expanded?: boolean;
|
|
35
|
-
/** Animate the cell border with a sweeping segment while pending/running. */
|
|
36
|
-
animate?: boolean;
|
|
37
35
|
width: number;
|
|
38
36
|
}
|
|
39
37
|
|
|
@@ -147,10 +145,7 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
147
145
|
sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
return renderOutputBlock(
|
|
151
|
-
{ header: title, headerMeta: meta, state, sections, width, animate: options.animate },
|
|
152
|
-
theme,
|
|
153
|
-
);
|
|
148
|
+
return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
export interface MarkdownCellOptions {
|