@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/package.json +8 -8
  3. package/scripts/build-binary.ts +61 -0
  4. package/src/autoresearch/helpers.ts +10 -0
  5. package/src/autoresearch/index.ts +1 -11
  6. package/src/autoresearch/tools/init-experiment.ts +1 -10
  7. package/src/autoresearch/tools/log-experiment.ts +1 -11
  8. package/src/autoresearch/tools/run-experiment.ts +1 -10
  9. package/src/bun-imports.d.ts +6 -0
  10. package/src/cli/plugin-cli.ts +23 -45
  11. package/src/commit/agentic/tools/propose-commit.ts +1 -14
  12. package/src/commit/agentic/tools/split-commit.ts +1 -15
  13. package/src/commit/utils.ts +15 -1
  14. package/src/config/model-registry.ts +3 -3
  15. package/src/config/prompt-templates.ts +4 -12
  16. package/src/config/settings-schema.ts +27 -2
  17. package/src/config/settings.ts +1 -1
  18. package/src/dap/session.ts +8 -2
  19. package/src/discovery/claude-plugins.ts +61 -6
  20. package/src/discovery/codex.ts +2 -15
  21. package/src/discovery/gemini.ts +2 -15
  22. package/src/discovery/helpers.ts +40 -1
  23. package/src/discovery/opencode.ts +2 -15
  24. package/src/edit/apply-patch/index.ts +87 -0
  25. package/src/edit/apply-patch/parser.ts +174 -0
  26. package/src/edit/diff.ts +3 -14
  27. package/src/edit/index.ts +67 -3
  28. package/src/edit/modes/apply-patch.lark +19 -0
  29. package/src/edit/modes/apply-patch.ts +63 -0
  30. package/src/edit/modes/chunk.ts +6 -2
  31. package/src/edit/modes/hashline.ts +3 -3
  32. package/src/edit/modes/replace.ts +2 -13
  33. package/src/edit/read-file.ts +18 -0
  34. package/src/edit/renderer.ts +61 -33
  35. package/src/extensibility/extensions/compact-handler.ts +40 -0
  36. package/src/extensibility/extensions/runner.ts +11 -29
  37. package/src/extensibility/utils.ts +7 -1
  38. package/src/internal-urls/docs-index.generated.ts +9 -2
  39. package/src/lsp/client.ts +14 -5
  40. package/src/lsp/index.ts +53 -10
  41. package/src/lsp/render.ts +14 -2
  42. package/src/lsp/types.ts +2 -0
  43. package/src/main.ts +1 -0
  44. package/src/mcp/manager.ts +29 -48
  45. package/src/memories/index.ts +7 -1
  46. package/src/modes/acp/acp-agent.ts +3 -16
  47. package/src/modes/components/model-selector.ts +15 -24
  48. package/src/modes/components/plugin-settings.ts +16 -5
  49. package/src/modes/components/read-tool-group.ts +92 -9
  50. package/src/modes/components/settings-defs.ts +18 -0
  51. package/src/modes/components/settings-selector.ts +2 -6
  52. package/src/modes/components/tool-execution.ts +61 -28
  53. package/src/modes/controllers/event-controller.ts +3 -1
  54. package/src/modes/controllers/extension-ui-controller.ts +99 -150
  55. package/src/modes/controllers/selector-controller.ts +3 -12
  56. package/src/modes/interactive-mode.ts +4 -2
  57. package/src/modes/print-mode.ts +4 -22
  58. package/src/modes/rpc/rpc-mode.ts +18 -38
  59. package/src/modes/shared.ts +10 -1
  60. package/src/modes/utils/ui-helpers.ts +6 -2
  61. package/src/plan-mode/approved-plan.ts +5 -4
  62. package/src/prompts/system/subagent-system-prompt.md +4 -4
  63. package/src/prompts/system/subagent-user-prompt.md +2 -2
  64. package/src/prompts/system/system-prompt.md +208 -243
  65. package/src/prompts/tools/apply-patch.md +67 -0
  66. package/src/prompts/tools/ast-edit.md +18 -23
  67. package/src/prompts/tools/ast-grep.md +25 -32
  68. package/src/prompts/tools/bash.md +11 -23
  69. package/src/prompts/tools/debug.md +8 -22
  70. package/src/prompts/tools/find.md +0 -4
  71. package/src/prompts/tools/grep.md +3 -5
  72. package/src/prompts/tools/hashline.md +16 -10
  73. package/src/prompts/tools/python.md +10 -14
  74. package/src/prompts/tools/read.md +17 -24
  75. package/src/prompts/tools/task.md +57 -21
  76. package/src/prompts/tools/todo-write.md +45 -67
  77. package/src/session/agent-session.ts +4 -4
  78. package/src/session/session-manager.ts +15 -7
  79. package/src/session/streaming-output.ts +24 -0
  80. package/src/slash-commands/builtin-registry.ts +3 -14
  81. package/src/task/executor.ts +13 -34
  82. package/src/task/index.ts +82 -18
  83. package/src/task/simple-mode.ts +27 -0
  84. package/src/task/template.ts +17 -3
  85. package/src/task/types.ts +77 -30
  86. package/src/tools/ask.ts +2 -4
  87. package/src/tools/ast-edit.ts +41 -17
  88. package/src/tools/ast-grep.ts +8 -27
  89. package/src/tools/bash-skill-urls.ts +9 -7
  90. package/src/tools/bash.ts +66 -24
  91. package/src/tools/browser.ts +1 -1
  92. package/src/tools/fetch.ts +1 -14
  93. package/src/tools/file-recorder.ts +35 -0
  94. package/src/tools/find.ts +25 -29
  95. package/src/tools/gh-format.ts +12 -0
  96. package/src/tools/gh-renderer.ts +1 -8
  97. package/src/tools/gh.ts +6 -13
  98. package/src/tools/grep.ts +103 -59
  99. package/src/tools/jtd-to-json-schema.ts +16 -0
  100. package/src/tools/match-line-format.ts +20 -0
  101. package/src/tools/path-utils.ts +61 -5
  102. package/src/tools/plan-mode-guard.ts +6 -5
  103. package/src/tools/python.ts +1 -1
  104. package/src/tools/read.ts +1 -1
  105. package/src/tools/render-utils.ts +38 -6
  106. package/src/tools/renderers.ts +1 -0
  107. package/src/tools/resolve.ts +12 -3
  108. package/src/tools/ssh.ts +3 -11
  109. package/src/tools/submit-result.ts +1 -13
  110. package/src/tools/todo-write.ts +137 -103
  111. package/src/tools/vim.ts +1 -1
  112. package/src/tools/write.ts +2 -23
  113. package/src/tui/code-cell.ts +12 -7
  114. package/src/utils/edit-mode.ts +3 -2
  115. package/src/utils/git.ts +1 -1
  116. package/src/vim/engine.ts +41 -58
  117. package/src/web/scrapers/crates-io.ts +1 -14
  118. package/src/web/scrapers/types.ts +13 -0
  119. package/src/web/search/providers/base.ts +13 -0
  120. package/src/web/search/providers/brave.ts +2 -5
  121. package/src/web/search/providers/codex.ts +20 -24
  122. package/src/web/search/providers/gemini.ts +39 -1
  123. package/src/web/search/providers/jina.ts +2 -5
  124. package/src/web/search/providers/kagi.ts +3 -8
  125. package/src/web/search/providers/kimi.ts +3 -7
  126. package/src/web/search/providers/parallel.ts +3 -8
  127. package/src/web/search/providers/synthetic.ts +3 -7
  128. package/src/web/search/providers/tavily.ts +15 -11
  129. package/src/web/search/providers/utils.ts +36 -0
  130. package/src/web/search/providers/zai.ts +3 -7
