@oh-my-pi/pi-coding-agent 8.0.20 → 8.1.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 (166) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/package.json +14 -11
  3. package/scripts/generate-wasm-b64.ts +24 -0
  4. package/src/capability/context-file.ts +1 -1
  5. package/src/capability/extension-module.ts +1 -1
  6. package/src/capability/extension.ts +1 -1
  7. package/src/capability/hook.ts +1 -1
  8. package/src/capability/instruction.ts +1 -1
  9. package/src/capability/mcp.ts +1 -1
  10. package/src/capability/prompt.ts +1 -1
  11. package/src/capability/rule.ts +1 -1
  12. package/src/capability/settings.ts +1 -1
  13. package/src/capability/skill.ts +1 -1
  14. package/src/capability/slash-command.ts +1 -1
  15. package/src/capability/ssh.ts +1 -1
  16. package/src/capability/system-prompt.ts +1 -1
  17. package/src/capability/tool.ts +1 -1
  18. package/src/cli/args.ts +1 -1
  19. package/src/cli/plugin-cli.ts +1 -5
  20. package/src/commit/agentic/agent.ts +309 -0
  21. package/src/commit/agentic/fallback.ts +96 -0
  22. package/src/commit/agentic/index.ts +359 -0
  23. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  24. package/src/commit/agentic/prompts/session-user.md +26 -0
  25. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  26. package/src/commit/agentic/prompts/system.md +40 -0
  27. package/src/commit/agentic/state.ts +74 -0
  28. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  29. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  30. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  31. package/src/commit/agentic/tools/git-overview.ts +84 -0
  32. package/src/commit/agentic/tools/index.ts +56 -0
  33. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  34. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  35. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  36. package/src/commit/agentic/tools/split-commit.ts +284 -0
  37. package/src/commit/agentic/topo-sort.ts +44 -0
  38. package/src/commit/agentic/trivial.ts +51 -0
  39. package/src/commit/agentic/validation.ts +200 -0
  40. package/src/commit/analysis/conventional.ts +169 -0
  41. package/src/commit/analysis/index.ts +4 -0
  42. package/src/commit/analysis/scope.ts +242 -0
  43. package/src/commit/analysis/summary.ts +114 -0
  44. package/src/commit/analysis/validation.ts +66 -0
  45. package/src/commit/changelog/detect.ts +36 -0
  46. package/src/commit/changelog/generate.ts +112 -0
  47. package/src/commit/changelog/index.ts +233 -0
  48. package/src/commit/changelog/parse.ts +44 -0
  49. package/src/commit/cli.ts +93 -0
  50. package/src/commit/git/diff.ts +148 -0
  51. package/src/commit/git/errors.ts +11 -0
  52. package/src/commit/git/index.ts +217 -0
  53. package/src/commit/git/operations.ts +53 -0
  54. package/src/commit/index.ts +5 -0
  55. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  56. package/src/commit/map-reduce/index.ts +63 -0
  57. package/src/commit/map-reduce/map-phase.ts +193 -0
  58. package/src/commit/map-reduce/reduce-phase.ts +147 -0
  59. package/src/commit/map-reduce/utils.ts +9 -0
  60. package/src/commit/message.ts +11 -0
  61. package/src/commit/model-selection.ts +84 -0
  62. package/src/commit/pipeline.ts +242 -0
  63. package/src/commit/prompts/analysis-system.md +155 -0
  64. package/src/commit/prompts/analysis-user.md +41 -0
  65. package/src/commit/prompts/changelog-system.md +56 -0
  66. package/src/commit/prompts/changelog-user.md +19 -0
  67. package/src/commit/prompts/file-observer-system.md +26 -0
  68. package/src/commit/prompts/file-observer-user.md +9 -0
  69. package/src/commit/prompts/reduce-system.md +60 -0
  70. package/src/commit/prompts/reduce-user.md +17 -0
  71. package/src/commit/prompts/summary-retry.md +4 -0
  72. package/src/commit/prompts/summary-system.md +52 -0
  73. package/src/commit/prompts/summary-user.md +13 -0
  74. package/src/commit/prompts/types-description.md +2 -0
  75. package/src/commit/types.ts +109 -0
  76. package/src/commit/utils/exclusions.ts +42 -0
  77. package/src/config/file-lock.ts +111 -0
  78. package/src/config/model-registry.ts +16 -7
  79. package/src/config/settings-manager.ts +115 -40
  80. package/src/config.ts +5 -5
  81. package/src/discovery/agents-md.ts +1 -1
  82. package/src/discovery/builtin.ts +1 -1
  83. package/src/discovery/claude.ts +1 -1
  84. package/src/discovery/cline.ts +1 -1
  85. package/src/discovery/codex.ts +1 -1
  86. package/src/discovery/cursor.ts +1 -1
  87. package/src/discovery/gemini.ts +1 -1
  88. package/src/discovery/github.ts +1 -1
  89. package/src/discovery/index.ts +11 -11
  90. package/src/discovery/mcp-json.ts +1 -1
  91. package/src/discovery/ssh.ts +1 -1
  92. package/src/discovery/vscode.ts +1 -1
  93. package/src/discovery/windsurf.ts +1 -1
  94. package/src/extensibility/custom-commands/loader.ts +1 -1
  95. package/src/extensibility/custom-commands/types.ts +1 -1
  96. package/src/extensibility/custom-tools/loader.ts +1 -1
  97. package/src/extensibility/custom-tools/types.ts +1 -1
  98. package/src/extensibility/extensions/loader.ts +1 -1
  99. package/src/extensibility/extensions/types.ts +1 -1
  100. package/src/extensibility/hooks/loader.ts +1 -1
  101. package/src/extensibility/hooks/types.ts +3 -3
  102. package/src/index.ts +10 -10
  103. package/src/ipy/executor.ts +97 -1
  104. package/src/lsp/index.ts +1 -1
  105. package/src/lsp/render.ts +90 -46
  106. package/src/main.ts +16 -3
  107. package/src/mcp/loader.ts +3 -3
  108. package/src/migrations.ts +3 -3
  109. package/src/modes/components/assistant-message.ts +29 -1
  110. package/src/modes/components/tool-execution.ts +5 -3
  111. package/src/modes/components/tree-selector.ts +1 -1
  112. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  113. package/src/modes/controllers/selector-controller.ts +1 -1
  114. package/src/modes/interactive-mode.ts +5 -3
  115. package/src/modes/rpc/rpc-client.ts +1 -1
  116. package/src/modes/rpc/rpc-mode.ts +1 -4
  117. package/src/modes/rpc/rpc-types.ts +1 -1
  118. package/src/modes/theme/mermaid-cache.ts +89 -0
  119. package/src/modes/theme/theme.ts +2 -0
  120. package/src/modes/types.ts +2 -2
  121. package/src/patch/index.ts +3 -9
  122. package/src/patch/shared.ts +33 -5
  123. package/src/prompts/tools/task.md +2 -0
  124. package/src/sdk.ts +60 -22
  125. package/src/session/agent-session.ts +3 -3
  126. package/src/session/agent-storage.ts +32 -28
  127. package/src/session/artifacts.ts +24 -1
  128. package/src/session/auth-storage.ts +25 -10
  129. package/src/session/storage-migration.ts +12 -53
  130. package/src/system-prompt.ts +2 -2
  131. package/src/task/.executor.ts.kate-swp +0 -0
  132. package/src/task/executor.ts +1 -1
  133. package/src/task/index.ts +10 -1
  134. package/src/task/output-manager.ts +94 -0
  135. package/src/task/render.ts +7 -12
  136. package/src/task/worker.ts +1 -1
  137. package/src/tools/ask.ts +35 -13
  138. package/src/tools/bash.ts +80 -87
  139. package/src/tools/calculator.ts +42 -40
  140. package/src/tools/complete.ts +1 -1
  141. package/src/tools/fetch.ts +67 -104
  142. package/src/tools/find.ts +83 -86
  143. package/src/tools/grep.ts +80 -96
  144. package/src/tools/index.ts +10 -7
  145. package/src/tools/ls.ts +39 -65
  146. package/src/tools/notebook.ts +48 -64
  147. package/src/tools/output-utils.ts +1 -1
  148. package/src/tools/python.ts +71 -183
  149. package/src/tools/read.ts +74 -15
  150. package/src/tools/render-utils.ts +1 -15
  151. package/src/tools/ssh.ts +43 -24
  152. package/src/tools/todo-write.ts +27 -15
  153. package/src/tools/write.ts +93 -64
  154. package/src/tui/code-cell.ts +115 -0
  155. package/src/tui/file-list.ts +48 -0
  156. package/src/tui/index.ts +11 -0
  157. package/src/tui/output-block.ts +73 -0
  158. package/src/tui/status-line.ts +40 -0
  159. package/src/tui/tree-list.ts +56 -0
  160. package/src/tui/types.ts +17 -0
  161. package/src/tui/utils.ts +49 -0
  162. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  163. package/src/web/search/auth.ts +1 -1
  164. package/src/web/search/index.ts +1 -1
  165. package/src/web/search/render.ts +119 -163
  166. package/tsconfig.json +0 -42
