@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/dist/cli.js +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
package/src/task/render.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { Container, Markdown, Text } from "@oh-my-pi/pi-tui";
|
|
10
|
-
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import { settings } from "../config/settings";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
@@ -249,11 +249,11 @@ function getRenderYieldLabels(type: RenderYieldItem["type"]): string[] {
|
|
|
249
249
|
function formatYieldPreview(item: RenderYieldItem): string {
|
|
250
250
|
if (item.useLastTurn === true && item.data === undefined) return "last assistant turn";
|
|
251
251
|
if (item.data === undefined) return "last assistant turn";
|
|
252
|
-
if (typeof item.data === "string") return previewLine(replaceTabs(item.data), 70);
|
|
252
|
+
if (typeof item.data === "string") return previewLine(replaceTabs(sanitizeText(item.data)), 70);
|
|
253
253
|
try {
|
|
254
|
-
return previewLine(replaceTabs(JSON.stringify(item.data) ?? "null"), 70);
|
|
254
|
+
return previewLine(replaceTabs(sanitizeText(JSON.stringify(item.data) ?? "null")), 70);
|
|
255
255
|
} catch {
|
|
256
|
-
return previewLine(replaceTabs(String(item.data)), 70);
|
|
256
|
+
return previewLine(replaceTabs(sanitizeText(String(item.data))), 70);
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -281,7 +281,7 @@ function renderTypedYieldSections(value: unknown, continuePrefix: string, expand
|
|
|
281
281
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
282
282
|
if (value === null) return "null";
|
|
283
283
|
if (typeof value === "string") {
|
|
284
|
-
const trimmed = truncateToWidth(value, 70);
|
|
284
|
+
const trimmed = truncateToWidth(sanitizeText(value), 70);
|
|
285
285
|
return `"${trimmed}"`;
|
|
286
286
|
}
|
|
287
287
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
@@ -291,8 +291,9 @@ function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
|
291
291
|
export function formatTaskId(id: string): string {
|
|
292
292
|
// Ids are name-based (e.g. "Anna", "Anna-2"); a "." separates nesting levels
|
|
293
293
|
// (e.g. "Anna.Bob"). Render the hierarchy with a ">" breadcrumb.
|
|
294
|
-
const
|
|
295
|
-
|
|
294
|
+
const sanitizedId = sanitizeText(id);
|
|
295
|
+
const segments = sanitizedId.split(".");
|
|
296
|
+
return segments.length < 2 ? sanitizedId : segments.join(">");
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
|
|
@@ -347,13 +348,13 @@ function renderJsonTreeLines(
|
|
|
347
348
|
const scalar = formatJsonScalar(val, theme);
|
|
348
349
|
|
|
349
350
|
if (scalar) {
|
|
350
|
-
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
351
|
+
const label = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "value");
|
|
351
352
|
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
|
|
352
353
|
return;
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
if (Array.isArray(val)) {
|
|
356
|
-
const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
|
|
357
|
+
const header = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "array");
|
|
357
358
|
pushLine(`${prefix}${iconArray} ${header}`);
|
|
358
359
|
if (val.length === 0) {
|
|
359
360
|
pushLine(
|
|
@@ -385,7 +386,7 @@ function renderJsonTreeLines(
|
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
if (val && typeof val === "object") {
|
|
388
|
-
const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
|
|
389
|
+
const header = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "object");
|
|
389
390
|
pushLine(`${prefix}${iconObject} ${header}`);
|
|
390
391
|
const entries = Object.entries(val as Record<string, unknown>);
|
|
391
392
|
if (entries.length === 0) {
|
|
@@ -418,8 +419,8 @@ function renderJsonTreeLines(
|
|
|
418
419
|
return;
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
422
|
-
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", String(val))}`);
|
|
422
|
+
const label = key ? theme.fg("muted", sanitizeText(key)) : theme.fg("muted", "value");
|
|
423
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", sanitizeText(String(val)))}`);
|
|
423
424
|
};
|
|
424
425
|
|
|
425
426
|
const renderRoot = (val: unknown) => {
|
|
@@ -466,7 +467,7 @@ function stripRecentOutputNoticeLine(text: string): string {
|
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
function sanitizeRecentOutput(output: string): string {
|
|
469
|
-
let text = output.trimEnd();
|
|
470
|
+
let text = sanitizeText(output).trimEnd();
|
|
470
471
|
while (text) {
|
|
471
472
|
const withoutArtifactNotice = stripRawOutputArtifactNotice(text).text;
|
|
472
473
|
if (withoutArtifactNotice !== text) {
|
|
@@ -498,7 +499,8 @@ function renderOutputSection(
|
|
|
498
499
|
warning?: string,
|
|
499
500
|
): string[] {
|
|
500
501
|
const lines: string[] = [];
|
|
501
|
-
const
|
|
502
|
+
const sanitizedOutput = sanitizeText(output);
|
|
503
|
+
const trimmedOutput = sanitizedOutput.trimEnd();
|
|
502
504
|
if (!trimmedOutput && !warning) return lines;
|
|
503
505
|
|
|
504
506
|
if (warning) {
|
|
@@ -506,7 +508,7 @@ function renderOutputSection(
|
|
|
506
508
|
lines.push(
|
|
507
509
|
`${continuePrefix} ${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
508
510
|
"dim",
|
|
509
|
-
truncateToWidth(warning, 80),
|
|
511
|
+
truncateToWidth(sanitizeText(warning), 80),
|
|
510
512
|
)}`,
|
|
511
513
|
);
|
|
512
514
|
|
|
@@ -538,7 +540,7 @@ function renderOutputSection(
|
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
542
|
|
|
541
|
-
const outputLines =
|
|
543
|
+
const outputLines = trimmedOutput.split("\n");
|
|
542
544
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
543
545
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
544
546
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
@@ -582,7 +584,7 @@ function renderOutputSection(
|
|
|
582
584
|
|
|
583
585
|
lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
|
|
584
586
|
|
|
585
|
-
const outputLines =
|
|
587
|
+
const outputLines = trimmedOutput.split("\n");
|
|
586
588
|
const previewCount = expanded ? maxExpanded : maxCollapsed;
|
|
587
589
|
for (const line of outputLines.slice(0, previewCount)) {
|
|
588
590
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
@@ -603,7 +605,7 @@ function renderTaskSection(
|
|
|
603
605
|
maxExpanded = 20,
|
|
604
606
|
): string[] {
|
|
605
607
|
const lines: string[] = [];
|
|
606
|
-
const trimmed = task.trim();
|
|
608
|
+
const trimmed = sanitizeText(task).trim();
|
|
607
609
|
if (!expanded || !trimmed) return lines;
|
|
608
610
|
|
|
609
611
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
|
@@ -624,10 +626,11 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
|
|
|
624
626
|
if (typeof value === "boolean") return String(value);
|
|
625
627
|
if (typeof value === "number") return String(value);
|
|
626
628
|
if (typeof value === "string") {
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
+
const sanitizedValue = sanitizeText(value);
|
|
630
|
+
const firstLine = sanitizedValue.split("\n")[0].trim();
|
|
631
|
+
if (firstLine.length === 0) return `"" (${sanitizedValue.split("\n").length} lines)`;
|
|
629
632
|
const preview = truncateToWidth(firstLine, maxLen);
|
|
630
|
-
if (
|
|
633
|
+
if (sanitizedValue.includes("\n")) return `"${preview}…" (${sanitizedValue.split("\n").length} lines)`;
|
|
631
634
|
return `"${preview}"`;
|
|
632
635
|
}
|
|
633
636
|
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
@@ -635,7 +638,7 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
|
|
|
635
638
|
const keys = Object.keys(value);
|
|
636
639
|
return `{${keys.length} keys}`;
|
|
637
640
|
}
|
|
638
|
-
return String(value);
|
|
641
|
+
return sanitizeText(String(value));
|
|
639
642
|
}
|
|
640
643
|
|
|
641
644
|
function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string {
|
|
@@ -662,7 +665,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
662
665
|
|
|
663
666
|
for (const [key, value] of entries) {
|
|
664
667
|
const valueStr = formatScalarInline(value, 24, theme);
|
|
665
|
-
const pairStr = `${key}=${valueStr}`;
|
|
668
|
+
const pairStr = `${sanitizeText(key)}=${valueStr}`;
|
|
666
669
|
const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length; // +2 for ", "
|
|
667
670
|
|
|
668
671
|
if (totalLen + addLen > maxWidth && pairs.length > 0) {
|
|
@@ -683,7 +686,7 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
|
|
|
683
686
|
*/
|
|
684
687
|
function taskFirstLine(task: unknown): string {
|
|
685
688
|
if (typeof task !== "string") return "";
|
|
686
|
-
const trimmed = task.trim();
|
|
689
|
+
const trimmed = sanitizeText(task).trim();
|
|
687
690
|
const newline = trimmed.indexOf("\n");
|
|
688
691
|
return newline === -1 ? trimmed : trimmed.slice(0, newline);
|
|
689
692
|
}
|
|
@@ -791,7 +794,9 @@ function createAssignmentSectionRenderer(
|
|
|
791
794
|
// `renderResult` receives the raw tool args (unlike `renderCall`, which is
|
|
792
795
|
// fed through `repairTaskParams`), so undo any per-field double-encoding
|
|
793
796
|
// here too. The repair is idempotent on already-clean text.
|
|
794
|
-
const assignment =
|
|
797
|
+
const assignment = sanitizeText(
|
|
798
|
+
repairDoubleEncodedJsonString(typeof args?.task === "string" ? args.task : ""),
|
|
799
|
+
).trim();
|
|
795
800
|
if (!assignment) return undefined;
|
|
796
801
|
return createMarkdownSectionRenderer(assignment, theme);
|
|
797
802
|
}
|
|
@@ -805,7 +810,9 @@ function createContextSectionRenderer(
|
|
|
805
810
|
args: Partial<TaskParams> | undefined,
|
|
806
811
|
theme: Theme,
|
|
807
812
|
): AssignmentSectionRenderer | undefined {
|
|
808
|
-
const context =
|
|
813
|
+
const context = sanitizeText(
|
|
814
|
+
repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : ""),
|
|
815
|
+
).trim();
|
|
809
816
|
if (!context) return undefined;
|
|
810
817
|
return createMarkdownSectionRenderer(context, theme);
|
|
811
818
|
}
|
|
@@ -894,7 +901,7 @@ function renderAgentProgress(
|
|
|
894
901
|
|
|
895
902
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
896
903
|
const trimmedDescription = progress.description?.trim();
|
|
897
|
-
const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
|
|
904
|
+
const description = trimmedDescription ? previewLine(sanitizeText(trimmedDescription), 64) : undefined;
|
|
898
905
|
const displayId = formatTaskId(progress.id);
|
|
899
906
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
900
907
|
const indent = prefix ? `${prefix} ` : "";
|
|
@@ -936,7 +943,7 @@ function renderAgentProgress(
|
|
|
936
943
|
const showBadge = settings.get("task.showResolvedModelBadge");
|
|
937
944
|
if (progress.status === "running") {
|
|
938
945
|
if (!description) {
|
|
939
|
-
const taskPreview = previewLine(progress.assignment ?? progress.task, 40);
|
|
946
|
+
const taskPreview = previewLine(sanitizeText(progress.assignment ?? progress.task), 40);
|
|
940
947
|
statusLine += ` ${theme.fg("muted", taskPreview)}`;
|
|
941
948
|
}
|
|
942
949
|
statusLine = appendAgentStats(statusLine, { ...progress, showResolvedModelBadge: showBadge }, theme);
|
|
@@ -951,10 +958,10 @@ function renderAgentProgress(
|
|
|
951
958
|
// Current tool (if running) or most recent completed tool
|
|
952
959
|
if (progress.status === "running") {
|
|
953
960
|
if (progress.currentTool) {
|
|
954
|
-
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
|
|
961
|
+
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", sanitizeText(progress.currentTool))}`;
|
|
955
962
|
const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
|
|
956
963
|
if (toolDetail) {
|
|
957
|
-
toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
|
|
964
|
+
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
958
965
|
}
|
|
959
966
|
if (progress.currentToolStartMs) {
|
|
960
967
|
const elapsed = Date.now() - progress.currentToolStartMs;
|
|
@@ -966,10 +973,10 @@ function renderAgentProgress(
|
|
|
966
973
|
} else if (progress.recentTools.length > 0) {
|
|
967
974
|
// Show most recent completed tool when idle between tools
|
|
968
975
|
const recent = progress.recentTools[0];
|
|
969
|
-
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
|
|
976
|
+
let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", sanitizeText(recent.tool))}`;
|
|
970
977
|
const toolDetail = progress.lastIntent ?? recent.args;
|
|
971
978
|
if (toolDetail) {
|
|
972
|
-
toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
|
|
979
|
+
toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
|
|
973
980
|
}
|
|
974
981
|
lines.push(toolLine);
|
|
975
982
|
}
|
|
@@ -983,12 +990,12 @@ function renderAgentProgress(
|
|
|
983
990
|
const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
|
|
984
991
|
const summary =
|
|
985
992
|
`retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
|
|
986
|
-
previewLine(progress.retryState.errorMessage, 60);
|
|
993
|
+
previewLine(sanitizeText(progress.retryState.errorMessage), 60);
|
|
987
994
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
988
995
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
989
996
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
990
997
|
progress.retryFailure.attempt === 1 ? "" : "s"
|
|
991
|
-
}: ${previewLine(progress.retryFailure.errorMessage, 80)}`;
|
|
998
|
+
}: ${previewLine(sanitizeText(progress.retryFailure.errorMessage), 80)}`;
|
|
992
999
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("error", summary)}`);
|
|
993
1000
|
}
|
|
994
1001
|
|
|
@@ -1134,13 +1141,13 @@ function renderReviewResult(
|
|
|
1134
1141
|
if (summary.explanation) {
|
|
1135
1142
|
if (expanded) {
|
|
1136
1143
|
lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
|
|
1137
|
-
const explanationLines = summary.explanation.split("\n");
|
|
1144
|
+
const explanationLines = sanitizeText(summary.explanation).split("\n");
|
|
1138
1145
|
for (const line of explanationLines) {
|
|
1139
1146
|
lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
|
|
1140
1147
|
}
|
|
1141
1148
|
} else {
|
|
1142
1149
|
// Preview: first sentence or ~100 chars (flatten tabs/newlines first)
|
|
1143
|
-
const flat = replaceTabs(summary.explanation).replace(/[\r\n]+/g, " ");
|
|
1150
|
+
const flat = replaceTabs(sanitizeText(summary.explanation)).replace(/[\r\n]+/g, " ");
|
|
1144
1151
|
const firstSentence = flat.split(/[.!?]/)[0].trim();
|
|
1145
1152
|
const preview = truncateToWidth(`${firstSentence}.`, 100);
|
|
1146
1153
|
lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
|
|
@@ -1181,9 +1188,9 @@ function renderFindings(
|
|
|
1181
1188
|
const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
|
|
1182
1189
|
|
|
1183
1190
|
const { color } = getPriorityInfo(finding.priority);
|
|
1184
|
-
const rawTitle = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
|
|
1191
|
+
const rawTitle = sanitizeText(finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled");
|
|
1185
1192
|
const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
|
|
1186
|
-
const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
|
|
1193
|
+
const loc = `${path.basename(sanitizeText(finding.file_path || "<unknown>"))}:${finding.line_start}`;
|
|
1187
1194
|
|
|
1188
1195
|
lines.push(
|
|
1189
1196
|
`${continuePrefix}${findingPrefix} ${theme.fg(color, `[${finding.priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
|
|
@@ -1192,7 +1199,7 @@ function renderFindings(
|
|
|
1192
1199
|
// Show body when expanded
|
|
1193
1200
|
if (expanded && finding.body) {
|
|
1194
1201
|
// Wrap body text
|
|
1195
|
-
const bodyLines = finding.body.split("\n");
|
|
1202
|
+
const bodyLines = sanitizeText(finding.body).split("\n");
|
|
1196
1203
|
for (const bodyLine of bodyLines) {
|
|
1197
1204
|
lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", replaceTabs(bodyLine))}`);
|
|
1198
1205
|
}
|
|
@@ -1244,7 +1251,7 @@ function renderAgentResult(
|
|
|
1244
1251
|
: "failed";
|
|
1245
1252
|
|
|
1246
1253
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
1247
|
-
const trimmedDescription = result.description
|
|
1254
|
+
const trimmedDescription = result.description ? sanitizeText(result.description).trim() : undefined;
|
|
1248
1255
|
const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
|
|
1249
1256
|
const displayId = formatTaskId(result.id);
|
|
1250
1257
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
@@ -1278,7 +1285,10 @@ function renderAgentResult(
|
|
|
1278
1285
|
|
|
1279
1286
|
if (aborted && result.abortReason) {
|
|
1280
1287
|
lines.push(
|
|
1281
|
-
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg(
|
|
1288
|
+
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg(
|
|
1289
|
+
"dim",
|
|
1290
|
+
previewLine(sanitizeText(result.abortReason), 80),
|
|
1291
|
+
)}`,
|
|
1282
1292
|
);
|
|
1283
1293
|
}
|
|
1284
1294
|
// Check for review result, preferring incremental yield sections and falling
|
|
@@ -1382,7 +1392,7 @@ function renderAgentResult(
|
|
|
1382
1392
|
lines.push(
|
|
1383
1393
|
`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
|
|
1384
1394
|
"dim",
|
|
1385
|
-
truncateToWidth(missingCompleteWarning, 80),
|
|
1395
|
+
truncateToWidth(sanitizeText(missingCompleteWarning), 80),
|
|
1386
1396
|
)}`,
|
|
1387
1397
|
);
|
|
1388
1398
|
}
|
|
@@ -1406,7 +1416,9 @@ function renderAgentResult(
|
|
|
1406
1416
|
|
|
1407
1417
|
// Error message
|
|
1408
1418
|
if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
|
|
1409
|
-
lines.push(
|
|
1419
|
+
lines.push(
|
|
1420
|
+
`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", previewLine(sanitizeText(result.error), 70))}`,
|
|
1421
|
+
);
|
|
1410
1422
|
}
|
|
1411
1423
|
|
|
1412
1424
|
return lines;
|
|
@@ -27,6 +27,7 @@ export interface InternalUrlExpansionOptions {
|
|
|
27
27
|
noEscape?: boolean;
|
|
28
28
|
internalRouter?: InternalUrlResolver;
|
|
29
29
|
localOptions?: LocalProtocolOptions;
|
|
30
|
+
cwd?: string;
|
|
30
31
|
ensureLocalParentDirs?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -175,6 +176,7 @@ async function resolveInternalUrlToPath(
|
|
|
175
176
|
internalRouter?: InternalUrlResolver,
|
|
176
177
|
localOptions?: LocalProtocolOptions,
|
|
177
178
|
ensureLocalParentDirs?: boolean,
|
|
179
|
+
cwd?: string,
|
|
178
180
|
): Promise<string> {
|
|
179
181
|
const url = normalizeLocalScheme(rawUrl);
|
|
180
182
|
const scheme = extractScheme(url);
|
|
@@ -208,7 +210,7 @@ async function resolveInternalUrlToPath(
|
|
|
208
210
|
|
|
209
211
|
let resource: InternalResource;
|
|
210
212
|
try {
|
|
211
|
-
resource = await internalRouter.resolve(url, { pathOnly: true });
|
|
213
|
+
resource = await internalRouter.resolve(url, { cwd, pathOnly: true });
|
|
212
214
|
} catch (error) {
|
|
213
215
|
const message = error instanceof Error ? error.message : String(error);
|
|
214
216
|
throw new ToolError(`Failed to resolve ${scheme}:// URL in bash command: ${url}\n${message}`);
|
|
@@ -268,6 +270,7 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
|
|
|
268
270
|
options.internalRouter,
|
|
269
271
|
options.localOptions,
|
|
270
272
|
options.ensureLocalParentDirs,
|
|
273
|
+
options.cwd,
|
|
271
274
|
);
|
|
272
275
|
} catch {
|
|
273
276
|
continue;
|
package/src/tools/bash.ts
CHANGED
|
@@ -751,6 +751,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
751
751
|
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
752
752
|
skills: this.session.skills ?? [],
|
|
753
753
|
internalRouter: InternalUrlRouter.instance(),
|
|
754
|
+
cwd: this.session.cwd,
|
|
754
755
|
localOptions: {
|
|
755
756
|
getArtifactsDir: this.session.getArtifactsDir,
|
|
756
757
|
getSessionId: this.session.getSessionId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { logger, withTimeout } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
4
|
import type { Browser, CDPSession } from "puppeteer-core";
|
|
5
5
|
import { ToolAbortError, ToolError } from "../tool-errors";
|
|
@@ -17,6 +17,13 @@ export type BrowserKind = PuppeteerBrowserKind | CmuxKind;
|
|
|
17
17
|
|
|
18
18
|
export type BrowserKindTag = BrowserKind["kind"];
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Upper bound on `browser.close()` for headless Chromium. Puppeteer waits for
|
|
22
|
+
* the process to fully exit; a wedged Chromium would otherwise hang cleanup
|
|
23
|
+
* forever (issue #5260), so we cap the wait and force-kill on timeout.
|
|
24
|
+
*/
|
|
25
|
+
const HEADLESS_CLOSE_TIMEOUT_MS = 5_000;
|
|
26
|
+
|
|
20
27
|
interface BrowserHandleCommon {
|
|
21
28
|
key: string;
|
|
22
29
|
kind: BrowserKind;
|
|
@@ -232,10 +239,17 @@ async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean
|
|
|
232
239
|
}
|
|
233
240
|
if (handle.kind.kind === "headless") {
|
|
234
241
|
if (handle.browser.connected) {
|
|
242
|
+
// Puppeteer's `browser.close()` resolves only once the Chromium
|
|
243
|
+
// process fully exits. A wedged Chromium (a known Windows failure
|
|
244
|
+
// mode) leaves this await pending forever, freezing `releaseTab` in
|
|
245
|
+
// the "Closing tab" phase (issue #5260). Bound it, then SIGKILL the
|
|
246
|
+
// process tree so cleanup always completes.
|
|
247
|
+
const proc = handle.browser.process();
|
|
235
248
|
try {
|
|
236
|
-
await handle.browser.close();
|
|
249
|
+
await withTimeout(handle.browser.close(), HEADLESS_CLOSE_TIMEOUT_MS, "Timed out closing headless browser");
|
|
237
250
|
} catch (err) {
|
|
238
|
-
logger.debug("Failed to close headless browser", { error: (err as Error).message });
|
|
251
|
+
logger.debug("Failed to close headless browser; force-killing", { error: (err as Error).message });
|
|
252
|
+
if (proc?.pid !== undefined) await gracefulKillTreeOnce(proc.pid).catch(() => undefined);
|
|
239
253
|
}
|
|
240
254
|
}
|
|
241
255
|
return;
|
package/src/tools/eval.ts
CHANGED
|
@@ -85,7 +85,7 @@ function enabledEvalLanguages(backends: EvalBackendsAllowance): EvalLanguageToke
|
|
|
85
85
|
|
|
86
86
|
const evalCellCommonFields = {
|
|
87
87
|
"title?": type("string").describe('short label shown in transcript (e.g. "imports", "load config")'),
|
|
88
|
-
"timeout?": type("number").describe("timeout for this eval call in seconds"),
|
|
88
|
+
"timeout?": type("number").describe("timeout for this eval call in seconds; 0 disables the cell timeout"),
|
|
89
89
|
"reset?": type("boolean").describe("wipe this language's kernel before running. Other languages are untouched."),
|
|
90
90
|
};
|
|
91
91
|
|
|
@@ -538,11 +538,16 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
538
538
|
// ordinary tool calls all count against the budget. The watchdog drives
|
|
539
539
|
// `combinedSignal`; we pass no wall-clock deadline downstream so the
|
|
540
540
|
// backends never arm a competing fixed timer.
|
|
541
|
-
const idleTimeoutMs = timeoutSecondsFromMs(cell.timeoutMs) * 1000;
|
|
542
|
-
const idle = new IdleTimeout(idleTimeoutMs);
|
|
543
|
-
const combinedSignal =
|
|
544
|
-
|
|
545
|
-
|
|
541
|
+
const idleTimeoutMs = cell.timeoutMs === 0 ? undefined : timeoutSecondsFromMs(cell.timeoutMs) * 1000;
|
|
542
|
+
const idle = idleTimeoutMs === undefined ? undefined : new IdleTimeout(idleTimeoutMs);
|
|
543
|
+
const combinedSignal =
|
|
544
|
+
signal && idle
|
|
545
|
+
? AbortSignal.any([signal, idle.signal, sessionAbortController.signal])
|
|
546
|
+
: signal
|
|
547
|
+
? AbortSignal.any([signal, sessionAbortController.signal])
|
|
548
|
+
: idle
|
|
549
|
+
? AbortSignal.any([idle.signal, sessionAbortController.signal])
|
|
550
|
+
: sessionAbortController.signal;
|
|
546
551
|
|
|
547
552
|
const cellResult = cellResults[i];
|
|
548
553
|
cellResult.status = "running";
|
|
@@ -570,11 +575,11 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
570
575
|
},
|
|
571
576
|
onStatus: event => {
|
|
572
577
|
if (event.op === EVAL_TIMEOUT_PAUSE_OP) {
|
|
573
|
-
idle
|
|
578
|
+
idle?.pause();
|
|
574
579
|
return;
|
|
575
580
|
}
|
|
576
581
|
if (event.op === EVAL_TIMEOUT_RESUME_OP) {
|
|
577
|
-
idle
|
|
582
|
+
idle?.resume();
|
|
578
583
|
return;
|
|
579
584
|
}
|
|
580
585
|
cellResult.statusEvents ??= [];
|
|
@@ -583,7 +588,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
583
588
|
},
|
|
584
589
|
});
|
|
585
590
|
} finally {
|
|
586
|
-
idle
|
|
591
|
+
idle?.dispose();
|
|
587
592
|
activeLiveCell = undefined;
|
|
588
593
|
}
|
|
589
594
|
const durationMs = Date.now() - startTime;
|
package/src/tools/gh.ts
CHANGED
|
@@ -3419,7 +3419,9 @@ async function executeSearchCode(
|
|
|
3419
3419
|
signal: AbortSignal | undefined,
|
|
3420
3420
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
3421
3421
|
const query = requireNonEmpty(params.query, "query");
|
|
3422
|
-
|
|
3422
|
+
const since = normalizeOptionalString(params.since);
|
|
3423
|
+
const until = normalizeOptionalString(params.until);
|
|
3424
|
+
if (since !== undefined || until !== undefined) {
|
|
3423
3425
|
throw new ToolError("search_code does not support since/until; GitHub code search has no date qualifier.");
|
|
3424
3426
|
}
|
|
3425
3427
|
const limit = resolveSearchLimit(params.limit);
|
package/src/tools/grep.ts
CHANGED
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
type LineRange,
|
|
49
49
|
parseLineRanges,
|
|
50
50
|
pathTargetsSsh,
|
|
51
|
-
probeLiteralPathExists,
|
|
52
51
|
type ResolvedSearchTarget,
|
|
53
52
|
resolveReadPath,
|
|
54
53
|
resolveToolSearchScope,
|
|
@@ -79,9 +78,6 @@ const searchSchema = type({
|
|
|
79
78
|
"path?": searchPathEntry.describe(
|
|
80
79
|
'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 (".")',
|
|
81
80
|
),
|
|
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
|
-
),
|
|
85
81
|
"case?": type("boolean").describe("case-sensitive search"),
|
|
86
82
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
87
83
|
"skip?": type("number")
|
|
@@ -126,7 +122,6 @@ interface GrepPathSpec {
|
|
|
126
122
|
clean: string;
|
|
127
123
|
literalFilesystemMatch?: boolean;
|
|
128
124
|
ranges?: [LineRange, ...LineRange[]];
|
|
129
|
-
rangeSource?: "explicit" | "path";
|
|
130
125
|
}
|
|
131
126
|
|
|
132
127
|
/**
|
|
@@ -154,39 +149,9 @@ function isReadSelectorGrammar(sel: string): boolean {
|
|
|
154
149
|
return lower === "raw" || lower === "conflicts" || parseLineRanges(sel) !== null;
|
|
155
150
|
}
|
|
156
151
|
|
|
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
|
-
}
|
|
152
|
+
async function parsePathSpecs(rawEntries: readonly string[], cwd: string): Promise<GrepPathSpec[]> {
|
|
169
153
|
const specs: GrepPathSpec[] = [];
|
|
170
154
|
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
|
-
}
|
|
190
155
|
// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
|
|
191
156
|
// which peels selector-shaped tails only for selector-capable schemes and
|
|
192
157
|
// leaves opaque ones (`mcp://`) intact. Unlike filesystem paths, their
|
|
@@ -212,7 +177,6 @@ async function parsePathSpecs(
|
|
|
212
177
|
const literalFilesystemMatch = strictSplit.sel !== undefined && split.sel === undefined;
|
|
213
178
|
let clean = literalFilesystemMatch ? resolveReadPath(entry, cwd) : entry;
|
|
214
179
|
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
215
|
-
let rangeSource: "path" | undefined;
|
|
216
180
|
if (!literalFilesystemMatch && split.sel) {
|
|
217
181
|
const parsed = parseLineRanges(split.sel);
|
|
218
182
|
if (!parsed) {
|
|
@@ -225,14 +189,12 @@ async function parsePathSpecs(
|
|
|
225
189
|
}
|
|
226
190
|
clean = split.path;
|
|
227
191
|
ranges = parsed;
|
|
228
|
-
rangeSource = "path";
|
|
229
192
|
}
|
|
230
193
|
specs.push({
|
|
231
194
|
original: entry,
|
|
232
195
|
clean,
|
|
233
196
|
literalFilesystemMatch,
|
|
234
197
|
ranges,
|
|
235
|
-
rangeSource: ranges ? rangeSource : undefined,
|
|
236
198
|
});
|
|
237
199
|
}
|
|
238
200
|
return specs;
|
|
@@ -950,7 +912,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
950
912
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
951
913
|
_toolContext?: AgentToolContext,
|
|
952
914
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
953
|
-
const { pattern, path: rawPath,
|
|
915
|
+
const { pattern, path: rawPath, case: caseSensitive, gitignore, skip } = params;
|
|
954
916
|
|
|
955
917
|
return untilAborted(signal, async () => {
|
|
956
918
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -968,7 +930,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
968
930
|
const scopedPaths = toPathList(rawPath);
|
|
969
931
|
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
970
932
|
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
971
|
-
const pathSpecs = await parsePathSpecs(rawEntries, this.session.cwd
|
|
933
|
+
const pathSpecs = await parsePathSpecs(rawEntries, this.session.cwd);
|
|
972
934
|
const materializedExternalPaths = new Map<string, string>();
|
|
973
935
|
const materializeExternalUrlForSearch = async (rawPath: string) => {
|
|
974
936
|
const target = parseReadUrlTarget(rawPath);
|
|
@@ -1002,7 +964,6 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1002
964
|
const searchablePaths = internalResolution.paths;
|
|
1003
965
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
1004
966
|
const rangesByAbsPath = new Map<string, LineRange[]>();
|
|
1005
|
-
const globalRanges = pathSpecs.find(spec => spec.rangeSource === "explicit")?.ranges;
|
|
1006
967
|
|
|
1007
968
|
if (
|
|
1008
969
|
archiveUnreadable.length > 0 &&
|
|
@@ -1062,7 +1023,6 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1062
1023
|
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
1063
1024
|
const spec = pathSpecs[idx];
|
|
1064
1025
|
if (!spec.ranges) continue;
|
|
1065
|
-
if (spec.rangeSource === "explicit") continue;
|
|
1066
1026
|
if (virtualInputIndexes.has(idx)) continue;
|
|
1067
1027
|
const resolved = internalResolution.resolvedPathsByInput[idx];
|
|
1068
1028
|
if (!resolved) continue;
|
|
@@ -1268,11 +1228,11 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1268
1228
|
throw err;
|
|
1269
1229
|
}
|
|
1270
1230
|
result = mergeGrepResults(result, virtualResult, nativeMaxCount);
|
|
1271
|
-
if (rangesByAbsPath.size > 0
|
|
1231
|
+
if (rangesByAbsPath.size > 0) {
|
|
1272
1232
|
const filteredMatches: GrepMatch[] = [];
|
|
1273
1233
|
for (const match of result.matches) {
|
|
1274
1234
|
const abs = matchAbsolutePath(match.path, searchPath);
|
|
1275
|
-
const ranges = rangesByAbsPath.get(abs)
|
|
1235
|
+
const ranges = rangesByAbsPath.get(abs);
|
|
1276
1236
|
if (!ranges) {
|
|
1277
1237
|
// Path has no line-range constraint (e.g. a peer entry without `:N-M`).
|
|
1278
1238
|
filteredMatches.push(match);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -147,8 +147,14 @@ export function expandTilde(filePath: string, home?: string): string {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function expandPath(filePath: string): string {
|
|
150
|
+
// Some models intermittently prefix an otherwise-valid path with a stray
|
|
151
|
+
// `:` (e.g. `:/abs/path`, `:../rel`). No real path starts with `:` and it
|
|
152
|
+
// never begins a selector against an absolute/relative path, so strip it
|
|
153
|
+
// before resolution — mirroring the `@`-prefix normalization above and the
|
|
154
|
+
// implicit stripping `write` already tolerates (issue #5508).
|
|
155
|
+
const deColoned = /^:(?=[/~]|\.\.?\/)/.test(filePath) ? filePath.slice(1) : filePath;
|
|
150
156
|
const normalized = stripWindowsExtendedLengthPathPrefix(
|
|
151
|
-
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(
|
|
157
|
+
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(deColoned))),
|
|
152
158
|
);
|
|
153
159
|
return expandTilde(normalized);
|
|
154
160
|
}
|