@oh-my-pi/pi-coding-agent 14.2.0 → 14.3.0
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 +59 -0
- package/package.json +19 -19
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/config/settings-schema.ts +60 -1
- package/src/dap/session.ts +8 -2
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/index.ts +3 -1
- package/src/edit/modes/chunk.ts +133 -53
- package/src/edit/modes/hashline.ts +36 -11
- package/src/edit/renderer.ts +98 -133
- package/src/edit/streaming.ts +351 -0
- package/src/exec/bash-executor.ts +60 -5
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +22 -6
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +53 -10
- package/src/lsp/types.ts +2 -0
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +1 -34
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/tool-execution.ts +111 -101
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +25 -22
- package/src/prompts/tools/gh-pr-push.md +2 -1
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/read-chunk.md +46 -7
- package/src/prompts/tools/read.md +7 -4
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +36 -20
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +1 -0
- package/src/tools/ast-edit.ts +37 -2
- package/src/tools/bash.ts +75 -12
- package/src/tools/find.ts +19 -26
- package/src/tools/gh.ts +6 -16
- 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/sqlite-reader.ts +116 -3
- package/src/tools/vim.ts +1 -1
- package/src/web/search/providers/codex.ts +129 -6
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,13 +23,24 @@ 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
|
|
|
30
30
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
31
31
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
32
32
|
|
|
33
|
+
async function saveBashOriginalArtifact(session: ToolSession, originalText: string): Promise<string | undefined> {
|
|
34
|
+
try {
|
|
35
|
+
const alloc = await session.allocateOutputArtifact?.("bash-original");
|
|
36
|
+
if (!alloc?.path || !alloc.id) return undefined;
|
|
37
|
+
await Bun.write(alloc.path, originalText);
|
|
38
|
+
return alloc.id;
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
const bashSchemaBase = Type.Object({
|
|
34
45
|
command: Type.String({ description: "Command to execute" }),
|
|
35
46
|
env: Type.Optional(
|
|
@@ -74,6 +85,7 @@ export interface BashToolInput {
|
|
|
74
85
|
export interface BashToolDetails {
|
|
75
86
|
meta?: OutputMeta;
|
|
76
87
|
timeoutSeconds?: number;
|
|
88
|
+
requestedTimeoutSeconds?: number;
|
|
77
89
|
async?: {
|
|
78
90
|
state: "running" | "completed" | "failed";
|
|
79
91
|
jobId: string;
|
|
@@ -219,6 +231,13 @@ function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | un
|
|
|
219
231
|
if (partialEnv && args.env) return { ...partialEnv, ...args.env };
|
|
220
232
|
return args.env ?? partialEnv;
|
|
221
233
|
}
|
|
234
|
+
|
|
235
|
+
function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutSec: number): string | undefined {
|
|
236
|
+
return requestedTimeoutSec !== effectiveTimeoutSec
|
|
237
|
+
? `Timeout clamped to ${effectiveTimeoutSec}s (requested ${requestedTimeoutSec}s; allowed range ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}s).`
|
|
238
|
+
: undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
222
241
|
/**
|
|
223
242
|
* Bash tool implementation.
|
|
224
243
|
*
|
|
@@ -289,9 +308,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
289
308
|
timeoutSec: number,
|
|
290
309
|
headLines?: number,
|
|
291
310
|
tailLines?: number,
|
|
311
|
+
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
292
312
|
): AgentToolResult<BashToolDetails> {
|
|
293
|
-
const
|
|
313
|
+
const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
|
|
314
|
+
const notices = options.notices?.filter(Boolean) ?? [];
|
|
315
|
+
if (notices.length > 0) outputLines.push("", ...notices);
|
|
316
|
+
const outputText = outputLines.join("\n");
|
|
294
317
|
const details: BashToolDetails = { timeoutSeconds: timeoutSec };
|
|
318
|
+
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
319
|
+
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
320
|
+
}
|
|
295
321
|
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
296
322
|
this.#buildResultText(result, timeoutSec, outputText);
|
|
297
323
|
return resultBuilder.done();
|
|
@@ -302,16 +328,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
302
328
|
label: string,
|
|
303
329
|
previewText: string,
|
|
304
330
|
timeoutSec: number,
|
|
331
|
+
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
|
305
332
|
): AgentToolResult<BashToolDetails> {
|
|
306
333
|
const details: BashToolDetails = {
|
|
307
334
|
timeoutSeconds: timeoutSec,
|
|
308
335
|
async: { state: "running", jobId, type: "bash" },
|
|
309
336
|
};
|
|
337
|
+
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
338
|
+
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
339
|
+
}
|
|
310
340
|
const lines: string[] = [];
|
|
311
341
|
const trimmedPreview = previewText.trimEnd();
|
|
312
342
|
if (trimmedPreview.length > 0) {
|
|
313
343
|
lines.push(trimmedPreview, "");
|
|
314
344
|
}
|
|
345
|
+
if (options.notices?.length) {
|
|
346
|
+
lines.push(...options.notices, "");
|
|
347
|
+
}
|
|
315
348
|
lines.push(`Background job ${jobId} started: ${label}`);
|
|
316
349
|
lines.push("Result will be delivered automatically when complete.");
|
|
317
350
|
lines.push(`Use \`poll\`, \`read jobs://${jobId}\`, or \`cancel_job\` if needed.`);
|
|
@@ -330,6 +363,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
330
363
|
commandCwd: string;
|
|
331
364
|
timeoutMs: number;
|
|
332
365
|
timeoutSec: number;
|
|
366
|
+
requestedTimeoutSec?: number;
|
|
367
|
+
timeoutClampNotice?: string;
|
|
333
368
|
headLines?: number;
|
|
334
369
|
tailLines?: number;
|
|
335
370
|
resolvedEnv?: Record<string, string>;
|
|
@@ -366,12 +401,17 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
366
401
|
latestText = tailBuffer.text();
|
|
367
402
|
void reportProgress(latestText, { async: { state: "running", jobId, type: "bash" } });
|
|
368
403
|
},
|
|
404
|
+
onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
369
405
|
});
|
|
370
406
|
const finalResult = this.#buildCompletedResult(
|
|
371
407
|
result,
|
|
372
408
|
options.timeoutSec,
|
|
373
409
|
options.headLines,
|
|
374
410
|
options.tailLines,
|
|
411
|
+
{
|
|
412
|
+
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
413
|
+
notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
414
|
+
},
|
|
375
415
|
);
|
|
376
416
|
const finalText = this.#extractTextResult(finalResult);
|
|
377
417
|
latestText = finalText;
|
|
@@ -531,8 +571,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
531
571
|
}
|
|
532
572
|
|
|
533
573
|
// Clamp to reasonable range: 1s - 3600s (1 hour)
|
|
534
|
-
const
|
|
574
|
+
const requestedTimeoutSec = rawTimeout;
|
|
575
|
+
const timeoutSec = clampTimeout("bash", requestedTimeoutSec);
|
|
535
576
|
const timeoutMs = timeoutSec * 1000;
|
|
577
|
+
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
536
578
|
|
|
537
579
|
if (asyncRequested) {
|
|
538
580
|
if (!this.session.asyncJobManager) {
|
|
@@ -543,13 +585,18 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
543
585
|
commandCwd,
|
|
544
586
|
timeoutMs,
|
|
545
587
|
timeoutSec,
|
|
588
|
+
requestedTimeoutSec,
|
|
589
|
+
timeoutClampNotice,
|
|
546
590
|
headLines,
|
|
547
591
|
tailLines,
|
|
548
592
|
resolvedEnv,
|
|
549
593
|
onUpdate,
|
|
550
594
|
startBackgrounded: true,
|
|
551
595
|
});
|
|
552
|
-
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec
|
|
596
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
597
|
+
requestedTimeoutSec,
|
|
598
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
599
|
+
});
|
|
553
600
|
}
|
|
554
601
|
|
|
555
602
|
if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
|
|
@@ -560,6 +607,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
560
607
|
commandCwd,
|
|
561
608
|
timeoutMs,
|
|
562
609
|
timeoutSec,
|
|
610
|
+
requestedTimeoutSec,
|
|
611
|
+
timeoutClampNotice,
|
|
563
612
|
headLines,
|
|
564
613
|
tailLines,
|
|
565
614
|
resolvedEnv,
|
|
@@ -567,7 +616,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
567
616
|
startBackgrounded,
|
|
568
617
|
});
|
|
569
618
|
if (startBackgrounded) {
|
|
570
|
-
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec
|
|
619
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
620
|
+
requestedTimeoutSec,
|
|
621
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
622
|
+
});
|
|
571
623
|
}
|
|
572
624
|
const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
|
|
573
625
|
if (waitResult.kind === "completed") {
|
|
@@ -584,7 +636,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
584
636
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
585
637
|
}
|
|
586
638
|
job.setBackgrounded(true);
|
|
587
|
-
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec
|
|
639
|
+
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
640
|
+
requestedTimeoutSec,
|
|
641
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
642
|
+
});
|
|
588
643
|
}
|
|
589
644
|
|
|
590
645
|
// Track output for streaming updates (tail only)
|
|
@@ -613,6 +668,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
613
668
|
artifactPath,
|
|
614
669
|
artifactId,
|
|
615
670
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
671
|
+
onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
616
672
|
});
|
|
617
673
|
if (result.cancelled) {
|
|
618
674
|
if (signal?.aborted) {
|
|
@@ -623,7 +679,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
623
679
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
624
680
|
throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
|
|
625
681
|
}
|
|
626
|
-
return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines
|
|
682
|
+
return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
|
|
683
|
+
requestedTimeoutSec,
|
|
684
|
+
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
|
685
|
+
});
|
|
627
686
|
}
|
|
628
687
|
}
|
|
629
688
|
|
|
@@ -700,12 +759,16 @@ export const bashToolRenderer = {
|
|
|
700
759
|
|
|
701
760
|
// Build truncation warning
|
|
702
761
|
const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
|
|
703
|
-
const
|
|
762
|
+
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
763
|
+
const timeoutLabel =
|
|
704
764
|
typeof timeoutSeconds === "number"
|
|
705
|
-
?
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
765
|
+
? requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
|
|
766
|
+
? `Timeout: ${timeoutSeconds}s (requested ${requestedTimeoutSeconds}s clamped)`
|
|
767
|
+
: `Timeout: ${timeoutSeconds}s`
|
|
768
|
+
: undefined;
|
|
769
|
+
const timeoutLine =
|
|
770
|
+
timeoutLabel !== undefined
|
|
771
|
+
? uiTheme.fg("dim", `${uiTheme.format.bracketLeft}${timeoutLabel}${uiTheme.format.bracketRight}`)
|
|
709
772
|
: undefined;
|
|
710
773
|
let warningLine: string | undefined;
|
|
711
774
|
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/gh.ts
CHANGED
|
@@ -611,14 +611,6 @@ function toLocalBranchRef(value: string): string {
|
|
|
611
611
|
return `refs/heads/${value}`;
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
function stripHeadsRef(value: string | undefined): string | undefined {
|
|
615
|
-
if (!value) {
|
|
616
|
-
return undefined;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
614
|
async function requireGitRepoRoot(cwd: string, signal?: AbortSignal): Promise<string> {
|
|
623
615
|
const repoRoot = await git.repo.root(cwd, signal);
|
|
624
616
|
if (!repoRoot) {
|
|
@@ -763,10 +755,13 @@ async function resolvePrBranchPushTarget(
|
|
|
763
755
|
maintainerCanModify?: boolean;
|
|
764
756
|
isCrossRepository: boolean;
|
|
765
757
|
}> {
|
|
758
|
+
const headRef = await git.config.getBranch(repoRoot, localBranch, "ompPrHeadRef", signal);
|
|
759
|
+
if (!headRef) {
|
|
760
|
+
throw new ToolError(`branch ${localBranch} has no PR push metadata; check it out via gh_pr_checkout first`);
|
|
761
|
+
}
|
|
762
|
+
|
|
766
763
|
const pushRemote = await git.config.getBranch(repoRoot, localBranch, "pushRemote", signal);
|
|
767
764
|
const remote = await git.config.getBranch(repoRoot, localBranch, "remote", signal);
|
|
768
|
-
const mergeRef = await git.config.getBranch(repoRoot, localBranch, "merge", signal);
|
|
769
|
-
const headRef = await git.config.getBranch(repoRoot, localBranch, "ompPrHeadRef", signal);
|
|
770
765
|
const prUrl = await git.config.getBranch(repoRoot, localBranch, "ompPrUrl", signal);
|
|
771
766
|
const maintainerCanModifyValue = await git.config.getBranch(
|
|
772
767
|
repoRoot,
|
|
@@ -781,14 +776,9 @@ async function resolvePrBranchPushTarget(
|
|
|
781
776
|
throw new ToolError(`branch ${localBranch} has no configured push remote`);
|
|
782
777
|
}
|
|
783
778
|
|
|
784
|
-
const remoteBranch = headRef ?? stripHeadsRef(mergeRef);
|
|
785
|
-
if (!remoteBranch) {
|
|
786
|
-
throw new ToolError(`branch ${localBranch} has no tracked PR head ref`);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
779
|
return {
|
|
790
780
|
remoteName,
|
|
791
|
-
remoteBranch,
|
|
781
|
+
remoteBranch: headRef,
|
|
792
782
|
remoteUrl: await git.remote.url(repoRoot, remoteName, signal),
|
|
793
783
|
prUrl,
|
|
794
784
|
maintainerCanModify:
|
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;
|