package/src/tools/find.ts CHANGED
@@ -3,10 +3,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { StringEnum } from "@oh-my-pi/pi-ai";
4
4
  import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
5
5
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
6
- import { getLanguageFromPath, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
7
7
  import findDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/find.md" with { type: "text" };
8
8
  import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
9
9
  import { ToolAbortError, ToolError, throwIfAborted } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
10
+ import { renderFileList, renderStatusLine, renderTreeList } from "@oh-my-pi/pi-coding-agent/tui";
10
11
  import { ensureTool } from "@oh-my-pi/pi-coding-agent/utils/tools-manager";
11
12
  import type { Component } from "@oh-my-pi/pi-tui";
12
13
  import { Text } from "@oh-my-pi/pi-tui";
@@ -14,10 +15,10 @@ import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
14
15
  import type { Static } from "@sinclair/typebox";
15
16
  import { Type } from "@sinclair/typebox";
16
17
 
17
- import type { ToolSession } from "./index";
18
+ import type { ToolSession } from ".";
18
19
  import { applyListLimit } from "./list-limit";
19
20
  import { resolveToCwd } from "./path-utils";
20
- import { PREVIEW_LIMITS, ToolUIKit } from "./render-utils";
21
+ import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
21
22
  import { toolResult } from "./tool-result";
22
23
  import { type TruncationResult, truncateHead } from "./truncate";
23
24
 
@@ -34,6 +35,7 @@ const findSchema = Type.Object({
34
35
  });
35
36
 
36
37
  const DEFAULT_LIMIT = 1000;
38
+ const FD_TIMEOUT_MS = 5000;
37
39
 
38
40
  export interface FindToolDetails {
39
41
  truncation?: TruncationResult;
@@ -132,6 +134,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
132
134
 
133
135
  return untilAborted(signal, async () => {
134
136
  const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
137
+
138
+ if (searchPath === "/") {
139
+ throw new ToolError("Searching from root directory '/' is not allowed");
140
+ }
141
+
135
142
  const scopePath = (() => {
136
143
  const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
137
144
  return relative.length === 0 ? "." : relative;
@@ -246,7 +253,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
246
253
  "--absolute-path",
247
254
  searchPath,
248
255
  ];
249
- const { stdout: gitignoreStdout } = await runFd(fdPath, gitignoreArgs, signal);
256
+ const timeoutSignal = AbortSignal.timeout(FD_TIMEOUT_MS);
257
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
258
+ const { stdout: gitignoreStdout } = await runFd(fdPath, gitignoreArgs, combinedSignal);
250
259
  for (const rawLine of gitignoreStdout.split("\n")) {
251
260
  const file = rawLine.trim();
252
261
  if (!file) continue;
@@ -266,8 +275,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
266
275
  // Pattern and path
267
276
  args.push(effectivePattern, searchPath);
268
277
 
269
- // Run fd
270
- const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
278
+ // Run fd with timeout
279
+ const mainTimeoutSignal = AbortSignal.timeout(FD_TIMEOUT_MS);
280
+ const mainCombinedSignal = signal ? AbortSignal.any([signal, mainTimeoutSignal]) : mainTimeoutSignal;
281
+ const { stdout, stderr, exitCode } = await runFd(fdPath, args, mainCombinedSignal);
271
282
  const output = stdout.trim();
272
283
 
273
284
  // fd exit codes: 0 = found files, 1 = no matches, other = error
@@ -371,10 +382,6 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
371
382
  export const findToolRenderer = {
372
383
  inline: true,
373
384
  renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
374
- const ui = new ToolUIKit(uiTheme);
375
- const label = ui.title("Find");
376
- let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.pattern || "*")}`;
377
-
378
385
  const meta: string[] = [];
379
386
  if (args.path) meta.push(`in ${args.path}`);
380
387
  if (args.type && args.type !== "all") meta.push(`type:${args.type}`);
@@ -382,8 +389,10 @@ export const findToolRenderer = {
382
389
  if (args.sortByMtime) meta.push("sort:mtime");
383
390
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
384
391
 
385
- text += ui.meta(meta);
386
-
392
+ const text = renderStatusLine(
393
+ { icon: "pending", title: "Find", description: args.pattern || "*", meta },
394
+ uiTheme,
395
+ );
387
396
  return new Text(text, 0, 0);
388
397
  },
389
398
 
@@ -391,106 +400,94 @@ export const findToolRenderer = {
391
400
  result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
392
401
  { expanded }: RenderResultOptions,
393
402
  uiTheme: Theme,
403
+ args?: FindRenderArgs,
394
404
  ): Component {
395
- const ui = new ToolUIKit(uiTheme);
396
405
  const details = result.details;
397
406
 
398
407
  if (result.isError || details?.error) {
399
408
  const errorText = details?.error || result.content?.find((c) => c.type === "text")?.text || "Unknown error";
400
- return new Text(` ${ui.errorMessage(errorText)}`, 0, 0);
409
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
401
410
  }
402
411
 
403
412
  const hasDetailedData = details?.fileCount !== undefined;
404
413
  const textContent = result.content?.find((c) => c.type === "text")?.text;
405
414
 
406
415
  if (!hasDetailedData) {
407
- if (!textContent || textContent.includes("No files matching") || textContent.trim() === "") {
408
- return new Text(` ${ui.emptyMessage("No files found")}`, 0, 0);
416
+ if (
417
+ !textContent ||
418
+ textContent.includes("No files matching") ||
419
+ textContent.includes("No files found") ||
420
+ textContent.trim() === ""
421
+ ) {
422
+ return new Text(formatEmptyMessage("No files found", uiTheme), 0, 0);
409
423
  }
410
424
 
411
425
  const lines = textContent.split("\n").filter((l) => l.trim());
412
- const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_LIST_LIMIT);
413
- const displayLines = lines.slice(0, maxLines);
414
- const remaining = lines.length - maxLines;
415
- const hasMore = remaining > 0;
416
-
417
- const icon = uiTheme.styledSymbol("status.success", "success");
418
- const summary = ui.count("file", lines.length);
419
- const expandHint = ui.expandHint(expanded, hasMore);
420
- let text = ` ${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
421
-
422
- for (let i = 0; i < displayLines.length; i++) {
423
- const isLast = i === displayLines.length - 1 && remaining === 0;
424
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
425
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", displayLines[i])}`;
426
- }
427
- if (remaining > 0) {
428
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "file"))}`;
429
- }
430
- return new Text(text, 0, 0);
426
+ const header = renderStatusLine(
427
+ {
428
+ icon: "success",
429
+ title: "Find",
430
+ description: args?.pattern,
431
+ meta: [formatCount("file", lines.length)],
432
+ },
433
+ uiTheme,
434
+ );
435
+ const listLines = renderTreeList(
436
+ {
437
+ items: lines,
438
+ expanded,
439
+ maxCollapsed: COLLAPSED_LIST_LIMIT,
440
+ itemType: "file",
441
+ renderItem: (line) => uiTheme.fg("accent", line),
442
+ },
443
+ uiTheme,
444
+ );
445
+ return new Text([header, ...listLines].join("\n"), 0, 0);
431
446
  }
432
447
 
433
448
  const fileCount = details?.fileCount ?? 0;
434
- const truncation = details?.meta?.truncation;
449
+ const truncation = details?.truncation ?? details?.meta?.truncation;
435
450
  const limits = details?.meta?.limits;
436
- const truncated = Boolean(
437
- details?.truncated || truncation || limits?.resultLimit || limits?.headLimit || limits?.matchLimit,
438
- );
451
+ const truncated = Boolean(details?.truncated || truncation || details?.resultLimitReached || limits?.resultLimit);
439
452
  const files = details?.files ?? [];
440
453
 
441
454
  if (fileCount === 0) {
442
- return new Text(` ${ui.emptyMessage("No files found")}`, 0, 0);
455
+ const header = renderStatusLine(
456
+ { icon: "warning", title: "Find", description: args?.pattern, meta: ["0 files"] },
457
+ uiTheme,
458
+ );
459
+ return new Text([header, formatEmptyMessage("No files found", uiTheme)].join("\n"), 0, 0);
443
460
  }
461
+ const meta: string[] = [formatCount("file", fileCount)];
462
+ if (details?.scopePath) meta.push(`in ${details.scopePath}`);
463
+ if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
464
+ const header = renderStatusLine(
465
+ { icon: truncated ? "warning" : "success", title: "Find", description: args?.pattern, meta },
466
+ uiTheme,
467
+ );
444
468
 
445
- const icon = uiTheme.styledSymbol("status.success", "success");
446
- const summaryText = ui.count("file", fileCount);
447
- const scopeLabel = ui.scope(details?.scopePath);
448
- const maxFiles = expanded ? files.length : Math.min(files.length, COLLAPSED_LIST_LIMIT);
449
- const hasMoreFiles = files.length > maxFiles;
450
- const expandHint = ui.expandHint(expanded, hasMoreFiles);
451
-
452
- let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
469
+ const fileLines = renderFileList(
470
+ {
471
+ files: files.map((entry) => ({ path: entry, isDirectory: entry.endsWith("/") })),
472
+ expanded,
473
+ maxCollapsed: COLLAPSED_LIST_LIMIT,
474
+ },
475
+ uiTheme,
476
+ );
453
477
 
454
478
  const truncationReasons: string[] = [];
455
- if (limits?.resultLimit) {
456
- truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
457
- }
458
- if (truncation) {
459
- truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
460
- }
461
- if (truncation?.artifactId) {
462
- truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
479
+ if (details?.resultLimitReached) truncationReasons.push(`limit ${details.resultLimitReached} results`);
480
+ if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
481
+ if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
482
+ const artifactId = truncation && "artifactId" in truncation ? truncation.artifactId : undefined;
483
+ if (artifactId) truncationReasons.push(`full output: artifact://${artifactId}`);
484
+
485
+ const extraLines: string[] = [];
486
+ if (truncationReasons.length > 0) {
487
+ extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
463
488
  }
464
489
 
465
- const hasTruncation = truncationReasons.length > 0;
466
-
467
- if (files.length > 0) {
468
- for (let i = 0; i < maxFiles; i++) {
469
- const isLast = i === maxFiles - 1 && !hasMoreFiles && !hasTruncation;
470
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
471
- const entry = files[i];
472
- const isDir = entry.endsWith("/");
473
- const entryPath = isDir ? entry.slice(0, -1) : entry;
474
- const lang = isDir ? undefined : getLanguageFromPath(entryPath);
475
- const entryIcon = isDir
476
- ? uiTheme.fg("accent", uiTheme.icon.folder)
477
- : uiTheme.fg("muted", uiTheme.getLangIcon(lang));
478
- text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry)}`;
479
- }
480
-
481
- if (hasMoreFiles) {
482
- const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
483
- text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
484
- "muted",
485
- ui.moreItems(files.length - maxFiles, "file"),
486
- )}`;
487
- }
488
- }
489
-
490
- if (hasTruncation) {
491
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
492
- }
493
-
494
- return new Text(text, 0, 0);
490
+ return new Text([header, ...fileLines, ...extraLines].join("\n"), 0, 0);
495
491
  },
492
+ mergeCallAndResult: true,
496
493
  };
