@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.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 (117) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/package.json +7 -7
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  5. package/src/config/model-registry.ts +67 -15
  6. package/src/config/prompt-templates.ts +5 -5
  7. package/src/config/settings-schema.ts +4 -4
  8. package/src/cursor.ts +3 -8
  9. package/src/discovery/helpers.ts +3 -3
  10. package/src/edit/diff.ts +50 -47
  11. package/src/edit/index.ts +86 -57
  12. package/src/edit/line-hash.ts +735 -19
  13. package/src/edit/modes/apply-patch.ts +0 -9
  14. package/src/edit/modes/atom.ts +658 -0
  15. package/src/edit/modes/chunk.ts +14 -24
  16. package/src/edit/modes/hashline.ts +188 -136
  17. package/src/edit/modes/patch.ts +5 -9
  18. package/src/edit/modes/replace.ts +6 -11
  19. package/src/edit/renderer.ts +14 -10
  20. package/src/edit/streaming.ts +50 -16
  21. package/src/exec/bash-executor.ts +2 -4
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +4 -12
  24. package/src/extensibility/custom-tools/types.ts +2 -0
  25. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/lsp/index.ts +1 -1
  28. package/src/mcp/render.ts +1 -8
  29. package/src/modes/components/assistant-message.ts +4 -0
  30. package/src/modes/components/diff.ts +23 -14
  31. package/src/modes/components/footer.ts +21 -16
  32. package/src/modes/components/settings-defs.ts +6 -1
  33. package/src/modes/components/todo-reminder.ts +1 -8
  34. package/src/modes/components/tool-execution.ts +1 -4
  35. package/src/modes/controllers/selector-controller.ts +1 -1
  36. package/src/modes/print-mode.ts +8 -0
  37. package/src/prompts/agents/librarian.md +1 -1
  38. package/src/prompts/agents/reviewer.md +4 -4
  39. package/src/prompts/ci-green-request.md +1 -1
  40. package/src/prompts/review-request.md +1 -1
  41. package/src/prompts/system/subagent-system-prompt.md +3 -3
  42. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  43. package/src/prompts/system/system-prompt.md +3 -0
  44. package/src/prompts/tools/ask.md +3 -2
  45. package/src/prompts/tools/ast-edit.md +15 -19
  46. package/src/prompts/tools/ast-grep.md +18 -24
  47. package/src/prompts/tools/atom.md +96 -0
  48. package/src/prompts/tools/chunk-edit.md +37 -161
  49. package/src/prompts/tools/debug.md +4 -5
  50. package/src/prompts/tools/exit-plan-mode.md +4 -5
  51. package/src/prompts/tools/find.md +4 -8
  52. package/src/prompts/tools/github.md +18 -0
  53. package/src/prompts/tools/grep.md +4 -5
  54. package/src/prompts/tools/hashline.md +22 -89
  55. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  56. package/src/prompts/tools/inspect-image.md +6 -6
  57. package/src/prompts/tools/lsp.md +1 -1
  58. package/src/prompts/tools/patch.md +12 -19
  59. package/src/prompts/tools/python.md +3 -2
  60. package/src/prompts/tools/read-chunk.md +2 -3
  61. package/src/prompts/tools/read.md +2 -2
  62. package/src/prompts/tools/ssh.md +8 -17
  63. package/src/prompts/tools/todo-write.md +54 -41
  64. package/src/sdk.ts +14 -9
  65. package/src/session/agent-session.ts +25 -2
  66. package/src/task/executor.ts +43 -48
  67. package/src/task/render.ts +11 -13
  68. package/src/tools/ask.ts +7 -7
  69. package/src/tools/ast-edit.ts +45 -41
  70. package/src/tools/ast-grep.ts +77 -85
  71. package/src/tools/bash.ts +8 -9
  72. package/src/tools/browser.ts +32 -30
  73. package/src/tools/calculator.ts +4 -4
  74. package/src/tools/cancel-job.ts +1 -1
  75. package/src/tools/checkpoint.ts +2 -2
  76. package/src/tools/debug.ts +41 -37
  77. package/src/tools/exit-plan-mode.ts +1 -1
  78. package/src/tools/find.ts +4 -4
  79. package/src/tools/gh-renderer.ts +12 -4
  80. package/src/tools/gh.ts +509 -697
  81. package/src/tools/grep.ts +115 -130
  82. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  83. package/src/tools/index.ts +14 -32
  84. package/src/tools/inspect-image.ts +3 -3
  85. package/src/tools/json-tree.ts +114 -114
  86. package/src/tools/match-line-format.ts +9 -8
  87. package/src/tools/notebook.ts +8 -7
  88. package/src/tools/poll-tool.ts +2 -1
  89. package/src/tools/python.ts +9 -23
  90. package/src/tools/read.ts +32 -21
  91. package/src/tools/render-mermaid.ts +1 -1
  92. package/src/tools/render-utils.ts +18 -0
  93. package/src/tools/renderers.ts +2 -2
  94. package/src/tools/report-tool-issue.ts +3 -2
  95. package/src/tools/resolve.ts +1 -1
  96. package/src/tools/review.ts +12 -10
  97. package/src/tools/search-tool-bm25.ts +2 -4
  98. package/src/tools/ssh.ts +4 -4
  99. package/src/tools/todo-write.ts +172 -147
  100. package/src/tools/vim.ts +14 -15
  101. package/src/tools/write.ts +4 -4
  102. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  103. package/src/utils/edit-mode.ts +2 -1
  104. package/src/utils/file-display-mode.ts +10 -5
  105. package/src/utils/git.ts +9 -5
  106. package/src/utils/shell-snapshot.ts +2 -3
  107. package/src/vim/render.ts +4 -4
  108. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  109. package/src/prompts/tools/gh-issue-view.md +0 -11
  110. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  111. package/src/prompts/tools/gh-pr-diff.md +0 -12
  112. package/src/prompts/tools/gh-pr-push.md +0 -12
  113. package/src/prompts/tools/gh-pr-view.md +0 -11
  114. package/src/prompts/tools/gh-repo-view.md +0 -11
  115. package/src/prompts/tools/gh-run-watch.md +0 -12
  116. package/src/prompts/tools/gh-search-issues.md +0 -11
  117. package/src/prompts/tools/gh-search-prs.md +0 -11
