@oh-my-pi/pi-coding-agent 15.0.1 → 15.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/examples/custom-tools/README.md +11 -7
  3. package/examples/custom-tools/hello/index.ts +2 -2
  4. package/examples/extensions/README.md +19 -8
  5. package/examples/extensions/api-demo.ts +15 -19
  6. package/examples/extensions/hello.ts +5 -6
  7. package/examples/extensions/plan-mode.ts +1 -1
  8. package/examples/extensions/reload-runtime.ts +4 -3
  9. package/examples/extensions/with-deps/index.ts +4 -3
  10. package/examples/sdk/06-extensions.ts +4 -2
  11. package/package.json +8 -18
  12. package/src/autoresearch/tools/init-experiment.ts +38 -41
  13. package/src/autoresearch/tools/log-experiment.ts +32 -41
  14. package/src/autoresearch/tools/run-experiment.ts +3 -3
  15. package/src/autoresearch/tools/update-notes.ts +11 -11
  16. package/src/commands/commit.ts +10 -0
  17. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  18. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  19. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  20. package/src/commit/agentic/tools/git-overview.ts +4 -4
  21. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  22. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  23. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  24. package/src/commit/agentic/tools/schemas.ts +28 -28
  25. package/src/commit/agentic/tools/split-commit.ts +22 -21
  26. package/src/commit/analysis/summary.ts +4 -4
  27. package/src/commit/changelog/generate.ts +7 -11
  28. package/src/commit/shared-llm.ts +22 -34
  29. package/src/config/config-file.ts +35 -13
  30. package/src/config/model-registry.ts +40 -191
  31. package/src/config/models-config-schema.ts +166 -0
  32. package/src/config/settings-schema.ts +29 -0
  33. package/src/discovery/claude-plugins.ts +19 -7
  34. package/src/edit/index.ts +2 -2
  35. package/src/edit/modes/apply-patch.ts +7 -6
  36. package/src/edit/modes/patch.ts +18 -25
  37. package/src/edit/modes/replace.ts +18 -20
  38. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  39. package/src/eval/py/executor.ts +233 -623
  40. package/src/eval/py/kernel.ts +27 -2
  41. package/src/eval/py/runner.py +42 -11
  42. package/src/eval/py/runtime.ts +1 -0
  43. package/src/exa/factory.ts +5 -4
  44. package/src/exa/mcp-client.ts +1 -1
  45. package/src/exa/researcher.ts +9 -20
  46. package/src/exa/search.ts +26 -52
  47. package/src/exa/types.ts +1 -1
  48. package/src/exa/websets.ts +54 -53
  49. package/src/exec/bash-executor.ts +2 -1
  50. package/src/extensibility/custom-commands/loader.ts +5 -3
  51. package/src/extensibility/custom-commands/types.ts +4 -2
  52. package/src/extensibility/custom-tools/loader.ts +5 -3
  53. package/src/extensibility/custom-tools/types.ts +7 -6
  54. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  55. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  56. package/src/extensibility/extensions/loader.ts +7 -3
  57. package/src/extensibility/extensions/types.ts +9 -5
  58. package/src/extensibility/extensions/wrapper.ts +1 -2
  59. package/src/extensibility/hooks/loader.ts +3 -1
  60. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  61. package/src/extensibility/hooks/types.ts +4 -2
  62. package/src/extensibility/plugins/legacy-pi-compat.ts +78 -31
  63. package/src/extensibility/shared-events.ts +1 -1
  64. package/src/extensibility/typebox.ts +391 -0
  65. package/src/goals/tools/goal-tool.ts +6 -12
  66. package/src/hashline/input.ts +2 -1
  67. package/src/hashline/parser.ts +27 -3
  68. package/src/hashline/types.ts +4 -4
  69. package/src/hindsight/state.ts +2 -2
  70. package/src/index.ts +0 -2
  71. package/src/internal-urls/docs-index.generated.ts +15 -15
  72. package/src/internal-urls/router.ts +8 -0
  73. package/src/internal-urls/types.ts +21 -0
  74. package/src/lsp/config.ts +15 -6
  75. package/src/lsp/defaults.json +6 -2
  76. package/src/lsp/types.ts +30 -38
  77. package/src/mcp/manager.ts +1 -1
  78. package/src/mcp/tool-bridge.ts +1 -1
  79. package/src/modes/acp/acp-agent.ts +248 -50
  80. package/src/modes/components/session-observer-overlay.ts +12 -1
  81. package/src/modes/components/status-line/segments.ts +39 -4
  82. package/src/modes/controllers/command-controller.ts +27 -2
  83. package/src/modes/controllers/event-controller.ts +3 -4
  84. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  85. package/src/modes/interactive-mode.ts +1 -1
  86. package/src/modes/rpc/host-tools.ts +1 -1
  87. package/src/modes/rpc/host-uris.ts +235 -0
  88. package/src/modes/rpc/rpc-client.ts +1 -1
  89. package/src/modes/rpc/rpc-mode.ts +27 -1
  90. package/src/modes/rpc/rpc-types.ts +58 -1
  91. package/src/modes/runtime-init.ts +2 -1
  92. package/src/modes/theme/defaults/dark-poimandres.json +1 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +1 -0
  94. package/src/modes/theme/theme.ts +117 -117
  95. package/src/modes/types.ts +1 -1
  96. package/src/modes/utils/context-usage.ts +2 -2
  97. package/src/prompts/tools/github.md +4 -4
  98. package/src/prompts/tools/hashline.md +22 -26
  99. package/src/prompts/tools/read.md +55 -37
  100. package/src/sdk.ts +31 -8
  101. package/src/session/agent-session.ts +74 -104
  102. package/src/session/messages.ts +16 -51
  103. package/src/session/session-manager.ts +22 -2
  104. package/src/session/streaming-output.ts +16 -6
  105. package/src/task/discovery.ts +5 -2
  106. package/src/task/executor.ts +210 -87
  107. package/src/task/index.ts +15 -11
  108. package/src/task/render.ts +32 -5
  109. package/src/task/types.ts +54 -39
  110. package/src/tools/ask.ts +12 -12
  111. package/src/tools/ast-edit.ts +11 -15
  112. package/src/tools/ast-grep.ts +9 -10
  113. package/src/tools/bash-command-fixup.ts +47 -0
  114. package/src/tools/bash.ts +48 -38
  115. package/src/tools/browser/render.ts +2 -2
  116. package/src/tools/browser.ts +39 -53
  117. package/src/tools/calculator.ts +12 -11
  118. package/src/tools/checkpoint.ts +7 -7
  119. package/src/tools/debug.ts +40 -43
  120. package/src/tools/eval.ts +16 -10
  121. package/src/tools/find.ts +10 -13
  122. package/src/tools/gh.ts +108 -132
  123. package/src/tools/hindsight-recall.ts +4 -6
  124. package/src/tools/hindsight-reflect.ts +5 -5
  125. package/src/tools/hindsight-retain.ts +15 -17
  126. package/src/tools/image-gen.ts +31 -81
  127. package/src/tools/index.ts +4 -1
  128. package/src/tools/inspect-image.ts +8 -9
  129. package/src/tools/irc.ts +15 -27
  130. package/src/tools/job.ts +30 -28
  131. package/src/tools/output-meta.ts +26 -0
  132. package/src/tools/read.ts +39 -12
  133. package/src/tools/recipe/index.ts +7 -9
  134. package/src/tools/render-mermaid.ts +12 -12
  135. package/src/tools/report-tool-issue.ts +4 -4
  136. package/src/tools/resolve.ts +11 -11
  137. package/src/tools/review.ts +14 -26
  138. package/src/tools/search-tool-bm25.ts +7 -9
  139. package/src/tools/search.ts +19 -22
  140. package/src/tools/ssh.ts +10 -9
  141. package/src/tools/todo-write.ts +26 -34
  142. package/src/tools/vim.ts +10 -26
  143. package/src/tools/write.ts +25 -5
  144. package/src/tools/yield.ts +100 -54
  145. package/src/web/search/index.ts +9 -24
  146. package/src/web/search/providers/anthropic.ts +5 -0
  147. package/src/web/search/providers/exa.ts +3 -0
  148. package/src/web/search/providers/gemini.ts +5 -0
  149. package/src/web/search/providers/jina.ts +5 -2
  150. package/src/web/search/providers/zai.ts +5 -2
  151. package/src/prompts/compaction/branch-summary-context.md +0 -5
  152. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  153. package/src/prompts/compaction/branch-summary.md +0 -30
  154. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  155. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  156. package/src/prompts/compaction/compaction-summary.md +0 -38
  157. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  158. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  159. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  160. package/src/prompts/system/file-operations.md +0 -10
  161. package/src/prompts/system/handoff-document.md +0 -49
  162. package/src/prompts/system/summarization-system.md +0 -3
  163. package/src/session/compaction/branch-summarization.ts +0 -324
  164. package/src/session/compaction/compaction.ts +0 -1420
  165. package/src/session/compaction/errors.ts +0 -31
  166. package/src/session/compaction/index.ts +0 -8
  167. package/src/session/compaction/pruning.ts +0 -91
  168. package/src/session/compaction/utils.ts +0 -184