package/src/tools/grep.ts CHANGED
@@ -3,10 +3,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { StringEnum } from "@oh-my-pi/pi-ai";
4
4
  import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
5
5
  import type { RenderResultOptions } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
6
- import { getLanguageFromPath, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
7
7
  import grepDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/grep.md" with { type: "text" };
8
8
  import type { OutputMeta } from "@oh-my-pi/pi-coding-agent/tools/output-meta";
9
9
  import { ToolAbortError, ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
10
+ import { renderFileList, renderStatusLine, renderTreeList } from "@oh-my-pi/pi-coding-agent/tui";
10
11
  import { ensureTool } from "@oh-my-pi/pi-coding-agent/utils/tools-manager";
11
12
  import { untilAborted } from "@oh-my-pi/pi-coding-agent/utils/utils";
12
13
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -14,10 +15,10 @@ import { Text } from "@oh-my-pi/pi-tui";
14
15
  import { ptree, readLines } from "@oh-my-pi/pi-utils";
15
16
  import { Type } from "@sinclair/typebox";
16
17
  import { $ } from "bun";
17
- import type { ToolSession } from "./index";
18
+ import type { ToolSession } from ".";
18
19
  import { applyListLimit } from "./list-limit";
19
20
  import { resolveToCwd } from "./path-utils";
20
- import { PREVIEW_LIMITS, ToolUIKit } from "./render-utils";
21
+ import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
21
22
  import { toolResult } from "./tool-result";
22
23
  import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "./truncate";
23
24
 
@@ -600,10 +601,6 @@ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
600
601
  export const grepToolRenderer = {
601
602
  inline: true,
602
603
  renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
603
- const ui = new ToolUIKit(uiTheme);
604
- const label = ui.title("Grep");
605
- let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
606
-
607
604
  const meta: string[] = [];
608
605
  if (args.path) meta.push(`in ${args.path}`);
609
606
  if (args.glob) meta.push(`glob:${args.glob}`);
@@ -619,8 +616,10 @@ export const grepToolRenderer = {
619
616
  if (args.context !== undefined) meta.push(`context:${args.context}`);
620
617
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
621
618
 
622
- text += ui.meta(meta);
623
-
619
+ const text = renderStatusLine(
620
+ { icon: "pending", title: "Grep", description: args.pattern || "?", meta },
621
+ uiTheme,
622
+ );
624
623
  return new Text(text, 0, 0);
625
624
  },
626
625
 
@@ -628,13 +627,13 @@ export const grepToolRenderer = {
628
627
  result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
629
628
  { expanded }: RenderResultOptions,
630
629
  uiTheme: Theme,
630
+ args?: GrepRenderArgs,
631
631
  ): Component {
632
- const ui = new ToolUIKit(uiTheme);
633
632
  const details = result.details;
634
633
 
635
634
  if (result.isError || details?.error) {
636
635
  const errorText = details?.error || result.content?.find((c) => c.type === "text")?.text || "Unknown error";
637
- return new Text(` ${ui.errorMessage(errorText)}`, 0, 0);
636
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
638
637
  }
639
638
 
640
639
  const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
@@ -642,31 +641,25 @@ export const grepToolRenderer = {
642
641
  if (!hasDetailedData) {
643
642
  const textContent = result.content?.find((c) => c.type === "text")?.text;
644
643
  if (!textContent || textContent === "No matches found") {
645
- return new Text(` ${ui.emptyMessage("No matches found")}`, 0, 0);
644
+ return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
646
645
  }
647
-
648
646
  const lines = textContent.split("\n").filter((line) => line.trim() !== "");
649
- const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_TEXT_LIMIT);
650
- const displayLines = lines.slice(0, maxLines);
651
- const remaining = lines.length - maxLines;
652
- const hasMore = remaining > 0;
653
-
654
- const icon = uiTheme.styledSymbol("status.success", "success");
655
- const summary = ui.count("item", lines.length);
656
- const expandHint = ui.expandHint(expanded, hasMore);
657
- let text = ` ${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
658
-
659
- for (let i = 0; i < displayLines.length; i++) {
660
- const isLast = i === displayLines.length - 1 && remaining === 0;
661
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
662
- text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", displayLines[i])}`;
663
- }
664
-
665
- if (remaining > 0) {
666
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "item"))}`;
667
- }
668
-
669
- return new Text(text, 0, 0);
647
+ const description = args?.pattern ?? undefined;
648
+ const header = renderStatusLine(
649
+ { icon: "success", title: "Grep", description, meta: [formatCount("item", lines.length)] },
650
+ uiTheme,
651
+ );
652
+ const listLines = renderTreeList(
653
+ {
654
+ items: lines,
655
+ expanded,
656
+ maxCollapsed: COLLAPSED_TEXT_LIMIT,
657
+ itemType: "item",
658
+ renderItem: (line) => uiTheme.fg("toolOutput", line),
659
+ },
660
+ uiTheme,
661
+ );
662
+ return new Text([header, ...listLines].join("\n"), 0, 0);
670
663
  }
671
664
 
672
665
  const matchCount = details?.matchCount ?? 0;
@@ -685,79 +678,70 @@ export const grepToolRenderer = {
685
678
  const files = details?.files ?? [];
686
679
 
687
680
  if (matchCount === 0) {
688
- return new Text(` ${ui.emptyMessage("No matches found")}`, 0, 0);
681
+ const header = renderStatusLine(
682
+ { icon: "warning", title: "Grep", description: args?.pattern, meta: ["0 matches"] },
683
+ uiTheme,
684
+ );
685
+ return new Text([header, formatEmptyMessage("No matches found", uiTheme)].join("\n"), 0, 0);
689
686
  }
690
687
 
691
- const icon = uiTheme.styledSymbol("status.success", "success");
692
688
  const summaryParts =
693
689
  mode === "files_with_matches"
694
- ? [ui.count("file", fileCount)]
695
- : [ui.count("match", matchCount), ui.count("file", fileCount)];
696
- const summaryText = summaryParts.join(uiTheme.sep.dot);
697
- const scopeLabel = ui.scope(details?.scopePath);
690
+ ? [formatCount("file", fileCount)]
691
+ : [formatCount("match", matchCount), formatCount("file", fileCount)];
692
+ const meta = [...summaryParts];
693
+ if (details?.scopePath) meta.push(`in ${details.scopePath}`);
694
+ if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
695
+ const description = args?.pattern ?? undefined;
696
+ const header = renderStatusLine(
697
+ { icon: truncated ? "warning" : "success", title: "Grep", description, meta },
698
+ uiTheme,
699
+ );
700
+
701
+ if (mode === "content") {
702
+ const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
703
+ const contentLines = textContent.split("\n").filter((line) => line.trim().length > 0);
704
+ const matchLines = renderTreeList(
705
+ {
706
+ items: contentLines,
707
+ expanded,
708
+ maxCollapsed: COLLAPSED_TEXT_LIMIT,
709
+ itemType: "match",
710
+ renderItem: (line) => uiTheme.fg("toolOutput", line),
711
+ },
712
+ uiTheme,
713
+ );
714
+ return new Text([header, ...matchLines].join("\n"), 0, 0);
715
+ }
698
716
 
699
717
  const fileEntries: Array<{ path: string; count?: number }> = details?.fileMatches?.length
700
718
  ? details.fileMatches.map((entry) => ({ path: entry.path, count: entry.count }))
701
719
  : files.map((path) => ({ path }));
702
- const maxFiles = expanded ? fileEntries.length : Math.min(fileEntries.length, COLLAPSED_LIST_LIMIT);
703
- const hasMoreFiles = fileEntries.length > maxFiles;
704
- const expandHint = ui.expandHint(expanded, hasMoreFiles);
705
-
706
- let text = ` ${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
720
+ const fileLines = renderFileList(
721
+ {
722
+ files: fileEntries.map((entry) => ({
723
+ path: entry.path,
724
+ isDirectory: entry.path.endsWith("/"),
725
+ meta: entry.count !== undefined ? `(${entry.count} match${entry.count !== 1 ? "es" : ""})` : undefined,
726
+ })),
727
+ expanded,
728
+ maxCollapsed: COLLAPSED_LIST_LIMIT,
729
+ },
730
+ uiTheme,
731
+ );
707
732
 
708
733
  const truncationReasons: string[] = [];
709
- if (limits?.matchLimit) {
710
- truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
711
- }
712
- if (limits?.resultLimit) {
713
- truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
714
- }
715
- if (limits?.headLimit) {
716
- truncationReasons.push(`head limit ${limits.headLimit.reached}`);
717
- }
718
- if (truncation) {
719
- truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
720
- }
721
- if (limits?.columnTruncated) {
722
- truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
723
- }
724
- if (truncation?.artifactId) {
725
- truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
726
- }
734
+ if (limits?.matchLimit) truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
735
+ if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
736
+ if (limits?.headLimit) truncationReasons.push(`head limit ${limits.headLimit.reached}`);
737
+ if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
738
+ if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
739
+ if (truncation?.artifactId) truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
727
740
 
728
- const hasTruncation = truncationReasons.length > 0;
729
-
730
- if (fileEntries.length > 0) {
731
- for (let i = 0; i < maxFiles; i++) {
732
- const entry = fileEntries[i];
733
- const isLast = i === maxFiles - 1 && !hasMoreFiles && !hasTruncation;
734
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
735
- const isDir = entry.path.endsWith("/");
736
- const entryPath = isDir ? entry.path.slice(0, -1) : entry.path;
737
- const lang = isDir ? undefined : getLanguageFromPath(entryPath);
738
- const entryIcon = isDir
739
- ? uiTheme.fg("accent", uiTheme.icon.folder)
740
- : uiTheme.fg("muted", uiTheme.getLangIcon(lang));
741
- const countLabel =
742
- entry.count !== undefined
743
- ? ` ${uiTheme.fg("dim", `(${entry.count} match${entry.count !== 1 ? "es" : ""})`)}`
744
- : "";
745
- text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry.path)}${countLabel}`;
746
- }
741
+ const extraLines =
742
+ truncationReasons.length > 0 ? [uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)] : [];
747
743
 
748
- if (hasMoreFiles) {
749
- const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
750
- text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
751
- "muted",
752
- ui.moreItems(fileEntries.length - maxFiles, "file"),
753
- )}`;
754
- }
755
- }
756
-
757
- if (hasTruncation) {
758
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
759
- }
760
-
761
- return new Text(text, 0, 0);
744
+ return new Text([header, ...fileLines, ...extraLines].join("\n"), 0, 0);
762
745
  },
