@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.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 (123) hide show
  1. package/CHANGELOG.md +107 -8
  2. package/docs/custom-tools.md +3 -3
  3. package/docs/extensions.md +226 -220
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +50 -53
  6. package/examples/custom-tools/README.md +2 -17
  7. package/examples/extensions/README.md +76 -74
  8. package/examples/extensions/todo.ts +2 -5
  9. package/examples/hooks/custom-compaction.ts +2 -4
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +7 -11
  14. package/package.json +6 -6
  15. package/src/cli/args.ts +9 -6
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +16 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/custom-tools/wrapper.ts +0 -1
  25. package/src/core/extensions/index.ts +1 -6
  26. package/src/core/extensions/runner.ts +1 -1
  27. package/src/core/extensions/types.ts +1 -1
  28. package/src/core/extensions/wrapper.ts +1 -8
  29. package/src/core/file-mentions.ts +5 -8
  30. package/src/core/hooks/runner.ts +2 -2
  31. package/src/core/hooks/types.ts +1 -1
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +1 -1
  35. package/src/core/sdk.ts +64 -105
  36. package/src/core/session-manager.ts +18 -22
  37. package/src/core/settings-manager.ts +66 -1
  38. package/src/core/slash-commands.ts +12 -5
  39. package/src/core/system-prompt.ts +49 -36
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/ask.ts +98 -4
  42. package/src/core/tools/bash-interceptor.ts +11 -4
  43. package/src/core/tools/bash.ts +121 -5
  44. package/src/core/tools/context.ts +7 -0
  45. package/src/core/tools/edit-diff.ts +73 -24
  46. package/src/core/tools/edit.ts +221 -34
  47. package/src/core/tools/exa/render.ts +4 -16
  48. package/src/core/tools/find.ts +149 -5
  49. package/src/core/tools/gemini-image.ts +279 -56
  50. package/src/core/tools/git.ts +17 -3
  51. package/src/core/tools/grep.ts +185 -5
  52. package/src/core/tools/index.test.ts +180 -0
  53. package/src/core/tools/index.ts +96 -242
  54. package/src/core/tools/ls.ts +133 -5
  55. package/src/core/tools/lsp/index.ts +32 -29
  56. package/src/core/tools/lsp/render.ts +21 -22
  57. package/src/core/tools/notebook.ts +112 -4
  58. package/src/core/tools/output.ts +175 -15
  59. package/src/core/tools/read.ts +127 -25
  60. package/src/core/tools/render-utils.ts +241 -0
  61. package/src/core/tools/renderers.ts +40 -828
  62. package/src/core/tools/review.ts +26 -25
  63. package/src/core/tools/rulebook.ts +11 -3
  64. package/src/core/tools/task/agents.ts +28 -7
  65. package/src/core/tools/task/discovery.ts +0 -6
  66. package/src/core/tools/task/executor.ts +264 -254
  67. package/src/core/tools/task/index.ts +48 -208
  68. package/src/core/tools/task/render.ts +26 -11
  69. package/src/core/tools/task/types.ts +7 -12
  70. package/src/core/tools/task/worker-protocol.ts +17 -0
  71. package/src/core/tools/task/worker.ts +238 -0
  72. package/src/core/tools/truncate.ts +27 -1
  73. package/src/core/tools/web-fetch.ts +25 -49
  74. package/src/core/tools/web-search/index.ts +132 -46
  75. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  76. package/src/core/tools/web-search/providers/exa.ts +2 -1
  77. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  78. package/src/core/tools/web-search/render.ts +6 -4
  79. package/src/core/tools/web-search/types.ts +13 -0
  80. package/src/core/tools/write.ts +96 -14
  81. package/src/core/voice.ts +1 -1
  82. package/src/discovery/helpers.test.ts +1 -1
  83. package/src/index.ts +5 -16
  84. package/src/main.ts +5 -5
  85. package/src/modes/interactive/components/assistant-message.ts +1 -1
  86. package/src/modes/interactive/components/custom-message.ts +1 -1
  87. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  88. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  89. package/src/modes/interactive/components/footer.ts +1 -1
  90. package/src/modes/interactive/components/hook-message.ts +1 -1
  91. package/src/modes/interactive/components/model-selector.ts +1 -1
  92. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  93. package/src/modes/interactive/components/settings-defs.ts +49 -0
  94. package/src/modes/interactive/components/status-line.ts +1 -1
  95. package/src/modes/interactive/components/tool-execution.ts +93 -538
  96. package/src/modes/interactive/interactive-mode.ts +19 -7
  97. package/src/modes/interactive/theme/theme.ts +4 -4
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +1 -1
  100. package/src/modes/rpc/rpc-types.ts +1 -1
  101. package/src/prompts/system-prompt.md +4 -0
  102. package/src/prompts/task.md +0 -7
  103. package/src/prompts/tools/gemini-image.md +5 -1
  104. package/src/prompts/tools/output.md +6 -2
  105. package/src/prompts/tools/task.md +68 -0
  106. package/src/prompts/tools/web-fetch.md +1 -0
  107. package/src/prompts/tools/web-search.md +2 -0
  108. package/src/utils/image-convert.ts +8 -2
  109. package/src/utils/image-magick.ts +247 -0
  110. package/src/utils/image-resize.ts +53 -13
  111. package/examples/custom-tools/question/index.ts +0 -84
  112. package/examples/custom-tools/subagent/README.md +0 -172
  113. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  114. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  115. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  116. package/examples/custom-tools/subagent/agents.ts +0 -156
  117. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  118. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  119. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  120. package/examples/custom-tools/subagent/index.ts +0 -1002
  121. package/examples/sdk/05-tools.ts +0 -94
  122. package/examples/sdk/12-full-control.ts +0 -95
  123. package/src/prompts/browser.md +0 -71