@@ -18,8 +18,8 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
18
18
  import type { Skill } from "../extensibility/skills";
19
19
  import { callTool } from "../mcp/client";
20
20
  import type { MCPManager } from "../mcp/manager";
21
- import submitReminderTemplate from "../prompts/system/subagent-submit-reminder.md" with { type: "text" };
22
21
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
22
+ import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
23
23
  import { createAgentSession, discoverAuthStorage } from "../sdk";
24
24
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
25
25
  import type { AuthStorage } from "../session/auth-storage";
@@ -223,7 +223,7 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
223
223
  return { data: candidate };
224
224
  }
225
225
 
226
- export interface SubmitResultItem {
226
+ export interface YieldItem {
227
227
  data?: unknown;
228
228
  status?: "success" | "aborted";
229
229
  error?: string;
@@ -235,7 +235,7 @@ interface FinalizeSubprocessOutputArgs {
235
235
  stderr: string;
236
236
  doneAborted: boolean;
237
237
  signalAborted: boolean;
238
- submitResultItems?: SubmitResultItem[];
238
+ yieldItems?: YieldItem[];
239
239
  reportFindings?: ReviewFinding[];
240
240
  outputSchema: unknown;
241
241
  }
@@ -244,44 +244,42 @@ interface FinalizeSubprocessOutputResult {
244
244
  rawOutput: string;
245
245
  exitCode: number;
246
246
  stderr: string;
247
- abortedViaSubmitResult: boolean;
248
- hasSubmitResult: boolean;
247
+ abortedViaYield: boolean;
248
+ hasYield: boolean;
249
249
  }
250
250
 
251
- export const SUBAGENT_WARNING_NULL_SUBMIT_RESULT = "SYSTEM WARNING: Subagent called submit_result with null data.";
252
- export const SUBAGENT_WARNING_MISSING_SUBMIT_RESULT =
253
- "SYSTEM WARNING: Subagent exited without calling submit_result tool after 3 reminders.";
251
+ export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
252
+ export const SUBAGENT_WARNING_MISSING_YIELD =
253
+ "SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
254
254
 
255
255
  export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
256
256
  let { rawOutput, exitCode, stderr } = args;
257
- const { submitResultItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
258
- let abortedViaSubmitResult = false;
259
- const hasSubmitResult = Array.isArray(submitResultItems) && submitResultItems.length > 0;
260
-
261
- if (hasSubmitResult) {
262
- const lastSubmitResult = submitResultItems[submitResultItems.length - 1];
263
- if (lastSubmitResult?.status === "aborted") {
264
- abortedViaSubmitResult = true;
257
+ const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
258
+ let abortedViaYield = false;
259
+ const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
260
+
261
+ if (hasYield) {
262
+ const lastYield = yieldItems[yieldItems.length - 1];
263
+ if (lastYield?.status === "aborted") {
264
+ abortedViaYield = true;
265
265
  exitCode = 0;
266
- stderr = lastSubmitResult.error || "Subagent aborted task";
266
+ stderr = lastYield.error || "Subagent aborted task";
267
267
  try {
268
- rawOutput = JSON.stringify({ aborted: true, error: lastSubmitResult.error }, null, 2);
268
+ rawOutput = JSON.stringify({ aborted: true, error: lastYield.error }, null, 2);
269
269
  } catch {
270
- rawOutput = `{"aborted":true,"error":"${lastSubmitResult.error || "Unknown error"}"}`;
270
+ rawOutput = `{"aborted":true,"error":"${lastYield.error || "Unknown error"}"}`;
271
271
  }
272
272
  } else {
273
- const submitData = lastSubmitResult?.data;
273
+ const submitData = lastYield?.data;
274
274
  if (submitData === null || submitData === undefined) {
275
- rawOutput = rawOutput
276
- ? `${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\n${rawOutput}`
277
- : SUBAGENT_WARNING_NULL_SUBMIT_RESULT;
275
+ rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
278
276
  } else {
279
277
  const completeData = normalizeCompleteData(submitData, reportFindings);
280
278
  try {
281
279
  rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
282
280
  } catch (err) {
283
281
  const errorMessage = err instanceof Error ? err.message : String(err);
284
- rawOutput = `{"error":"Failed to serialize submit_result data: ${errorMessage}"}`;
282
+ rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
285
283
  }
286
284
  exitCode = 0;
287
285
  stderr = "";
@@ -307,17 +305,15 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
307
305
  stderr = "";
308
306
  } else if (exitCode === 0) {
309
307
  const hasRawOutput = rawOutput.trim().length > 0;
310
- rawOutput = rawOutput
311
- ? `${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\n${rawOutput}`
312
- : SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
308
+ rawOutput = rawOutput ? `${SUBAGENT_WARNING_MISSING_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_MISSING_YIELD;
313
309
  if (hasOutputSchema || !hasRawOutput) {
314
310
  exitCode = 1;
315
- stderr = SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
311
+ stderr = SUBAGENT_WARNING_MISSING_YIELD;
316
312
  }
317
313
  }
318
314
  }
319
315
 
320
- return { rawOutput, exitCode, stderr, abortedViaSubmitResult, hasSubmitResult };
316
+ return { rawOutput, exitCode, stderr, abortedViaYield, hasYield };
321
317
  }
322
318
 
323
319
  /**
@@ -564,7 +560,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
564
560
  const abortSignal = abortController.signal;
565
561
  let activeSession: AgentSession | null = null;
566
562
  let unsubscribe: (() => void) | null = null;
567
- let submitResultCalled = false;
563
+ let yieldCalled = false;
568
564
 
569
565
  // Accumulate usage incrementally from message_end events (no memory for streaming events)
570
566
  const accumulatedUsage = {
@@ -789,8 +785,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
789
785
  existing.push(data);
790
786
  }
791
787
  progress.extractedToolData[event.toolName] = existing;
792
- if (event.toolName === "submit_result") {
793
- submitResultCalled = true;
788
+ if (event.toolName === "yield") {
789
+ yieldCalled = true;
794
790
  }
795
791
  }
796
792
  }
@@ -955,7 +951,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
955
951
  thinkingLevel: effectiveThinkingLevel,
956
952
  toolNames,
957
953
  outputSchema,
958
- requireSubmitResultTool: true,
954
+ requireYieldTool: true,
959
955
  contextFiles: options.contextFiles,
960
956
  skills: options.skills,
961
957
  promptTemplates: options.promptTemplates,
@@ -1070,7 +1066,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1070
1066
  await extensionRunner.emit({ type: "session_start" });
1071
1067
  }
1072
1068
 
1073
- const MAX_SUBMIT_RESULT_RETRIES = 3;
1069
+ const MAX_YIELD_RETRIES = 3;
1074
1070
  unsubscribe = session.subscribe(event => {
1075
1071
  if (isAgentEvent(event)) {
1076
1072
  try {
@@ -1087,15 +1083,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1087
1083
  await session.prompt(task, { attribution: "agent" });
1088
1084
  await session.waitForIdle();
1089
1085
 
1090
- const reminderToolChoice = buildNamedToolChoice("submit_result", session.model);
1086
+ const reminderToolChoice = buildNamedToolChoice("yield", session.model);
1091
1087
 
1092
1088
  let retryCount = 0;
1093
- while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
1089
+ while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
1094
1090
  try {
1095
1091
  retryCount++;
1096
1092
  const reminder = prompt.render(submitReminderTemplate, {
1097
1093
  retryCount,
1098
- maxRetries: MAX_SUBMIT_RESULT_RETRIES,
1094
+ maxRetries: MAX_YIELD_RETRIES,
1099
1095
  });
1100
1096
 
1101
1097
  await session.prompt(reminder, {
@@ -1111,7 +1107,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1111
1107
  }
1112
1108
 
1113
1109
  await session.waitForIdle();
1114
- if (!submitResultCalled && !abortSignal.aborted) {
1110
+ if (!yieldCalled && !abortSignal.aborted) {
1115
1111
  exitCode = 0;
1116
1112
  }
1117
1113
 
@@ -1186,7 +1182,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1186
1182
 
1187
1183
  // Use final output if available, otherwise accumulated output
1188
1184
  let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
1189
- const submitResultItems = progress.extractedToolData?.submit_result as SubmitResultItem[] | undefined;
1185
+ const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
1190
1186
  const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
1191
1187
  const finalized = finalizeSubprocessOutput({
1192
1188
  rawOutput,
@@ -1194,17 +1190,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1194
1190
  stderr,
1195
1191
  doneAborted: Boolean(done.aborted),
1196
1192
  signalAborted: Boolean(signal?.aborted),
1197
- submitResultItems,
1193
+ yieldItems,
1198
1194
  reportFindings,
1199
1195
  outputSchema,
1200
1196
  });
1201
1197
  rawOutput = finalized.rawOutput;
1202
1198
  exitCode = finalized.exitCode;
1203
1199
  stderr = finalized.stderr;
1204
- const lastSubmitResult = submitResultItems?.[submitResultItems.length - 1];
1205
- const submitResultAbortReason =
1206
- lastSubmitResult?.status === "aborted" ? lastSubmitResult.error || "Subagent aborted task" : undefined;
1207
- const { abortedViaSubmitResult, hasSubmitResult } = finalized;
1200
+ const lastYield = yieldItems?.[yieldItems.length - 1];
1201
+ const yieldAbortReason = lastYield?.status === "aborted" ? lastYield.error || "Subagent aborted task" : undefined;
1202
+ const { abortedViaYield, hasYield } = finalized;
1208
1203
  const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
1209
1204
  maxBytes: MAX_OUTPUT_BYTES,
1210
1205
  maxLines: MAX_OUTPUT_LINES,
@@ -1228,16 +1223,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1228
1223
  }
1229
1224
 
1230
1225
  // Update final progress
1231
- const wasAborted = abortedViaSubmitResult || (!hasSubmitResult && (done.aborted || signal?.aborted || false));
1226
+ const wasAborted = abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
1232
1227
  const finalAbortReason = wasAborted
1233
- ? abortedViaSubmitResult
1234
- ? submitResultAbortReason
1228
+ ? abortedViaYield
1229
+ ? yieldAbortReason
1235
1230
  : (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : "Subagent aborted task"))
1236
1231
  : undefined;
1237
1232
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
1238
1233
  scheduleProgress(true);
1239
1234
 
1240
- // Emit lifecycle end event after finalization so submit_result status is reflected
1235
+ // Emit lifecycle end event after finalization so yield status is reflected
1241
1236
  if (options.eventBus) {
1242
1237
  options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
1243
1238
  id,
@@ -101,12 +101,12 @@ function formatTaskId(id: string): string {
101
101
  return `${indices} ${labels}`;
102
102
  }
103
103
 
104
- const MISSING_SUBMIT_RESULT_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling submit_result tool";
104
+ const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
105
105
 
106
- function extractMissingSubmitResultWarning(output: string): { warning?: string; rest: string } {
106
+ function extractMissingYieldWarning(output: string): { warning?: string; rest: string } {
107
107
  const lines = output.split("\n");
108
108
  const firstLine = lines[0]?.trim() ?? "";
109
- if (!firstLine.startsWith(MISSING_SUBMIT_RESULT_WARNING_PREFIX)) {
109
+ if (!firstLine.startsWith(MISSING_YIELD_WARNING_PREFIX)) {
110
110
  return { rest: output };
111
111
  }
112
112
  const rest = lines
@@ -572,9 +572,9 @@ function renderAgentProgress(
572
572
 
573
573
  // Render extracted tool data inline (e.g., review findings)
574
574
  if (progress.extractedToolData) {
575
- // For completed tasks, check for review verdict from submit_result tool
575
+ // For completed tasks, check for review verdict from yield tool
576
576
  if (progress.status === "completed") {
577
- const completeData = progress.extractedToolData.submit_result as Array<{ data: unknown }> | undefined;
577
+ const completeData = progress.extractedToolData.yield as Array<{ data: unknown }> | undefined;
578
578
  const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
579
579
  const reviewData = completeData
580
580
  ?.map(c => c.data as SubmitReviewDetails)
@@ -731,9 +731,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
731
731
  const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
732
732
  const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
733
733
 
734
- const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingSubmitResultWarning(
735
- result.output,
736
- );
734
+ const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
737
735
  const aborted = result.aborted ?? false;
738
736
  const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
739
737
  const success = !aborted && result.exitCode === 0 && !result.error;
@@ -783,11 +781,11 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
783
781
  `${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortReason), 80))}`,
784
782
  );
785
783
  }
786
- // Check for review result (submit_result with review schema + report_finding)
787
- const completeData = result.extractedToolData?.submit_result as Array<{ data: unknown }> | undefined;
784
+ // Check for review result (yield with review schema + report_finding)
785
+ const completeData = result.extractedToolData?.yield as Array<{ data: unknown }> | undefined;
788
786
  const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
789
787
 
790
- // Extract review verdict from submit_result tool's data field if it matches SubmitReviewDetails
788
+ // Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
791
789
  const reviewData = completeData
792
790
  ?.map(c => c.data as SubmitReviewDetails)
793
791
  .filter(d => d && typeof d === "object" && "overall_correctness" in d);
@@ -804,7 +802,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
804
802
  const hasCompleteData = completeData && completeData.length > 0;
805
803
  const message = hasCompleteData
806
804
  ? "Review verdict missing expected fields"
807
- : "Review incomplete (submit_result not called)";
805
+ : "Review incomplete (yield not called)";
808
806
  lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`);
809
807
  lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
810
808
  lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
@@ -817,7 +815,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
817
815
  if (result.extractedToolData) {
818
816
  for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
819
817
  // Skip review tools - handled above
820
- if (toolName === "submit_result" || toolName === "report_finding") continue;
818
+ if (toolName === "yield" || toolName === "report_finding") continue;
821
819
 
822
820
  const handler = subprocessToolRegistry.getHandler(toolName);
823
821
  if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
package/src/tools/ask.ts CHANGED
@@ -32,19 +32,19 @@ import { ToolAbortError } from "./tool-errors";
32
32
  // =============================================================================
33
33
 
34
34
  const OptionItem = Type.Object({
35
- label: Type.String({ description: "Display label" }),
35
+ label: Type.String({ description: "display label" }),
36
36
  });
37
37
 
38
38
  const QuestionItem = Type.Object({
39
- id: Type.String({ description: "Question ID, e.g. 'auth', 'cache'" }),
40
- question: Type.String({ description: "Question text" }),
41
- options: Type.Array(OptionItem, { description: "Available options" }),
42
- multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections" })),
43
- recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed)" })),
39
+ id: Type.String({ description: "question id", examples: ["auth", "cache"] }),
40
+ question: Type.String({ description: "question text" }),
41
+ options: Type.Array(OptionItem, { description: "available options" }),
42
+ multi: Type.Optional(Type.Boolean({ description: "allow multiple selections" })),
43
+ recommended: Type.Optional(Type.Number({ description: "recommended option index" })),
44
44
  });
45
45
 
46
46
  const askSchema = Type.Object({
47
- questions: Type.Array(QuestionItem, { description: "Questions to ask", minItems: 1 }),
47
+ questions: Type.Array(QuestionItem, { description: "questions to ask", minItems: 1 }),
48
48
  });
49
49
 
50
50
  export type AskToolInput = Static<typeof askSchema>;
@@ -5,7 +5,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
- import { computeLineHash } from "../edit/line-hash";
8
+ import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
@@ -15,7 +15,6 @@ import type { ToolSession } from ".";
15
15
  import { createFileRecorder, formatResultPath } from "./file-recorder";
16
16
  import type { OutputMeta } from "./output-meta";
17
17
  import {
18
- combineSearchGlobs,
19
18
  hasGlobPathChars,
20
19
  normalizePathLikeInput,
21
20
  parseSearchPath,
@@ -24,6 +23,7 @@ import {
24
23
  } from "./path-utils";
25
24
  import {
26
25
  dedupeParseErrors,
26
+ formatCodeFrameLine,
27
27
  formatCount,
28
28
  formatEmptyMessage,
29
29
  formatErrorMessage,
@@ -36,20 +36,19 @@ import { ToolError } from "./tool-errors";
36
36
  import { toolResult } from "./tool-result";
37
37
 
38
38
  const astEditOpSchema = Type.Object({
39
- pat: Type.String({ description: "AST pattern to match" }),
40
- out: Type.String({ description: "Replacement template" }),
39
+ pat: Type.String({ description: "ast pattern", examples: ["oldFn($$$ARGS)"] }),
40
+ out: Type.String({ description: "replacement template", examples: ["newFn($$$ARGS)"] }),
41
41
  });
42
42
 
43
43
  const astEditSchema = Type.Object({
44
44
  ops: Type.Array(astEditOpSchema, {
45
45
  minItems: 1,
46
- description: "Rewrite ops as [{ pat, out }]",
46
+ description: "rewrite ops",
47
+ }),
48
+ path: Type.String({
49
+ description: "file, directory, glob, or comma-separated paths to rewrite",
50
+ examples: ["src/", "src/foo.ts", "src/**/*.ts"],
47
51
  }),
48
- lang: Type.Optional(Type.String({ description: "Language override" })),
49
- path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to rewrite (default: cwd)" })),
50
- glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
51
- sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
52
- limit: Type.Optional(Type.Number({ description: "Max total replacements" })),
53
52
  });
54
53
 
55
54
  export interface AstEditToolDetails {
@@ -63,6 +62,9 @@ export interface AstEditToolDetails {
63
62
  files?: string[];
64
63
  fileReplacements?: Array<{ path: string; count: number }>;
65
64
  meta?: OutputMeta;
65
+ /** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
66
+ * a `│` gutter (no model-only hashline anchors). The TUI uses this directly so it never parses model-facing text. */
67
+ displayContent?: string;
66
68
  }
67
69
 
68
70
  export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
@@ -101,10 +103,6 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
101
103
  seenPatterns.add(pat);
102
104
  }
103
105
  const normalizedRewrites = Object.fromEntries(ops);
104
- const maxReplacements = params.limit !== undefined ? Math.floor(params.limit) : undefined;
105
- if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
106
- throw new ToolError("limit must be a positive number");
107
- }
108
106
  const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
109
107
 
110
108
  const formatScopePath = (targetPath: string): string => {
@@ -113,8 +111,11 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
113
111
  };
114
112
  let searchPath: string | undefined;
115
113
  let scopePath: string | undefined;
116
- let globFilter = params.glob ? normalizePathLikeInput(params.glob) || undefined : undefined;
117
- const rawPath = params.path ? normalizePathLikeInput(params.path) || undefined : undefined;
114
+ let globFilter: string | undefined;
115
+ const rawPath = normalizePathLikeInput(params.path);
116
+ if (rawPath.length === 0) {
117
+ throw new ToolError("`path` must be a non-empty path or glob");
118
+ }
118
119
  if (rawPath) {
119
120
  const internalRouter = this.session.internalRouter;
120
121
  if (internalRouter?.canHandle(rawPath)) {
@@ -136,7 +137,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
136
137
  } else {
137
138
  const parsedPath = parseSearchPath(rawPath);
138
139
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
139
- globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
140
+ globFilter = parsedPath.glob;
140
141
  scopePath = formatScopePath(searchPath);
141
142
  }
142
143
  }
@@ -153,12 +154,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
153
154
 
154
155
  const result = await astEdit({
155
156
  rewrites: normalizedRewrites,
156
- lang: params.lang?.trim(),
157
157
  path: resolvedSearchPath,
158
158
  glob: globFilter,
159
- selector: params.sel?.trim(),
160
159
  dryRun: true,
161
- maxReplacements,
162
160
  maxFiles,
163
161
  failOnParseError: false,
164
162
  signal,
@@ -205,24 +203,29 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
205
203
 
206
204
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
207
205
  const outputLines: string[] = [];
206
+ const displayLines: string[] = [];
208
207
  const renderChangesForFile = (relativePath: string) => {
209
208
  const fileChanges = changesByFile.get(relativePath) ?? [];
210
- const lineWidth =
211
- fileChanges.length > 0 ? Math.max(...fileChanges.map(change => change.startLine.toString().length)) : 1;
209
+ const lineNumberWidth = fileChanges.reduce(
210
+ (width, change) => Math.max(width, String(change.startLine).length),
211
+ 0,
212
+ );
212
213
  for (const change of fileChanges) {
213
214
  const beforeFirstLine = change.before.split("\n", 1)[0] ?? "";
214
215
  const afterFirstLine = change.after.split("\n", 1)[0] ?? "";
215
216
  const beforeLine = beforeFirstLine.slice(0, 120);
216
217
  const afterLine = afterFirstLine.slice(0, 120);
217
218
  const beforeRef = useHashLines
218
- ? `${change.startLine}#${computeLineHash(change.startLine, beforeFirstLine)}`
219
- : `${change.startLine.toString().padStart(lineWidth, " ")}:${change.startColumn}`;
219
+ ? `${change.startLine}${computeLineHash(change.startLine, beforeFirstLine)}`
220
+ : `${change.startLine}:${change.startColumn}`;
220
221
  const afterRef = useHashLines
221
- ? `${change.startLine}#${computeLineHash(change.startLine, afterFirstLine)}`
222
- : `${change.startLine.toString().padStart(lineWidth, " ")}:${change.startColumn}`;
223
- const lineSeparator = useHashLines ? ":" : " ";
222
+ ? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
223
+ : `${change.startLine}:${change.startColumn}`;
224
+ const lineSeparator = useHashLines ? HASHLINE_CONTENT_SEPARATOR : " ";
224
225
  outputLines.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
225
226
  outputLines.push(`+${afterRef}${lineSeparator}${afterLine}`);
227
+ displayLines.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
228
+ displayLines.push(formatCodeFrameLine("+", change.startLine, afterLine, lineNumberWidth));
226
229
  }
227
230
  };
228
231
 
@@ -240,20 +243,28 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
240
243
  for (const relativePath of directoryFiles) {
241
244
  if (outputLines.length > 0) {
242
245
  outputLines.push("");
246
+ displayLines.push("");
243
247
  }
244
248
  const count = fileReplacementCounts.get(relativePath) ?? 0;
245
- outputLines.push(`# ${path.basename(relativePath)} (${formatCount("replacement", count)})`);
249
+ const header = `# ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
250
+ outputLines.push(header);
251
+ displayLines.push(header);
246
252
  renderChangesForFile(relativePath);
247
253
  }
248
254
  continue;
249
255
  }
250
256
  if (outputLines.length > 0) {
251
257
  outputLines.push("");
258
+ displayLines.push("");
252
259
  }
253
- outputLines.push(`# ${directory}`);
260
+ const dirHeader = `# ${directory}`;
261
+ outputLines.push(dirHeader);
262
+ displayLines.push(dirHeader);
254
263
  for (const relativePath of directoryFiles) {
255
264
  const count = fileReplacementCounts.get(relativePath) ?? 0;
256
- outputLines.push(`## └─ ${path.basename(relativePath)} (${formatCount("replacement", count)})`);
265
+ const fileHeader = `## └─ ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
266
+ outputLines.push(fileHeader);
267
+ displayLines.push(fileHeader);
257
268
  renderChangesForFile(relativePath);
258
269
  }
259
270
  }
@@ -268,7 +279,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
268
279
  count: fileReplacementCounts.get(filePath) ?? 0,
269
280
  }));
270
281
  if (result.limitReached) {
271
- outputLines.push("", "Limit reached; narrow path or increase limit.");
282
+ outputLines.push("", "Limit reached; narrow path.");
272
283
  }
273
284
  if (dedupedParseErrors.length) {
274
285
  outputLines.push("", ...formatParseErrors(dedupedParseErrors));
@@ -284,12 +295,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
284
295
  apply: async (_reason: string) => {
285
296
  const applyResult = await astEdit({
286
297
  rewrites: normalizedRewrites,
287
- lang: params.lang?.trim(),
288
298
  path: resolvedSearchPath,
289
299
  glob: globFilter,
290
- selector: params.sel?.trim(),
291
300
  dryRun: false,
292
- maxReplacements,
293
301
  maxFiles,
294
302
  failOnParseError: false,
295
303
  });
@@ -351,6 +359,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
351
359
  const details: AstEditToolDetails = {
352
360
  ...baseDetails,
353
361
  fileReplacements,
362
+ displayContent: displayLines.join("\n"),
354
363
  };
355
364
  return toolResult(details).text(outputLines.join("\n")).done();
356
365
  });
@@ -363,10 +372,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
363
372
 
364
373
  interface AstEditRenderArgs {
365
374
  ops?: Array<{ pat?: string; out?: string }>;
366
- lang?: string;
367
375
  path?: string;
368
- sel?: string;
369
- limit?: number;
370
376
  }
371
377
 
372
378
  const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
@@ -375,9 +381,7 @@ export const astEditToolRenderer = {
375
381
  inline: true,
376
382
  renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
377
383
  const meta: string[] = [];
378
- if (args.lang) meta.push(`lang:${args.lang}`);
379
384
  if (args.path) meta.push(`in ${args.path}`);
380
- if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
381
385
  const rewriteCount = args.ops?.length ?? 0;
382
386
  if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
383
387
 
@@ -432,7 +436,7 @@ export const astEditToolRenderer = {
432
436
  const rewriteCount = args?.ops?.length ?? 0;
433
437
  const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
434
438
 
435
- const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
439
+ const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
436
440
  const rawLines = textContent.split("\n");
437
441
  const hasSeparators = rawLines.some(line => line.trim().length === 0);
438
442
  const allGroups: string[][] = [];
@@ -467,7 +471,7 @@ export const astEditToolRenderer = {
467
471
 
468
472
  const extraLines: string[] = [];
469
473
  if (limitReached) {
470
- extraLines.push(uiTheme.fg("warning", "limit reached; narrow path or increase limit"));
474
+ extraLines.push(uiTheme.fg("warning", "limit reached; narrow path"));
471
475
  }
472
476
  if (details?.parseErrors?.length) {
473
477
  const total = details.parseErrors.length;