@oh-my-pi/pi-coding-agent 3.14.0 → 3.15.1

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 (213) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/docs/theme.md +38 -5
  3. package/examples/sdk/11-sessions.ts +2 -2
  4. package/package.json +7 -4
  5. package/src/cli/file-processor.ts +51 -2
  6. package/src/cli/plugin-cli.ts +25 -19
  7. package/src/cli/update-cli.ts +4 -3
  8. package/src/core/agent-session.ts +31 -4
  9. package/src/core/compaction/branch-summarization.ts +4 -32
  10. package/src/core/compaction/compaction.ts +6 -84
  11. package/src/core/compaction/utils.ts +2 -3
  12. package/src/core/custom-tools/types.ts +2 -0
  13. package/src/core/export-html/index.ts +1 -1
  14. package/src/core/hooks/tool-wrapper.ts +0 -1
  15. package/src/core/hooks/types.ts +2 -2
  16. package/src/core/plugins/doctor.ts +9 -1
  17. package/src/core/sdk.ts +2 -1
  18. package/src/core/session-manager.ts +518 -40
  19. package/src/core/settings-manager.ts +174 -0
  20. package/src/core/system-prompt.ts +9 -14
  21. package/src/core/title-generator.ts +2 -8
  22. package/src/core/tools/ask.ts +19 -37
  23. package/src/core/tools/bash.ts +2 -37
  24. package/src/core/tools/edit.ts +2 -9
  25. package/src/core/tools/exa/render.ts +52 -48
  26. package/src/core/tools/find.ts +10 -8
  27. package/src/core/tools/grep.ts +45 -17
  28. package/src/core/tools/ls.ts +22 -2
  29. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  30. package/src/core/tools/lsp/clients/index.ts +49 -0
  31. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  32. package/src/core/tools/lsp/config.ts +3 -0
  33. package/src/core/tools/lsp/index.ts +107 -55
  34. package/src/core/tools/lsp/render.ts +192 -79
  35. package/src/core/tools/lsp/types.ts +27 -0
  36. package/src/core/tools/lsp/utils.ts +62 -22
  37. package/src/core/tools/notebook.ts +9 -1
  38. package/src/core/tools/output.ts +37 -14
  39. package/src/core/tools/read.ts +349 -34
  40. package/src/core/tools/renderers.ts +290 -89
  41. package/src/core/tools/review.ts +12 -5
  42. package/src/core/tools/task/agents.ts +5 -5
  43. package/src/core/tools/task/commands.ts +3 -3
  44. package/src/core/tools/task/executor.ts +33 -1
  45. package/src/core/tools/task/index.ts +93 -6
  46. package/src/core/tools/task/render.ts +147 -66
  47. package/src/core/tools/task/types.ts +14 -9
  48. package/src/core/tools/web-fetch.ts +242 -103
  49. package/src/core/tools/web-search/index.ts +64 -20
  50. package/src/core/tools/web-search/providers/exa.ts +68 -172
  51. package/src/core/tools/web-search/render.ts +264 -74
  52. package/src/core/tools/write.ts +2 -8
  53. package/src/main.ts +10 -6
  54. package/src/modes/cleanup.ts +23 -0
  55. package/src/modes/index.ts +9 -4
  56. package/src/modes/interactive/components/bash-execution.ts +6 -3
  57. package/src/modes/interactive/components/branch-summary-message.ts +1 -1
  58. package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  59. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  60. package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
  61. package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
  62. package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
  63. package/src/modes/interactive/components/hook-editor.ts +1 -0
  64. package/src/modes/interactive/components/hook-message.ts +2 -2
  65. package/src/modes/interactive/components/hook-selector.ts +1 -1
  66. package/src/modes/interactive/components/model-selector.ts +22 -9
  67. package/src/modes/interactive/components/oauth-selector.ts +20 -4
  68. package/src/modes/interactive/components/plugin-settings.ts +4 -2
  69. package/src/modes/interactive/components/session-selector.ts +9 -6
  70. package/src/modes/interactive/components/settings-defs.ts +285 -1
  71. package/src/modes/interactive/components/settings-selector.ts +176 -3
  72. package/src/modes/interactive/components/status-line/index.ts +4 -0
  73. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  74. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  75. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  76. package/src/modes/interactive/components/status-line/types.ts +81 -0
  77. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  78. package/src/modes/interactive/components/status-line.ts +172 -223
  79. package/src/modes/interactive/components/tool-execution.ts +446 -211
  80. package/src/modes/interactive/components/tree-selector.ts +17 -6
  81. package/src/modes/interactive/components/ttsr-notification.ts +4 -4
  82. package/src/modes/interactive/components/welcome.ts +27 -19
  83. package/src/modes/interactive/interactive-mode.ts +99 -13
  84. package/src/modes/interactive/theme/dark.json +3 -2
  85. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  86. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  87. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  88. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  89. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  90. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  91. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  92. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  93. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  94. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  95. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  96. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  97. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  98. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  99. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  100. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  101. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  102. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  103. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  104. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  105. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  106. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  107. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  108. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  109. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  110. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  111. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  112. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  113. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  114. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  115. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  116. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  117. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  118. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  119. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  120. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  121. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  122. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  123. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  124. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  125. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  126. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  127. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  128. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  129. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  130. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  131. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  132. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  133. package/src/modes/interactive/theme/defaults/index.ts +195 -0
  134. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  135. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  136. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  137. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  138. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  139. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  140. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  141. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  142. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  143. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  144. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  145. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  146. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  147. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  148. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  149. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  150. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  151. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  152. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  154. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  155. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  156. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  157. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  158. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  159. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  160. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  161. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  162. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  163. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  166. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  167. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  168. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  169. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  170. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  171. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  172. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  173. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  174. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  175. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  176. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  177. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  178. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  179. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  180. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  181. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  182. package/src/modes/interactive/theme/light.json +3 -2
  183. package/src/modes/interactive/theme/theme-schema.json +120 -4
  184. package/src/modes/interactive/theme/theme.ts +1228 -14
  185. package/src/prompts/branch-summary-preamble.md +3 -0
  186. package/src/prompts/branch-summary.md +28 -0
  187. package/src/prompts/compaction-summary.md +34 -0
  188. package/src/prompts/compaction-turn-prefix.md +16 -0
  189. package/src/prompts/compaction-update-summary.md +41 -0
  190. package/src/prompts/init.md +30 -0
  191. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  192. package/src/prompts/summarization-system.md +3 -0
  193. package/src/prompts/system-prompt.md +27 -0
  194. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  195. package/src/prompts/title-system.md +8 -0
  196. package/src/prompts/tools/ask.md +24 -0
  197. package/src/prompts/tools/bash.md +23 -0
  198. package/src/prompts/tools/edit.md +9 -0
  199. package/src/prompts/tools/find.md +6 -0
  200. package/src/prompts/tools/grep.md +12 -0
  201. package/src/prompts/tools/lsp.md +14 -0
  202. package/src/prompts/tools/output.md +23 -0
  203. package/src/prompts/tools/read.md +25 -0
  204. package/src/prompts/tools/web-fetch.md +8 -0
  205. package/src/prompts/tools/web-search.md +10 -0
  206. package/src/prompts/tools/write.md +10 -0
  207. package/src/commands/init.md +0 -20
  208. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  209. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  210. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  211. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  212. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  213. /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
