@oh-my-pi/pi-coding-agent 14.2.1 → 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 (137) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/package.json +19 -19
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/cli/args.ts +10 -1
  5. package/src/cli/shell-cli.ts +15 -3
  6. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  7. package/src/config/model-registry.ts +67 -15
  8. package/src/config/prompt-templates.ts +5 -5
  9. package/src/config/settings-schema.ts +63 -4
  10. package/src/cursor.ts +3 -8
  11. package/src/debug/system-info.ts +6 -2
  12. package/src/discovery/claude.ts +58 -36
  13. package/src/discovery/helpers.ts +3 -3
  14. package/src/discovery/opencode.ts +20 -2
  15. package/src/edit/diff.ts +50 -47
  16. package/src/edit/index.ts +87 -57
  17. package/src/edit/line-hash.ts +735 -19
  18. package/src/edit/modes/apply-patch.ts +0 -9
  19. package/src/edit/modes/atom.ts +658 -0
  20. package/src/edit/modes/chunk.ts +144 -78
  21. package/src/edit/modes/hashline.ts +223 -146
  22. package/src/edit/modes/patch.ts +5 -9
  23. package/src/edit/modes/replace.ts +6 -11
  24. package/src/edit/renderer.ts +112 -143
  25. package/src/edit/streaming.ts +385 -0
  26. package/src/exec/bash-executor.ts +58 -5
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +4 -12
  29. package/src/extensibility/custom-tools/types.ts +2 -0
  30. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  31. package/src/internal-urls/docs-index.generated.ts +7 -7
  32. package/src/internal-urls/pi-protocol.ts +0 -2
  33. package/src/lsp/client.ts +8 -1
  34. package/src/lsp/defaults.json +2 -1
  35. package/src/lsp/index.ts +1 -1
  36. package/src/mcp/render.ts +1 -8
  37. package/src/modes/acp/acp-agent.ts +76 -2
  38. package/src/modes/components/assistant-message.ts +5 -34
  39. package/src/modes/components/diff.ts +23 -14
  40. package/src/modes/components/footer.ts +21 -16
  41. package/src/modes/components/hook-editor.ts +1 -1
  42. package/src/modes/components/settings-defs.ts +6 -1
  43. package/src/modes/components/todo-reminder.ts +1 -8
  44. package/src/modes/components/tool-execution.ts +112 -105
  45. package/src/modes/controllers/input-controller.ts +1 -1
  46. package/src/modes/controllers/selector-controller.ts +1 -1
  47. package/src/modes/interactive-mode.ts +0 -2
  48. package/src/modes/print-mode.ts +8 -0
  49. package/src/modes/theme/mermaid-cache.ts +13 -52
  50. package/src/modes/theme/theme.ts +2 -2
  51. package/src/prompts/agents/librarian.md +1 -1
  52. package/src/prompts/agents/reviewer.md +4 -4
  53. package/src/prompts/ci-green-request.md +1 -1
  54. package/src/prompts/review-request.md +1 -1
  55. package/src/prompts/system/subagent-system-prompt.md +3 -3
  56. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  57. package/src/prompts/system/system-prompt.md +4 -1
  58. package/src/prompts/tools/ask.md +3 -2
  59. package/src/prompts/tools/ast-edit.md +15 -19
  60. package/src/prompts/tools/ast-grep.md +18 -24
  61. package/src/prompts/tools/atom.md +96 -0
  62. package/src/prompts/tools/browser.md +1 -0
  63. package/src/prompts/tools/chunk-edit.md +58 -179
  64. package/src/prompts/tools/debug.md +4 -5
  65. package/src/prompts/tools/exit-plan-mode.md +4 -5
  66. package/src/prompts/tools/find.md +4 -8
  67. package/src/prompts/tools/github.md +18 -0
  68. package/src/prompts/tools/grep.md +8 -8
  69. package/src/prompts/tools/hashline.md +22 -89
  70. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  71. package/src/prompts/tools/inspect-image.md +6 -6
  72. package/src/prompts/tools/lsp.md +6 -0
  73. package/src/prompts/tools/patch.md +12 -19
  74. package/src/prompts/tools/python.md +3 -2
  75. package/src/prompts/tools/read-chunk.md +46 -8
  76. package/src/prompts/tools/read.md +9 -6
  77. package/src/prompts/tools/ssh.md +8 -17
  78. package/src/prompts/tools/todo-write.md +54 -41
  79. package/src/sdk.ts +22 -14
  80. package/src/session/agent-session.ts +61 -22
  81. package/src/session/session-manager.ts +228 -57
  82. package/src/session/streaming-output.ts +11 -0
  83. package/src/system-prompt.ts +7 -2
  84. package/src/task/executor.ts +44 -48
  85. package/src/task/render.ts +11 -13
  86. package/src/tools/ask.ts +7 -7
  87. package/src/tools/ast-edit.ts +45 -41
  88. package/src/tools/ast-grep.ts +77 -85
  89. package/src/tools/bash.ts +21 -9
  90. package/src/tools/browser.ts +32 -30
  91. package/src/tools/calculator.ts +4 -4
  92. package/src/tools/cancel-job.ts +1 -1
  93. package/src/tools/checkpoint.ts +2 -2
  94. package/src/tools/debug.ts +41 -37
  95. package/src/tools/exit-plan-mode.ts +1 -1
  96. package/src/tools/find.ts +4 -4
  97. package/src/tools/gh-renderer.ts +12 -4
  98. package/src/tools/gh.ts +514 -712
  99. package/src/tools/grep.ts +115 -130
  100. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  101. package/src/tools/index.ts +14 -32
  102. package/src/tools/inspect-image.ts +3 -3
  103. package/src/tools/json-tree.ts +114 -114
  104. package/src/tools/match-line-format.ts +9 -8
  105. package/src/tools/notebook.ts +8 -7
  106. package/src/tools/poll-tool.ts +2 -1
  107. package/src/tools/python.ts +9 -23
  108. package/src/tools/read.ts +32 -21
  109. package/src/tools/render-mermaid.ts +1 -1
  110. package/src/tools/render-utils.ts +18 -0
  111. package/src/tools/renderers.ts +2 -2
  112. package/src/tools/report-tool-issue.ts +3 -2
  113. package/src/tools/resolve.ts +1 -1
  114. package/src/tools/review.ts +12 -10
  115. package/src/tools/search-tool-bm25.ts +2 -4
  116. package/src/tools/sqlite-reader.ts +116 -3
  117. package/src/tools/ssh.ts +4 -4
  118. package/src/tools/todo-write.ts +172 -147
  119. package/src/tools/vim.ts +14 -15
  120. package/src/tools/write.ts +4 -4
  121. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  122. package/src/utils/edit-mode.ts +2 -1
  123. package/src/utils/file-display-mode.ts +10 -5
  124. package/src/utils/git.ts +9 -5
  125. package/src/utils/shell-snapshot.ts +2 -3
  126. package/src/vim/render.ts +4 -4
  127. package/src/web/search/providers/codex.ts +129 -6
  128. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  129. package/src/prompts/tools/gh-issue-view.md +0 -11
  130. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  131. package/src/prompts/tools/gh-pr-diff.md +0 -12
  132. package/src/prompts/tools/gh-pr-push.md +0 -11
  133. package/src/prompts/tools/gh-pr-view.md +0 -11
  134. package/src/prompts/tools/gh-repo-view.md +0 -11
  135. package/src/prompts/tools/gh-run-watch.md +0 -12
  136. package/src/prompts/tools/gh-search-issues.md +0 -11
  137. 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,
