@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
@@ -14,14 +14,7 @@ import {
14
14
  type TUI,
15
15
  } from "@oh-my-pi/pi-tui";
16
16
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
17
- import {
18
- computeEditDiff,
19
- computeHashlineDiff,
20
- computePatchDiff,
21
- type DiffError,
22
- type DiffResult,
23
- expandApplyPatchToEntries,
24
- } from "../../edit";
17
+ import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
25
18
  import type { Theme } from "../../modes/theme/theme";
26
19
  import { theme } from "../../modes/theme/theme";
27
20
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
@@ -34,7 +27,6 @@ import {
34
27
  JSON_TREE_SCALAR_LEN_COLLAPSED,
35
28
  JSON_TREE_SCALAR_LEN_EXPANDED,
36
29
  renderJsonTreeLines,
37
- stripInternalArgs,
38
30
  } from "../../tools/json-tree";
39
31
  import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python";
40
32
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
@@ -65,6 +57,12 @@ function isEditLikeToolName(toolName: string): boolean {
65
57
  return toolName === "edit" || toolName === "apply_patch";
66
58
  }
67
59
 
60
+ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined): EditMode | undefined {
61
+ if (toolName === "apply_patch") return "apply_patch";
62
+ if (toolName !== "edit") return undefined;
63
+ return (tool as { mode?: EditMode } | undefined)?.mode;
64
+ }
65
+
68
66
  export interface ToolExecutionOptions {
69
67
  showImages?: boolean; // default: true (only used if terminal supports images)
70
68
  editFuzzyThreshold?: number;
@@ -111,9 +109,12 @@ export class ToolExecutionComponent extends Container {
111
109
  isError?: boolean;
112
110
  details?: any;
113
111
  };
114
- // Cached edit diff preview (computed when args arrive, before tool executes)
115
- #editDiffPreview?: DiffResult | DiffError;
116
- #editDiffArgsKey?: string; // Track which args the preview is for
112
+ // Edit preview state (single-file for legacy modes, multi-file for chunk)
113
+ #editMode?: EditMode;
114
+ #editDiffPreview?: PerFileDiffPreview[];
115
+ #editDiffScheduleTimer?: NodeJS.Timeout;
116
+ #editDiffAbort?: AbortController;
117
+ #editDiffLastArgsKey?: string;
117
118
  // Cached converted images for Kitty protocol (which requires PNG), keyed by index
118
119
  #convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
119
120
  // Spinner animation for partial task results
@@ -166,116 +167,98 @@ export class ToolExecutionComponent extends Container {
166
167
  this.addChild(this.#contentText);
167
168
  }
168
169
 
170
+ this.#editMode = resolveEditModeForTool(toolName, tool);
171
+
169
172
  this.#updateDisplay();
173
+ this.#schedulePreviewDiff(0);
170
174
  }
171
175
 
172
176
  updateArgs(args: any, _toolCallId?: string): void {
173
177
  this.#args = cloneToolArgs(args);
174
178
  this.#updateSpinnerAnimation();
179
+ this.#schedulePreviewDiff();
175
180
  this.#updateDisplay();
176
181
  }
177
182
 
178
183
  /**
179
184
  * Signal that args are complete (tool is about to execute).
180
- * This triggers diff computation for edit-like tools.
185
+ * This triggers an immediate final diff computation for edit-like tools.
181
186
  */
182
187
  setArgsComplete(_toolCallId?: string): void {
183
188
  this.#argsComplete = true;
184
189
  this.#updateSpinnerAnimation();
185
- this.#maybeComputeEditDiff();
190
+ this.#schedulePreviewDiff(0);
186
191
  }
187
192
 
188
193
  /**
189
- * Compute edit diff preview when we have complete args.
190
- * This runs async and updates display when done.
194
+ * Schedule a debounced compute of the streaming edit-diff preview.
195
+ * `delayMs === 0` runs immediately (used on construction and on
196
+ * `setArgsComplete`). All other calls coalesce to a trailing-edge timer.
191
197
  */
192
- #maybeComputeEditDiff(): void {
193
- if (!isEditLikeToolName(this.#toolName)) return;
194
-
195
- const edits = this.#args?.edits;
196
- if (!Array.isArray(edits) || edits.length === 0) {
197
- if (this.#toolName !== "apply_patch" || typeof this.#args?.input !== "string") {
198
- return;
199
- }
200
-
201
- const input = this.#args.input;
202
- const argsKey = JSON.stringify({ input });
203
- if (this.#editDiffArgsKey === argsKey) return;
204
- this.#editDiffArgsKey = argsKey;
205
-
206
- try {
207
- const first = expandApplyPatchToEntries({ input })[0];
208
- if (!first?.path) return;
209
- computePatchDiff({ ...first, op: first.op ?? "update" }, this.#cwd, {
210
- fuzzyThreshold: this.#editFuzzyThreshold,
211
- allowFuzzy: this.#editAllowFuzzy,
212
- }).then(result => {
213
- if (this.#editDiffArgsKey === argsKey) {
214
- this.#editDiffPreview = result;
215
- this.#updateDisplay();
216
- this.#ui.requestRender();
217
- }
218
- });
219
- } catch (err) {
220
- this.#editDiffPreview = { error: err instanceof Error ? err.message : String(err) };
221
- this.#updateDisplay();
222
- this.#ui.requestRender();
223
- }
198
+ #schedulePreviewDiff(delayMs = 80): void {
199
+ if (!this.#editMode) return;
200
+ if (this.#editDiffScheduleTimer) {
201
+ clearTimeout(this.#editDiffScheduleTimer);
202
+ this.#editDiffScheduleTimer = undefined;
203
+ }
204
+ if (delayMs === 0) {
205
+ void this.#runPreviewDiff();
224
206
  return;
225
207
  }
208
+ this.#editDiffScheduleTimer = setTimeout(() => {
209
+ this.#editDiffScheduleTimer = undefined;
210
+ void this.#runPreviewDiff();
211
+ }, delayMs);
212
+ }
226
213
 
227
- const first = edits[0];
228
- if (!first || typeof first !== "object") return;
229
-
230
- // Detect mode from first edit entry shape and compute preview for first file
231
- if ("old_text" in first && "new_text" in first) {
232
- // Replace mode
233
- const { path, old_text: oldText, new_text: newText, all } = first;
234
- if (!path || oldText === undefined || newText === undefined) return;
235
-
236
- const argsKey = JSON.stringify({ path, oldText, newText, all });
237
- if (this.#editDiffArgsKey === argsKey) return;
238
- this.#editDiffArgsKey = argsKey;
214
+ async #runPreviewDiff(): Promise<void> {
215
+ const editMode = this.#editMode;
216
+ if (!editMode) return;
217
+ const strategy = EDIT_MODE_STRATEGIES[editMode];
218
+ if (!strategy) return;
219
+
220
+ const args = this.#args;
221
+ if (args == null || typeof args !== "object") return;
222
+
223
+ const partialJson = (args as { __partialJson?: string }).__partialJson;
224
+ let effectiveArgs: unknown;
225
+ try {
226
+ effectiveArgs = strategy.extractCompleteEdits(args, partialJson);
227
+ } catch {
228
+ effectiveArgs = args;
229
+ }
239
230
 
240
- computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result =>
241
- this.#applyEditDiffResult(argsKey, result),
242
- );
243
- } else if ("path" in first && ("diff" in first || ("op" in first && !("content" in first)))) {
244
- // Patch mode (has diff or op without content — chunk edits always have content)
245
- const { path, op, rename, diff } = first;
246
- if (!path) return;
231
+ // Coalesce duplicate computes for identical args.
232
+ let argsKey: string;
233
+ try {
234
+ argsKey = JSON.stringify(effectiveArgs);
235
+ } catch {
236
+ argsKey = String(Date.now());
237
+ }
238
+ if (argsKey === this.#editDiffLastArgsKey) return;
239
+ this.#editDiffLastArgsKey = argsKey;
247
240
 
248
- const argsKey = JSON.stringify({ path, op, rename, diff });
249
- if (this.#editDiffArgsKey === argsKey) return;
250
- this.#editDiffArgsKey = argsKey;
241
+ this.#editDiffAbort?.abort();
242
+ const controller = new AbortController();
243
+ this.#editDiffAbort = controller;
251
244
 
252
- computePatchDiff({ path, op, rename, diff }, this.#cwd, {
245
+ try {
246
+ const previews = await strategy.computeDiffPreview(effectiveArgs, {
247
+ cwd: this.#cwd,
248
+ signal: controller.signal,
253
249
  fuzzyThreshold: this.#editFuzzyThreshold,
254
250
  allowFuzzy: this.#editAllowFuzzy,
255
- }).then(result => this.#applyEditDiffResult(argsKey, result));
256
- } else if ("loc" in first && "path" in first) {
257
- // Hashline mode — group edits by path, preview first file
258
- const path = first.path;
259
- if (!path) return;
260
- const fileEdits = edits.filter((e: any) => e.path === path);
261
- const move = this.#args?.move;
262
-
263
- const argsKey = JSON.stringify({ path, edits: fileEdits, move });
264
- if (this.#editDiffArgsKey === argsKey) return;
265
- this.#editDiffArgsKey = argsKey;
266
-
267
- computeHashlineDiff({ path, edits: fileEdits, move }, this.#cwd).then(result =>
268
- this.#applyEditDiffResult(argsKey, result),
269
- );
251
+ });
252
+ if (controller.signal.aborted) return;
253
+ if (previews) {
254
+ this.#editDiffPreview = previews;
255
+ this.#updateDisplay();
256
+ this.#ui.requestRender();
257
+ }
258
+ } catch (err) {
259
+ if (controller.signal.aborted) return;
260
+ logger.warn("Edit preview diff failed", { tool: this.#toolName, error: String(err) });
270
261
  }
271
- // Chunk mode edits don't have a pre-execution diff preview
272
- }
273
-
274
- #applyEditDiffResult(argsKey: string, result: DiffResult | DiffError): void {
275
- if (this.#editDiffArgsKey !== argsKey) return;
276
- this.#editDiffPreview = result;
277
- this.#updateDisplay();
278
- this.#ui.requestRender();
279
262
  }
280
263
 
281
264
  updateResult(
@@ -378,6 +361,12 @@ export class ToolExecutionComponent extends Container {
378
361
  this.#spinnerInterval = undefined;
379
362
  this.#spinnerFrame = undefined;
380
363
  }
364
+ if (this.#editDiffScheduleTimer) {
365
+ clearTimeout(this.#editDiffScheduleTimer);
366
+ this.#editDiffScheduleTimer = undefined;
367
+ }
368
+ this.#editDiffAbort?.abort();
369
+ this.#editDiffAbort = undefined;
381
370
  }
382
371
 
383
372
  setExpanded(expanded: boolean): void {
@@ -549,6 +538,9 @@ export class ToolExecutionComponent extends Container {
549
538
  this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
550
539
  this.#contentBox.clear();
551
540
 
541
+ const renderContext = this.#buildRenderContext();
542
+ this.#renderState.renderContext = renderContext;
543
+
552
544
  const shouldRenderCall = !this.#result || !renderer.mergeCallAndResult;
553
545
  if (shouldRenderCall) {
554
546
  // Render call component
@@ -567,10 +559,6 @@ export class ToolExecutionComponent extends Container {
567
559
  // Render result component if we have a result
568
560
  if (this.#result) {
569
561
  try {
570
- // Build render context for tools that need extra state
571
- const renderContext = this.#buildRenderContext();
572
- this.#renderState.renderContext = renderContext;
573
-
574
562
  const resultComponent = renderer.renderResult(
575
563
  {
576
564
  content: this.#result.content as any,
@@ -646,10 +634,20 @@ export class ToolExecutionComponent extends Container {
646
634
  if (!isEditLikeToolName(this.#toolName)) {
647
635
  return this.#args;
648
636
  }
649
- if (!this.#editDiffPreview || !("diff" in this.#editDiffPreview) || !this.#editDiffPreview.diff) {
637
+ const previews = this.#editDiffPreview;
638
+ if (!previews || previews.length === 0) {
650
639
  return this.#args;
651
640
  }
652
- return { ...(this.#args as Record<string, unknown>), previewDiff: this.#editDiffPreview.diff };
641
+ // Single-file previews feed the existing `previewDiff` channel consumed
642
+ // by `formatStreamingDiff` in the renderer. Multi-file previews are
643
+ // piped via `renderContext.perFileDiffPreview`, so the args we hand to
644
+ // `renderCall` only need the first file's diff to preserve prior
645
+ // single-file behavior.
646
+ const first = previews[0];
647
+ if (!first?.diff) {
648
+ return this.#args;
649
+ }
650
+ return { ...(this.#args as Record<string, unknown>), previewDiff: first.diff };
653
651
  }
654
652
 
655
653
  /**
@@ -680,8 +678,19 @@ export class ToolExecutionComponent extends Container {
680
678
  context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
681
679
  context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 600);
682
680
  } else if (isEditLikeToolName(this.#toolName)) {
683
- // Edit needs diff preview and renderDiff function
684
- context.editDiffPreview = this.#editDiffPreview;
681
+ context.editMode = this.#editMode;
682
+ const previews = this.#editDiffPreview;
683
+ if (previews && previews.length > 0) {
684
+ const first = previews[0];
685
+ if (first?.diff || first?.error) {
686
+ context.editDiffPreview = first.error
687
+ ? { error: first.error }
688
+ : { diff: first.diff ?? "", firstChangedLine: first.firstChangedLine };
689
+ }
690
+ if (previews.length > 1) {
691
+ context.perFileDiffPreview = previews;
692
+ }
693
+ }
685
694
  context.renderDiff = renderDiff;
686
695
  }
687
696
 
@@ -733,9 +742,7 @@ export class ToolExecutionComponent extends Container {
733
742
  lines.push("");
734
743
  lines.push(theme.fg("dim", "Args"));
735
744
  const tree = renderJsonTreeLines(
736
- this.#args && typeof this.#args === "object" && !Array.isArray(this.#args)
737
- ? stripInternalArgs(this.#args as Record<string, unknown>)
738
- : this.#args,
745
+ this.#args,
739
746
  theme,
740
747
  JSON_TREE_MAX_DEPTH_EXPANDED,
741
748
  JSON_TREE_MAX_LINES_EXPANDED,
@@ -715,7 +715,7 @@ export class InputController {
715
715
  return;
716
716
  }
717
717
 
718
- const currentText = this.ctx.editor.getText();
718
+ const currentText = this.ctx.editor.getExpandedText?.() ?? this.ctx.editor.getText();
719
719
 
720
720
  let ttyHandle: fs.FileHandle | null = null;
721
721
  try {
@@ -362,7 +362,7 @@ export class SelectorController {
362
362
  }
363
363
  break;
364
364
  case "providers.image":
365
- if (value === "auto" || value === "gemini" || value === "openrouter") {
365
+ if (value === "auto" || value === "openai" || value === "gemini" || value === "openrouter") {
366
366
  setPreferredImageProvider(value);
367
367
  }
368
368
  break;
@@ -63,7 +63,6 @@ import { SelectorController } from "./controllers/selector-controller";
63
63
  import { SSHCommandController } from "./controllers/ssh-command-controller";
64
64
  import { OAuthManualInputManager } from "./oauth-manual-input";
65
65
  import { SessionObserverRegistry } from "./session-observer-registry";
66
- import { setMermaidRenderCallback } from "./theme/mermaid-cache";
67
66
  import type { Theme } from "./theme/theme";
68
67
  import {
69
68
  getEditorTheme,
@@ -220,7 +219,6 @@ export class InteractiveMode implements InteractiveModeContext {
220
219
 
221
220
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
222
221
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
223
- setMermaidRenderCallback(() => this.ui.requestRender());
224
222
  this.chatContainer = new Container();
225
223
  this.pendingMessagesContainer = new Container();
226
224
  this.statusContainer = new Container();
@@ -161,6 +161,14 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
161
161
  }
162
162
  }
163
163
 
164
+ if (
165
+ assistantMsg.errorMessage &&
166
+ assistantMsg.stopReason !== "error" &&
167
+ assistantMsg.stopReason !== "aborted"
168
+ ) {
169
+ process.stderr.write(`${sanitizeText(assistantMsg.errorMessage)}\n`);
170
+ }
171
+
164
172
  // Output text content
165
173
  for (const content of assistantMsg.content) {
166
174
  if (content.type === "text") {
@@ -1,63 +1,24 @@
1
- import { extractMermaidBlocks, logger, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
1
+ import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
2
2
 
3
- const cache = new Map<bigint | number, string | null>();
3
+ const cache = new Map<string, string | null>();
4
4
 
5
- let onRenderNeeded: (() => void) | null = null;
6
-
7
- /**
8
- * Set callback to trigger TUI re-render when mermaid ASCII renders become available.
9
- */
10
- export function setMermaidRenderCallback(callback: (() => void) | null): void {
11
- onRenderNeeded = callback;
12
- }
13
-
14
- /**
15
- * Get a pre-rendered mermaid ASCII diagram by hash.
16
- * Returns null if not cached or rendering failed.
17
- */
18
- export function getMermaidAscii(hash: bigint | number): string | null {
19
- return cache.get(hash) ?? null;
5
+ function normalizeMermaidSource(source: string): string {
6
+ return source.replace(/\r\n?/g, "\n").trim();
20
7
  }
21
8
 
22
9
  /**
23
- * Render all mermaid blocks in markdown text.
24
- * Caches results and calls render callback when new diagrams are available.
10
+ * Resolve mermaid ASCII from fenced block source text.
11
+ * Returns null when rendering fails, while memoizing failures to avoid repeated work.
25
12
  */
26
- export function prerenderMermaid(markdown: string): void {
27
- const blocks = extractMermaidBlocks(markdown);
28
- if (blocks.length === 0) return;
29
-
30
- let hasNew = false;
31
-
32
- for (const { source, hash } of blocks) {
33
- if (cache.has(hash)) continue;
34
-
35
- const ascii = renderMermaidAsciiSafe(source);
36
- if (ascii) {
37
- cache.set(hash, ascii);
38
- hasNew = true;
39
- } else {
40
- cache.set(hash, null);
41
- }
13
+ export function resolveMermaidAscii(source: string): string | null {
14
+ const normalizedSource = normalizeMermaidSource(source);
15
+ if (cache.has(normalizedSource)) {
16
+ return cache.get(normalizedSource) ?? null;
42
17
  }
43
18
 
44
- if (hasNew && onRenderNeeded) {
45
- try {
46
- onRenderNeeded();
47
- } catch (error) {
48
- logger.warn("Mermaid render callback failed", {
49
- error: error instanceof Error ? error.message : String(error),
50
- });
51
- }
52
- }
53
- }
54
-
55
- /**
56
- * Check if markdown contains mermaid blocks that aren't cached yet.
57
- */
58
- export function hasPendingMermaid(markdown: string): boolean {
59
- const blocks = extractMermaidBlocks(markdown);
60
- return blocks.some(({ hash }) => !cache.has(hash));
19
+ const ascii = normalizedSource ? renderMermaidAsciiSafe(normalizedSource) : null;
20
+ cache.set(normalizedSource, ascii);
21
+ return ascii;
61
22
  }
62
23
 
63
24
  /**
@@ -18,7 +18,7 @@ import chalk from "chalk";
18
18
  import darkThemeJson from "./dark.json" with { type: "json" };
19
19
  import { defaultThemes } from "./defaults";
20
20
  import lightThemeJson from "./light.json" with { type: "json" };
21
- import { getMermaidAscii } from "./mermaid-cache";
21
+ import { resolveMermaidAscii } from "./mermaid-cache";
22
22
 
23
23
  export { getLanguageFromPath } from "../../utils/lang-from-path";
24
24
 
@@ -2339,7 +2339,7 @@ export function getMarkdownTheme(): MarkdownTheme {
2339
2339
  underline: (text: string) => theme.underline(text),
2340
2340
  strikethrough: (text: string) => chalk.strikethrough(text),
2341
2341
  symbols: getSymbolTheme(),
2342
- getMermaidAscii,
2342
+ resolveMermaidAscii,
2343
2343
  highlightCode: (code: string, lang?: string): string[] => {
2344
2344
  const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2345
2345
  try {
@@ -98,7 +98,7 @@ Before acting, determine what kind of question this is:
98
98
  - For API signatures: copy verbatim from source. You **MUST NOT** paraphrase or reconstruct from memory.
99
99
 
100
100
  ## 5. Report
101
- - Call `submit_result` with structured findings.
101
+ - Call `yield` with structured findings.
102
102
  - Every `sources` entry **MUST** include a verbatim excerpt.
103
103
  - The `api` array **MUST** contain exact signatures copied from source.
104
104
  - Clean up cloned repos: `rm -rf /tmp/librarian-*`.
@@ -44,7 +44,7 @@ output:
44
44
  type: number
45
45
  file_path:
46
46
  metadata:
47
- description: Absolute path to affected file
47
+ description: Path to affected file
48
48
  type: string
49
49
  line_start:
50
50
  metadata:
@@ -63,7 +63,7 @@ Your goal is to identify bugs the author would want fixed before merge.
63
63
  1. Run `git diff` (or `gh pr diff <number>`) to view patch
64
64
  2. Read modified files for full context
65
65
  3. Call `report_finding` per issue
66
- 4. Call `submit_result` with verdict
66
+ 4. Call `yield` with verdict
67
67
 
68
68
  Bash is read-only: `git diff`, `git log`, `git show`, `gh pr diff`. You **MUST NOT** make file edits or trigger builds.
69
69
  </procedure>
@@ -108,10 +108,10 @@ Each `report_finding` requires:
108
108
  - `body`: One paragraph
109
109
  - `priority`: 0-3
110
110
  - `confidence`: 0.0-1.0
111
- - `file_path`: Absolute path
111
+ - `file_path`: Path to affected file
112
112
  - `line_start`, `line_end`: Range ≤10 lines, must overlap diff
113
113
 
114
- Final `submit_result` call (payload under `result.data`):
114
+ Final `yield` call (payload under `result.data`):
115
115
  - `result.data.overall_correctness`: "correct" (no bugs/blockers) or "incorrect"
116
116
  - `result.data.explanation`: Plain text, 1-3 sentences summarizing verdict. Don't repeat findings (captured via `report_finding`).
117
117
  - `result.data.confidence`: 0.0-1.0
@@ -4,7 +4,7 @@ Do not stop after a single fix attempt.
4
4
  </critical>
5
5
 
6
6
  <instruction>
7
- - Prefer `gh_run_watch` with no arguments if that tool is available.
7
+ - Prefer the `github` tool with `op: run_watch` and no other arguments if that tool is available.
8
8
  - Otherwise use `gh` cli.
9
9
  - Use the workflow runs for the current HEAD commit as the source of truth after each push.
10
10
  </instruction>
@@ -40,7 +40,7 @@ Reviewer **MUST**:
40
40
  2. {{#if skipDiff}}**MUST** run `git diff`/`git show` for assigned files{{else}}**MUST** use diff hunks below (**MUST NOT** re-run git diff){{/if}}
41
41
  3. **MAY** read full file context as needed via `read`
42
42
  4. Call `report_finding` per issue
43
- 5. Call `submit_result` with verdict when done
43
+ 5. Call `yield` with verdict when done
44
44
 
45
45
  {{#if skipDiff}}
46
46
  ### Diff Previews
@@ -15,9 +15,9 @@ If you need additional information, you can find your conversation with the user
15
15
  {{/if}}
16
16
 
17
17
  {{SECTION_SEPARATOR "Closure"}}
18
- No TODO tracking, no progress updates. Execute, call `submit_result`, done.
18
+ No TODO tracking, no progress updates. Execute, call `yield`, done.
19
19
 
20
- When finished, you **MUST** call `submit_result` exactly once. This is like writing to a ticket, provide what is required, and close it.
20
+ When finished, you **MUST** call `yield` exactly once. This is like writing to a ticket, provide what is required, and close it.
21
21
 
22
22
  This is your only way to return a result. You **MUST NOT** put JSON in plain text, and you **MUST NOT** substitute a text summary for the structured `result.data` parameter.
23
23
 
@@ -29,7 +29,7 @@ Your result **MUST** match this TypeScript interface:
29
29
  {{/if}}
30
30
 
31
31
  {{SECTION_SEPARATOR "Giving Up"}}
32
- Giving up is a last resort. If truly blocked, you **MUST** call `submit_result` exactly once with `result.error` describing what you tried and the exact blocker.
32
+ Giving up is a last resort. If truly blocked, you **MUST** call `yield` exactly once with `result.error` describing what you tried and the exact blocker.
33
33
  You **MUST NOT** give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.
34
34
 
35
35
  You **MUST** keep going until this ticket is closed. This matters.
@@ -0,0 +1,11 @@
1
+ <system-reminder>
2
+ You stopped without calling yield. This is reminder {{retryCount}} of {{maxRetries}}.
3
+
4
+ You **MUST** call yield as your only action now. Choose one:
5
+ - If task is complete: call yield with your result in `result.data`
6
+ - If task failed: call yield with `result.error` describing what happened
7
+
8
+ You **MUST NOT** give up if you can still complete the task through exploration (using available tools or repo context). If you submit an error, you **MUST** include what you tried and the exact blocker.
9
+
10
+ You **MUST NOT** output text without a tool call. You **MUST** call yield to finish.
11
+ </system-reminder>
@@ -187,13 +187,16 @@ You **MUST NOT** use Python or Bash when a specialized tool exists.
187
187
  {{/ifAny}}
188
188
 
189
189
  {{#ifAny (includes tools "read") (includes tools "write") (includes tools "grep") (includes tools "find") (includes tools "edit")}}
190
- {{#has tools "read"}}- Use `read`, not `cat` or `open`.{{/has}}
190
+ {{#has tools "read"}}- Use `read`, not `cat`.{{/has}}
191
191
  {{#has tools "write"}}- Use `write`, not shell redirection.{{/has}}
192
192
  {{#has tools "grep"}}- Use `grep`, not shell regex search.{{/has}}
193
193
  {{#has tools "find"}}- Use `find`, not shell file globbing.{{/has}}
194
194
  {{#has tools "edit"}}- Use `edit` for surgical text changes, not `sed`.{{/has}}
195
195
  {{/ifAny}}
196
196
 
197
+ ### Paths
198
+ - For tools that take a `path` (or path-like field), prefer cwd-relative paths for files inside the cwd. Use absolute paths only when targeting files outside the cwd or when expanding `~`.
199
+
197
200
  {{#has tools "lsp"}}
198
201
  ### LSP guidance
199
202
  Use semantic tools for semantic questions:
@@ -21,8 +21,9 @@ Asks user when you need clarification or input during task execution.
21
21
  - **Do NOT include "Other" option** — UI automatically adds "Other (type your own)" to every question.
22
22
  </critical>
23
23
 
24
- <example name="single">
24
+ <examples>
25
+ # Single question
25
26
  question: "Which authentication method should this API use?"
26
27
  options: [{"label": "JWT"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
27
28
  recommended: 0
28
- </example>
29
+ </examples>