@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.
Files changed (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -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 segments = id.split(".");
295
- return segments.length < 2 ? id : segments.join(">");
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 trimmedOutput = output.trimEnd();
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 = output.trimEnd().split("\n");
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 = output.trimEnd().split("\n");
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 firstLine = value.split("\n")[0].trim();
628
- if (firstLine.length === 0) return `"" (${value.split("\n").length} lines)`;
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 (value.includes("\n")) return `"${preview}…" (${value.split("\n").length} lines)`;
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 = repairDoubleEncodedJsonString(typeof args?.task === "string" ? args.task : "").trim();
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 = repairDoubleEncodedJsonString(typeof args?.context === "string" ? args.context : "").trim();
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?.trim();
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("dim", previewLine(result.abortReason, 80))}`,
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(`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", previewLine(result.error, 70))}`);
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 = signal
544
- ? AbortSignal.any([signal, idle.signal, sessionAbortController.signal])
545
- : AbortSignal.any([idle.signal, sessionAbortController.signal]);
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.pause();
578
+ idle?.pause();
574
579
  return;
575
580
  }
576
581
  if (event.op === EVAL_TIMEOUT_RESUME_OP) {
577
- idle.resume();
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.dispose();
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
- if (params.since !== undefined || params.until !== undefined) {
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, selector, case: caseSensitive, gitignore, skip } = params;
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, selector);
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 || globalRanges) {
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) ?? globalRanges;
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);
@@ -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(filePath))),
157
+ stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(deColoned))),
152
158
  );
153
159
  return expandTilde(normalized);
154
160
  }