package/src/tools/job.ts CHANGED
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { type Static, Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
@@ -22,28 +22,21 @@ import {
22
22
  } from "./render-utils";
23
23
  import { ToolError } from "./tool-errors";
24
24
 
25
- const jobSchema = Type.Object({
26
- poll: Type.Optional(
27
- Type.Array(Type.String(), {
28
- description: "background job ids to wait for; omit (with no `cancel`) to wait on all running jobs",
29
- examples: [["job-1234"]],
30
- }),
31
- ),
32
- cancel: Type.Optional(
33
- Type.Array(Type.String(), {
34
- description: "background job ids to cancel",
35
- examples: [["job-1234"]],
36
- }),
37
- ),
38
- list: Type.Optional(
39
- Type.Boolean({
40
- description:
41
- "Return an immediate snapshot of every job spawned by this agent (running + completed within retention). Read-only \u2014 cannot be combined with `poll` or `cancel`.",
42
- }),
43
- ),
25
+ const jobSchema = z.object({
26
+ poll: z
27
+ .array(z.string())
28
+ .optional()
29
+ .describe("background job ids to wait for; omit (with no `cancel`) to wait on all running jobs"),
30
+ cancel: z.array(z.string()).optional().describe("background job ids to cancel"),
31
+ list: z
32
+ .boolean()
33
+ .optional()
34
+ .describe(
35
+ "Return an immediate snapshot of every job spawned by this agent (running + completed within retention). Read-only \u2014 cannot be combined with `poll` or `cancel`.",
36
+ ),
44
37
  });
45
38
 
46
- type JobParams = Static<typeof jobSchema>;
39
+ type JobParams = z.infer<typeof jobSchema>;
47
40
 
48
41
  const WAIT_DURATION_MS: Record<string, number> = {
49
42
  "5s": 5_000,
@@ -362,6 +355,8 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
362
355
  const LABEL_MAX_WIDTH = 60;
363
356
  const PREVIEW_LINES_COLLAPSED = 1;
364
357
  const PREVIEW_LINES_EXPANDED = 4;
358
+ const LABEL_LINES_COLLAPSED = 1;
359
+ const LABEL_LINES_EXPANDED = 3;
365
360
  const PREVIEW_LINE_WIDTH = 80;
366
361
 
367
362
  function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
@@ -488,14 +483,21 @@ export const jobToolRenderer = {
488
483
  );
489
484
  const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
490
485
  const idText = uiTheme.fg("muted", job.id);
491
- const label = truncateToWidth(
492
- replaceTabs(job.label || "(no label)"),
493
- LABEL_MAX_WIDTH,
494
- Ellipsis.Unicode,
495
- );
496
- const labelText = uiTheme.fg("toolOutput", label);
486
+ const rawLabelLines = (job.label || "(no label)").split(/\r?\n/);
487
+ const maxLabelLines = expanded ? LABEL_LINES_EXPANDED : LABEL_LINES_COLLAPSED;
488
+ const visibleLabelLines = rawLabelLines
489
+ .slice(0, maxLabelLines)
490
+ .map(l => truncateToWidth(replaceTabs(l), LABEL_MAX_WIDTH, Ellipsis.Unicode));
491
+ if (rawLabelLines.length > maxLabelLines && visibleLabelLines.length > 0) {
492
+ const last = visibleLabelLines[visibleLabelLines.length - 1]!;
493
+ visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
494
+ }
497
495
  const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
498
- lines.push(`${icon} ${idText} ${typeBadge} ${labelText} ${durationText}`);
496
+ const headLabel = uiTheme.fg("toolOutput", visibleLabelLines[0] ?? "");
497
+ lines.push(`${icon} ${idText} ${typeBadge} ${headLabel} ${durationText}`);
498
+ for (let i = 1; i < visibleLabelLines.length; i++) {
499
+ lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
500
+ }
499
501
 
500
502
  const preview = job.errorText?.trim() || job.resultText?.trim();
501
503
  if (preview) {
@@ -489,6 +489,32 @@ export function formatStyledTruncationWarning(meta: OutputMeta | undefined, them
489
489
  return theme.fg("warning", wrapBrackets(message, theme));
490
490
  }
491
491
 
492
+ /**
493
+ * Strip the trailing notice that {@link appendOutputNotice} bakes into the
494
+ * LLM-facing content body. Renderers should call this before printing
495
+ * `result.content` text in the TUI, because they emit a styled warning line of
496
+ * their own; without this, users see the same `[Showing lines …]` string twice
497
+ * (once verbatim from the body, once as the styled `⟨…⟩` warning).
498
+ *
499
+ * Safe to call eagerly: returns the input unchanged when no notice is present
500
+ * (e.g. during streaming, before {@link wrappedExecute} runs).
501
+ */
502
+ export function stripOutputNotice(text: string, meta: OutputMeta | undefined): string {
503
+ const notice = formatOutputNotice(meta);
504
+ if (!notice) return text;
505
+ // Trim trailing whitespace from `text` and from the notice itself so we
506
+ // match regardless of whether: (a) the caller already trimEnd()'d, (b)
507
+ // extra blank lines slipped in after the notice (diagnostics blocks add
508
+ // `\n\n` between sections, OutputSink may pad), or (c) neither. Returns
509
+ // the prefix before the notice so the caller can re-trim as needed.
510
+ const trimmedText = text.trimEnd();
511
+ const trimmedNotice = notice.trimEnd();
512
+ if (trimmedText.endsWith(trimmedNotice)) {
513
+ return trimmedText.slice(0, -trimmedNotice.length);
514
+ }
515
+ return text;
516
+ }
517
+
492
518
  // =============================================================================
493
519
  // Tool wrapper
494
520
  // =============================================================================
package/src/tools/read.ts CHANGED
@@ -7,7 +7,7 @@ import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { Text } from "@oh-my-pi/pi-tui";
9
9
  import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
10
- import { type Static, Type } from "@sinclair/typebox";
10
+ import * as z from "zod/v4";
11
11
  import { getFileReadCache } from "../edit/file-read-cache";
12
12
  import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
13
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -60,6 +60,7 @@ import {
60
60
  formatStyledTruncationWarning,
61
61
  type OutputMeta,
62
62
  resolveOutputMaxColumns,
63
+ stripOutputNotice,
63
64
  } from "./output-meta";
64
65
  import { expandPath, formatPathRelativeToCwd, resolveReadPath, splitPathAndSel } from "./path-utils";
65
66
  import { formatBytes, replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
@@ -172,6 +173,19 @@ function countTextLines(text: string): number {
172
173
  if (text.length === 0) return 0;
173
174
  return text.split("\n").length;
174
175
  }
176
+
177
+ /**
178
+ * Footer appended to summarized reads telling the model how to recover the
179
+ * elided body. Without this hint, agents either ignore the `...`/`{ .. }`
180
+ * markers or burn a turn guessing the right selector (see issue #1046).
181
+ */
182
+ function formatSummaryElisionFooter(readPath: string, elidedSpans: number, elidedLines: number): string {
183
+ if (elidedSpans <= 0) return "";
184
+ const spanWord = elidedSpans === 1 ? "region" : "regions";
185
+ const lineWord = elidedLines === 1 ? "line" : "lines";
186
+ const linePart = elidedLines > 0 ? `${elidedLines} ${lineWord} across ` : "";
187
+ return `[${linePart}${elidedSpans} elided ${spanWord}; read ${readPath}:raw or a line range like ${readPath}:1-9999 for verbatim content]`;
188
+ }
175
189
  const READ_CHUNK_SIZE = 8 * 1024;
176
190
 
177
191
  /**
@@ -459,14 +473,13 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
459
473
  return text ? `${notice}\n${text}` : notice;
460
474
  }
461
475
 
462
- const readSchema = Type.Object({
463
- path: Type.String({
464
- description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
465
- examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com/:1-40"],
466
- }),
467
- });
476
+ const readSchema = z
477
+ .object({
478
+ path: z.string().describe('path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")'),
479
+ })
480
+ .strict();
468
481
 
469
- export type ReadToolInput = Static<typeof readSchema>;
482
+ export type ReadToolInput = z.infer<typeof readSchema>;
470
483
 
471
484
  export interface ReadToolDetails {
472
485
  kind?: "file" | "url";
@@ -484,7 +497,7 @@ export interface ReadToolDetails {
484
497
  * Mirrors the same lines the model receives but without hashline/line-number prefixes,
485
498
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
486
499
  displayContent?: { text: string; startLine: number };
487
- summary?: { lines: number; elidedSpans: number };
500
+ summary?: { lines: number; elidedSpans: number; elidedLines: number };
488
501
  /** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
489
502
  conflictCount?: number;
490
503
  }
@@ -1317,6 +1330,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1317
1330
  text: string;
1318
1331
  displayText: string;
1319
1332
  elidedSpans: number;
1333
+ elidedLines: number;
1320
1334
  } {
1321
1335
  const displayMode = resolveFileDisplayMode(this.session);
1322
1336
  const shouldAddHashLines = displayMode.hashLines;
@@ -1377,11 +1391,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1377
1391
  const modelParts: string[] = [];
1378
1392
  const displayParts: string[] = [];
1379
1393
  let elidedSpans = 0;
1394
+ let elidedLines = 0;
1380
1395
  for (const unit of units) {
1381
1396
  if (unit.kind === "elided") {
1382
1397
  modelParts.push("...");
1383
1398
  displayParts.push("...");
1384
1399
  elidedSpans++;
1400
+ elidedLines += unit.endLine - unit.startLine + 1;
1385
1401
  continue;
1386
1402
  }
1387
1403
  if (unit.kind === "merged") {
@@ -1396,13 +1412,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1396
1412
  modelParts.push(formatted.model);
1397
1413
  displayParts.push(formatted.display);
1398
1414
  elidedSpans++;
1415
+ // Merged brace pair encloses (start+1)..(end-1) as elided.
1416
+ elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
1399
1417
  continue;
1400
1418
  }
1401
1419
  modelParts.push(formatSingleLine(unit.line, unit.text, shouldAddHashLines, shouldAddLineNumbers));
1402
1420
  displayParts.push(unit.text);
1403
1421
  }
1404
1422
 
1405
- return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedSpans };
1423
+ return { text: modelParts.join("\n"), displayText: displayParts.join("\n"), elidedSpans, elidedLines };
1406
1424
  }
1407
1425
 
1408
1426
  async execute(
@@ -1646,16 +1664,23 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1646
1664
  const summary = await this.#trySummarize(absolutePath, fileSize, signal);
1647
1665
  if (summary?.parsed && summary.elided) {
1648
1666
  const renderedSummary = this.#renderSummary(summary);
1667
+ const footer = formatSummaryElisionFooter(
1668
+ localReadPath,
1669
+ renderedSummary.elidedSpans,
1670
+ renderedSummary.elidedLines,
1671
+ );
1672
+ const modelText = footer ? `${renderedSummary.text}\n\n${footer}` : renderedSummary.text;
1649
1673
  details = {
1650
1674
  displayContent: { text: renderedSummary.displayText, startLine: 1 },
1651
1675
  summary: {
1652
1676
  lines: countTextLines(renderedSummary.text),
1653
1677
  elidedSpans: renderedSummary.elidedSpans,
1678
+ elidedLines: renderedSummary.elidedLines,
1654
1679
  },
1655
1680
  };
1656
1681
 
1657
1682
  sourcePath = absolutePath;
1658
- content = [{ type: "text", text: renderedSummary.text }];
1683
+ content = [{ type: "text", text: modelText }];
1659
1684
  }
1660
1685
  }
1661
1686
 
@@ -2169,7 +2194,9 @@ export const readToolRenderer = {
2169
2194
  const rawText = result.content?.find(c => c.type === "text")?.text ?? "";
2170
2195
  // Prefer structured `displayContent` from details when available so the TUI
2171
2196
  // shows clean file content (no model-only hashline anchors) without parsing the formatted text.
2172
- const contentText = details?.displayContent?.text ?? rawText;
2197
+ // Fall back to the raw text, but strip the LLM-facing notice so it doesn't
2198
+ // echo next to the styled warning line below.
2199
+ const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
2173
2200
  const imageContent = result.content?.find(c => c.type === "image");
2174
2201
  const rawPath = args?.file_path || args?.path || "";
2175
2202
  const filePath = shortenPath(rawPath);
@@ -1,7 +1,7 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { prompt } from "@oh-my-pi/pi-utils";
4
- import { type Static, Type } from "@sinclair/typebox";
4
+ import * as z from "zod/v4";
5
5
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
6
6
  import type { Theme } from "../../modes/theme/theme";
7
7
  import recipeDescription from "../../prompts/tools/recipe.md" with { type: "text" };
@@ -11,14 +11,12 @@ import { createRecipeToolRenderer, type RecipeRenderArgs } from "./render";
11
11
  import { buildPromptModel, type DetectedRunner, resolveCommand } from "./runner";
12
12
  import { RUNNERS } from "./runners";
13
13
 
14
- const recipeSchema = Type.Object({
15
- op: Type.String({
16
- description: 'task name and args, e.g. "test" or "build --release"',
17
- examples: ["test", "build --release", "pkg:test --watch"],
18
- }),
19
- });
20
-
21
- type RecipeParams = Static<typeof recipeSchema>;
14
+ const recipeSchema = z
15
+ .object({
16
+ op: z.string().describe('task name and args, e.g. "test" or "build --release"'),
17
+ })
18
+ .strict();
19
+ type RecipeParams = z.infer<typeof recipeSchema>;
22
20
 
23
21
  type RecipeRenderResult = {
24
22
  content: Array<{ type: string; text?: string }>;
@@ -1,22 +1,22 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { type MermaidAsciiRenderOptions, prompt, renderMermaidAscii } from "@oh-my-pi/pi-utils";
3
- import { type Static, Type } from "@sinclair/typebox";
3
+ import * as z from "zod/v4";
4
4
  import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
5
5
  import type { ToolSession } from "./index";
6
6
 
7
- const renderMermaidSchema = Type.Object({
8
- mermaid: Type.String({ description: "mermaid source", examples: ["graph TD; A-->B"] }),
9
- config: Type.Optional(
10
- Type.Object({
11
- useAscii: Type.Optional(Type.Boolean()),
12
- paddingX: Type.Optional(Type.Number()),
13
- paddingY: Type.Optional(Type.Number()),
14
- boxBorderPadding: Type.Optional(Type.Number()),
15
- }),
16
- ),
7
+ const renderMermaidSchema = z.object({
8
+ mermaid: z.string().describe("mermaid source"),
9
+ config: z
10
+ .object({
11
+ useAscii: z.boolean().optional(),
12
+ paddingX: z.number().optional(),
13
+ paddingY: z.number().optional(),
14
+ boxBorderPadding: z.number().optional(),
15
+ })
16
+ .optional(),
17
17
  });
18
18
 
19
- type RenderMermaidParams = Static<typeof renderMermaidSchema>;
19
+ type RenderMermaidParams = z.infer<typeof renderMermaidSchema>;
20
20
 
21
21
  function sanitizeRenderConfig(config: MermaidAsciiRenderOptions | undefined): MermaidAsciiRenderOptions | undefined {
22
22
  if (!config) return undefined;
@@ -9,13 +9,13 @@ import { Database } from "bun:sqlite";
9
9
  import path from "node:path";
10
10
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
11
11
  import { $flag, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
12
- import { Type } from "@sinclair/typebox";
12
+ import * as z from "zod/v4";
13
13
  import type { Settings } from "..";
14
14
  import type { ToolSession } from "./index";
15
15
 
16
- const ReportToolIssueParams = Type.Object({
17
- tool: Type.String({ description: "tool name", examples: ["bash", "read"] }),
18
- report: Type.String({ description: "unexpected behavior" }),
16
+ const ReportToolIssueParams = z.object({
17
+ tool: z.string().describe("tool name"),
18
+ report: z.string().describe("unexpected behavior"),
19
19
  });
20
20
 
21
21
  export function isAutoQaEnabled(settings?: Settings): boolean {
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
- import { type Static, Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
7
7
  import type { Theme } from "../modes/theme/theme";
8
8
  import resolveDescription from "../prompts/tools/resolve.md" with { type: "text" };
@@ -11,18 +11,18 @@ import type { ToolSession } from ".";
11
11
  import { replaceTabs } from "./render-utils";
12
12
  import { ToolError } from "./tool-errors";
13
13
 
14
- const resolveSchema = Type.Object({
15
- action: Type.Union([Type.Literal("apply"), Type.Literal("discard")]),
16
- reason: Type.String({ description: "reason for action", examples: ["approved by user"] }),
17
- extra: Type.Optional(
18
- Type.Record(Type.String(), Type.Unknown(), {
19
- description:
20
- 'Free-form metadata interpreted by the resolving tool (e.g. plan-mode approval requires `{ title: "<PLAN_TITLE>" }`).',
21
- }),
22
- ),
14
+ const resolveSchema = z.object({
15
+ action: z.union([z.literal("apply"), z.literal("discard")]),
16
+ reason: z.string().describe("reason for action"),
17
+ extra: z
18
+ .record(z.string(), z.unknown())
19
+ .optional()
20
+ .describe(
21
+ 'Free-form metadata interpreted by the resolving tool (e.g. plan-mode approval requires `{ title: "<PLAN_TITLE>" }`).',
22
+ ),
23
23
  });
24
24
 
25
- type ResolveParams = Static<typeof resolveSchema>;
25
+ type ResolveParams = z.infer<typeof resolveSchema>;
26
26
 
27
27
  export interface ResolveToolDetails {
28
28
  action: "apply" | "discard";
@@ -6,18 +6,15 @@
6
6
  * Reviewers finish via `yield` tool with SubmitReviewDetails schema.
7
7
  */
8
8
  // ─────────────────────────────────────────────────────────────────────────────
9
- // Subprocess tool handlers - registered for extraction/rendering in task tool
10
- // ─────────────────────────────────────────────────────────────────────────────
9
+
11
10
  import path from "node:path";
12
11
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
13
- import { StringEnum } from "@oh-my-pi/pi-ai";
14
12
  import type { Component } from "@oh-my-pi/pi-tui";
15
13
  import { Container, Text } from "@oh-my-pi/pi-tui";
16
14
  import { isRecord } from "@oh-my-pi/pi-utils";
17
- import { Type } from "@sinclair/typebox";
15
+ import * as z from "zod/v4";
18
16
  import type { Theme, ThemeColor } from "../modes/theme/theme";
19
17
  import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
20
-
21
18
  export type FindingPriority = "P0" | "P1" | "P2" | "P3";
22
19
 
23
20
  export interface FindingPriorityInfo {
@@ -53,27 +50,18 @@ function getPriorityDisplay(
53
50
  }
54
51
 
55
52
  // report_finding schema
56
- const ReportFindingParams = Type.Object({
57
- title: Type.String({
58
- description: "prefixed imperative title",
59
- examples: ["[P1] un-padding wrong dimension"],
60
- }),
61
- body: Type.String({
62
- description: "problem explanation",
63
- }),
64
- priority: StringEnum(["P0", "P1", "P2", "P3"], {
65
- description: "priority 0-3",
66
- }),
67
- confidence: Type.Number({
68
- minimum: 0,
69
- maximum: 1,
70
- description: "confidence score",
71
- examples: [0.0, 0.5, 1.0],
72
- }),
73
- file_path: Type.String({ description: "file path" }),
74
- line_start: Type.Number({ description: "start line" }),
75
- line_end: Type.Number({ description: "end line" }),
76
- });
53
+ // report_finding schema
54
+ const ReportFindingParams = z
55
+ .object({
56
+ title: z.string().describe("prefixed imperative title"),
57
+ body: z.string().describe("problem explanation"),
58
+ priority: z.enum(["P0", "P1", "P2", "P3"] as const).describe("priority 0-3"),
59
+ confidence: z.number().min(0).max(1).describe("confidence score"),
60
+ file_path: z.string().describe("file path"),
61
+ line_start: z.number().describe("start line"),
62
+ line_end: z.number().describe("end line"),
63
+ })
64
+ .strict();
77
65
 
78
66
  interface ReportFindingDetails {
79
67
  title: string;
@@ -1,7 +1,8 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { type Component, Text } from "@oh-my-pi/pi-tui";
2
+ import type { Component } from "@oh-my-pi/pi-tui";
3
+ import { Text } from "@oh-my-pi/pi-tui";
3
4
  import { prompt } from "@oh-my-pi/pi-utils";
4
- import { type Static, Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
5
6
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
6
7
  import type { Theme } from "../modes/theme/theme";
7
8
  import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
@@ -30,15 +31,12 @@ const COLLAPSED_MATCH_LIMIT = 5;
30
31
  const MATCH_LABEL_LEN = 72;
31
32
  const MATCH_DESCRIPTION_LEN = 96;
32
33
 
33
- const searchToolBm25Schema = Type.Object({
34
- query: Type.String({
35
- description: "tool search query",
36
- examples: ["kubernetes pod", "image processing", "git commit"],
37
- }),
38
- limit: Type.Optional(Type.Integer({ description: "max matches", minimum: 1 })),
34
+ const searchToolBm25Schema = z.object({
35
+ query: z.string().describe("tool search query"),
36
+ limit: z.number().int().min(1).optional().describe("max matches"),
39
37
  });
40
38
 
41
- type SearchToolBm25Params = Static<typeof searchToolBm25Schema>;
39
+ type SearchToolBm25Params = z.infer<typeof searchToolBm25Schema>;
42
40
 
43
41
  interface SearchToolBm25Match {
44
42
  name: string;
@@ -1,11 +1,10 @@
1
1
  import * as path from "node:path";
2
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
-
4
3
  import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi/pi-natives";
5
4
  import type { Component } from "@oh-my-pi/pi-tui";
6
5
  import { Text } from "@oh-my-pi/pi-tui";
7
6
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
8
- import { type Static, Type } from "@sinclair/typebox";
7
+ import * as z from "zod/v4";
9
8
  import { getFileReadCache } from "../edit/file-read-cache";
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
10
  import type { Theme } from "../modes/theme/theme";
@@ -31,25 +30,23 @@ import {
31
30
  import { ToolError } from "./tool-errors";
32
31
  import { toolResult } from "./tool-result";
33
32
 
34
- const searchSchema = Type.Object({
35
- pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
36
- paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to search" }), {
37
- minItems: 1,
38
- description: "files, directories, globs, or internal URLs to search",
39
- examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
40
- }),
41
- i: Type.Optional(Type.Boolean({ description: "case-insensitive search", default: false })),
42
- gitignore: Type.Optional(Type.Boolean({ description: "respect gitignore", default: true })),
43
- skip: Type.Optional(
44
- Type.Number({
45
- description:
46
- "files to skip before collecting results — use to paginate when the prior call hit the file limit",
47
- default: 0,
48
- }),
49
- ),
50
- });
51
-
52
- export type SearchToolInput = Static<typeof searchSchema>;
33
+ const searchSchema = z
34
+ .object({
35
+ pattern: z.string().describe("regex pattern"),
36
+ paths: z
37
+ .array(z.string().describe("file, directory, glob, or internal URL to search"))
38
+ .min(1)
39
+ .describe("files, directories, globs, or internal URLs to search"),
40
+ i: z.boolean().optional().describe("case-insensitive search"),
41
+ gitignore: z.boolean().optional().describe("respect gitignore"),
42
+ skip: z
43
+ .number()
44
+ .optional()
45
+ .describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
46
+ })
47
+ .strict();
48
+
49
+ export type SearchToolInput = z.infer<typeof searchSchema>;
53
50
 
54
51
  /** Maximum number of distinct files surfaced in a single response. The
55
52
  * agent paginates further pages via `skip`. */
@@ -89,7 +86,7 @@ export interface SearchToolDetails {
89
86
  missingPaths?: string[];
90
87
  }
91
88
 
92
- type SearchParams = Static<typeof searchSchema>;
89
+ type SearchParams = z.infer<typeof searchSchema>;
93
90
 
94
91
  export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
95
92
  readonly name = "search";
package/src/tools/ssh.ts CHANGED
@@ -2,7 +2,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { type Static, Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import type { SSHHost } from "../capability/ssh";
7
7
  import { sshCapability } from "../capability/ssh";
8
8
  import { loadCapability } from "../discovery";
@@ -16,16 +16,16 @@ import { executeSSH } from "../ssh/ssh-executor";
16
16
  import { renderStatusLine } from "../tui";
17
17
  import { CachedOutputBlock } from "../tui/output-block";
18
18
  import type { ToolSession } from ".";
19
- import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
19
+ import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
20
20
  import { ToolError } from "./tool-errors";
21
21
  import { toolResult } from "./tool-result";
22
22
  import { clampTimeout } from "./tool-timeouts";
23
23
 
24
- const sshSchema = Type.Object({
25
- host: Type.String({ description: "ssh host", examples: ["my-server", "prod-1"] }),
26
- command: Type.String({ description: "remote command", examples: ["ls -la", "uptime"] }),
27
- cwd: Type.Optional(Type.String({ description: "remote working directory", examples: ["/var/log"] })),
28
- timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 60 })),
24
+ const sshSchema = z.object({
25
+ host: z.string().describe("ssh host"),
26
+ command: z.string().describe("remote command"),
27
+ cwd: z.string().optional().describe("remote working directory"),
28
+ timeout: z.number().optional().describe("timeout in seconds").default(60),
29
29
  });
30
30
 
31
31
  export interface SSHToolDetails {
@@ -116,7 +116,7 @@ async function loadHosts(session: ToolSession): Promise<{
116
116
  return { hostNames, hostsByName };
117
117
  }
118
118
 
119
- type SshToolParams = Static<typeof sshSchema>;
119
+ type SshToolParams = z.infer<typeof sshSchema>;
120
120
 
121
121
  export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
122
122
  readonly name = "ssh";
@@ -253,7 +253,8 @@ export const sshToolRenderer = {
253
253
  render: (width: number): string[] => {
254
254
  // REACTIVE: read mutable options at render time
255
255
  const { expanded, renderContext } = options;
256
- const output = textContent.trimEnd();
256
+ // Strip LLM-facing notice so we don't echo it next to the styled warning.
257
+ const output = stripOutputNotice(textContent, details?.meta).trimEnd();
257
258
  const outputLines: string[] = [];
258
259
 
259
260
  if (output) {