@oh-my-pi/pi-coding-agent 14.2.0 → 14.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/package.json +7 -7
- package/src/dap/session.ts +8 -2
- package/src/edit/index.ts +2 -1
- package/src/edit/modes/chunk.ts +6 -2
- package/src/lsp/client.ts +14 -5
- package/src/lsp/index.ts +53 -10
- package/src/lsp/types.ts +2 -0
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/tools/ast-edit.ts +37 -2
- package/src/tools/bash.ts +62 -12
- package/src/tools/find.ts +19 -26
- package/src/tools/grep.ts +94 -37
- package/src/tools/path-utils.ts +31 -3
- package/src/tools/resolve.ts +12 -3
- package/src/tools/vim.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.2.
|
|
4
|
+
"version": "14.2.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.2.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.2.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.2.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.2.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.2.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.2.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.2.1",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.2.1",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.2.1",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.2.1",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.2.1",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.2.1",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
package/src/dap/session.ts
CHANGED
|
@@ -1075,11 +1075,17 @@ export class DapSessionManager {
|
|
|
1075
1075
|
* MUST be called before the command that triggers the event.
|
|
1076
1076
|
*/
|
|
1077
1077
|
#prepareStopOutcome(session: DapSession, signal?: AbortSignal, timeoutMs: number = 30_000): Promise<unknown> {
|
|
1078
|
-
|
|
1078
|
+
const promises = [
|
|
1079
1079
|
session.client.waitForEvent("stopped", undefined, signal, timeoutMs),
|
|
1080
1080
|
session.client.waitForEvent("terminated", undefined, signal, timeoutMs),
|
|
1081
1081
|
session.client.waitForEvent("exited", undefined, signal, timeoutMs),
|
|
1082
|
-
]
|
|
1082
|
+
];
|
|
1083
|
+
// Promise.race leaves the losing waiters pending; their timeouts would
|
|
1084
|
+
// otherwise surface as unhandled rejections once they fire.
|
|
1085
|
+
for (const p of promises) {
|
|
1086
|
+
p.catch(() => {});
|
|
1087
|
+
}
|
|
1088
|
+
return Promise.race(promises);
|
|
1083
1089
|
}
|
|
1084
1090
|
|
|
1085
1091
|
/**
|
package/src/edit/index.ts
CHANGED
|
@@ -322,7 +322,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
322
322
|
chunkAutoIndent: resolveChunkAutoIndent(),
|
|
323
323
|
}),
|
|
324
324
|
parameters: chunkEditParamsSchema,
|
|
325
|
-
invalidParamsMessage:
|
|
325
|
+
invalidParamsMessage:
|
|
326
|
+
"Invalid edit parameters for chunk mode. Expected `{ edits: [{ path: 'file:selector', ...op }, ...] }` with at least one edit. Each edit needs a `path`; supply one of `write` (string content; pass an empty string or omit it together with `replace`/`insert` to delete the chunk), `replace: { old, new }`, or `insert: { loc, body }`.",
|
|
326
327
|
validate: isChunkParams,
|
|
327
328
|
execute: (
|
|
328
329
|
tool: EditTool,
|
package/src/edit/modes/chunk.ts
CHANGED
|
@@ -549,8 +549,12 @@ export function isChunkParams(params: unknown): params is ChunkParams {
|
|
|
549
549
|
return false;
|
|
550
550
|
}
|
|
551
551
|
const first = params.edits[0];
|
|
552
|
-
|
|
553
|
-
|
|
552
|
+
// Accept a bare `{ path }` entry: it is interpreted downstream as a chunk
|
|
553
|
+
// delete. Some providers strip `null` values from tool-call JSON, so a
|
|
554
|
+
// documented `{ path, write: null }` delete can arrive here as just
|
|
555
|
+
// `{ path }`. Rejecting that surfaced as a misleading
|
|
556
|
+
// "Invalid edit parameters for chunk mode." error.
|
|
557
|
+
return typeof first === "object" && first !== null && "path" in first;
|
|
554
558
|
}
|
|
555
559
|
|
|
556
560
|
/** Auto-correct indentation for content targeting a body region (`~`) when autoIndent is on.
|
package/src/lsp/client.ts
CHANGED
|
@@ -211,11 +211,19 @@ async function writeMessage(
|
|
|
211
211
|
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
212
212
|
): Promise<void> {
|
|
213
213
|
const content = JSON.stringify(message);
|
|
214
|
-
sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
|
215
|
-
sink.write(content);
|
|
214
|
+
sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`);
|
|
216
215
|
await sink.flush();
|
|
217
216
|
}
|
|
218
217
|
|
|
218
|
+
function queueWriteMessage(
|
|
219
|
+
client: LspClient,
|
|
220
|
+
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const write = client.writeQueue.catch(() => {}).then(() => writeMessage(client.proc.stdin, message));
|
|
223
|
+
client.writeQueue = write.catch(() => {});
|
|
224
|
+
return write;
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
// =============================================================================
|
|
220
228
|
// Message Reader
|
|
221
229
|
// =============================================================================
|
|
@@ -382,7 +390,7 @@ async function sendResponse(
|
|
|
382
390
|
};
|
|
383
391
|
|
|
384
392
|
try {
|
|
385
|
-
await
|
|
393
|
+
await queueWriteMessage(client, response);
|
|
386
394
|
} catch (err) {
|
|
387
395
|
logger.error("LSP failed to respond.", { method, error: String(err) });
|
|
388
396
|
}
|
|
@@ -461,6 +469,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
461
469
|
messageBuffer: new Uint8Array(0),
|
|
462
470
|
isReading: false,
|
|
463
471
|
lastActivity: Date.now(),
|
|
472
|
+
writeQueue: Promise.resolve(),
|
|
464
473
|
activeProgressTokens: new Set(),
|
|
465
474
|
projectLoaded,
|
|
466
475
|
resolveProjectLoaded,
|
|
@@ -848,7 +857,7 @@ export async function sendRequest(
|
|
|
848
857
|
});
|
|
849
858
|
|
|
850
859
|
// Write request
|
|
851
|
-
|
|
860
|
+
queueWriteMessage(client, request).catch(err => {
|
|
852
861
|
if (timeout) clearTimeout(timeout);
|
|
853
862
|
client.pendingRequests.delete(id);
|
|
854
863
|
cleanup();
|
|
@@ -868,7 +877,7 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
868
877
|
};
|
|
869
878
|
|
|
870
879
|
client.lastActivity = Date.now();
|
|
871
|
-
await
|
|
880
|
+
await queueWriteMessage(client, notification);
|
|
872
881
|
}
|
|
873
882
|
|
|
874
883
|
/**
|
package/src/lsp/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type LspParams,
|
|
41
41
|
type LspToolDetails,
|
|
42
42
|
lspSchema,
|
|
43
|
+
type Position,
|
|
43
44
|
type PublishedDiagnostics,
|
|
44
45
|
type ServerConfig,
|
|
45
46
|
type SymbolInformation,
|
|
@@ -262,6 +263,10 @@ function getLspServerForFile(config: LspConfig, filePath: string): [string, Serv
|
|
|
262
263
|
return servers.length > 0 ? servers[0] : null;
|
|
263
264
|
}
|
|
264
265
|
|
|
266
|
+
function isProjectAwareLspServer(serverConfig: ServerConfig): boolean {
|
|
267
|
+
return !serverConfig.createClient && !serverConfig.isLinter;
|
|
268
|
+
}
|
|
269
|
+
|
|
265
270
|
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
|
|
266
271
|
const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
|
|
267
272
|
const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
|
|
@@ -278,6 +283,21 @@ function limitDiagnosticMessages(messages: string[]): string[] {
|
|
|
278
283
|
const LOCATION_CONTEXT_LINES = 1;
|
|
279
284
|
const REFERENCE_CONTEXT_LIMIT = 50;
|
|
280
285
|
|
|
286
|
+
const REFERENCES_RETRY_COUNT = 2;
|
|
287
|
+
const REFERENCES_RETRY_DELAY_MS = 250;
|
|
288
|
+
|
|
289
|
+
function comparePosition(a: Position, b: Position): number {
|
|
290
|
+
return a.line === b.line ? a.character - b.character : a.line - b.line;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function rangeContainsPosition(range: Location["range"], position: Position): boolean {
|
|
294
|
+
return comparePosition(range.start, position) <= 0 && comparePosition(position, range.end) <= 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isOnlyQueriedDeclaration(locations: Location[], uri: string, position: Position): boolean {
|
|
298
|
+
return locations.length === 1 && locations[0]?.uri === uri && rangeContainsPosition(locations[0].range, position);
|
|
299
|
+
}
|
|
300
|
+
|
|
281
301
|
function normalizeLocationResult(result: Location | Location[] | LocationLink | LocationLink[] | null): Location[] {
|
|
282
302
|
if (!result) return [];
|
|
283
303
|
const raw = Array.isArray(result) ? result : [result];
|
|
@@ -560,6 +580,10 @@ async function getDiagnosticsForFile(
|
|
|
560
580
|
// Default: use LSP
|
|
561
581
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
562
582
|
throwIfAborted(signal);
|
|
583
|
+
if (isProjectAwareLspServer(serverConfig)) {
|
|
584
|
+
await waitForProjectLoaded(client, signal);
|
|
585
|
+
throwIfAborted(signal);
|
|
586
|
+
}
|
|
563
587
|
// Content already synced + didSave sent, wait for fresh diagnostics
|
|
564
588
|
const minVersion = minVersions?.get(serverName);
|
|
565
589
|
const expectedDocumentVersion = expectedDocumentVersions?.get(serverName);
|
|
@@ -1220,6 +1244,10 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1220
1244
|
continue;
|
|
1221
1245
|
}
|
|
1222
1246
|
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1247
|
+
if (isProjectAwareLspServer(serverConfig)) {
|
|
1248
|
+
await waitForProjectLoaded(client, signal);
|
|
1249
|
+
throwIfAborted(signal);
|
|
1250
|
+
}
|
|
1223
1251
|
const minVersion = client.diagnosticsVersion;
|
|
1224
1252
|
await refreshFile(client, resolved, signal);
|
|
1225
1253
|
const expectedDocumentVersion = client.openFiles.get(uri)?.version;
|
|
@@ -1512,16 +1540,31 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1512
1540
|
break;
|
|
1513
1541
|
}
|
|
1514
1542
|
case "references": {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
textDocument
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1543
|
+
let result: Location[] | null = null;
|
|
1544
|
+
for (let attempt = 0; attempt <= REFERENCES_RETRY_COUNT; attempt++) {
|
|
1545
|
+
result = (await sendRequest(
|
|
1546
|
+
client,
|
|
1547
|
+
"textDocument/references",
|
|
1548
|
+
{
|
|
1549
|
+
textDocument: { uri },
|
|
1550
|
+
position,
|
|
1551
|
+
context: { includeDeclaration: true },
|
|
1552
|
+
},
|
|
1553
|
+
signal,
|
|
1554
|
+
)) as Location[] | null;
|
|
1555
|
+
|
|
1556
|
+
const locations = result ?? [];
|
|
1557
|
+
if (!isProjectAwareLspServer(serverConfig) || attempt === REFERENCES_RETRY_COUNT) {
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
if (locations.length > 0 && !isOnlyQueriedDeclaration(locations, uri, position)) {
|
|
1561
|
+
break;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
await waitForProjectLoaded(client, signal);
|
|
1565
|
+
throwIfAborted(signal);
|
|
1566
|
+
await untilAborted(signal, () => Bun.sleep(REFERENCES_RETRY_DELAY_MS));
|
|
1567
|
+
}
|
|
1525
1568
|
|
|
1526
1569
|
if (!result || result.length === 0) {
|
|
1527
1570
|
output = "No references found";
|
package/src/lsp/types.ts
CHANGED
|
@@ -411,6 +411,8 @@ export interface LspClient {
|
|
|
411
411
|
isReading: boolean;
|
|
412
412
|
serverCapabilities?: LspServerCapabilities;
|
|
413
413
|
lastActivity: number;
|
|
414
|
+
/** Serializes outbound JSON-RPC writes to the server process. */
|
|
415
|
+
writeQueue: Promise<void>;
|
|
414
416
|
/** Tracks active work-done progress tokens from the server */
|
|
415
417
|
activeProgressTokens: Set<string | number>;
|
|
416
418
|
/** Resolves when the server's initial project loading completes (or after timeout) */
|
|
@@ -10,6 +10,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
10
10
|
- Metavariable names are UPPERCASE and must be the whole AST node — partial-text like `prefix$VAR`, `"hello $NAME"`, or `a $OP b` does NOT work; match the whole node instead
|
|
11
11
|
- When the same metavariable appears twice, both occurrences **MUST** match identical code (`$A == $A` matches `x == x`, not `x == y`)
|
|
12
12
|
- Patterns **MUST** parse as a single valid AST node for the target language. For method fragments or body snippets that don't parse standalone, wrap in valid context (e.g. `class $_ { … }`) and set `sel` to target the inner node — results return for the selected node, not the outer wrapper. If ast-grep reports `Multiple AST nodes are detected`, the pattern isn't a single parseable node — wrap and use `sel`
|
|
13
|
+
- C++ qualified calls used as expression statements need the statement semicolon in the pattern: use `ns::doThing($ARG);`, `$CALLEE($ARG)`, or wrap a statement snippet and select `call_expression`. Without `;`, tree-sitter-cpp may parse `ns::doThing($ARG)` as declaration-like syntax and return no matches
|
|
13
14
|
- For TS declarations/methods, tolerate unknown annotations: `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($ARG: $_): $_ { $$$BODY } }`
|
|
14
15
|
- Declaration forms are structurally distinct — top-level `function foo`, class method `foo()`, and `const foo = () => {}` are different AST shapes; search the right form before concluding absence
|
|
15
16
|
- Loosest existence check: `pat: ["executeBash"]` with `sel: "identifier"`
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -294,6 +294,23 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
294
294
|
failOnParseError: false,
|
|
295
295
|
});
|
|
296
296
|
const dedupedApplyParseErrors = dedupeParseErrors(applyResult.parseErrors);
|
|
297
|
+
const { record: recordAppliedFile, list: appliedFileList } = createFileRecorder();
|
|
298
|
+
const appliedFileReplacementCounts = new Map<string, number>();
|
|
299
|
+
for (const fileChange of applyResult.fileChanges) {
|
|
300
|
+
const relativePath = formatPath(fileChange.path);
|
|
301
|
+
recordAppliedFile(relativePath);
|
|
302
|
+
appliedFileReplacementCounts.set(
|
|
303
|
+
relativePath,
|
|
304
|
+
(appliedFileReplacementCounts.get(relativePath) ?? 0) + fileChange.count,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
for (const change of applyResult.changes) {
|
|
308
|
+
recordAppliedFile(formatPath(change.path));
|
|
309
|
+
}
|
|
310
|
+
const appliedFileReplacements = appliedFileList.map(filePath => ({
|
|
311
|
+
path: filePath,
|
|
312
|
+
count: appliedFileReplacementCounts.get(filePath) ?? 0,
|
|
313
|
+
}));
|
|
297
314
|
const appliedDetails: AstEditToolDetails = {
|
|
298
315
|
totalReplacements: applyResult.totalReplacements,
|
|
299
316
|
filesTouched: applyResult.filesTouched,
|
|
@@ -302,9 +319,27 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
302
319
|
limitReached: applyResult.limitReached,
|
|
303
320
|
...(dedupedApplyParseErrors.length > 0 ? { parseErrors: dedupedApplyParseErrors } : {}),
|
|
304
321
|
scopePath,
|
|
305
|
-
files:
|
|
306
|
-
fileReplacements,
|
|
322
|
+
files: appliedFileList,
|
|
323
|
+
fileReplacements: appliedFileReplacements,
|
|
307
324
|
};
|
|
325
|
+
const stalePreview =
|
|
326
|
+
applyResult.totalReplacements !== result.totalReplacements ||
|
|
327
|
+
applyResult.filesTouched !== result.filesTouched ||
|
|
328
|
+
fileList.some(
|
|
329
|
+
filePath => appliedFileReplacementCounts.get(filePath) !== fileReplacementCounts.get(filePath),
|
|
330
|
+
) ||
|
|
331
|
+
appliedFileList.some(
|
|
332
|
+
filePath => fileReplacementCounts.get(filePath) !== appliedFileReplacementCounts.get(filePath),
|
|
333
|
+
);
|
|
334
|
+
if (stalePreview) {
|
|
335
|
+
const text =
|
|
336
|
+
applyResult.totalReplacements === 0
|
|
337
|
+
? `Preview is stale / no longer matches; no replacements were applied. Preview expected ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}.`
|
|
338
|
+
: applyResult.totalReplacements < result.totalReplacements
|
|
339
|
+
? `Preview is stale / no longer matches; only ${applyResult.totalReplacements} of ${result.totalReplacements} replacements were applied in ${applyResult.filesTouched} of ${result.filesTouched} files.`
|
|
340
|
+
: `Preview is stale / no longer matches; applied ${applyResult.totalReplacements} replacements but preview expected ${result.totalReplacements}.`;
|
|
341
|
+
return { ...toolResult(appliedDetails).text(text).done(), isError: true };
|
|
342
|
+
}
|
|
308
343
|
const appliedReplacementPlural = applyResult.totalReplacements !== 1 ? "s" : "";
|
|
309
344
|
const appliedFilePlural = applyResult.filesTouched !== 1 ? "s" : "";
|
|
310
345
|
const text = `Applied ${applyResult.totalReplacements} replacement${appliedReplacementPlural} in ${applyResult.filesTouched} file${appliedFilePlural}.`;
|
package/src/tools/bash.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { resolveToCwd } from "./path-utils";
|
|
|
23
23
|
import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
24
24
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
25
25
|
import { toolResult } from "./tool-result";
|
|
26
|
-
import { clampTimeout } from "./tool-timeouts";
|
|
26
|
+
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
27
27
|
|
|
28
28
|
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
29
29
|
|
|
@@ -74,6 +74,7 @@ export interface BashToolInput {
|
|
|
74
74
|
export interface BashToolDetails {
|
|
75
75
|
meta?: OutputMeta;
|
|
76
76
|
timeoutSeconds?: number;
|
|
77
|
+
requestedTimeoutSeconds?: number;
|
|
77
78
|
async?: {
|
|
78
79
|
state: "running" | "completed" | "failed";
|
|
79
80
|
jobId: string;
|
|
@@ -219,6 +220,13 @@ function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | un
|
|
|
219
220
|
if (partialEnv && args.env) return { ...partialEnv, ...args.env };
|
|
220
221
|
return args.env ?? partialEnv;
|
|
221
222
|
}
|
|
223
|
+
|
|
224
|
+
function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutSec: number): string | undefined {
|
|
225
|
+
return requestedTimeoutSec !== effectiveTimeoutSec
|
|
226
|
+
? `Timeout clamped to ${effectiveTimeoutSec}s (requested ${requestedTimeoutSec}s; allowed range ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}s).`
|
|
227
|
+
: undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
222
230
|
/**
|
|
223
231
|
* Bash tool implementation.
|
|
224
232
|
*
|
|
@@ -289,9 +297,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
289
297
|
timeoutSec: number,
|
|
290
298
|
headLines?: number,
|
|
291
299
|
tailLines?: number,
|
|
300
|
+
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
292
301
|
): AgentToolResult<BashToolDetails> {
|
|
293
|
-
const
|
|
302
|
+
const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
|
|
303
|
+
const notices = options.notices?.filter(Boolean) ?? [];
|
|
304
|
+
if (notices.length > 0) outputLines.push("", ...notices);
|
|
305
|
+
const outputText = outputLines.join("\n");
|
|
294
306
|
const details: BashToolDetails = { timeoutSeconds: timeoutSec };
|
|
307
|
+
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
308
|
+
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
309
|
+
}
|
|
295
310
|
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
296
311
|
this.#buildResultText(result, timeoutSec, outputText);
|
|
297
312
|
return resultBuilder.done();
|
|
@@ -302,16 +317,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
302
317
|
label: string,
|
|
303
318
|
previewText: string,
|
|
304
319
|
timeoutSec: number,
|
|
320
|
+
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
305
321
|
): AgentToolResult<BashToolDetails> {
|
|
306
322
|
const details: BashToolDetails = {
|
|
307
323
|
timeoutSeconds: timeoutSec,
|
|
308
324
|
async: { state: "running", jobId, type: "bash" },
|
|
309
325
|
};
|
|
326
|
+
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
327
|
+
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
328
|
+
}
|
|
310
329
|
const lines: string[] = [];
|
|
311
330
|
const trimmedPreview = previewText.trimEnd();
|
|
312
331
|
if (trimmedPreview.length > 0) {
|
|
313
332
|
lines.push(trimmedPreview, "");
|
|
314
333
|
}
|
|
334
|
+
if (options.notices?.length) {
|
|
335
|
+
lines.push(...options.notices, "");
|
|
336
|
+
}
|
|
315
337
|
lines.push(`Background job ${jobId} started: ${label}`);
|
|
316
338
|
lines.push("Result will be delivered automatically when complete.");
|
|
317
339
|
lines.push(`Use \`poll\`, \`read jobs://${jobId}\`, or \`cancel_job\` if needed.`);
|
|
@@ -330,6 +352,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
330
352
|
commandCwd: string;
|
|
331
353
|
timeoutMs: number;
|
|
332
354
|
timeoutSec: number;
|
|
355
|
+
requestedTimeoutSec?: number;
|
|
356
|
+
timeoutClampNotice?: string;
|
|
333
357
|
headLines?: number;
|
|
334
358
|
tailLines?: number;
|
|
335
359
|
resolvedEnv?: Record<string, string>;
|
|
@@ -372,6 +396,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
372
396
|
options.timeoutSec,
|
|
373
397
|
options.headLines,
|
|
374
398
|
options.tailLines,
|
|
399
|
+
{
|
|
400
|
+
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
401
|
+
notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
402
|
+
},
|
|
375
403
|
);
|
|
376
404
|
const finalText = this.#extractTextResult(finalResult);
|
|
377
405
|
latestText = finalText;
|
|
@@ -531,8 +559,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
531
559
|
}
|
|
532
560
|
|
|
533
561
|
// Clamp to reasonable range: 1s - 3600s (1 hour)
|
|
534
|
-
const
|
|
562
|
+
const requestedTimeoutSec = rawTimeout;
|
|
563
|
+
const timeoutSec = clampTimeout("bash", requestedTimeoutSec);
|
|
535
564
|
const timeoutMs = timeoutSec * 1000;
|
|
565
|
+
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
536
566
|
|
|
537
567
|
if (asyncRequested) {
|
|
538
568
|
if (!this.session.asyncJobManager) {
|
|
@@ -543,13 +573,18 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
543
573
|
commandCwd,
|
|
544
574
|
timeoutMs,
|
|
545
575
|
timeoutSec,
|
|
576
|
+
requestedTimeoutSec,
|
|
577
|
+
timeoutClampNotice,
|
|
546
578
|
headLines,
|
|
547
579
|
tailLines,
|
|
548
580
|
resolvedEnv,
|
|
549
581
|
onUpdate,
|
|
550
582
|
startBackgrounded: true,
|
|
551
583
|
});
|
|
552
|
-
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec
|
|
584
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
585
|
+
requestedTimeoutSec,
|
|
586
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
587
|
+
});
|
|
553
588
|
}
|
|
554
589
|
|
|
555
590
|
if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
|
|
@@ -560,6 +595,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
560
595
|
commandCwd,
|
|
561
596
|
timeoutMs,
|
|
562
597
|
timeoutSec,
|
|
598
|
+
requestedTimeoutSec,
|
|
599
|
+
timeoutClampNotice,
|
|
563
600
|
headLines,
|
|
564
601
|
tailLines,
|
|
565
602
|
resolvedEnv,
|
|
@@ -567,7 +604,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
567
604
|
startBackgrounded,
|
|
568
605
|
});
|
|
569
606
|
if (startBackgrounded) {
|
|
570
|
-
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec
|
|
607
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
608
|
+
requestedTimeoutSec,
|
|
609
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
610
|
+
});
|
|
571
611
|
}
|
|
572
612
|
const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
|
|
573
613
|
if (waitResult.kind === "completed") {
|
|
@@ -584,7 +624,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
584
624
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
585
625
|
}
|
|
586
626
|
job.setBackgrounded(true);
|
|
587
|
-
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec
|
|
627
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
628
|
+
requestedTimeoutSec,
|
|
629
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
630
|
+
});
|
|
588
631
|
}
|
|
589
632
|
|
|
590
633
|
// Track output for streaming updates (tail only)
|
|
@@ -623,7 +666,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
623
666
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
624
667
|
throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
|
|
625
668
|
}
|
|
626
|
-
return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines
|
|
669
|
+
return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
|
|
670
|
+
requestedTimeoutSec,
|
|
671
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
672
|
+
});
|
|
627
673
|
}
|
|
628
674
|
}
|
|
629
675
|
|
|
@@ -700,12 +746,16 @@ export const bashToolRenderer = {
|
|
|
700
746
|
|
|
701
747
|
// Build truncation warning
|
|
702
748
|
const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
|
|
703
|
-
const
|
|
749
|
+
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
750
|
+
const timeoutLabel =
|
|
704
751
|
typeof timeoutSeconds === "number"
|
|
705
|
-
?
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
752
|
+
? requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
|
|
753
|
+
? `Timeout: ${timeoutSeconds}s (requested ${requestedTimeoutSeconds}s clamped)`
|
|
754
|
+
: `Timeout: ${timeoutSeconds}s`
|
|
755
|
+
: undefined;
|
|
756
|
+
const timeoutLine =
|
|
757
|
+
timeoutLabel !== undefined
|
|
758
|
+
? uiTheme.fg("dim", `${uiTheme.format.bracketLeft}${timeoutLabel}${uiTheme.format.bracketRight}`)
|
|
709
759
|
: undefined;
|
|
710
760
|
let warningLine: string | undefined;
|
|
711
761
|
if (details?.meta?.truncation && !showingFullOutput) {
|
package/src/tools/find.ts
CHANGED
|
@@ -129,6 +129,19 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
129
129
|
const includeHidden = hidden ?? true;
|
|
130
130
|
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
131
131
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
132
|
+
const formatMatchPath = (matchPath: string, fileType?: natives.FileType): string => {
|
|
133
|
+
const hadTrailingSlash = matchPath.endsWith("/") || matchPath.endsWith("\\");
|
|
134
|
+
const absolutePath = path.isAbsolute(matchPath) ? matchPath : path.resolve(searchPath, matchPath);
|
|
135
|
+
let relativePath = path.relative(this.session.cwd, absolutePath).replace(/\\/g, "/");
|
|
136
|
+
if (relativePath.length === 0) {
|
|
137
|
+
relativePath = ".";
|
|
138
|
+
}
|
|
139
|
+
if ((fileType === natives.FileType.Dir || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
140
|
+
relativePath += "/";
|
|
141
|
+
}
|
|
142
|
+
return relativePath;
|
|
143
|
+
};
|
|
144
|
+
|
|
132
145
|
const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
|
|
133
146
|
if (files.length === 0) {
|
|
134
147
|
const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
|
|
@@ -176,12 +189,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
176
189
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
177
190
|
limit: effectiveLimit,
|
|
178
191
|
});
|
|
179
|
-
const relativized = results.map(p =>
|
|
180
|
-
if (p.startsWith(searchPath)) {
|
|
181
|
-
return p.slice(searchPath.length + 1);
|
|
182
|
-
}
|
|
183
|
-
return path.relative(searchPath, p);
|
|
184
|
-
});
|
|
192
|
+
const relativized = results.map(p => formatMatchPath(p));
|
|
185
193
|
|
|
186
194
|
return buildResult(relativized);
|
|
187
195
|
}
|
|
@@ -225,12 +233,8 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
225
233
|
};
|
|
226
234
|
const onMatch = onUpdate
|
|
227
235
|
? (err: Error | null, match: natives.GlobMatch | null) => {
|
|
228
|
-
if (err || signal?.aborted || !match) return;
|
|
229
|
-
|
|
230
|
-
if (!relativePath) return;
|
|
231
|
-
if (match.fileType === natives.FileType.Dir && !relativePath.endsWith("/")) {
|
|
232
|
-
relativePath += "/";
|
|
233
|
-
}
|
|
236
|
+
if (err || signal?.aborted || !match?.path) return;
|
|
237
|
+
const relativePath = formatMatchPath(match.path, match.fileType);
|
|
234
238
|
onUpdateMatches.push(relativePath);
|
|
235
239
|
emitUpdate();
|
|
236
240
|
}
|
|
@@ -254,10 +258,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
254
258
|
);
|
|
255
259
|
|
|
256
260
|
try {
|
|
257
|
-
|
|
258
|
-
if (result.matches.length === 0 && !timeoutSignal.aborted) {
|
|
259
|
-
result = await doGlob(false);
|
|
260
|
-
}
|
|
261
|
+
const result = await doGlob(true);
|
|
261
262
|
// Sort by mtime descending (most recent first) in JS instead of native.
|
|
262
263
|
// This allows native glob to early-terminate at maxResults.
|
|
263
264
|
result.matches.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
|
|
@@ -276,19 +277,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
276
277
|
const relativized: string[] = [];
|
|
277
278
|
for (const match of matches) {
|
|
278
279
|
throwIfAborted(signal);
|
|
279
|
-
|
|
280
|
-
if (!line) {
|
|
280
|
+
if (!match.path) {
|
|
281
281
|
continue;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
let relativePath = line;
|
|
286
|
-
const isDirectory = match.fileType === natives.FileType.Dir;
|
|
287
|
-
if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
288
|
-
relativePath += "/";
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
relativized.push(relativePath);
|
|
284
|
+
relativized.push(formatMatchPath(match.path, match.fileType));
|
|
292
285
|
}
|
|
293
286
|
|
|
294
287
|
return buildResult(relativized);
|
package/src/tools/grep.ts
CHANGED
|
@@ -124,6 +124,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
124
124
|
};
|
|
125
125
|
let searchPath: string;
|
|
126
126
|
let scopePath: string;
|
|
127
|
+
let exactFilePaths: string[] | undefined;
|
|
127
128
|
let globFilter = glob ? normalizePathLikeInput(glob) || undefined : undefined;
|
|
128
129
|
const internalRouter = this.session.internalRouter;
|
|
129
130
|
if (searchDir?.trim()) {
|
|
@@ -142,7 +143,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
142
143
|
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
143
144
|
if (multiSearchPath) {
|
|
144
145
|
searchPath = multiSearchPath.basePath;
|
|
145
|
-
globFilter = multiSearchPath.glob;
|
|
146
|
+
globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
|
|
147
|
+
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
146
148
|
scopePath = multiSearchPath.scopePath;
|
|
147
149
|
} else {
|
|
148
150
|
const parsedPath = parseSearchPath(rawPath);
|
|
@@ -174,26 +176,61 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
174
176
|
// Run grep
|
|
175
177
|
let result: GrepResult;
|
|
176
178
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
179
|
+
if (exactFilePaths) {
|
|
180
|
+
const matches: GrepMatch[] = [];
|
|
181
|
+
let limitReached = false;
|
|
182
|
+
for (const exactFilePath of exactFilePaths) {
|
|
183
|
+
const fileResult = await grep(
|
|
184
|
+
{
|
|
185
|
+
pattern: normalizedPattern,
|
|
186
|
+
path: exactFilePath,
|
|
187
|
+
type: type?.trim() || undefined,
|
|
188
|
+
ignoreCase,
|
|
189
|
+
multiline: effectiveMultiline,
|
|
190
|
+
hidden: true,
|
|
191
|
+
gitignore: useGitignore,
|
|
192
|
+
cache: false,
|
|
193
|
+
contextBefore: normalizedContextBefore,
|
|
194
|
+
contextAfter: normalizedContextAfter,
|
|
195
|
+
maxColumns: DEFAULT_MAX_COLUMN,
|
|
196
|
+
mode: effectiveOutputMode,
|
|
197
|
+
},
|
|
198
|
+
undefined,
|
|
199
|
+
);
|
|
200
|
+
limitReached = limitReached || Boolean(fileResult.limitReached);
|
|
201
|
+
const relativeFilePath = path.relative(searchPath, exactFilePath).replace(/\\/g, "/");
|
|
202
|
+
matches.push(...fileResult.matches.map(match => ({ ...match, path: relativeFilePath })));
|
|
203
|
+
}
|
|
204
|
+
const offsetMatches = matches.slice(normalizedOffset);
|
|
205
|
+
result = {
|
|
206
|
+
matches: offsetMatches,
|
|
207
|
+
totalMatches: offsetMatches.length,
|
|
208
|
+
filesWithMatches: new Set(offsetMatches.map(match => match.path)).size,
|
|
209
|
+
filesSearched: exactFilePaths.length,
|
|
210
|
+
limitReached,
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
result = await grep(
|
|
214
|
+
{
|
|
215
|
+
pattern: normalizedPattern,
|
|
216
|
+
path: searchPath,
|
|
217
|
+
glob: globFilter,
|
|
218
|
+
type: type?.trim() || undefined,
|
|
219
|
+
ignoreCase,
|
|
220
|
+
multiline: effectiveMultiline,
|
|
221
|
+
hidden: true,
|
|
222
|
+
gitignore: useGitignore,
|
|
223
|
+
cache: false,
|
|
224
|
+
maxCount: internalLimit,
|
|
225
|
+
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
|
226
|
+
contextBefore: normalizedContextBefore,
|
|
227
|
+
contextAfter: normalizedContextAfter,
|
|
228
|
+
maxColumns: DEFAULT_MAX_COLUMN,
|
|
229
|
+
mode: effectiveOutputMode,
|
|
230
|
+
},
|
|
231
|
+
undefined,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
197
234
|
} catch (err) {
|
|
198
235
|
if (err instanceof Error && err.message.startsWith("regex parse error")) {
|
|
199
236
|
throw new ToolError(err.message);
|
|
@@ -258,6 +295,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
258
295
|
}
|
|
259
296
|
const outputLines: string[] = [];
|
|
260
297
|
let linesTruncated = false;
|
|
298
|
+
const hasContextLines = normalizedContextBefore > 0 || normalizedContextAfter > 0;
|
|
261
299
|
const matchesByFile = new Map<string, GrepMatch[]>();
|
|
262
300
|
for (const match of selectedMatches) {
|
|
263
301
|
const relativePath = formatPath(match.path);
|
|
@@ -289,10 +327,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
289
327
|
}
|
|
290
328
|
chunkMatchesByFile.get(match.displayPath)!.push(match);
|
|
291
329
|
}
|
|
292
|
-
const renderChunkedMatchesForFile = (relativePath: string) => {
|
|
330
|
+
const renderChunkedMatchesForFile = (relativePath: string): string[] => {
|
|
331
|
+
const renderedLines: string[] = [];
|
|
293
332
|
const fileMatches = chunkMatchesByFile.get(relativePath) ?? [];
|
|
294
333
|
if (fileMatches.length === 0) {
|
|
295
|
-
return;
|
|
334
|
+
return renderedLines;
|
|
296
335
|
}
|
|
297
336
|
const lineWidth = fileMatches[0]?.fileLineCount.toString().length ?? 1;
|
|
298
337
|
const matchesByChunk = new Map<string, ChunkedGrepMatch[]>();
|
|
@@ -310,13 +349,14 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
310
349
|
const anchor = chunkChecksum
|
|
311
350
|
? `${dashes}@${chunkPath}#${chunkChecksum}`
|
|
312
351
|
: `${dashes}@${chunkPath}`;
|
|
313
|
-
|
|
352
|
+
renderedLines.push(anchor);
|
|
314
353
|
}
|
|
315
354
|
for (const match of chunkMatches) {
|
|
316
|
-
|
|
355
|
+
renderedLines.push(` ${match.lineNumber.toString().padStart(lineWidth, " ")} |${match.line}`);
|
|
317
356
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
318
357
|
}
|
|
319
358
|
}
|
|
359
|
+
return renderedLines;
|
|
320
360
|
};
|
|
321
361
|
if (isDirectory) {
|
|
322
362
|
const filesByDirectory = new Map<string, string[]>();
|
|
@@ -330,26 +370,32 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
330
370
|
for (const [directory, directoryFiles] of filesByDirectory) {
|
|
331
371
|
if (directory === ".") {
|
|
332
372
|
for (const relativePath of directoryFiles) {
|
|
373
|
+
const renderedLines = renderChunkedMatchesForFile(relativePath);
|
|
374
|
+
if (renderedLines.length === 0) continue;
|
|
333
375
|
if (outputLines.length > 0) {
|
|
334
376
|
outputLines.push("");
|
|
335
377
|
}
|
|
336
378
|
outputLines.push(`# ${path.basename(relativePath)}`);
|
|
337
|
-
|
|
379
|
+
outputLines.push(...renderedLines);
|
|
338
380
|
}
|
|
339
381
|
continue;
|
|
340
382
|
}
|
|
383
|
+
const renderedFiles = directoryFiles
|
|
384
|
+
.map(relativePath => ({ relativePath, lines: renderChunkedMatchesForFile(relativePath) }))
|
|
385
|
+
.filter(file => file.lines.length > 0);
|
|
386
|
+
if (renderedFiles.length === 0) continue;
|
|
341
387
|
if (outputLines.length > 0) {
|
|
342
388
|
outputLines.push("");
|
|
343
389
|
}
|
|
344
390
|
outputLines.push(`# ${directory}`);
|
|
345
|
-
for (const relativePath of
|
|
391
|
+
for (const { relativePath, lines } of renderedFiles) {
|
|
346
392
|
outputLines.push(`## └─ ${path.basename(relativePath)}`);
|
|
347
|
-
|
|
393
|
+
outputLines.push(...lines);
|
|
348
394
|
}
|
|
349
395
|
}
|
|
350
396
|
} else {
|
|
351
397
|
for (const relativePath of fileList) {
|
|
352
|
-
renderChunkedMatchesForFile(relativePath);
|
|
398
|
+
outputLines.push(...renderChunkedMatchesForFile(relativePath));
|
|
353
399
|
}
|
|
354
400
|
}
|
|
355
401
|
const rawOutput = outputLines.join("\n");
|
|
@@ -380,7 +426,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
380
426
|
}
|
|
381
427
|
return resultBuilder.done();
|
|
382
428
|
}
|
|
383
|
-
const renderMatchesForFile = (relativePath: string) => {
|
|
429
|
+
const renderMatchesForFile = (relativePath: string): string[] => {
|
|
430
|
+
const renderedLines: string[] = [];
|
|
384
431
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
385
432
|
for (const match of fileMatches) {
|
|
386
433
|
const lineNumbers: number[] = [match.lineNumber];
|
|
@@ -399,20 +446,21 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
399
446
|
formatMatchLine(lineNumber, line, isMatch, { useHashLines, lineWidth });
|
|
400
447
|
if (match.contextBefore) {
|
|
401
448
|
for (const ctx of match.contextBefore) {
|
|
402
|
-
|
|
449
|
+
renderedLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
403
450
|
}
|
|
404
451
|
}
|
|
405
|
-
|
|
452
|
+
renderedLines.push(formatLine(match.lineNumber, match.line, true));
|
|
406
453
|
if (match.truncated) {
|
|
407
454
|
linesTruncated = true;
|
|
408
455
|
}
|
|
409
456
|
if (match.contextAfter) {
|
|
410
457
|
for (const ctx of match.contextAfter) {
|
|
411
|
-
|
|
458
|
+
renderedLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
412
459
|
}
|
|
413
460
|
}
|
|
414
461
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
415
462
|
}
|
|
463
|
+
return renderedLines;
|
|
416
464
|
};
|
|
417
465
|
if (isDirectory) {
|
|
418
466
|
const filesByDirectory = new Map<string, string[]>();
|
|
@@ -426,28 +474,37 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
426
474
|
for (const [directory, directoryFiles] of filesByDirectory) {
|
|
427
475
|
if (directory === ".") {
|
|
428
476
|
for (const relativePath of directoryFiles) {
|
|
477
|
+
const renderedLines = renderMatchesForFile(relativePath);
|
|
478
|
+
if (renderedLines.length === 0) continue;
|
|
429
479
|
if (outputLines.length > 0) {
|
|
430
480
|
outputLines.push("");
|
|
431
481
|
}
|
|
432
482
|
outputLines.push(`# ${path.basename(relativePath)}`);
|
|
433
|
-
|
|
483
|
+
outputLines.push(...renderedLines);
|
|
434
484
|
}
|
|
435
485
|
continue;
|
|
436
486
|
}
|
|
487
|
+
const renderedFiles = directoryFiles
|
|
488
|
+
.map(relativePath => ({ relativePath, lines: renderMatchesForFile(relativePath) }))
|
|
489
|
+
.filter(file => file.lines.length > 0);
|
|
490
|
+
if (renderedFiles.length === 0) continue;
|
|
437
491
|
if (outputLines.length > 0) {
|
|
438
492
|
outputLines.push("");
|
|
439
493
|
}
|
|
440
494
|
outputLines.push(`# ${directory}`);
|
|
441
|
-
for (const relativePath of
|
|
495
|
+
for (const { relativePath, lines } of renderedFiles) {
|
|
442
496
|
outputLines.push(`## └─ ${path.basename(relativePath)}`);
|
|
443
|
-
|
|
497
|
+
outputLines.push(...lines);
|
|
444
498
|
}
|
|
445
499
|
}
|
|
446
500
|
} else {
|
|
447
501
|
for (const relativePath of fileList) {
|
|
448
|
-
renderMatchesForFile(relativePath);
|
|
502
|
+
outputLines.push(...renderMatchesForFile(relativePath));
|
|
449
503
|
}
|
|
450
504
|
}
|
|
505
|
+
if (hasContextLines && outputLines.length > 0) {
|
|
506
|
+
outputLines.unshift("[grep] match lines use ':'; context lines use '-'.");
|
|
507
|
+
}
|
|
451
508
|
const rawOutput = outputLines.join("\n");
|
|
452
509
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
453
510
|
const output = truncation.content;
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -193,6 +193,7 @@ export interface ResolvedMultiSearchPath {
|
|
|
193
193
|
basePath: string;
|
|
194
194
|
glob?: string;
|
|
195
195
|
scopePath: string;
|
|
196
|
+
exactFilePaths?: string[];
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
export interface ResolvedMultiFindPattern {
|
|
@@ -438,6 +439,28 @@ async function areDelimitedTokensResolvable(
|
|
|
438
439
|
return true;
|
|
439
440
|
}
|
|
440
441
|
|
|
442
|
+
async function filterResolvableTokens(
|
|
443
|
+
tokens: string[],
|
|
444
|
+
cwd: string,
|
|
445
|
+
parseBasePath: (value: string) => string,
|
|
446
|
+
): Promise<string[]> {
|
|
447
|
+
const out: string[] = [];
|
|
448
|
+
for (const token of tokens) {
|
|
449
|
+
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) continue;
|
|
450
|
+
const basePath = parseBasePath(token);
|
|
451
|
+
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
452
|
+
if (await pathExists(resolvedBasePath)) {
|
|
453
|
+
out.push(token);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
457
|
+
if (await pathExists(resolvedExactPath)) {
|
|
458
|
+
out.push(token);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
441
464
|
async function splitDelimitedSearchInput(
|
|
442
465
|
rawInput: string,
|
|
443
466
|
cwd: string,
|
|
@@ -452,8 +475,11 @@ async function splitDelimitedSearchInput(
|
|
|
452
475
|
}
|
|
453
476
|
|
|
454
477
|
const commaSeparated = splitTopLevel(trimmed, "comma");
|
|
455
|
-
if (commaSeparated.length > 1
|
|
456
|
-
|
|
478
|
+
if (commaSeparated.length > 1) {
|
|
479
|
+
const resolvable = await filterResolvableTokens(commaSeparated, cwd, parseBasePath);
|
|
480
|
+
if (resolvable.length >= 1) {
|
|
481
|
+
return [...new Set(resolvable)];
|
|
482
|
+
}
|
|
457
483
|
}
|
|
458
484
|
|
|
459
485
|
const whitespaceSeparated = splitTopLevel(trimmed, "whitespace");
|
|
@@ -473,7 +499,7 @@ export async function resolveMultiSearchPath(
|
|
|
473
499
|
suffixGlob?: string,
|
|
474
500
|
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
475
501
|
const pathItems = await splitDelimitedSearchInput(rawPath, cwd, value => parseSearchPath(value).basePath);
|
|
476
|
-
if (!pathItems || pathItems.length
|
|
502
|
+
if (!pathItems || pathItems.length < 1) {
|
|
477
503
|
return undefined;
|
|
478
504
|
}
|
|
479
505
|
|
|
@@ -486,6 +512,7 @@ export async function resolveMultiSearchPath(
|
|
|
486
512
|
}),
|
|
487
513
|
);
|
|
488
514
|
|
|
515
|
+
const allExactFiles = !suffixGlob && parsedItems.every(item => !item.parsedPath.glob && item.stat.isFile());
|
|
489
516
|
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
490
517
|
const combinedPatterns = parsedItems.map(item => {
|
|
491
518
|
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
@@ -507,6 +534,7 @@ export async function resolveMultiSearchPath(
|
|
|
507
534
|
basePath: commonBasePath,
|
|
508
535
|
glob: buildBraceUnion(combinedPatterns),
|
|
509
536
|
scopePath: toScopeDisplay(pathItems),
|
|
537
|
+
exactFilePaths: allExactFiles ? parsedItems.map(item => item.absoluteBasePath) : undefined,
|
|
510
538
|
};
|
|
511
539
|
}
|
|
512
540
|
|
package/src/tools/resolve.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface ResolveToolDetails {
|
|
|
23
23
|
reason: string;
|
|
24
24
|
sourceToolName?: string;
|
|
25
25
|
label?: string;
|
|
26
|
+
sourceResultDetails?: unknown;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function resolveReasonPreview(reason?: string): string | undefined {
|
|
@@ -67,14 +68,21 @@ export function queueResolveHandler(
|
|
|
67
68
|
onRejected: () => "requeue",
|
|
68
69
|
onInvoked: async (input: unknown) => {
|
|
69
70
|
const params = input as ResolveParams;
|
|
71
|
+
const withResolveDetails = (result: AgentToolResult<unknown>): AgentToolResult<ResolveToolDetails> => ({
|
|
72
|
+
...result,
|
|
73
|
+
details: {
|
|
74
|
+
...detailsFor(params),
|
|
75
|
+
...(result.details != null ? { sourceResultDetails: result.details } : {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
70
78
|
if (params.action === "apply") {
|
|
71
79
|
const result = await options.apply(params.reason);
|
|
72
|
-
return
|
|
80
|
+
return withResolveDetails(result);
|
|
73
81
|
}
|
|
74
82
|
if (params.action === "discard" && options.reject != null) {
|
|
75
83
|
const result = await options.reject(params.reason);
|
|
76
84
|
if (result != null) {
|
|
77
|
-
return
|
|
85
|
+
return withResolveDetails(result);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
return {
|
|
@@ -154,9 +162,10 @@ export const resolveToolRenderer = {
|
|
|
154
162
|
const reason = replaceTabs(details?.reason?.trim() || "No reason provided");
|
|
155
163
|
const action = details?.action ?? "apply";
|
|
156
164
|
const isApply = action === "apply" && !result.isError;
|
|
165
|
+
const isFailedApply = action === "apply" && result.isError;
|
|
157
166
|
const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
|
|
158
167
|
const icon = isApply ? uiTheme.status.success : uiTheme.status.error;
|
|
159
|
-
const verb = isApply ? "Accept" : "Discard";
|
|
168
|
+
const verb = isApply ? "Accept" : isFailedApply ? "Failed" : "Discard";
|
|
160
169
|
const separator = ": ";
|
|
161
170
|
const separatorIndex = label.indexOf(separator);
|
|
162
171
|
const sourceLabel = separatorIndex > 0 ? label.slice(0, separatorIndex).trim() : undefined;
|
package/src/tools/vim.ts
CHANGED
|
@@ -639,7 +639,7 @@ export class VimTool implements AgentTool<typeof vimSchema, VimToolDetails> {
|
|
|
639
639
|
|
|
640
640
|
await executeVimSteps(engine, steps, {
|
|
641
641
|
pauseLastStep: params.pause === true,
|
|
642
|
-
onKbdStep: emitUpdate ? () => emitUpdate() : undefined,
|
|
642
|
+
onKbdStep: emitUpdate ? () => emitUpdate(true) : undefined,
|
|
643
643
|
onInsertStep: emitUpdate ? () => emitUpdate(true) : undefined,
|
|
644
644
|
});
|
|
645
645
|
|