@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.13
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 +65 -0
- package/dist/cli.js +3176 -3087
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/keybindings.d.ts +9 -4
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/mnemopi/state.d.ts +7 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +1 -0
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/renderers.d.ts +12 -5
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tools/write.d.ts +1 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/keybindings.ts +62 -10
- package/src/config/model-registry.ts +94 -20
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +59 -21
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +13 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/mnemopi/state.ts +19 -5
- package/src/modes/acp/acp-agent.ts +69 -8
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/read-tool-group.ts +5 -1
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/components/tool-execution.ts +28 -24
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -35
- package/src/modes/controllers/input-controller.ts +23 -57
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +97 -12
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/rpc/rpc-client.ts +42 -13
- package/src/modes/rpc/rpc-mode.ts +21 -19
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/agents/plan.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +2 -1
- package/src/prompts/tools/memory-edit.md +2 -0
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +136 -19
- package/src/system-prompt.ts +3 -2
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +105 -21
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +11 -0
- package/src/tools/memory-edit.ts +3 -1
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +135 -57
- package/src/tools/renderers.ts +13 -5
- package/src/tools/ssh.ts +10 -3
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +26 -0
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
|
@@ -140,6 +140,30 @@ function unquoteToken(token: string): string {
|
|
|
140
140
|
return token;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function isInsideShellQuote(command: string, index: number): boolean {
|
|
144
|
+
let quote: "'" | '"' | undefined;
|
|
145
|
+
for (let i = 0; i < index; i++) {
|
|
146
|
+
const char = command[i];
|
|
147
|
+
if (char === "\\" && quote !== "'") {
|
|
148
|
+
i++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (char === "'" && quote !== '"') {
|
|
152
|
+
quote = quote === "'" ? undefined : "'";
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (char === '"' && quote !== "'") {
|
|
156
|
+
quote = quote === '"' ? undefined : '"';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return quote !== undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isEmbeddedInQuotedText(command: string, token: string, index: number): boolean {
|
|
163
|
+
if (token.startsWith("'") || token.startsWith('"')) return false;
|
|
164
|
+
return isInsideShellQuote(command, index);
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
/** Shell-escape a path using single quotes. */
|
|
144
168
|
function shellEscape(p: string): string {
|
|
145
169
|
return `'${p.replace(/'/g, "'\\''")}'`;
|
|
@@ -216,6 +240,7 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
|
|
|
216
240
|
|
|
217
241
|
/**
|
|
218
242
|
* Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
|
|
243
|
+
* Unresolvable URLs and literal mentions inside larger quoted text are left unchanged.
|
|
219
244
|
* Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
|
|
220
245
|
*/
|
|
221
246
|
export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
|
|
@@ -231,15 +256,22 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
|
|
|
231
256
|
const index = match.index;
|
|
232
257
|
if (index === undefined) continue;
|
|
233
258
|
|
|
259
|
+
if (isEmbeddedInQuotedText(command, token, index)) continue;
|
|
260
|
+
|
|
234
261
|
const rawUrl = unquoteToken(token);
|
|
235
262
|
const url = normalizeLocalScheme(rawUrl);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
263
|
+
let resolvedPath: string;
|
|
264
|
+
try {
|
|
265
|
+
resolvedPath = await resolveInternalUrlToPath(
|
|
266
|
+
url,
|
|
267
|
+
options.skills,
|
|
268
|
+
options.internalRouter,
|
|
269
|
+
options.localOptions,
|
|
270
|
+
options.ensureLocalParentDirs,
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
243
275
|
const replacement = options.noEscape ? resolvedPath : shellEscape(resolvedPath);
|
|
244
276
|
expanded = `${expanded.slice(0, index)}${replacement}${expanded.slice(index + token.length)}`;
|
|
245
277
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-intera
|
|
|
27
27
|
import { checkBashInterception } from "./bash-interceptor";
|
|
28
28
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
29
29
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
30
|
+
import { resolveEvalBackends } from "./eval-backends";
|
|
30
31
|
import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
|
|
31
32
|
import {
|
|
32
33
|
formatStyledTruncationWarning,
|
|
@@ -131,7 +132,7 @@ async function saveBashOriginalArtifact(session: ToolSession, originalText: stri
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
const BASH_TIMEOUT_DESCRIPTION = `timeout in seconds; clamped to ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}`;
|
|
135
|
+
const BASH_TIMEOUT_DESCRIPTION = `timeout in seconds; 0 disables the command deadline; nonzero values are clamped to ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}`;
|
|
135
136
|
|
|
136
137
|
const bashSchemaBase = type({
|
|
137
138
|
command: type("string").describe("command to execute"),
|
|
@@ -166,6 +167,7 @@ export interface BashToolDetails {
|
|
|
166
167
|
meta?: OutputMeta;
|
|
167
168
|
timeoutSeconds?: number;
|
|
168
169
|
requestedTimeoutSeconds?: number;
|
|
170
|
+
timeoutDisabled?: boolean;
|
|
169
171
|
wallTimeMs?: number;
|
|
170
172
|
/** Exit code of a command that ran to completion but failed (non-zero). */
|
|
171
173
|
exitCode?: number;
|
|
@@ -375,7 +377,24 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
375
377
|
};
|
|
376
378
|
readonly label = "Bash";
|
|
377
379
|
readonly loadMode = "essential";
|
|
378
|
-
|
|
380
|
+
get description(): string {
|
|
381
|
+
const evalBackends = resolveEvalBackends(this.session);
|
|
382
|
+
const isToolActive = (name: string, fallback: boolean): boolean => this.session.isToolActive?.(name) ?? fallback;
|
|
383
|
+
return prompt.render(bashDescription, {
|
|
384
|
+
asyncEnabled: this.#asyncEnabled,
|
|
385
|
+
autoBackgroundEnabled: this.#autoBackgroundEnabled,
|
|
386
|
+
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
|
|
387
|
+
hasAstGrep: isToolActive("ast_grep", this.session.settings.get("astGrep.enabled")),
|
|
388
|
+
hasAstEdit: isToolActive("ast_edit", this.session.settings.get("astEdit.enabled")),
|
|
389
|
+
hasGrep: isToolActive("grep", this.session.settings.get("grep.enabled")),
|
|
390
|
+
hasGlob: isToolActive("glob", this.session.settings.get("glob.enabled")),
|
|
391
|
+
hasRead: isToolActive("read", true),
|
|
392
|
+
hasEval: isToolActive(
|
|
393
|
+
"eval",
|
|
394
|
+
evalBackends.python || evalBackends.js || evalBackends.ruby || evalBackends.julia,
|
|
395
|
+
),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
379
398
|
readonly parameters: BashToolSchema;
|
|
380
399
|
// Non-pty calls run alongside each other (the executor isolates overlapping
|
|
381
400
|
// runs on the same shell session); pty takes over the terminal UI and must
|
|
@@ -397,15 +416,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
397
416
|
),
|
|
398
417
|
);
|
|
399
418
|
this.parameters = this.#asyncEnabled ? bashSchemaWithAsync : bashSchemaBase;
|
|
400
|
-
this.description = prompt.render(bashDescription, {
|
|
401
|
-
asyncEnabled: this.#asyncEnabled,
|
|
402
|
-
autoBackgroundEnabled: this.#autoBackgroundEnabled,
|
|
403
|
-
autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
|
|
404
|
-
hasAstGrep: this.session.settings.get("astGrep.enabled"),
|
|
405
|
-
hasAstEdit: this.session.settings.get("astEdit.enabled"),
|
|
406
|
-
hasGrep: this.session.settings.get("grep.enabled"),
|
|
407
|
-
hasGlob: this.session.settings.get("glob.enabled"),
|
|
408
|
-
});
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
#formatResultOutput(result: BashResult | BashInteractiveResult): string {
|
|
@@ -421,7 +431,11 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
421
431
|
* completed command that failed; #buildCompletedResult surfaces it as an
|
|
422
432
|
* error *result* (carrying execution details) rather than a throw.
|
|
423
433
|
*/
|
|
424
|
-
#throwIfUnfinished(
|
|
434
|
+
#throwIfUnfinished(
|
|
435
|
+
result: BashResult | BashInteractiveResult,
|
|
436
|
+
timeoutSec: number | undefined,
|
|
437
|
+
outputText: string,
|
|
438
|
+
): void {
|
|
425
439
|
if (result.cancelled) {
|
|
426
440
|
// executeBash output already carries a `[Command cancelled]` notice from
|
|
427
441
|
// the sink; PTY/bridge interactive output does not, so annotate it here.
|
|
@@ -431,11 +445,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
431
445
|
}
|
|
432
446
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
433
447
|
const out = normalizeResultOutput(result);
|
|
434
|
-
|
|
435
|
-
out
|
|
436
|
-
|
|
437
|
-
: `Command timed out after ${timeoutSec} seconds`,
|
|
438
|
-
);
|
|
448
|
+
const message =
|
|
449
|
+
timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
|
|
450
|
+
throw new ToolError(out ? `${out}\n\n[${message}]` : message);
|
|
439
451
|
}
|
|
440
452
|
if (result.exitCode === undefined) {
|
|
441
453
|
throw new ToolError(`${outputText}\n\nCommand failed: missing exit status`);
|
|
@@ -444,7 +456,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
444
456
|
|
|
445
457
|
async #buildCompletedResult(
|
|
446
458
|
result: BashResult | BashInteractiveResult,
|
|
447
|
-
timeoutSec: number,
|
|
459
|
+
timeoutSec: number | undefined,
|
|
448
460
|
options: {
|
|
449
461
|
requestedTimeoutSec?: number;
|
|
450
462
|
notices?: readonly string[];
|
|
@@ -472,7 +484,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
472
484
|
// Aborts / timeouts / missing-status still propagate as thrown errors.
|
|
473
485
|
this.#throwIfUnfinished(result, timeoutSec, outputText);
|
|
474
486
|
|
|
475
|
-
const details: BashToolDetails = {
|
|
487
|
+
const details: BashToolDetails = {};
|
|
488
|
+
if (timeoutSec === undefined) {
|
|
489
|
+
details.timeoutDisabled = true;
|
|
490
|
+
} else {
|
|
491
|
+
details.timeoutSeconds = timeoutSec;
|
|
492
|
+
}
|
|
476
493
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
477
494
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
478
495
|
}
|
|
@@ -503,13 +520,17 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
503
520
|
jobId: string,
|
|
504
521
|
label: string,
|
|
505
522
|
previewText: string,
|
|
506
|
-
timeoutSec: number,
|
|
523
|
+
timeoutSec: number | undefined,
|
|
507
524
|
options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {},
|
|
508
525
|
): AgentToolResult<BashToolDetails> {
|
|
509
526
|
const details: BashToolDetails = {
|
|
510
|
-
timeoutSeconds: timeoutSec,
|
|
511
527
|
async: { state: "running", jobId, type: "bash" },
|
|
512
528
|
};
|
|
529
|
+
if (timeoutSec === undefined) {
|
|
530
|
+
details.timeoutDisabled = true;
|
|
531
|
+
} else {
|
|
532
|
+
details.timeoutSeconds = timeoutSec;
|
|
533
|
+
}
|
|
513
534
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
514
535
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
515
536
|
}
|
|
@@ -539,8 +560,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
539
560
|
#startManagedBashJob(options: {
|
|
540
561
|
command: string;
|
|
541
562
|
commandCwd: string;
|
|
542
|
-
timeoutMs: number;
|
|
543
|
-
timeoutSec: number;
|
|
563
|
+
timeoutMs: number | undefined;
|
|
564
|
+
timeoutSec: number | undefined;
|
|
544
565
|
requestedTimeoutSec?: number;
|
|
545
566
|
notices?: readonly string[];
|
|
546
567
|
|
|
@@ -569,7 +590,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
569
590
|
const result = await executeBash(options.command, {
|
|
570
591
|
cwd: options.commandCwd,
|
|
571
592
|
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
|
572
|
-
timeout: options.timeoutMs,
|
|
593
|
+
timeout: options.timeoutMs ?? 0,
|
|
573
594
|
signal: runSignal,
|
|
574
595
|
env: options.resolvedEnv,
|
|
575
596
|
artifactPath,
|
|
@@ -661,8 +682,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
661
682
|
}
|
|
662
683
|
}
|
|
663
684
|
|
|
664
|
-
#resolveAutoBackgroundWaitMs(timeoutMs: number): number {
|
|
685
|
+
#resolveAutoBackgroundWaitMs(timeoutMs: number | undefined): number {
|
|
665
686
|
if (this.#autoBackgroundThresholdMs <= 0) return 0;
|
|
687
|
+
if (timeoutMs === undefined) return this.#autoBackgroundThresholdMs;
|
|
666
688
|
const timeoutBufferMs = 1_000;
|
|
667
689
|
return Math.max(0, Math.min(this.#autoBackgroundThresholdMs, timeoutMs - timeoutBufferMs));
|
|
668
690
|
}
|
|
@@ -765,13 +787,17 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
765
787
|
throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
|
|
766
788
|
}
|
|
767
789
|
|
|
768
|
-
//
|
|
790
|
+
// A timeout of 0 is an explicit long-running-command contract: the user
|
|
791
|
+
// must still cancel the call or job, but OMP does not impose a deadline.
|
|
769
792
|
const requestedTimeoutSec = rawTimeout;
|
|
770
|
-
const
|
|
771
|
-
const
|
|
793
|
+
const timeoutDisabled = requestedTimeoutSec === 0;
|
|
794
|
+
const timeoutSec = timeoutDisabled ? undefined : clampTimeout("bash", requestedTimeoutSec);
|
|
795
|
+
const timeoutMs = timeoutSec === undefined ? undefined : timeoutSec * 1000;
|
|
772
796
|
const pendingNotices: string[] = [];
|
|
773
|
-
|
|
774
|
-
|
|
797
|
+
if (timeoutSec !== undefined) {
|
|
798
|
+
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
799
|
+
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
800
|
+
}
|
|
775
801
|
|
|
776
802
|
if (asyncRequested) {
|
|
777
803
|
if (!this.session.asyncJobManager) {
|
|
@@ -909,14 +935,16 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
909
935
|
throw new ToolAbortError("Command aborted");
|
|
910
936
|
}
|
|
911
937
|
|
|
912
|
-
const timeoutPromise =
|
|
938
|
+
const timeoutPromise = timeoutMs
|
|
939
|
+
? Bun.sleep(timeoutMs).then(() => ({ kind: "timeout" as const }))
|
|
940
|
+
: undefined;
|
|
913
941
|
// Poll until the process exits, times out, or the caller aborts.
|
|
914
942
|
for (;;) {
|
|
915
943
|
const racers: Array<Promise<BridgeRaceResult>> = [
|
|
916
944
|
exitPromise.then(s => ({ kind: "exit" as const, status: s })),
|
|
917
|
-
timeoutPromise,
|
|
918
945
|
Bun.sleep(250).then(() => ({ kind: "poll" as const })),
|
|
919
946
|
];
|
|
947
|
+
if (timeoutPromise) racers.push(timeoutPromise);
|
|
920
948
|
if (signal) {
|
|
921
949
|
racers.push(abortedP.then(() => ({ kind: "aborted" as const })));
|
|
922
950
|
}
|
|
@@ -1053,7 +1081,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
1053
1081
|
: await executeBash(command, {
|
|
1054
1082
|
cwd: commandCwd,
|
|
1055
1083
|
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
1056
|
-
timeout: timeoutMs,
|
|
1084
|
+
timeout: timeoutMs ?? 0,
|
|
1057
1085
|
signal,
|
|
1058
1086
|
env: resolvedEnv,
|
|
1059
1087
|
artifactPath,
|
|
@@ -1074,11 +1102,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
1074
1102
|
}
|
|
1075
1103
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
1076
1104
|
const out = normalizeResultOutput(result);
|
|
1077
|
-
|
|
1078
|
-
out
|
|
1079
|
-
|
|
1080
|
-
: `Command timed out after ${timeoutSec} seconds`,
|
|
1081
|
-
);
|
|
1105
|
+
const message =
|
|
1106
|
+
timeoutSec === undefined ? "Command timed out" : `Command timed out after ${timeoutSec} seconds`;
|
|
1107
|
+
throw new ToolError(out ? `${out}\n\n[${message}]` : message);
|
|
1082
1108
|
}
|
|
1083
1109
|
return this.#buildCompletedResult(result, timeoutSec, {
|
|
1084
1110
|
requestedTimeoutSec,
|
|
@@ -1286,13 +1312,17 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1286
1312
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
1287
1313
|
|
|
1288
1314
|
// Build truncation warning
|
|
1289
|
-
const
|
|
1315
|
+
const timeoutDisabled = details?.timeoutDisabled === true || renderContext?.timeout === 0;
|
|
1316
|
+
const timeoutSeconds = timeoutDisabled ? undefined : (details?.timeoutSeconds ?? renderContext?.timeout);
|
|
1290
1317
|
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
1291
1318
|
const wallTimeMs = details?.wallTimeMs;
|
|
1292
1319
|
const statsParts: string[] = [];
|
|
1293
1320
|
if (wallTimeMs !== undefined) {
|
|
1294
1321
|
statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
|
|
1295
1322
|
}
|
|
1323
|
+
if (timeoutDisabled) {
|
|
1324
|
+
statsParts.push("Timeout: disabled");
|
|
1325
|
+
}
|
|
1296
1326
|
if (typeof timeoutSeconds === "number") {
|
|
1297
1327
|
statsParts.push(
|
|
1298
1328
|
requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
|
|
@@ -29,15 +29,20 @@ export const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1
|
|
|
29
29
|
* connection dropped, etc.).
|
|
30
30
|
*/
|
|
31
31
|
export const BROWSER_PROTOCOL_TIMEOUT_MS = 60_000;
|
|
32
|
+
const ENABLE_AUTOMATION_FLAG = "--enable-automation";
|
|
32
33
|
// Automation-tell launch flags that puppeteer-core adds by default. We suppress
|
|
33
34
|
// them via `ignoreDefaultArgs` (the supported escape hatch) to mirror xxxx's
|
|
34
|
-
// chromiumSwitches patch. `--enable-automation` is the loudest: it sets
|
|
35
|
+
// chromiumSwitches patch. `--enable-automation` is the loudest: it normally sets
|
|
35
36
|
// navigator.webdriver=true and shows the "controlled by automated software" infobar.
|
|
37
|
+
// Edge is the launch-stability exception: it can exit before CDP opens when this
|
|
38
|
+
// default flag is stripped, so Edge keeps Puppeteer's flag while our explicit
|
|
39
|
+
// `--disable-blink-features=AutomationControlled` launch arg still handles
|
|
40
|
+
// navigator.webdriver.
|
|
36
41
|
// `ignoreDefaultArgs` does exact-string matching, so each entry must be a flag that
|
|
37
42
|
// puppeteer emits verbatim. The default `--disable-features=...` string can't be
|
|
38
43
|
// matched this way; it is neutralized in the puppeteer-core patch (ChromeLauncher).
|
|
39
44
|
const STEALTH_IGNORE_DEFAULT_ARGS = [
|
|
40
|
-
|
|
45
|
+
ENABLE_AUTOMATION_FLAG,
|
|
41
46
|
"--disable-extensions",
|
|
42
47
|
"--disable-default-apps",
|
|
43
48
|
"--disable-component-extensions-with-background-pages",
|
|
@@ -47,6 +52,23 @@ const STEALTH_IGNORE_DEFAULT_ARGS = [
|
|
|
47
52
|
"--disable-ipc-flooding-protection",
|
|
48
53
|
"--metrics-recording-only",
|
|
49
54
|
];
|
|
55
|
+
|
|
56
|
+
function isMicrosoftEdgeExecutable(executablePath: string | undefined): boolean {
|
|
57
|
+
if (!executablePath) return false;
|
|
58
|
+
const normalizedPath = executablePath.replaceAll("\\", "/").toLowerCase();
|
|
59
|
+
const executableName = normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1);
|
|
60
|
+
return (
|
|
61
|
+
executableName === "msedge.exe" ||
|
|
62
|
+
executableName === "microsoft edge" ||
|
|
63
|
+
executableName.startsWith("microsoft-edge")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stealthIgnoreDefaultArgs(executablePath: string | undefined): string[] {
|
|
68
|
+
if (!isMicrosoftEdgeExecutable(executablePath)) return [...STEALTH_IGNORE_DEFAULT_ARGS];
|
|
69
|
+
return STEALTH_IGNORE_DEFAULT_ARGS.filter(arg => arg !== ENABLE_AUTOMATION_FLAG);
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
const STEALTH_ACCEPT_LANGUAGE = "en-US,en";
|
|
51
73
|
|
|
52
74
|
const USER_AGENT_TARGET_TIMEOUT_MS = 5_000;
|
|
@@ -282,12 +304,13 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
282
304
|
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|
|
283
305
|
launchArgs.push("--ignore-certificate-errors");
|
|
284
306
|
}
|
|
307
|
+
const executablePath = await ensureChromiumExecutable();
|
|
285
308
|
return await puppeteer.launch({
|
|
286
309
|
headless: opts.headless,
|
|
287
310
|
defaultViewport: opts.headless ? initialViewport : null,
|
|
288
|
-
executablePath
|
|
311
|
+
executablePath,
|
|
289
312
|
args: launchArgs,
|
|
290
|
-
ignoreDefaultArgs:
|
|
313
|
+
ignoreDefaultArgs: stealthIgnoreDefaultArgs(executablePath),
|
|
291
314
|
protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
|
|
292
315
|
});
|
|
293
316
|
}
|
|
@@ -737,6 +760,10 @@ export async function applyStealthPatches(
|
|
|
737
760
|
await injectStealthScripts(page);
|
|
738
761
|
}
|
|
739
762
|
|
|
763
|
+
export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
|
|
764
|
+
return stealthIgnoreDefaultArgs(executablePath);
|
|
765
|
+
}
|
|
766
|
+
|
|
740
767
|
export function targetSupportsUserAgentOverrideForTest(target: Target): boolean {
|
|
741
768
|
return targetSupportsUserAgentOverride(target);
|
|
742
769
|
}
|
package/src/tools/grep.ts
CHANGED
|
@@ -48,12 +48,14 @@ import {
|
|
|
48
48
|
type LineRange,
|
|
49
49
|
parseLineRanges,
|
|
50
50
|
pathTargetsSsh,
|
|
51
|
+
probeLiteralPathExists,
|
|
51
52
|
type ResolvedSearchTarget,
|
|
52
53
|
resolveReadPath,
|
|
53
54
|
resolveToolSearchScope,
|
|
54
55
|
selectorLineRanges,
|
|
55
56
|
splitInternalUrlSel,
|
|
56
57
|
splitPathAndSel,
|
|
58
|
+
splitPathAndSelPreferringLiteral,
|
|
57
59
|
toPathList,
|
|
58
60
|
} from "./path-utils";
|
|
59
61
|
import {
|
|
@@ -77,6 +79,9 @@ const searchSchema = type({
|
|
|
77
79
|
"path?": searchPathEntry.describe(
|
|
78
80
|
'file, directory, glob, internal URL, or "<file>:<lines>" selector to search; pass several as a semicolon-delimited list ("src; tests"). Omitted -> searches the workspace root (".")',
|
|
79
81
|
),
|
|
82
|
+
"selector?": type("string").describe(
|
|
83
|
+
'line selector applied to every searched file (e.g. "50-100", "50+10", "50-100,200-300"); never a path like "/"',
|
|
84
|
+
),
|
|
80
85
|
"case?": type("boolean").describe("case-sensitive search"),
|
|
81
86
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
82
87
|
"skip?": type("number")
|
|
@@ -119,7 +124,9 @@ const SEARCH_GREP_TIMEOUT_MS = 30_000;
|
|
|
119
124
|
interface GrepPathSpec {
|
|
120
125
|
original: string;
|
|
121
126
|
clean: string;
|
|
127
|
+
literalFilesystemMatch?: boolean;
|
|
122
128
|
ranges?: [LineRange, ...LineRange[]];
|
|
129
|
+
rangeSource?: "explicit" | "path";
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/**
|
|
@@ -147,9 +154,39 @@ function isReadSelectorGrammar(sel: string): boolean {
|
|
|
147
154
|
return lower === "raw" || lower === "conflicts" || parseLineRanges(sel) !== null;
|
|
148
155
|
}
|
|
149
156
|
|
|
150
|
-
function parsePathSpecs(
|
|
157
|
+
async function parsePathSpecs(
|
|
158
|
+
rawEntries: readonly string[],
|
|
159
|
+
cwd: string,
|
|
160
|
+
explicitSelector?: string,
|
|
161
|
+
): Promise<GrepPathSpec[]> {
|
|
162
|
+
const normalizedSelector = explicitSelector?.trim() || undefined;
|
|
163
|
+
const explicitRanges = normalizedSelector === undefined ? undefined : parseLineRanges(normalizedSelector);
|
|
164
|
+
if (normalizedSelector !== undefined && !explicitRanges) {
|
|
165
|
+
throw new ToolError(
|
|
166
|
+
`selector "${normalizedSelector}" is invalid — use line ranges like "50-100", "50+10", or "50-100,200-300" without a leading colon`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
151
169
|
const specs: GrepPathSpec[] = [];
|
|
152
170
|
for (const entry of rawEntries) {
|
|
171
|
+
if (explicitRanges) {
|
|
172
|
+
// Separate selector parameter makes `path` deterministic: first try the
|
|
173
|
+
// exact local filesystem path (with read-path normalization), then let
|
|
174
|
+
// archive/internal/URL resolution handle non-literal structured paths.
|
|
175
|
+
const rawPathHasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(entry);
|
|
176
|
+
const probe = rawPathHasScheme ? "missing" : await probeLiteralPathExists(entry, cwd);
|
|
177
|
+
// `"unknown"` covers EACCES/IO where we cannot confirm existence — treat
|
|
178
|
+
// it as a literal so a real file such as `test:1-2` under an unreadable
|
|
179
|
+
// parent is never silently reinterpreted as `test` + selector.
|
|
180
|
+
const literalMatch = probe !== "missing";
|
|
181
|
+
specs.push({
|
|
182
|
+
original: entry,
|
|
183
|
+
clean: literalMatch && !rawPathHasScheme ? resolveReadPath(entry, cwd) : entry,
|
|
184
|
+
literalFilesystemMatch: literalMatch,
|
|
185
|
+
ranges: explicitRanges,
|
|
186
|
+
rangeSource: "explicit",
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
153
190
|
// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
|
|
154
191
|
// which peels selector-shaped tails only for selector-capable schemes and
|
|
155
192
|
// leaves opaque ones (`mcp://`) intact. Unlike filesystem paths, their
|
|
@@ -168,10 +205,15 @@ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
|
|
|
168
205
|
specs.push({ original: entry, clean: internalSplit.path, ranges: selectorLineRanges(internalSplit.sel) });
|
|
169
206
|
continue;
|
|
170
207
|
}
|
|
171
|
-
|
|
172
|
-
|
|
208
|
+
// Prefer a literal filesystem match when one exists — a real file named
|
|
209
|
+
// `test:1-2` outranks the `:1-2` selector interpretation (issue #4618).
|
|
210
|
+
const strictSplit = splitPathAndSel(entry);
|
|
211
|
+
const split = await splitPathAndSelPreferringLiteral(entry, cwd);
|
|
212
|
+
const literalFilesystemMatch = strictSplit.sel !== undefined && split.sel === undefined;
|
|
213
|
+
let clean = literalFilesystemMatch ? resolveReadPath(entry, cwd) : entry;
|
|
173
214
|
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
174
|
-
|
|
215
|
+
let rangeSource: "path" | undefined;
|
|
216
|
+
if (!literalFilesystemMatch && split.sel) {
|
|
175
217
|
const parsed = parseLineRanges(split.sel);
|
|
176
218
|
if (!parsed) {
|
|
177
219
|
throw new ToolError(
|
|
@@ -183,8 +225,15 @@ function parsePathSpecs(rawEntries: readonly string[]): GrepPathSpec[] {
|
|
|
183
225
|
}
|
|
184
226
|
clean = split.path;
|
|
185
227
|
ranges = parsed;
|
|
228
|
+
rangeSource = "path";
|
|
186
229
|
}
|
|
187
|
-
specs.push({
|
|
230
|
+
specs.push({
|
|
231
|
+
original: entry,
|
|
232
|
+
clean,
|
|
233
|
+
literalFilesystemMatch,
|
|
234
|
+
ranges,
|
|
235
|
+
rangeSource: ranges ? rangeSource : undefined,
|
|
236
|
+
});
|
|
188
237
|
}
|
|
189
238
|
return specs;
|
|
190
239
|
}
|
|
@@ -220,7 +269,7 @@ function matchAbsolutePath(matchPath: string, searchPath: string): string {
|
|
|
220
269
|
* cleanup hook the caller MUST invoke in a `finally`.
|
|
221
270
|
*/
|
|
222
271
|
async function resolveArchiveSearchPaths(
|
|
223
|
-
|
|
272
|
+
pathSpecs: readonly GrepPathSpec[],
|
|
224
273
|
cwd: string,
|
|
225
274
|
): Promise<{
|
|
226
275
|
resolvedPaths: string[];
|
|
@@ -229,17 +278,18 @@ async function resolveArchiveSearchPaths(
|
|
|
229
278
|
unreadable: string[];
|
|
230
279
|
cleanup: () => Promise<void>;
|
|
231
280
|
}> {
|
|
232
|
-
const resolvedPaths =
|
|
281
|
+
const resolvedPaths = pathSpecs.map(spec => spec.clean);
|
|
233
282
|
const displayMap = new Map<string, string>();
|
|
234
283
|
const displaySet = new Set<string>();
|
|
235
284
|
const unreadable: string[] = [];
|
|
236
285
|
let tempDir: string | undefined;
|
|
237
286
|
const archiveCache = new Map<string, ArchiveReader>();
|
|
238
287
|
|
|
239
|
-
for (let idx = 0; idx <
|
|
240
|
-
const
|
|
288
|
+
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
289
|
+
const spec = pathSpecs[idx];
|
|
290
|
+
if (!spec || spec.literalFilesystemMatch) continue;
|
|
291
|
+
const entry = spec.clean;
|
|
241
292
|
const candidates = parseArchivePathCandidates(entry);
|
|
242
|
-
// Longest archive prefix first; we want the one whose member portion is non-empty.
|
|
243
293
|
const member = candidates.find(c => c.subPath !== "" && c.archivePath !== entry);
|
|
244
294
|
if (!member) continue;
|
|
245
295
|
|
|
@@ -360,6 +410,27 @@ function lineAllowed(lineNumber: number, ranges: readonly LineRange[] | undefine
|
|
|
360
410
|
return !ranges || isLineInRanges(lineNumber, ranges);
|
|
361
411
|
}
|
|
362
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Per-file native fetch budget that guarantees the JS range filter can still
|
|
415
|
+
* surface `perFileKeep` in-range hits. Matches arrive one entry per matched
|
|
416
|
+
* line in line order, so a bounded range's hits all sit within the first
|
|
417
|
+
* `endLine` entries, and an open-ended range starting at S is preceded by at
|
|
418
|
+
* most S-1 out-of-range entries — S-1+perFileKeep entries cover the kept
|
|
419
|
+
* window or exhaust the file. Clamped to the native file-size ceiling (a
|
|
420
|
+
* ≤4 MiB file cannot have more matched lines than bytes), which also keeps
|
|
421
|
+
* the scaled global budget inside the native layer's u32 bounds.
|
|
422
|
+
*/
|
|
423
|
+
function lineRangeFetchCap(pathSpecs: readonly GrepPathSpec[], perFileKeep: number): number {
|
|
424
|
+
let cap = 0;
|
|
425
|
+
for (const spec of pathSpecs) {
|
|
426
|
+
if (!spec.ranges) continue;
|
|
427
|
+
for (const range of spec.ranges) {
|
|
428
|
+
cap = Math.max(cap, range.endLine ?? range.startLine - 1 + perFileKeep);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return Math.min(cap, NATIVE_GREP_MAX_FILE_BYTES);
|
|
432
|
+
}
|
|
433
|
+
|
|
363
434
|
/** Binary search for the index of the line containing byte `offset`. */
|
|
364
435
|
function findLineIndex(starts: readonly number[], offset: number): number {
|
|
365
436
|
if (starts.length === 0) return -1;
|
|
@@ -879,7 +950,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
879
950
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
880
951
|
_toolContext?: AgentToolContext,
|
|
881
952
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
882
|
-
const { pattern, path: rawPath, case: caseSensitive, gitignore, skip } = params;
|
|
953
|
+
const { pattern, path: rawPath, selector, case: caseSensitive, gitignore, skip } = params;
|
|
883
954
|
|
|
884
955
|
return untilAborted(signal, async () => {
|
|
885
956
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -897,8 +968,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
897
968
|
const scopedPaths = toPathList(rawPath);
|
|
898
969
|
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
899
970
|
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
900
|
-
const pathSpecs = parsePathSpecs(rawEntries);
|
|
901
|
-
const paths = pathSpecs.map(spec => spec.clean);
|
|
971
|
+
const pathSpecs = await parsePathSpecs(rawEntries, this.session.cwd, selector);
|
|
902
972
|
const materializedExternalPaths = new Map<string, string>();
|
|
903
973
|
const materializeExternalUrlForSearch = async (rawPath: string) => {
|
|
904
974
|
const target = parseReadUrlTarget(rawPath);
|
|
@@ -917,7 +987,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
917
987
|
displaySet: archiveDisplaySet,
|
|
918
988
|
unreadable: archiveUnreadable,
|
|
919
989
|
cleanup: cleanupArchiveScratch,
|
|
920
|
-
} = await resolveArchiveSearchPaths(
|
|
990
|
+
} = await resolveArchiveSearchPaths(pathSpecs, this.session.cwd);
|
|
921
991
|
try {
|
|
922
992
|
const internalResolution = await resolveInternalSearchInputs({
|
|
923
993
|
pathSpecs,
|
|
@@ -932,6 +1002,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
932
1002
|
const searchablePaths = internalResolution.paths;
|
|
933
1003
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
934
1004
|
const rangesByAbsPath = new Map<string, LineRange[]>();
|
|
1005
|
+
const globalRanges = pathSpecs.find(spec => spec.rangeSource === "explicit")?.ranges;
|
|
935
1006
|
|
|
936
1007
|
if (
|
|
937
1008
|
archiveUnreadable.length > 0 &&
|
|
@@ -991,6 +1062,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
991
1062
|
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
992
1063
|
const spec = pathSpecs[idx];
|
|
993
1064
|
if (!spec.ranges) continue;
|
|
1065
|
+
if (spec.rangeSource === "explicit") continue;
|
|
994
1066
|
if (virtualInputIndexes.has(idx)) continue;
|
|
995
1067
|
const resolved = internalResolution.resolvedPathsByInput[idx];
|
|
996
1068
|
if (!resolved) continue;
|
|
@@ -1056,6 +1128,18 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1056
1128
|
Boolean(multiTargets) ||
|
|
1057
1129
|
(virtualResources.length > 0 && (virtualResources.length > 1 || searchablePaths.length > 0));
|
|
1058
1130
|
const perFileMatchCap = isMultiScope ? MULTI_FILE_PER_FILE_MATCHES : SINGLE_FILE_MATCHES;
|
|
1131
|
+
// Range filtering happens in JS after the native fetch, so out-of-range
|
|
1132
|
+
// matches consume fetch budget. Widen the per-file budget just enough
|
|
1133
|
+
// that filtering can still yield `perFileMatchCap` in-range hits, and
|
|
1134
|
+
// scale the global safety ceiling by the same amplification so ranged
|
|
1135
|
+
// searches keep the baseline file coverage while staying finite.
|
|
1136
|
+
const hasLineRangeFilters = pathSpecs.some(spec => spec.ranges);
|
|
1137
|
+
const nativeMaxCountPerFile = hasLineRangeFilters
|
|
1138
|
+
? Math.max(perFileMatchCap + 1, lineRangeFetchCap(pathSpecs, perFileMatchCap + 1))
|
|
1139
|
+
: perFileMatchCap + 1;
|
|
1140
|
+
const nativeMaxCount = hasLineRangeFilters
|
|
1141
|
+
? Math.ceil(INTERNAL_TOTAL_CAP / (perFileMatchCap + 1)) * nativeMaxCountPerFile
|
|
1142
|
+
: INTERNAL_TOTAL_CAP;
|
|
1059
1143
|
|
|
1060
1144
|
// Run grep
|
|
1061
1145
|
let result: GrepResult = {
|
|
@@ -1090,12 +1174,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1090
1174
|
multiline: effectiveMultiline,
|
|
1091
1175
|
hidden: true,
|
|
1092
1176
|
gitignore: useGitignore,
|
|
1093
|
-
maxCount:
|
|
1177
|
+
maxCount: nativeMaxCount,
|
|
1094
1178
|
contextBefore: normalizedContextBefore,
|
|
1095
1179
|
contextAfter: normalizedContextAfter,
|
|
1096
1180
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
1097
1181
|
mode: effectiveOutputMode,
|
|
1098
|
-
maxCountPerFile:
|
|
1182
|
+
maxCountPerFile: nativeMaxCountPerFile,
|
|
1099
1183
|
signal,
|
|
1100
1184
|
timeoutMs: SEARCH_GREP_TIMEOUT_MS,
|
|
1101
1185
|
},
|
|
@@ -1137,12 +1221,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1137
1221
|
multiline: effectiveMultiline,
|
|
1138
1222
|
hidden: true,
|
|
1139
1223
|
gitignore: useGitignore,
|
|
1140
|
-
maxCount:
|
|
1224
|
+
maxCount: nativeMaxCount,
|
|
1141
1225
|
contextBefore: normalizedContextBefore,
|
|
1142
1226
|
contextAfter: normalizedContextAfter,
|
|
1143
1227
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
1144
1228
|
mode: effectiveOutputMode,
|
|
1145
|
-
maxCountPerFile:
|
|
1229
|
+
maxCountPerFile: nativeMaxCountPerFile,
|
|
1146
1230
|
signal,
|
|
1147
1231
|
timeoutMs: SEARCH_GREP_TIMEOUT_MS,
|
|
1148
1232
|
},
|
|
@@ -1183,12 +1267,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1183
1267
|
}
|
|
1184
1268
|
throw err;
|
|
1185
1269
|
}
|
|
1186
|
-
result = mergeGrepResults(result, virtualResult,
|
|
1187
|
-
if (rangesByAbsPath.size > 0) {
|
|
1270
|
+
result = mergeGrepResults(result, virtualResult, nativeMaxCount);
|
|
1271
|
+
if (rangesByAbsPath.size > 0 || globalRanges) {
|
|
1188
1272
|
const filteredMatches: GrepMatch[] = [];
|
|
1189
1273
|
for (const match of result.matches) {
|
|
1190
1274
|
const abs = matchAbsolutePath(match.path, searchPath);
|
|
1191
|
-
const ranges = rangesByAbsPath.get(abs);
|
|
1275
|
+
const ranges = rangesByAbsPath.get(abs) ?? globalRanges;
|
|
1192
1276
|
if (!ranges) {
|
|
1193
1277
|
// Path has no line-range constraint (e.g. a peer entry without `:N-M`).
|
|
1194
1278
|
filteredMatches.push(match);
|