@@ -975,6 +971,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
975
971
  enableLsp: lspEnabled,
976
972
  skipPythonPreflight,
977
973
  enableMCP,
974
+ mcpManager: options.mcpManager,
978
975
  customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
979
976
  });
980
977
 
@@ -1069,7 +1066,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1069
1066
  await extensionRunner.emit({ type: "session_start" });
1070
1067
  }
1071
1068
 
1072
- const MAX_SUBMIT_RESULT_RETRIES = 3;
1069
+ const MAX_YIELD_RETRIES = 3;
1073
1070
  unsubscribe = session.subscribe(event => {
1074
1071
  if (isAgentEvent(event)) {
1075
1072
  try {
@@ -1086,15 +1083,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1086
1083
  await session.prompt(task, { attribution: "agent" });
1087
1084
  await session.waitForIdle();
1088
1085
 
1089
- const reminderToolChoice = buildNamedToolChoice("submit_result", session.model);
1086
+ const reminderToolChoice = buildNamedToolChoice("yield", session.model);
1090
1087
 
1091
1088
  let retryCount = 0;
1092
- while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
1089
+ while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
1093
1090
  try {
1094
1091
  retryCount++;
1095
1092
  const reminder = prompt.render(submitReminderTemplate, {
1096
1093
  retryCount,
1097
- maxRetries: MAX_SUBMIT_RESULT_RETRIES,
1094
+ maxRetries: MAX_YIELD_RETRIES,
1098
1095
  });
1099
1096
 
1100
1097
  await session.prompt(reminder, {
@@ -1110,7 +1107,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1110
1107
  }
1111
1108
 
1112
1109
  await session.waitForIdle();
1113
- if (!submitResultCalled && !abortSignal.aborted) {
1110
+ if (!yieldCalled && !abortSignal.aborted) {
1114
1111
  exitCode = 0;
1115
1112
  }
1116
1113
 
@@ -1185,7 +1182,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1185
1182
 
1186
1183
  // Use final output if available, otherwise accumulated output
1187
1184
  let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
1188
- const submitResultItems = progress.extractedToolData?.submit_result as SubmitResultItem[] | undefined;
1185
+ const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
1189
1186
  const reportFindings = progress.extractedToolData?.report_finding as ReviewFinding[] | undefined;
1190
1187
  const finalized = finalizeSubprocessOutput({
1191
1188
  rawOutput,
@@ -1193,17 +1190,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1193
1190
  stderr,
1194
1191
  doneAborted: Boolean(done.aborted),
1195
1192
  signalAborted: Boolean(signal?.aborted),
1196
- submitResultItems,
1193
+ yieldItems,
1197
1194
  reportFindings,
1198
1195
  outputSchema,
1199
1196
  });
1200
1197
  rawOutput = finalized.rawOutput;
1201
1198
  exitCode = finalized.exitCode;
1202
1199
  stderr = finalized.stderr;
1203
- const lastSubmitResult = submitResultItems?.[submitResultItems.length - 1];
1204
- const submitResultAbortReason =
1205
- lastSubmitResult?.status === "aborted" ? lastSubmitResult.error || "Subagent aborted task" : undefined;
1206
- 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;
1207
1203
  const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
1208
1204
  maxBytes: MAX_OUTPUT_BYTES,
1209
1205
  maxLines: MAX_OUTPUT_LINES,
@@ -1227,16 +1223,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1227
1223
  }
1228
1224
 
1229
1225
  // Update final progress
1230
- const wasAborted = abortedViaSubmitResult || (!hasSubmitResult && (done.aborted || signal?.aborted || false));
1226
+ const wasAborted = abortedViaYield || (!hasYield && (done.aborted || signal?.aborted || false));
1231
1227
  const finalAbortReason = wasAborted
1232
- ? abortedViaSubmitResult
1233
- ? submitResultAbortReason
1228
+ ? abortedViaYield
1229
+ ? yieldAbortReason
1234
1230
  : (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : "Subagent aborted task"))
1235
1231
  : undefined;
1236
1232
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
1237
1233
  scheduleProgress(true);
1238
1234
 
1239
- // Emit lifecycle end event after finalization so submit_result status is reflected
1235
+ // Emit lifecycle end event after finalization so yield status is reflected
1240
1236
  if (options.eventBus) {
1241
1237
  options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
1242
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;