@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
@@ -977,13 +977,14 @@ export function findContextLine(
977
977
  }
978
978
 
979
979
  export const replaceEditEntrySchema = Type.Object({
980
- path: Type.String({ description: "File path (relative or absolute)" }),
980
+ path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
981
981
  old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
982
982
  new_text: Type.String({ description: "Replacement text" }),
983
983
  all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
984
984
  });
985
985
 
986
986
  export const replaceEditSchema = Type.Object({
987
+ path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
987
988
  edits: Type.Array(replaceEditEntrySchema, { description: "Replacements", minItems: 1 }),
988
989
  });
989
990
 
@@ -1001,13 +1002,6 @@ export interface ExecuteReplaceSingleOptions {
1001
1002
  beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
1002
1003
  }
1003
1004
 
1004
- export function isReplaceParams(params: unknown): params is ReplaceParams {
1005
- if (typeof params !== "object" || params === null) return false;
1006
- if (!("edits" in params) || !Array.isArray((params as any).edits)) return false;
1007
- const first = (params as any).edits[0];
1008
- return first && typeof first === "object" && "old_text" in first && "new_text" in first;
1009
- }
1010
-
1011
1005
  export async function executeReplaceSingle(
1012
1006
  options: ExecuteReplaceSingleOptions,
1013
1007
  ): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
@@ -1022,6 +1016,9 @@ export async function executeReplaceSingle(
1022
1016
  beginDeferredDiagnosticsForPath,
1023
1017
  } = options;
1024
1018
  const { path, old_text, new_text, all } = params;
1019
+ if (typeof path !== "string" || path.length === 0) {
1020
+ throw new Error("replace edit: missing `path`. Provide `path` on the edit or supply a top-level `path`.");
1021
+ }
1025
1022
 
1026
1023
  enforcePlanModeWrite(session, path);
1027
1024
 
@@ -1065,9 +1062,7 @@ export async function executeReplaceSingle(
1065
1062
  }
1066
1063
 