@@ -11,17 +11,11 @@ import type { RenderResultOptions } from "../../custom-tools/types";
11
11
  import { logger } from "../../logger";
12
12
  import type { ExaRenderDetails } from "./types";
13
13
 
14
- // Tree formatting constants
15
- const TREE_MID = "├─";
16
- const TREE_END = "└─";
17
- const TREE_PIPE = "│";
18
- const TREE_SPACE = " ";
19
- const TREE_HOOK = "⎿";
20
-
21
14
  /** Truncate text to max length with ellipsis */
22
- function truncate(text: string, maxLen: number): string {
15
+ function truncate(text: string, maxLen: number, ellipsis: string): string {
23
16
  if (text.length <= maxLen) return text;
24
- return `${text.slice(0, maxLen - 1)}…`;
17
+ const sliceLen = Math.max(0, maxLen - ellipsis.length);
18
+ return `${text.slice(0, sliceLen)}${ellipsis}`;
25
19
  }
26
20
 
27
21
  /** Extract domain from URL */
@@ -35,16 +29,16 @@ function getDomain(url: string): string {
35
29
  }
36
30
 
37
31
  /** Get first N lines of text as preview */
38
- function getPreviewLines(text: string, maxLines: number, maxLineLen: number): string[] {
32
+ function getPreviewLines(text: string, maxLines: number, maxLineLen: number, ellipsis: string): string[] {
39
33
  const lines = text.split("\n").filter((l) => l.trim());
40
- return lines.slice(0, maxLines).map((l) => truncate(l.trim(), maxLineLen));
34
+ return lines.slice(0, maxLines).map((l) => truncate(l.trim(), maxLineLen, ellipsis));
41
35
  }
42
36
 
43
37
  /** Render Exa result with tree-based layout */
44
38
  export function renderExaResult(
45
39
  result: { content: Array<{ type: string; text?: string }>; details?: ExaRenderDetails },
46
40
  options: RenderResultOptions,
47
- theme: Theme,
41
+ uiTheme: Theme,
48
42
  ): Component {
49
43
  const { expanded } = options;
50
44
  const details = result.details;
@@ -52,7 +46,7 @@ export function renderExaResult(
52
46
  // Handle error case
53
47
  if (details?.error) {
54
48
  logger.error("Exa render error", { error: details.error, toolName: details.toolName });
55
- return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
49
+ return new Text(uiTheme.fg("error", `Error: ${details.error}`), 0, 0);
56
50
  }
57
51
 
58
52
  const response = details?.response;
@@ -60,15 +54,15 @@ export function renderExaResult(
60
54
  // Non-search response: show raw result
61
55
  if (details?.raw) {
62
56
  const rawText = typeof details.raw === "string" ? details.raw : JSON.stringify(details.raw, null, 2);
63
- const preview = expanded ? rawText : truncate(rawText, 200);
57
+ const preview = expanded ? rawText : truncate(rawText, 200, uiTheme.format.ellipsis);
64
58
  const toolLabel = details?.toolName ?? "Exa";
65
59
  return new Text(
66
- `${theme.fg("success", "●")} ${theme.fg("toolTitle", toolLabel)}\n ${theme.fg("dim", TREE_PIPE)} ${preview}`,
60
+ `${uiTheme.fg("success", uiTheme.format.bullet)} ${uiTheme.fg("toolTitle", toolLabel)}\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${preview}`,
67
61
  0,
68
62
  0,
69
63
  );
70
64
  }
71
- return new Text(theme.fg("error", "No response data"), 0, 0);
65
+ return new Text(uiTheme.fg("error", "No response data"), 0, 0);
72
66
  }
73
67
 
74
68
  const results = response.results ?? [];
@@ -76,21 +70,22 @@ export function renderExaResult(
76
70
  const cost = response.costDollars?.total;
77
71
  const time = response.searchTime;
78
72
 
79
- // Build header: Exa Search · N results · $X.XX · Xs
80
- const icon = resultCount > 0 ? theme.fg("success", "●") : theme.fg("warning", "●");
81
- const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
73
+ // Build header: Exa Search · N results · $X.XX · Xs
74
+ const icon =
75
+ resultCount > 0 ? uiTheme.fg("success", uiTheme.format.bullet) : uiTheme.fg("warning", uiTheme.format.bullet);
76
+ const expandHint = expanded ? "" : uiTheme.fg("dim", " (Ctrl+O for full results)");
82
77
  const toolLabel = details?.toolName ?? "Exa Search";
83
78
 
84
- let headerParts = `${icon} ${theme.fg("toolTitle", toolLabel)} · ${theme.fg(
79
+ let headerParts = `${icon} ${uiTheme.fg("toolTitle", toolLabel)}${uiTheme.sep.dot}${uiTheme.fg(
85
80
  "dim",
86
81
  `${resultCount} result${resultCount !== 1 ? "s" : ""}`,
87
82
  )}`;
88
83
 
89
84
  if (cost !== undefined) {
90
- headerParts += ` · ${theme.fg("muted", `$${cost.toFixed(4)}`)}`;
85
+ headerParts += `${uiTheme.sep.dot}${uiTheme.fg("muted", `$${cost.toFixed(4)}`)}`;
91
86
  }
92
87
  if (time !== undefined) {
93
- headerParts += ` · ${theme.fg("muted", `${time.toFixed(2)}s`)}`;
88
+ headerParts += `${uiTheme.sep.dot}${uiTheme.fg("muted", `${time.toFixed(2)}s`)}`;
94
89
  }
95
90
 
96
91
  let text = headerParts + expandHint;
@@ -100,19 +95,22 @@ export function renderExaResult(
100
95
  if (resultCount > 0) {
101
96
  const first = results[0];
102
97
  const previewText = first.text ?? first.title ?? "";
103
- const previewLines = getPreviewLines(previewText, 3, 100);
98
+ const previewLines = getPreviewLines(previewText, 3, 100, uiTheme.format.ellipsis);
104
99
 
105
100
  for (const line of previewLines) {
106
- text += `\n ${theme.fg("dim", TREE_PIPE)} ${theme.fg("dim", line)}`;
101
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${uiTheme.fg("dim", line)}`;
107
102
  }
108
103
 
109
104
  const totalLines = previewText.split("\n").filter((l) => l.trim()).length;
110
105
  if (totalLines > 3) {
111
- text += `\n ${theme.fg("dim", TREE_PIPE)} ${theme.fg("muted", `… ${totalLines - 3} more lines`)}`;
106
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)} ${uiTheme.fg(
107
+ "muted",
108
+ `${uiTheme.format.ellipsis} ${totalLines - 3} more lines`,
109
+ )}`;
112
110
  }
113
111
 
114
112
  if (resultCount > 1) {
115
- text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg(
113
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
116
114
  "muted",
117
115
  `${resultCount - 1} more result${resultCount !== 2 ? "s" : ""}`,
118
116
  )}`;
@@ -121,38 +119,35 @@ export function renderExaResult(
121
119
  } else {
122
120
  // Expanded view: full results tree
123
121
  if (resultCount > 0) {
124
- text += `\n ${theme.fg("dim", TREE_PIPE)}`;
125
- text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("accent", "Results")}`;
122
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.vertical)}`;
123
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("accent", "Results")}`;
126
124
 
127
125
  for (let i = 0; i < results.length; i++) {
128
126
  const res = results[i];
129
127
  const isLast = i === results.length - 1;
130
- const branch = isLast ? TREE_END : TREE_MID;
131
- const cont = isLast ? TREE_SPACE : TREE_PIPE;
128
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
129
+ const cont = isLast ? " " : uiTheme.tree.vertical;
132
130
 
133
131
  // Title + domain
134
- const title = truncate(res.title ?? "Untitled", 60);
132
+ const title = truncate(res.title ?? "Untitled", 60, uiTheme.format.ellipsis);
135
133
  const domain = res.url ? getDomain(res.url) : "";
136
- const domainPart = domain ? theme.fg("dim", ` (${domain})`) : "";
134
+ const domainPart = domain ? uiTheme.fg("dim", ` (${domain})`) : "";
137
135
 
138
- text += `\n ${theme.fg("dim", TREE_SPACE)} ${theme.fg("dim", branch)} ${theme.fg(
139
- "accent",
140
- title,
141
- )}${domainPart}`;
136
+ text += `\n ${uiTheme.fg("dim", " ")} ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", title)}${domainPart}`;
142
137
 
143
138
  // URL
144
139
  if (res.url) {
145
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("dim", TREE_HOOK)} ${theme.fg("mdLinkUrl", res.url)}`;
140
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("mdLinkUrl", res.url)}`;
146
141
  }
147
142
 
148
143
  // Author
149
144
  if (res.author) {
150
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("muted", `Author: ${res.author}`)}`;
145
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("muted", `Author: ${res.author}`)}`;
151
146
  }
152
147
 
153
148
  // Published date
154
149
  if (res.publishedDate) {
155
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("muted", `Published: ${res.publishedDate}`)}`;
150
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("muted", `Published: ${res.publishedDate}`)}`;
156
151
  }