package/src/tools/bash.ts CHANGED
@@ -9,7 +9,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
12
- import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
12
+ import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
13
13
  import { renderStatusLine } from "../tui";
14
14
  import { CachedOutputBlock } from "../tui/output-block";
15
15
  import { getSixelLineMask } from "../utils/sixel";
@@ -23,7 +23,7 @@ import { resolveToCwd } from "./path-utils";
23
23
  import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
24
24
  import { ToolAbortError, ToolError } from "./tool-errors";
25
25
  import { toolResult } from "./tool-result";
26
- import { clampTimeout } from "./tool-timeouts";
26
+ import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
27
27
 
28
28
  export const BASH_DEFAULT_PREVIEW_LINES = 10;
29
29
 
@@ -38,7 +38,7 @@ const bashSchemaBase = Type.Object({
38
38
  "Additional environment variables passed to the command and rendered inline as shell assignments; prefer this for multiline or quote-heavy content",
39
39
  }),
40
40
  ),
41
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 300)" })),
41
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 300 })),
42
42
  cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
43
43
  head: Type.Optional(Type.Number({ description: "Return only first N lines of output" })),
44
44
  tail: Type.Optional(Type.Number({ description: "Return only last N lines of output" })),
@@ -74,6 +74,7 @@ export interface BashToolInput {
74
74
  export interface BashToolDetails {
75
75
  meta?: OutputMeta;
76
76
  timeoutSeconds?: number;
77
+ requestedTimeoutSeconds?: number;
77
78
  async?: {
78
79
  state: "running" | "completed" | "failed";
79
80
  jobId: string;
@@ -219,6 +220,13 @@ function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | un
219
220
  if (partialEnv && args.env) return { ...partialEnv, ...args.env };
220
221
  return args.env ?? partialEnv;
221
222
  }
223
+
224
+ function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutSec: number): string | undefined {
225
+ return requestedTimeoutSec !== effectiveTimeoutSec
226
+ ? `Timeout clamped to ${effectiveTimeoutSec}s (requested ${requestedTimeoutSec}s; allowed range ${TOOL_TIMEOUTS.bash.min}-${TOOL_TIMEOUTS.bash.max}s).`
227
+ : undefined;
228
+ }
229
+
222
230
  /**
223
231
  * Bash tool implementation.
224
232
  *
@@ -289,9 +297,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
289
297
  timeoutSec: number,
290
298
  headLines?: number,
291
299
  tailLines?: number,
300
+ options: { requestedTimeoutSec?: number; notices?: string[] } = {},
292
301
  ): AgentToolResult<BashToolDetails> {
293
- const outputText = this.#formatResultOutput(result, headLines, tailLines);
302
+ const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
303
+ const notices = options.notices?.filter(Boolean) ?? [];
304
+ if (notices.length > 0) outputLines.push("", ...notices);
305
+ const outputText = outputLines.join("\n");
294
306
  const details: BashToolDetails = { timeoutSeconds: timeoutSec };
307
+ if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
308
+ details.requestedTimeoutSeconds = options.requestedTimeoutSec;
309
+ }
295
310
  const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
296
311
  this.#buildResultText(result, timeoutSec, outputText);
297
312
  return resultBuilder.done();
@@ -302,16 +317,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
302
317
  label: string,
303
318
  previewText: string,
304
319
  timeoutSec: number,
320
+ options: { requestedTimeoutSec?: number; notices?: string[] } = {},
305
321
  ): AgentToolResult<BashToolDetails> {
306
322
  const details: BashToolDetails = {
307
323
  timeoutSeconds: timeoutSec,
308
324
  async: { state: "running", jobId, type: "bash" },
309
325
  };
326
+ if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
327
+ details.requestedTimeoutSeconds = options.requestedTimeoutSec;
328
+ }
310
329
  const lines: string[] = [];
311
330
  const trimmedPreview = previewText.trimEnd();
312
331
  if (trimmedPreview.length > 0) {
313
332
  lines.push(trimmedPreview, "");
314
333
  }
334
+ if (options.notices?.length) {
335
+ lines.push(...options.notices, "");
336
+ }
315
337
  lines.push(`Background job ${jobId} started: ${label}`);
316
338
  lines.push("Result will be delivered automatically when complete.");
317
339
  lines.push(`Use \`poll\`, \`read jobs://${jobId}\`, or \`cancel_job\` if needed.`);
@@ -330,6 +352,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
330
352
  commandCwd: string;
331
353
  timeoutMs: number;
332
354
  timeoutSec: number;
355
+ requestedTimeoutSec?: number;
356
+ timeoutClampNotice?: string;
333
357
  headLines?: number;
334
358
  tailLines?: number;
335
359
  resolvedEnv?: Record<string, string>;
@@ -372,6 +396,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
372
396
  options.timeoutSec,
373
397
  options.headLines,
374
398
  options.tailLines,
399
+ {
400
+ requestedTimeoutSec: options.requestedTimeoutSec,
401
+ notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
402
+ },
375
403
  );
376
404
  const finalText = this.#extractTextResult(finalResult);
377
405
  latestText = finalText;
@@ -512,7 +540,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
512
540
  : undefined;
513
541
 
514
542
  // Resolve protocol URLs (skill://, agent://, etc.) in extracted cwd.
515
- if (cwd?.includes("://")) {
543
+ if (cwd?.includes("://") || cwd?.includes("local:/")) {
516
544
  cwd = await expandInternalUrls(cwd, { ...internalUrlOptions, noEscape: true });
517
545
  }
518
546
 
@@ -531,8 +559,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
531
559
  }
532
560
 
533
561
  // Clamp to reasonable range: 1s - 3600s (1 hour)
534
- const timeoutSec = clampTimeout("bash", rawTimeout);
562
+ const requestedTimeoutSec = rawTimeout;
563
+ const timeoutSec = clampTimeout("bash", requestedTimeoutSec);
535
564
  const timeoutMs = timeoutSec * 1000;
565
+ const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
536
566
 
537
567
  if (asyncRequested) {
538
568
  if (!this.session.asyncJobManager) {
@@ -543,13 +573,18 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
543
573
  commandCwd,
544
574
  timeoutMs,
545
575
  timeoutSec,
576
+ requestedTimeoutSec,
577
+ timeoutClampNotice,
546
578
  headLines,
547
579
  tailLines,
548
580
  resolvedEnv,
549
581
  onUpdate,
550
582
  startBackgrounded: true,
551
583
  });
552
- return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec);
584
+ return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
585
+ requestedTimeoutSec,
586
+ notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
587
+ });
553
588
  }
554
589
 
555
590
  if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
@@ -560,6 +595,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
560
595
  commandCwd,
561
596
  timeoutMs,
562
597
  timeoutSec,
598
+ requestedTimeoutSec,
599
+ timeoutClampNotice,
563
600
  headLines,
564
601
  tailLines,
565
602
  resolvedEnv,
@@ -567,7 +604,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
567
604
  startBackgrounded,
568
605
  });
569
606
  if (startBackgrounded) {
570
- return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec);
607
+ return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
608
+ requestedTimeoutSec,
609
+ notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
610
+ });
571
611
  }
572
612
  const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
573
613
  if (waitResult.kind === "completed") {
@@ -584,7 +624,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
584
624
  throw new ToolAbortError(job.getLatestText() || "Command aborted");
585
625
  }
586
626
  job.setBackgrounded(true);
587
- return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec);
627
+ return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
628
+ requestedTimeoutSec,
629
+ notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
630
+ });
588
631
  }
589
632
 
590
633
  // Track output for streaming updates (tail only)
@@ -612,15 +655,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
612
655
  env: resolvedEnv,
613
656
  artifactPath,
614
657
  artifactId,
615
- onChunk: chunk => {
616
- tailBuffer.append(chunk);
617
- if (onUpdate) {
618
- onUpdate({
619
- content: [{ type: "text", text: tailBuffer.text() }],
620
- details: {},
621
- });
622
- }
623
- },
658
+ onChunk: streamTailUpdates(tailBuffer, onUpdate),
624
659
  });
625
660
  if (result.cancelled) {
626
661
  if (signal?.aborted) {
@@ -631,7 +666,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
631
666
  if (isInteractiveResult(result) && result.timedOut) {
632
667
  throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
633
668
  }
634
- return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines);
669
+ return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
670
+ requestedTimeoutSec,
671
+ notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
672
+ });
635
673
  }
636
674
  }
637
675
 
@@ -708,12 +746,16 @@ export const bashToolRenderer = {
708
746
 
709
747
  // Build truncation warning
710
748
  const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
711
- const timeoutLine =
749
+ const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
750
+ const timeoutLabel =
712
751
  typeof timeoutSeconds === "number"
713
- ? uiTheme.fg(
714
- "dim",
715
- `${uiTheme.format.bracketLeft}Timeout: ${timeoutSeconds}s${uiTheme.format.bracketRight}`,
716
- )
752
+ ? requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
753
+ ? `Timeout: ${timeoutSeconds}s (requested ${requestedTimeoutSeconds}s clamped)`
754
+ : `Timeout: ${timeoutSeconds}s`
755
+ : undefined;
756
+ const timeoutLine =
757
+ timeoutLabel !== undefined
758
+ ? uiTheme.fg("dim", `${uiTheme.format.bracketLeft}${timeoutLabel}${uiTheme.format.bracketRight}`)
717
759
  : undefined;
718
760
  let warningLine: string | undefined;
719
761
  if (details?.meta?.truncation && !showingFullOutput) {
@@ -411,7 +411,7 @@ const browserSchema = Type.Object({
411
411
  value: Type.Optional(Type.String({ description: "Value to set (fill)" })),
412
412
  attribute: Type.Optional(Type.String({ description: "Attribute name to read (get_attribute)" })),
413
413
  key: Type.Optional(Type.String({ description: "Keyboard key to press (press)" })),
414
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 30)" })),
414
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 30 })),
415
415
  wait_until: Type.Optional(
416
416
  StringEnum(["load", "domcontentloaded", "networkidle0", "networkidle2"], {
417
417
  description: "Navigation wait condition (goto)",
@@ -18,7 +18,7 @@ import { ensureTool } from "../utils/tools-manager";
18
18
  import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
19
19
  import { specialHandlers } from "../web/scrapers";
20
20
  import type { RenderResult } from "../web/scrapers/types";
21
- import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
21
+ import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
22
22
  import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
23
23
  import { applyListLimit } from "./list-limit";
24
24
  import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
@@ -253,19 +253,6 @@ function isInlineImageMimeTypeSupported(mimeType: string): boolean {
253
253
  return SUPPORTED_INLINE_IMAGE_MIME_TYPES.has(mimeType);
254
254
  }
255
255
 
256
- /**
257
- * Check if content looks like HTML
258
- */
259
- function looksLikeHtml(content: string): boolean {
260
- const trimmed = content.trim().toLowerCase();
261
- return (
262
- trimmed.startsWith("<!doctype") ||
263
- trimmed.startsWith("<html") ||
264
- trimmed.startsWith("<head") ||
265
- trimmed.startsWith("<body")
266
- );
267
- }
268
-
269
256
  /**
270
257
  * Try fetching URL with .md appended (llms.txt convention)
271
258
  */
@@ -0,0 +1,35 @@
1
+ import * as path from "node:path";
2
+
3
+ /**
4
+ * Creates a deduplicating recorder for relative file paths.
5
+ * Preserves insertion order in `list`; subsequent duplicates are ignored.
6
+ */
7
+ export function createFileRecorder(): {
8
+ record: (relativePath: string) => void;
9
+ list: string[];
10
+ } {
11
+ const seen = new Set<string>();
12
+ const list: string[] = [];
13
+ return {
14
+ record(relativePath: string) {
15
+ if (!seen.has(relativePath)) {
16
+ seen.add(relativePath);
17
+ list.push(relativePath);
18
+ }
19
+ },
20
+ list,
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Strip a leading slash and, when the search scope is a directory, normalize
26
+ * Windows-style separators. For single-file scopes, fall back to the basename
27
+ * so tool output does not leak absolute paths.
28
+ */
29
+ export function formatResultPath(filePath: string, isDirectory: boolean): string {
30
+ const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
31
+ if (isDirectory) {
32
+ return cleanPath.replace(/\\/g, "/");
33
+ }
34
+ return path.basename(cleanPath);
35
+ }
package/src/tools/find.ts CHANGED
@@ -29,9 +29,12 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
29
29
  import { toolResult } from "./tool-result";
30
30
 
31
31
  const findSchema = Type.Object({
32
- pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', 'src/**/*.json', 'lib/*.tsx'" }),
33
- hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories (default: true)" })),
34
- limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })),
32
+ pattern: Type.String({
33
+ description:
34
+ "Glob pattern including the search path (no separate path param), e.g. 'src/**/*.ts', 'lib/*.json'. Supports comma-separated lists like 'apps/,packages/,phases/'. Simple patterns like '*.ts' recurse from cwd.",
35
+ }),
36
+ hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories", default: true })),
37
+ limit: Type.Optional(Type.Number({ description: "Max results", default: 1000 })),
35
38
  });
36
39
 
37
40
  export type FindToolInput = Static<typeof findSchema>;
@@ -126,6 +129,19 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
126
129
  const includeHidden = hidden ?? true;
127
130
  const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
128
131
  const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
132
+ const formatMatchPath = (matchPath: string, fileType?: natives.FileType): string => {
133
+ const hadTrailingSlash = matchPath.endsWith("/") || matchPath.endsWith("\\");
134
+ const absolutePath = path.isAbsolute(matchPath) ? matchPath : path.resolve(searchPath, matchPath);
135
+ let relativePath = path.relative(this.session.cwd, absolutePath).replace(/\\/g, "/");
136
+ if (relativePath.length === 0) {
137
+ relativePath = ".";
138
+ }
139
+ if ((fileType === natives.FileType.Dir || hadTrailingSlash) && !relativePath.endsWith("/")) {
140
+ relativePath += "/";
141
+ }
142
+ return relativePath;
143
+ };
144
+
129
145
  const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
130
146
  if (files.length === 0) {
131
147
  const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
@@ -173,12 +189,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
173
189
  ignore: ["**/node_modules/**", "**/.git/**"],
174
190
  limit: effectiveLimit,
175
191
  });
176
- const relativized = results.map(p => {
177
- if (p.startsWith(searchPath)) {
178
- return p.slice(searchPath.length + 1);
179
- }
180
- return path.relative(searchPath, p);
181
- });
192
+ const relativized = results.map(p => formatMatchPath(p));
182
193
 
183
194
  return buildResult(relativized);
184
195
  }
@@ -222,12 +233,8 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
222
233
  };
223
234
  const onMatch = onUpdate
224
235
  ? (err: Error | null, match: natives.GlobMatch | null) => {
225
- if (err || signal?.aborted || !match) return;
226
- let relativePath = match.path;
227
- if (!relativePath) return;
228
- if (match.fileType === natives.FileType.Dir && !relativePath.endsWith("/")) {
229
- relativePath += "/";
230
- }
236
+ if (err || signal?.aborted || !match?.path) return;
237
+ const relativePath = formatMatchPath(match.path, match.fileType);
231
238
  onUpdateMatches.push(relativePath);
232
239
  emitUpdate();
233
240
  }
@@ -251,10 +258,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
251
258
  );
252
259
 
253
260
  try {
254
- let result = await doGlob(true);
255
- if (result.matches.length === 0 && !timeoutSignal.aborted) {
256
- result = await doGlob(false);
257
- }
261
+ const result = await doGlob(true);
258
262
  // Sort by mtime descending (most recent first) in JS instead of native.
259
263
  // This allows native glob to early-terminate at maxResults.
260
264
  result.matches.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
@@ -273,19 +277,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
273
277
  const relativized: string[] = [];
274
278
  for (const match of matches) {
275
279
  throwIfAborted(signal);
276
- const line = match.path;
277
- if (!line) {
280
+ if (!match.path) {
278
281
  continue;
279
282
  }
280
283
 
281
- const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
282
- let relativePath = line;
283
- const isDirectory = match.fileType === natives.FileType.Dir;
284
- if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
285
- relativePath += "/";
286
- }
287
-
288
- relativized.push(relativePath);
284
+ relativized.push(formatMatchPath(match.path, match.fileType));
289
285
  }
290
286
 
291
287
  return buildResult(relativized);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Return the first 12 hex characters of a commit SHA, or undefined when the
3
+ * input is missing. Shared between GitHub tool argument normalization and the
4
+ * run-watch renderer.
5
+ */
6
+ export function formatShortSha(value: string | undefined): string | undefined {
7
+ if (!value) {
8
+ return undefined;
9
+ }
10
+
11
+ return value.slice(0, 12);
12
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  GhRunWatchViewDetails,
10
10
  GhToolDetails,
11
11
  } from "./gh";
12
+ import { formatShortSha } from "./gh-format";
12
13
  import {
13
14
  formatExpandHint,
14
15
  formatStatusIcon,
@@ -29,14 +30,6 @@ const RUNNING_STATUSES = new Set(["in_progress"]);
29
30
  const PENDING_STATUSES = new Set(["queued", "requested", "waiting", "pending"]);
30
31
  const FALLBACK_WIDTH = 80;
31
32
 
32
- function formatShortSha(value: string | undefined): string | undefined {
33
- if (!value) {
34
- return undefined;
35
- }
36
-
37
- return value.slice(0, 12);
38
- }
39
-
40
33
  function getWatchHeader(watch: GhRunWatchViewDetails): string {
41
34
  if (watch.mode === "run" && watch.run) {
42
35
  if (watch.state === "watching") {
package/src/tools/gh.ts CHANGED
@@ -14,6 +14,7 @@ import ghSearchIssuesDescription from "../prompts/tools/gh-search-issues.md" wit
14
14
  import ghSearchPrsDescription from "../prompts/tools/gh-search-prs.md" with { type: "text" };
15
15
  import * as git from "../utils/git";
16
16
  import type { ToolSession } from ".";
17
+ import { formatShortSha } from "./gh-format";
17
18
  import type { OutputMeta } from "./output-meta";
18
19
  import { ToolError, throwIfAborted } from "./tool-errors";
19
20
  import { toolResult } from "./tool-result";
@@ -148,7 +149,7 @@ const ghIssueViewSchema = Type.Object({
148
149
  repo: Type.Optional(
149
150
  Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full issue URL." }),
150
151
  ),
151
- comments: Type.Optional(Type.Boolean({ description: "Include issue comments (default: true)." })),
152
+ comments: Type.Optional(Type.Boolean({ description: "Include issue comments.", default: true })),
152
153
  });
153
154
 
154
155
  const ghPrViewSchema = Type.Object({
@@ -161,7 +162,7 @@ const ghPrViewSchema = Type.Object({
161
162
  repo: Type.Optional(
162
163
  Type.String({ description: "Repository in OWNER/REPO format. Omit when passing a full pull request URL." }),
163
164
  ),
164
- comments: Type.Optional(Type.Boolean({ description: "Include pull request comments (default: true)." })),
165
+ comments: Type.Optional(Type.Boolean({ description: "Include pull request comments.", default: true })),
165
166
  });
166
167
 
167
168
  const ghPrDiffSchema = Type.Object({
@@ -217,13 +218,13 @@ const ghPrPushSchema = Type.Object({
217
218
  const ghSearchIssuesSchema = Type.Object({
218
219
  query: Type.String({ description: "GitHub issue search query. Supports GitHub search syntax." }),
219
220
  repo: Type.Optional(Type.String({ description: "Repository in OWNER/REPO format to scope the search." })),
220
- limit: Type.Optional(Type.Number({ description: "Maximum results to return (default: 10, max: 50)." })),
221
+ limit: Type.Optional(Type.Number({ description: "Maximum results to return (max: 50).", default: 10 })),
221
222
  });
222
223
 
223
224
  const ghSearchPrsSchema = Type.Object({
224
225
  query: Type.String({ description: "GitHub pull request search query. Supports GitHub search syntax." }),
225
226
  repo: Type.Optional(Type.String({ description: "Repository in OWNER/REPO format to scope the search." })),
226
- limit: Type.Optional(Type.Number({ description: "Maximum results to return (default: 10, max: 50)." })),
227
+ limit: Type.Optional(Type.Number({ description: "Maximum results to return (max: 50).", default: 10 })),
227
228
  });
228
229
 
229
230
  const ghRunWatchSchema = Type.Object({
@@ -239,7 +240,7 @@ const ghRunWatchSchema = Type.Object({
239
240
  }),
240
241
  ),
241
242
  tail: Type.Optional(
242
- Type.Number({ description: "Number of log lines to include per failed job (default: 15, max: 200)." }),
243
+ Type.Number({ description: "Number of log lines to include per failed job (max: 200).", default: 15 }),
243
244
  ),
244
245
  });
245
246
 
@@ -545,14 +546,6 @@ function normalizeOptionalString(value: string | null | undefined): string | und
545
546
  return normalized ? normalized : undefined;
546
547
  }
547
548
 
548
- function formatShortSha(value: string | undefined): string | undefined {
549
- if (!value) {
550
- return undefined;
551
- }
552
-
553
- return value.slice(0, 12);
554
- }
555
-
556
549
  function requireNonEmpty(value: string | null | undefined, label: string): string {
557
550
  const normalized = normalizeOptionalString(value);
558
551
  if (!normalized) {