746
+ mergeCallAndResult: true,
763
747
  };
@@ -1,5 +1,5 @@
1
1
  // Exa MCP tools (22 tools)
2
- export { exaTools } from "@oh-my-pi/pi-coding-agent/exa/index";
2
+ export { exaTools } from "@oh-my-pi/pi-coding-agent/exa";
3
3
  export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "@oh-my-pi/pi-coding-agent/exa/types";
4
4
  export {
5
5
  type FileDiagnosticsResult,
@@ -11,9 +11,9 @@ export {
11
11
  type LspWarmupOptions,
12
12
  type LspWarmupResult,
13
13
  warmupLspServers,
14
- } from "@oh-my-pi/pi-coding-agent/lsp/index";
14
+ } from "@oh-my-pi/pi-coding-agent/lsp";
15
15
  export { EditTool, type EditToolDetails } from "@oh-my-pi/pi-coding-agent/patch";
16
- export { BUNDLED_AGENTS, TaskTool } from "@oh-my-pi/pi-coding-agent/task/index";
16
+ export { BUNDLED_AGENTS, TaskTool } from "@oh-my-pi/pi-coding-agent/task";
17
17
  export {
18
18
  companyWebSearchTools,
19
19
  exaWebSearchTools,
@@ -31,7 +31,7 @@ export {
31
31
  webSearchCustomTool,
32
32
  webSearchDeepTool,
33
33
  webSearchLinkedinTool,
34
- } from "@oh-my-pi/pi-coding-agent/web/search/index";
34
+ } from "@oh-my-pi/pi-coding-agent/web/search";
35
35
  export { AskTool, type AskToolDetails } from "./ask";