157
152
 
158
153
  // Text content
@@ -160,22 +155,31 @@ export function renderExaResult(
160
155
  const textLines = res.text.split("\n").filter((l) => l.trim());
161
156
  const displayLines = textLines.slice(0, 5); // Show first 5 lines
162
157
  for (const line of displayLines) {
163
- text += `\n ${theme.fg("dim", cont)} ${truncate(line.trim(), 90)}`;
158
+ text += `\n ${uiTheme.fg("dim", cont)} ${truncate(line.trim(), 90, uiTheme.format.ellipsis)}`;
164
159
  }
165
160
  if (textLines.length > 5) {
166
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("muted", `… ${textLines.length - 5} more lines`)}`;
161
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg(
162
+ "muted",
163
+ `${uiTheme.format.ellipsis} ${textLines.length - 5} more lines`,
164
+ )}`;
167
165
  }
168
166
  }
169
167
 
170
168
  // Highlights
171
169
  if (res.highlights?.length) {
172
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("accent", "Highlights:")}`;
170
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("accent", "Highlights:")}`;
173
171
  for (let j = 0; j < Math.min(res.highlights.length, 3); j++) {
174
172
  const h = res.highlights[j];
175
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("muted", `• ${truncate(h, 80)}`)}`;
173
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg(
174
+ "muted",
175
+ `${uiTheme.format.bullet} ${truncate(h, 80, uiTheme.format.ellipsis)}`,
176
+ )}`;
176
177
  }