@@ -204,6 +204,247 @@ export function formatMoreItems(remaining: number, itemType: string, theme: Them
204
204
  return `${theme.format.ellipsis} ${safeRemaining} more ${pluralize(itemType, safeRemaining)}`;
205
205
  }
206
206
 
207
+ export function formatMeta(meta: string[], theme: Theme): string {
208
+ return meta.length > 0 ? ` ${theme.fg("muted", meta.join(theme.sep.dot))}` : "";
209
+ }
210
+
211
+ export function formatScope(scopePath: string | undefined, theme: Theme): string {
212
+ return scopePath ? ` ${theme.fg("muted", `in ${scopePath}`)}` : "";
213
+ }
214
+
215
+ export function formatTruncationSuffix(truncated: boolean, theme: Theme): string {
216
+ return truncated ? theme.fg("warning", " (truncated)") : "";
217
+ }
218
+
219
+ export function formatErrorMessage(message: string | undefined, theme: Theme): string {
220
+ const clean = (message ?? "").replace(/^Error:\s*/, "").trim();
221
+ return `${theme.styledSymbol("status.error", "error")} ${theme.fg("error", `Error: ${clean || "Unknown error"}`)}`;
222
+ }
223
+
224
+ export function formatEmptyMessage(message: string, theme: Theme): string {
225
+ return `${theme.styledSymbol("status.warning", "warning")} ${theme.fg("muted", message)}`;
226
+ }
227
+
228
+ // =============================================================================
229
+ // Diagnostic Formatting
230
+ // =============================================================================
231
+
232
+ interface ParsedDiagnostic {
233
+ filePath: string;
234
+ line: number;
235
+ col: number;
236
+ severity: "error" | "warning" | "info" | "hint";
237
+ source?: string;
238
+ message: string;
239
+ code?: string;
240
+ }
241
+
242
+ function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
243
+ const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
244
+ if (!match) return null;
245
+ return {
246
+ filePath: match[1],
247
+ line: parseInt(match[2], 10),
248
+ col: parseInt(match[3], 10),
249
+ severity: match[4] as ParsedDiagnostic["severity"],
250
+ source: match[5],
251
+ message: match[6],
252
+ code: match[7],
253
+ };
254
+ }
255
+
256
+ export function formatDiagnostics(
257
+ diag: { errored: boolean; summary: string; messages: string[] },
258
+ expanded: boolean,
259
+ theme: Theme,
260
+ getLangIcon: (filePath: string) => string,
261
+ ): string {
262
+ if (diag.messages.length === 0) return "";
263
+
264
+ const byFile = new Map<string, ParsedDiagnostic[]>();
265
+ const unparsed: string[] = [];
266
+
267
+ for (const msg of diag.messages) {
268
+ const parsed = parseDiagnosticMessage(msg);
269
+ if (parsed) {
270
+ const existing = byFile.get(parsed.filePath) ?? [];
271
+ existing.push(parsed);
272
+ byFile.set(parsed.filePath, existing);
273
+ } else {
274
+ unparsed.push(msg);
275
+ }
276
+ }
277
+
278
+ const headerIcon = diag.errored
279
+ ? theme.styledSymbol("status.error", "error")
280
+ : theme.styledSymbol("status.warning", "warning");
281
+ let output = `\n\n${headerIcon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
282
+
283
+ const maxDiags = expanded ? diag.messages.length : 5;
284
+ let shown = 0;
285
+
286
+ const files = Array.from(byFile.entries());
287
+ for (let fi = 0; fi < files.length && shown < maxDiags; fi++) {
288
+ const [filePath, diagnostics] = files[fi];
289
+ const isLastFile = fi === files.length - 1 && unparsed.length === 0;
290
+ const fileBranch = isLastFile ? theme.tree.last : theme.tree.branch;
291
+
292
+ const fileIcon = theme.fg("muted", getLangIcon(filePath));
293
+ output += `\n ${theme.fg("dim", fileBranch)} ${fileIcon} ${theme.fg("accent", filePath)}`;
294
+ shown++;
295
+
296
+ for (let di = 0; di < diagnostics.length && shown < maxDiags; di++) {
297
+ const d = diagnostics[di];
298
+ const isLastDiag = di === diagnostics.length - 1;
299
+ const diagBranch = isLastFile
300
+ ? isLastDiag
301
+ ? ` ${theme.tree.last}`
302
+ : ` ${theme.tree.branch}`
303
+ : isLastDiag
304
+ ? ` ${theme.tree.vertical} ${theme.tree.last}`
305
+ : ` ${theme.tree.vertical} ${theme.tree.branch}`;
306
+
307
+ const sevIcon =
308
+ d.severity === "error"
309
+ ? theme.styledSymbol("status.error", "error")
310
+ : d.severity === "warning"
311
+ ? theme.styledSymbol("status.warning", "warning")
312
+ : theme.styledSymbol("status.info", "muted");
313
+ const location = theme.fg("dim", `:${d.line}:${d.col}`);
314
+ const codeTag = d.code ? theme.fg("dim", ` (${d.code})`) : "";
315
+ const msgColor = d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "toolOutput";
316
+
317
+ output += `\n ${theme.fg("dim", diagBranch)} ${sevIcon}${location} ${theme.fg(msgColor, d.message)}${codeTag}`;
318
+ shown++;
319
+ }
320
+ }
321
+
322
+ for (const msg of unparsed) {
323
+ if (shown >= maxDiags) break;
324
+ const color = msg.includes("[error]") ? "error" : msg.includes("[warning]") ? "warning" : "dim";
325
+ output += `\n ${theme.fg("dim", theme.tree.branch)} ${theme.fg(color, msg)}`;
326
+ shown++;
327
+ }
328
+
329
+ if (diag.messages.length > shown) {
330
+ const remaining = diag.messages.length - shown;
331
+ output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", `${theme.format.ellipsis} ${remaining} more`)} ${theme.fg("dim", "(Ctrl+O to expand)")}`;
332
+ }
333
+
334
+ return output;
335
+ }
336
+
337
+ // =============================================================================
338
+ // Diff Utilities
339
+ // =============================================================================
340
+
341
+ export interface DiffStats {
342
+ added: number;
343
+ removed: number;
344
+ hunks: number;
345
+ lines: number;
346
+ }
347
+
348
+ export function getDiffStats(diffText: string): DiffStats {
349
+ const lines = diffText ? diffText.split("\n") : [];
350
+ let added = 0;
351
+ let removed = 0;
352
+ let hunks = 0;
353
+ let inHunk = false;
354
+
355
+ for (const line of lines) {
356
+ const isAdded = line.startsWith("+");
357
+ const isRemoved = line.startsWith("-");
358
+ const isChange = isAdded || isRemoved;
359
+
360
+ if (isAdded) added++;
361
+ if (isRemoved) removed++;
362
+
363
+ if (isChange && !inHunk) {
364
+ hunks++;
365
+ inHunk = true;
366
+ } else if (!isChange) {
367
+ inHunk = false;
368
+ }
369
+ }
370
+
371
+ return { added, removed, hunks, lines: lines.length };
372
+ }
373
+
374
+ export function formatDiffStats(added: number, removed: number, hunks: number, theme: Theme): string {
375
+ const parts: string[] = [];
376
+ if (added > 0) parts.push(theme.fg("success", `+${added}`));
377
+ if (removed > 0) parts.push(theme.fg("error", `-${removed}`));
378
+ if (hunks > 0) parts.push(theme.fg("dim", `${hunks} hunk${hunks !== 1 ? "s" : ""}`));
379
+ return parts.join(theme.fg("dim", " / "));
380
+ }
381
+
382
+ export function truncateDiffByHunk(
383
+ diffText: string,
384
+ maxHunks: number,
385
+ maxLines: number,
386
+ ): { text: string; hiddenHunks: number; hiddenLines: number } {
387
+ const lines = diffText ? diffText.split("\n") : [];
388
+ const totalStats = getDiffStats(diffText);
389
+ const kept: string[] = [];
390
+ let inHunk = false;
391
+ let currentHunks = 0;
392
+ let reachedLimit = false;
393
+
394
+ for (const line of lines) {
395
+ const isChange = line.startsWith("+") || line.startsWith("-");
396
+ if (isChange && !inHunk) {
397
+ currentHunks++;
398
+ inHunk = true;
399
+ }
400
+ if (!isChange) {
401
+ inHunk = false;
402
+ }
403
+
404
+ if (currentHunks > maxHunks) {
405
+ reachedLimit = true;
406
+ break;
407
+ }
408
+
409
+ kept.push(line);
410
+ if (kept.length >= maxLines) {
411
+ reachedLimit = true;
412
+ break;
413
+ }
414
+ }
415
+
416
+ if (!reachedLimit) {
417
+ return { text: diffText, hiddenHunks: 0, hiddenLines: 0 };
418
+ }
419
+
420
+ const keptStats = getDiffStats(kept.join("\n"));
421
+ return {
422
+ text: kept.join("\n"),
423
+ hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
424
+ hiddenLines: Math.max(0, totalStats.lines - kept.length),
425
+ };
426
+ }
427
+
428
+ // =============================================================================
429
+ // Path Utilities
430
+ // =============================================================================
431
+
432
+ export function shortenPath(filePath: string, homeDir?: string): string {
433
+ const home = homeDir ?? process.env.HOME ?? process.env.USERPROFILE;
434
+ if (home && filePath.startsWith(home)) {
435
+ return `~${filePath.slice(home.length)}`;
436
+ }
437
+ return filePath;
438
+ }
439
+
440
+ export function wrapBrackets(text: string, theme: Theme): string {
441
+ return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
442
+ }
443
+
444
+ export function replaceTabs(text: string): string {
445
+ return text.replace(/\t/g, " ");
446
+ }
447
+
207
448
  function pluralize(label: string, count: number): string {
208
449
  if (count === 1) return label;
209
450
  if (/(?:ch|sh|s|x|z)$/i.test(label)) return `${label}es`;