36
36
  export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
37
37
  export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
@@ -64,12 +64,13 @@ import type { BashInterceptorRule } from "@oh-my-pi/pi-coding-agent/config/setti
64
64
  import type { InternalUrlRouter } from "@oh-my-pi/pi-coding-agent/internal-urls";
65
65
  import { getPreludeDocs, warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/ipy/executor";
66
66
  import { checkPythonKernelAvailability } from "@oh-my-pi/pi-coding-agent/ipy/kernel";
67
- import { LspTool } from "@oh-my-pi/pi-coding-agent/lsp/index";
67
+ import { LspTool } from "@oh-my-pi/pi-coding-agent/lsp";
68
68
  import { EditTool } from "@oh-my-pi/pi-coding-agent/patch";
69
69
  import type { ArtifactManager } from "@oh-my-pi/pi-coding-agent/session/artifacts";
70
- import { TaskTool } from "@oh-my-pi/pi-coding-agent/task/index";
70
+ import { TaskTool } from "@oh-my-pi/pi-coding-agent/task";
71
+ import type { AgentOutputManager } from "@oh-my-pi/pi-coding-agent/task/output-manager";
71
72
  import type { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
72
- import { WebSearchTool } from "@oh-my-pi/pi-coding-agent/web/search/index";
73
+ import { WebSearchTool } from "@oh-my-pi/pi-coding-agent/web/search";
73
74
  import { logger } from "@oh-my-pi/pi-utils";
74
75
  import { AskTool } from "./ask";
75
76
  import { BashTool } from "./bash";
@@ -125,6 +126,8 @@ export interface ToolSession {
125
126
  mcpManager?: import("../mcp/manager").MCPManager;
126
127
  /** Internal URL router for agent:// and skill:// URLs */
127
128
  internalRouter?: InternalUrlRouter;
129
+ /** Agent output manager for unique agent:// IDs across task invocations */
130
+ agentOutputManager?: AgentOutputManager;
128
131
  /** Settings manager for passing to subagents (avoids SQLite access in workers) */
129
132
  settingsManager?: { serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings };
130
133
  /** Settings manager (optional) */