1067
1064
  if (normalizedContent === result.content) {
1068
- throw new Error(
1069
- `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
1070
- );
1065
+ throw new Error(`Edits to ${path} resulted in no changes being made.`);
1071
1066
  }
1072
1067
 
1073
1068
  const finalContent = bom + restoreLineEndings(result.content, originalEnding);
@@ -50,6 +50,9 @@ export interface EditToolPerFileResult {
50
50
  move?: string;
51
51
  isError?: boolean;
52
52
  errorText?: string;
53
+ /** TUI-friendly error text. When present, rendered to the user instead of `errorText`.
54
+ * Set when the underlying error carries a `displayMessage` (e.g. {@link HashlineMismatchError}). */
55
+ displayErrorText?: string;
53
56
  meta?: OutputMeta;
54
57
  }
55
58
 
@@ -377,18 +380,18 @@ function wrapEditRendererLine(line: string, width: number): string[] {
377
380
  const startAnsi = line.match(/^((?:\x1b\[[0-9;]*m)*)/)?.[1] ?? "";
378
381
  const bodyWithReset = line.slice(startAnsi.length);
379
382
  const body = bodyWithReset.endsWith("\x1b[39m") ? bodyWithReset.slice(0, -"\x1b[39m".length) : bodyWithReset;
380
- const diffMatch = /^([+\-\s])(\s*\d+)\|(.*)$/s.exec(body);
383
+ const diffMatch = /^([+\-\s])(\s*\d+)([|│])(.*)$/s.exec(body);
381
384
 
382
385
  if (!diffMatch) {
383
386
  return wrapTextWithAnsi(line, width);
384
387
  }
385
388
 
386
- const [, marker, lineNum, content] = diffMatch;
387
- const prefix = `${marker}${lineNum}|`;
389
+ const [, marker, lineNum, separator, content] = diffMatch;
390
+ const prefix = `${marker}${lineNum}${separator}`;
388
391
  const prefixWidth = visibleWidth(prefix);
389
392
  const contentWidth = Math.max(1, width - prefixWidth);
390
- const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}|`;
391
- const wrappedContent = wrapTextWithAnsi(content, contentWidth);
393
+ const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}${separator}`;
394
+ const wrappedContent = wrapTextWithAnsi(content ?? "", contentWidth);
392
395
 
393
396
  return wrappedContent.map(
394
397
  (segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`,
@@ -489,13 +492,14 @@ function renderSingleFileResult(
489
492
  const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
490
493
  const { language } = formatEditDescription(rawPath, uiTheme, { rename });
491
494
 
492
- const metadataLine =
493
- op !== "delete"
494
- ? `\n${formatMetadataLine(countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? ""), language, uiTheme)}`
495
- : "";
495
+ const editTextSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch;
496
+ const metadataLineCount = editTextSource ? countLines(editTextSource) : null;
497
+ const metadataLine = op !== "delete" ? `\n${formatMetadataLine(metadataLineCount, language, uiTheme)}` : "";
496
498
 
499
+ const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
497
500
  const errorText = isError
498
- ? (details && "errorText" in details && details.errorText) ||
501
+ ? displayErrorText ||
502
+ (details && "errorText" in details && details.errorText) ||
499
503
  (result.content?.find(c => c.type === "text")?.text ?? "")
500
504
  : "";
501
505
 
@@ -131,6 +131,7 @@ export function dropIncompleteLastEdit<T>(edits: readonly T[], partialJson: stri
131
131
  // -----------------------------------------------------------------------------
132
132
 
133
133
  interface ReplaceArgs {
134
+ path?: string;
134
135
  edits?: ReplaceEditEntry[];
135
136
  __partialJson?: string;
136
137
  }
@@ -142,10 +143,12 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
142
143
  },
143
144
  async computeDiffPreview(args, ctx) {
144
145
  const first = args.edits?.[0];
145
- if (!first?.path || first.old_text === undefined || first.new_text === undefined) return null;
146
+ if (!first) return null;
147
+ const path = first.path ?? args.path;
148
+ if (!path || first.old_text === undefined || first.new_text === undefined) return null;
146
149
  ctx.signal.throwIfAborted();
147
150
  const result = await computeEditDiff(
148
- first.path,
151
+ path,
149
152
  first.old_text,
150
153
  first.new_text,
151
154
  ctx.cwd,
@@ -154,7 +157,7 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
154
157
  ctx.fuzzyThreshold,
155
158
  );
156
159
  ctx.signal.throwIfAborted();
157
- return [toPerFilePreview(first.path, result)];
160
+ return [toPerFilePreview(path, result)];
158
161
  },
159
162
  renderStreamingFallback() {
160
163
  return "";
@@ -162,6 +165,7 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
162
165
  };
163
166
 
164
167
  interface PatchArgs {
168
+ path?: string;
165
169
  edits?: PatchEditEntry[];
166
170
  __partialJson?: string;
167
171
  }
@@ -173,15 +177,16 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
173
177
  },
174
178
  async computeDiffPreview(args, ctx) {
175
179
  const first = args.edits?.[0];
176
- if (!first?.path) return null;
180
+ const path = first?.path ?? args.path;
181
+ if (!path) return null;
177
182
  ctx.signal.throwIfAborted();
178
183
  const result = await computePatchDiff(
179
- { path: first.path, op: first.op ?? "update", rename: first.rename, diff: first.diff },
184
+ { path, op: first?.op ?? "update", rename: first?.rename, diff: first?.diff },
180
185
  ctx.cwd,
181
186
  { fuzzyThreshold: ctx.fuzzyThreshold, allowFuzzy: ctx.allowFuzzy },
182
187
  );
183
188
  ctx.signal.throwIfAborted();
184
- return [toPerFilePreview(first.path, result)];
189
+ return [toPerFilePreview(path, result)];
185
190
  },
186
191
  renderStreamingFallback() {
187
192
  return "";
@@ -189,8 +194,8 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
189
194
  };
190
195
 
191
196
  interface HashlineArgs {
197
+ path?: string;
192
198
  edits?: HashlineToolEdit[];
193
- move?: string;
194
199
  __partialJson?: string;
195
200
  }
196
201
 
@@ -201,13 +206,18 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
201
206
  },