177
178
  if (res.highlights.length > 3) {
178
- text += `\n ${theme.fg("dim", cont)} ${theme.fg("muted", `… ${res.highlights.length - 3} more`)}`;
179
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg(
180
+ "muted",
181
+ `${uiTheme.format.ellipsis} ${res.highlights.length - 3} more`,
182
+ )}`;
179
183
  }
180
184
  }
181
185
  }
@@ -186,11 +190,11 @@ export function renderExaResult(
186
190
  }
187
191
 
188
192
  /** Render Exa call (query/args preview) */
189
- export function renderExaCall(args: Record<string, unknown>, toolName: string, theme: Theme): Component {
190
- const query = typeof args.query === "string" ? truncate(args.query, 80) : "";
193
+ export function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component {
194
+ const query = typeof args.query === "string" ? truncate(args.query, 80, uiTheme.format.ellipsis) : "";
191
195
  const numResults = typeof args.num_results === "number" ? args.num_results : undefined;
192
- const detail = numResults ? theme.fg("dim", ` (${numResults} results)`) : "";
196
+ const detail = numResults ? uiTheme.fg("dim", ` (${numResults} results)`) : "";
193
197
 
194
- const text = `${theme.fg("toolTitle", toolName)} ${theme.fg("muted", query)}${detail}`;
198
+ const text = `${uiTheme.fg("toolTitle", toolName)} ${uiTheme.fg("muted", query)}${detail}`;
195
199
  return new Text(text, 0, 0);
196
200
  }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import { globSync } from "glob";
6
+ import findDescription from "../../prompts/tools/find.md" with { type: "text" };
6
7
  import { ensureTool } from "../../utils/tools-manager";
7
8
  import { untilAborted } from "../utils";
8
9
  import { resolveToCwd } from "./path-utils";
@@ -32,6 +33,7 @@ export interface FindToolDetails {
32
33
  truncation?: TruncationResult;
33
34
  resultLimitReached?: number;
34
35
  // Fields for TUI rendering
36
+ scopePath?: string;
35
37
  fileCount?: number;
36
38
  files?: string[];
37
39
  truncated?: boolean;
@@ -42,12 +44,7 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
42
44
  return {
43
45
  name: "find",
44
46
  label: "Find",
45
- description: `- Fast file pattern matching tool that works with any codebase size
46
- - Supports glob patterns like "**/*.js" or "src/**/*.ts"
47
- - Returns matching file paths sorted by modification time
48
- - Use this tool when you need to find files by name patterns
49
- - When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
50
- - You can call multiple tools in a single response. It is always better to speculatively perform multiple searches in parallel if they are potentially useful.`,
47
+ description: findDescription,
51
48
  parameters: findSchema,
52
49
  execute: async (
53
50
  _toolCallId: string,
@@ -76,6 +73,10 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
76
73
  }
77
74
 
78
75
  const searchPath = resolveToCwd(searchDir || ".", cwd);
76
+ const scopePath = (() => {
77
+ const relative = path.relative(cwd, searchPath).replace(/\\/g, "/");
78
+ return relative.length === 0 ? "." : relative;
79
+ })();
79
80
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
80
81
  const effectiveType = type ?? "all";
81
82
  const includeHidden = hidden ?? false;
@@ -148,7 +149,7 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
148
149
  if (!output) {
149
150
  return {
150
151
  content: [{ type: "text", text: "No files found matching pattern" }],
151
- details: { fileCount: 0, files: [], truncated: false },
152
+ details: { scopePath, fileCount: 0, files: [], truncated: false },
152
153
  };
153
154
  }
154
155
 
@@ -205,8 +206,9 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
205
206
 
206
207
  let resultOutput = truncation.content;
207
208
  const details: FindToolDetails = {
209
+ scopePath,
208
210
  fileCount: relativized.length,
209
- files: relativized.slice(0, 50),
211
+ files: relativized,
210
212
  truncated: resultLimitReached || truncation.truncated,
211
213
  };
212
214
 
@@ -3,6 +3,7 @@ import nodePath from "node:path";
3
3
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import type { Subprocess } from "bun";
6
+ import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
6
7
  import { ensureTool } from "../../utils/tools-manager";
7
8
  import { resolveToCwd } from "./path-utils";
8
9
  import {
@@ -50,11 +51,14 @@ const DEFAULT_LIMIT = 100;
50
51
  export interface GrepToolDetails {
51
52
  truncation?: TruncationResult;
52
53
  matchLimitReached?: number;
54
+ headLimitReached?: number;
53
55
  linesTruncated?: boolean;
54
56
  // Fields for TUI rendering
57
+ scopePath?: string;
55
58
  matchCount?: number;
56
59
  fileCount?: number;
57
60
  files?: string[];
61
+ fileMatches?: Array<{ path: string; count: number }>;
58
62
  mode?: "content" | "files_with_matches" | "count";
59
63
  truncated?: boolean;
60
64
  error?: string;
@@ -64,17 +68,7 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
64
68
  return {
65
69
  name: "grep",
66
70
  label: "Grep",
67
- description: `A powerful search tool built on ripgrep
68
-
69
- Usage:
70
- - ALWAYS use grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a bash command. The grep tool has been optimized for correct permissions and access.
71
- - Searches recursively by default - no need for -r flag
72
- - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
73
- - Filter files with glob parameter (e.g., "*.ts", "**/*.spec.ts") or type parameter (e.g., "ts", "py", "rust") - equivalent to grep's --include
74
- - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
75
- - Pagination: Use headLimit to limit results (like \`| head -N\`), offset to skip first N results
76
- - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
77
- - Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\``,
71
+ description: grepDescription,
78
72
  parameters: grepSchema,
79
73
  execute: async (
80
74
  _toolCallId: string,
@@ -119,6 +113,10 @@ Usage:
119
113
  }
120
114
 
121
115
  const searchPath = resolveToCwd(searchDir || ".", cwd);
116
+ const scopePath = (() => {
117
+ const relative = nodePath.relative(cwd, searchPath).replace(/\\/g, "/");
118
+ return relative.length === 0 ? "." : relative;
119
+ })();
122
120
  let searchStat: Stats;
123
121
  try {
124
122
  searchStat = statSync(searchPath);
@@ -210,6 +208,7 @@ Usage:
210
208
  const outputLines: string[] = [];
211
209
  const files = new Set<string>();
212
210
  const fileList: string[] = [];
211
+ const fileMatchCounts = new Map<string, number>();
213
212
 
214
213
  const recordFile = (filePath: string) => {
215
214
  const relative = formatPath(filePath);
@@ -219,6 +218,11 @@ Usage:
219
218
  }
220
219
  };
221
220
 
221
+ const recordFileMatch = (filePath: string) => {
222
+ const relative = formatPath(filePath);
223
+ fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
224
+ };
225
+
222
226
  const stopChild = (dueToLimit: boolean = false) => {
223
227
  killedDueToLimit = dueToLimit;
224
228
  child.kill();
@@ -281,6 +285,7 @@ Usage:
281
285
  return {
282
286
  content: [{ type: "text", text: "No matches found" }],
283
287
  details: {
288
+ scopePath,
284
289
  matchCount: 0,
285
290
  fileCount: 0,
286
291
  files: [],
@@ -303,6 +308,7 @@ Usage:
303
308
  let fileCount = 0;
304
309
  const simpleFiles = new Set<string>();
305
310
  const simpleFileList: string[] = [];
311
+ const simpleFileMatchCounts = new Map<string, number>();
306
312
 
307
313
  const recordSimpleFile = (filePath: string) => {
308
314
  const relative = formatPath(filePath);
@@ -312,6 +318,11 @@ Usage:
312
318
  }
313
319
  };
314
320
 
321
+ const recordSimpleFileMatch = (filePath: string, count: number) => {
322
+ const relative = formatPath(filePath);
323
+ simpleFileMatchCounts.set(relative, count);
324
+ };
325
+
315
326
  if (effectiveOutputMode === "files_with_matches") {
316
327
  for (const line of lines) {
317
328
  recordSimpleFile(line);
@@ -327,12 +338,13 @@ Usage:
327
338
  recordSimpleFile(filePart);
328
339
  if (!Number.isNaN(count)) {
329
340
  simpleMatchCount += count;
341
+ recordSimpleFileMatch(filePart, count);
330
342
  }
331
343
  }
332
344
  fileCount = simpleFiles.size;
333
345
  }
334
346
 
335
- const truncated = hasHeadLimit && processedLines.length < lines.length;
347
+ const truncatedByHeadLimit = hasHeadLimit && processedLines.length < lines.length;
336
348
 
337
349
  // For count mode, format as "path:count"
338
350
  if (effectiveOutputMode === "count") {
@@ -346,11 +358,17 @@ Usage:
346
358
  return {
347
359
  content: [{ type: "text", text: output }],
348
360
  details: {
361
+ scopePath,
349
362
  matchCount: simpleMatchCount,
350
363
  fileCount,
351
- files: simpleFileList.slice(0, 50),
364
+ files: simpleFileList,
365
+ fileMatches: simpleFileList.map((path) => ({
366
+ path,
367
+ count: simpleFileMatchCounts.get(path) ?? 0,
368
+ })),
352
369
  mode: effectiveOutputMode,
353
- truncated,
370
+ truncated: truncatedByHeadLimit,
371
+ headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
354
372
  },
355
373
  };
356
374
  }
@@ -361,11 +379,13 @@ Usage:
361
379
  return {
362
380
  content: [{ type: "text", text: output }],
363
381
  details: {
382
+ scopePath,
364
383
  matchCount: simpleMatchCount,
365
384
  fileCount,
366
- files: simpleFileList.slice(0, 50),
385
+ files: simpleFileList,
367
386
  mode: effectiveOutputMode,
368
- truncated,
387
+ truncated: truncatedByHeadLimit,
388
+ headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
369
389
  },
370
390
  };
371
391
  }
@@ -421,6 +441,7 @@ Usage:
421
441
 
422
442
  if (filePath && typeof lineNumber === "number") {
423
443
  recordFile(filePath);
444
+ recordFileMatch(filePath);
424
445
  outputLines.push(...formatBlock(filePath, lineNumber));
425
446
  }
426
447
 
@@ -488,6 +509,7 @@ Usage:
488
509
  return {
489
510
  content: [{ type: "text", text: "No matches found" }],
490
511
  details: {
512
+ scopePath,
491
513
  matchCount: 0,
492
514
  fileCount: 0,
493
515
  files: [],
@@ -513,11 +535,17 @@ Usage:
513
535
  let output = truncation.content;
514
536
  const truncatedByHeadLimit = hasHeadLimit && processedLines.length < outputLines.length;
515
537
  const details: GrepToolDetails = {
538
+ scopePath,
516
539
  matchCount,
517
540
  fileCount: files.size,
518
- files: fileList.slice(0, 50),
541
+ files: fileList,
542
+ fileMatches: fileList.map((path) => ({
543
+ path,
544
+ count: fileMatchCounts.get(path) ?? 0,
545
+ })),
519
546
  mode: effectiveOutputMode,
520
547
  truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
548
+ headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
521
549
  };
522
550
 
523
551
  // Build notices
@@ -14,7 +14,11 @@ const lsSchema = Type.Object({
14
14
  const DEFAULT_LIMIT = 500;
15
15
 
16
16
  export interface LsToolDetails {
17
+ entries?: string[];
18
+ dirCount?: number;
19
+ fileCount?: number;
17
20
  truncation?: TruncationResult;
21
+ truncationReasons?: Array<"entryLimit" | "byteLimit">;
18
22
  entryLimitReached?: number;
19
23
  }
20
24
 
@@ -58,6 +62,8 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
58
62
  // Format entries with directory indicators
59
63
  const results: string[] = [];
60
64
  let entryLimitReached = false;
65
+ let dirCount = 0;
66
+ let fileCount = 0;
61
67
 
62
68
  for (const entry of entries) {
63
69
  if (results.length >= effectiveLimit) {
@@ -72,6 +78,9 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
72
78
  const entryStat = statSync(fullPath);
73
79
  if (entryStat.isDirectory()) {
74
80
  suffix = "/";
81
+ dirCount += 1;
82
+ } else {
83
+ fileCount += 1;
75
84
  }
76
85
  } catch {
77
86
  // Skip entries we can't stat
@@ -90,7 +99,12 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
90
99
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
91
100
 
92
101
  let output = truncation.content;
93
- const details: LsToolDetails = {};
102
+ const details: LsToolDetails = {
103
+ entries: results,
104
+ dirCount,
105
+ fileCount,
106
+ };
107
+ const truncationReasons: Array<"entryLimit" | "byteLimit"> = [];
94
108
 
95
109
  // Build notices
96
110
  const notices: string[] = [];
@@ -98,11 +112,17 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
98
112
  if (entryLimitReached) {
99
113
  notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
100
114
  details.entryLimitReached = effectiveLimit;
115
+ truncationReasons.push("entryLimit");
101
116
  }
102
117
 
103
118
  if (truncation.truncated) {
104
119
  notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
105
120
  details.truncation = truncation;
121
+ truncationReasons.push("byteLimit");
122
+ }
123
+
124
+ if (truncationReasons.length > 0) {
125
+ details.truncationReasons = truncationReasons;
106
126
  }
107
127
 
108
128
  if (notices.length > 0) {
@@ -111,7 +131,7 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
111
131
 
112
132
  return {
113
133
  content: [{ type: "text", text: output }],
114
- details: Object.keys(details).length > 0 ? details : undefined,
134
+ details,
115
135
  };
116
136
  });
117
137
  },