202
207
  async computeDiffPreview(args, ctx) {
203
208
  const first = args.edits?.[0] as (HashlineToolEdit & { path?: string }) | undefined;
204
- if (!first?.path) return null;
205
- const path = first.path;
206
- const fileEdits = (args.edits ?? []).filter((e): e is HashlineToolEdit & { path: string } => {
207
- return !!e && typeof e === "object" && (e as { path?: string }).path === path;
208
- });
209
+ const path = first?.path ?? args.path;
210
+ if (!path) return null;
211
+ const fileEdits = (args.edits ?? [])
212
+ .map(e => {
213
+ if (!e || typeof e !== "object") return undefined;
214
+ const entryPath = (e as { path?: string }).path ?? args.path;
215
+ if (!entryPath || entryPath !== path) return undefined;
216
+ return { ...(e as HashlineToolEdit), path } as HashlineToolEdit & { path: string };
217
+ })
218
+ .filter((e): e is HashlineToolEdit & { path: string } => e !== undefined);
209
219
  ctx.signal.throwIfAborted();
210
- const result = await computeHashlineDiff({ path, edits: fileEdits, move: args.move }, ctx.cwd);
220
+ const result = await computeHashlineDiff({ path, edits: fileEdits }, ctx.cwd);
211
221
  ctx.signal.throwIfAborted();
212
222
  return [toPerFilePreview(path, result)];
213
223
  },
@@ -217,6 +227,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
217
227
  };
218
228
 
219
229
  interface ChunkArgs {
230
+ path?: string;
220
231
  edits?: ChunkToolEdit[];
221
232
  __partialJson?: string;
222
233
  }
@@ -248,8 +259,10 @@ const chunkStrategy: EditStreamingStrategy<ChunkArgs> = {
248
259
  const groups = new Map<string, ChunkToolEdit[]>();
249
260
  const fileOrder: string[] = [];
250
261
  for (const edit of edits) {
251
- if (!edit?.path) continue;
252
- const { filePath } = parseChunkEditPath(edit.path);
262
+ if (!edit) continue;
263
+ const editPath = edit.path ?? args.path;
264
+ if (!editPath) continue;
265
+ const { filePath } = parseChunkEditPath(editPath);
253
266
  if (!filePath) continue;
254
267
  let bucket = groups.get(filePath);
255
268
  if (!bucket) {
@@ -257,7 +270,7 @@ const chunkStrategy: EditStreamingStrategy<ChunkArgs> = {
257
270
  groups.set(filePath, bucket);
258
271
  fileOrder.push(filePath);
259
272
  }
260
- bucket.push(edit);
273
+ bucket.push({ ...edit, path: editPath });
261
274
  }
262
275
  if (fileOrder.length === 0) return null;
263
276
 
@@ -328,6 +341,26 @@ const vimStrategy: EditStreamingStrategy<unknown> = {
328
341
  },
329
342
  };
330
343
 
344
+ interface AtomArgs {
345
+ path?: string;
346
+ edits?: unknown[];
347
+ }
348
+
349
+ const atomStrategy: EditStreamingStrategy<AtomArgs> = {
350
+ extractCompleteEdits(args, partialJson) {
351
+ if (!args.edits) return args;
352
+ return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
353
+ },
354
+ async computeDiffPreview() {
355
+ // Atom edits are line-anchored and validated against live file hashes; a
356
+ // streaming preview without that validation could mislead. Skip for now.
357
+ return null;
358
+ },
359
+ renderStreamingFallback() {
360
+ return "";
361
+ },
362
+ };
363
+
331
364
  export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknown>> = {
332
365
  replace: replaceStrategy as EditStreamingStrategy<unknown>,
333
366
  patch: patchStrategy as EditStreamingStrategy<unknown>,
@@ -335,6 +368,7 @@ export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknow
335
368
  chunk: chunkStrategy as EditStreamingStrategy<unknown>,
336
369
  apply_patch: applyPatchStrategy as EditStreamingStrategy<unknown>,
337
370
  vim: vimStrategy,
371
+ atom: atomStrategy as EditStreamingStrategy<unknown>,
338
372
  };
339
373
 
340
374
  export { resolveEditMode };
@@ -239,7 +239,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
239
239
  // artifact, and splice an `artifact://<id>` footer into the visible text so
240
240
  // the agent can retrieve the raw bytes losslessly.
241
241
  const minimized = winner.result.minimized;
242
- if (minimized) {
242
+ if (minimized && minimized.text !== minimized.originalText) {
243
243
  sink.replace(minimized.text);
244
244
  if (options?.onMinimizedSave) {
245
245
  const artifactId = await options.onMinimizedSave(minimized.originalText, {
@@ -248,9 +248,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
248
248
  outputBytes: minimized.outputBytes,
249
249
  });
250
250
  if (artifactId) {
251
- sink.push(
252
- `\n… full output: artifact://${artifactId} (${minimized.inputBytes} → ${minimized.outputBytes} bytes)\n`,
253
- );
251
+ sink.push(`\n[raw output: artifact://${artifactId}]\n`);
254
252
  }
255
253
  }
256
254
  }