@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,540 @@
1
+ /**
2
+ * TUI renderers for built-in tools.
3
+ *
4
+ * These provide rich visualization for tool calls and results in the TUI.
5
+ */
6
+
7
+ import type { Component } from "@oh-my-pi/pi-tui";
8
+ import { Text } from "@oh-my-pi/pi-tui";
9
+ import type { Theme } from "../../modes/interactive/theme/theme.js";
10
+ import type { RenderResultOptions } from "../custom-tools/types.js";
11
+ import type { AskToolDetails } from "./ask.js";
12
+ import type { FindToolDetails } from "./find.js";
13
+ import type { GrepToolDetails } from "./grep.js";
14
+ import type { LsToolDetails } from "./ls.js";
15
+ import { renderCall as renderLspCall, renderResult as renderLspResult } from "./lsp/render.js";
16
+ import type { LspToolDetails } from "./lsp/types.js";
17
+ import type { NotebookToolDetails } from "./notebook.js";
18
+ import { renderCall as renderTaskCall, renderResult as renderTaskResult } from "./task/render.js";
19
+ import type { TaskToolDetails } from "./task/types.js";
20
+ import { renderWebFetchCall, renderWebFetchResult, type WebFetchToolDetails } from "./web-fetch.js";
21
+ import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./web-search/render.js";
22
+
23
+ // Tree drawing characters
24
+ const TREE_MID = "├─";
25
+ const TREE_END = "└─";
26
+
27
+ // Icons
28
+ const ICON_SUCCESS = "●";
29
+ const ICON_WARNING = "●";
30
+ const ICON_ERROR = "●";
31
+
32
+ interface ToolRenderer<TArgs = any, TDetails = any> {
33
+ renderCall(args: TArgs, theme: Theme): Component;
34
+ renderResult(
35
+ result: { content: Array<{ type: string; text?: string }>; details?: TDetails },
36
+ options: RenderResultOptions,
37
+ theme: Theme,
38
+ ): Component;
39
+ }
40
+
41
+ // ============================================================================
42
+ // Grep Renderer
43
+ // ============================================================================
44
+
45
+ interface GrepArgs {
46
+ pattern: string;
47
+ path?: string;
48
+ glob?: string;
49
+ type?: string;
50
+ ignoreCase?: boolean;
51
+ caseSensitive?: boolean;
52
+ literal?: boolean;
53
+ multiline?: boolean;
54
+ context?: number;
55
+ limit?: number;
56
+ outputMode?: string;
57
+ }
58
+
59
+ const grepRenderer: ToolRenderer<GrepArgs, GrepToolDetails> = {
60
+ renderCall(args, theme) {
61
+ let text = theme.fg("toolTitle", theme.bold("grep "));
62
+ text += theme.fg("accent", args.pattern || "?");
63
+
64
+ const meta: string[] = [];
65
+ if (args.path) meta.push(args.path);
66
+ if (args.glob) meta.push(`glob:${args.glob}`);
67
+ if (args.type) meta.push(`type:${args.type}`);
68
+ if (args.outputMode && args.outputMode !== "files_with_matches") meta.push(args.outputMode);
69
+ if (args.caseSensitive) {
70
+ meta.push("--case-sensitive");
71
+ } else if (args.ignoreCase) {
72
+ meta.push("-i");
73
+ }
74
+ if (args.multiline) meta.push("multiline");
75
+
76
+ if (meta.length > 0) {
77
+ text += ` ${theme.fg("muted", meta.join(" "))}`;
78
+ }
79
+
80
+ return new Text(text, 0, 0);
81
+ },
82
+
83
+ renderResult(result, { expanded }, theme) {
84
+ const details = result.details;
85
+
86
+ // Error case
87
+ if (details?.error) {
88
+ return new Text(`${theme.fg("error", ICON_ERROR)} ${theme.fg("error", details.error)}`, 0, 0);
89
+ }
90
+
91
+ // Check for detailed rendering data - fall back to raw output if not available
92
+ const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
93
+
94
+ if (!hasDetailedData) {
95
+ // Fall back to showing raw text content
96
+ const textContent = result.content?.find((c) => c.type === "text")?.text;
97
+ if (!textContent || textContent === "No matches found") {
98
+ return new Text(`${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", "No matches found")}`, 0, 0);
99
+ }
100
+
101
+ // Show abbreviated output
102
+ const lines = textContent.split("\n");
103
+ const maxLines = expanded ? lines.length : 10;
104
+ const displayLines = lines.slice(0, maxLines);
105
+ const remaining = lines.length - maxLines;
106
+
107
+ let text = `${theme.fg("success", ICON_SUCCESS)} ${theme.fg("toolTitle", "grep")}`;
108
+ text += `\n${displayLines.map((l) => theme.fg("toolOutput", l)).join("\n")}`;
109
+ if (remaining > 0) {
110
+ text += `\n${theme.fg("muted", `... ${remaining} more lines`)}`;
111
+ }
112
+ return new Text(text, 0, 0);
113
+ }
114
+
115
+ const matchCount = details?.matchCount ?? 0;
116
+ const fileCount = details?.fileCount ?? 0;
117
+ const mode = details?.mode ?? "files_with_matches";
118
+ const truncated = details?.truncated ?? details?.truncation?.truncated ?? false;
119
+ const files = details?.files ?? [];
120
+
121
+ // No matches
122
+ if (matchCount === 0) {
123
+ return new Text(`${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", "No matches found")}`, 0, 0);
124
+ }
125
+
126
+ // Build summary
127
+ const icon = theme.fg("success", ICON_SUCCESS);
128
+ let summary: string;
129
+ if (mode === "files_with_matches") {
130
+ summary = `${fileCount} file${fileCount !== 1 ? "s" : ""}`;
131
+ } else if (mode === "count") {
132
+ summary = `${matchCount} match${matchCount !== 1 ? "es" : ""} in ${fileCount} file${fileCount !== 1 ? "s" : ""}`;
133
+ } else {
134
+ summary = `${matchCount} match${matchCount !== 1 ? "es" : ""} in ${fileCount} file${fileCount !== 1 ? "s" : ""}`;
135
+ }
136
+
137
+ if (truncated) {
138
+ summary += theme.fg("warning", " (truncated)");
139
+ }
140
+
141
+ const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
142
+ let text = `${icon} ${theme.fg("toolTitle", "grep")} ${theme.fg("dim", summary)}${expandHint}`;
143
+
144
+ // Show file tree if we have files
145
+ if (files.length > 0) {
146
+ const maxFiles = expanded ? files.length : Math.min(files.length, 8);
147
+ for (let i = 0; i < maxFiles; i++) {
148
+ const isLast = i === maxFiles - 1 && (expanded || files.length <= 8);
149
+ const branch = isLast ? TREE_END : TREE_MID;
150
+ text += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", files[i])}`;
151
+ }
152
+
153
+ if (!expanded && files.length > 8) {
154
+ text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${files.length - 8} more files`)}`;
155
+ }
156
+ }
157
+
158
+ return new Text(text, 0, 0);
159
+ },
160
+ };
161
+
162
+ // ============================================================================
163
+ // Find Renderer
164
+ // ============================================================================
165
+
166
+ interface FindArgs {
167
+ pattern: string;
168
+ path?: string;
169
+ type?: string;
170
+ hidden?: boolean;
171
+ sortByMtime?: boolean;
172
+ limit?: number;
173
+ }
174
+
175
+ const findRenderer: ToolRenderer<FindArgs, FindToolDetails> = {
176
+ renderCall(args, theme) {
177
+ let text = theme.fg("toolTitle", theme.bold("find "));
178
+ text += theme.fg("accent", args.pattern || "*");
179
+
180
+ const meta: string[] = [];
181
+ if (args.path) meta.push(args.path);
182
+ if (args.type && args.type !== "all") meta.push(`type:${args.type}`);
183
+ if (args.hidden) meta.push("--hidden");
184
+
185
+ if (meta.length > 0) {
186
+ text += ` ${theme.fg("muted", meta.join(" "))}`;
187
+ }
188
+
189
+ return new Text(text, 0, 0);
190
+ },
191
+
192
+ renderResult(result, { expanded }, theme) {
193
+ const details = result.details;
194
+
195
+ // Error case
196
+ if (details?.error) {
197
+ return new Text(`${theme.fg("error", ICON_ERROR)} ${theme.fg("error", details.error)}`, 0, 0);
198
+ }
199
+
200
+ // Check for detailed rendering data - fall back to parsing raw output if not available
201
+ const hasDetailedData = details?.fileCount !== undefined;
202
+
203
+ // Get text content for fallback or to extract file list
204
+ const textContent = result.content?.find((c) => c.type === "text")?.text;
205
+
206
+ if (!hasDetailedData) {
207
+ if (!textContent || textContent.includes("No files matching") || textContent.trim() === "") {
208
+ return new Text(`${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", "No files found")}`, 0, 0);
209
+ }
210
+
211
+ // Parse the raw output as file list
212
+ const lines = textContent.split("\n").filter((l) => l.trim());
213
+ const maxLines = expanded ? lines.length : Math.min(lines.length, 8);
214
+ const displayLines = lines.slice(0, maxLines);
215
+ const remaining = lines.length - maxLines;
216
+
217
+ let text = `${theme.fg("success", ICON_SUCCESS)} ${theme.fg("toolTitle", "find")} ${theme.fg(
218
+ "dim",
219
+ `${lines.length} file${lines.length !== 1 ? "s" : ""}`,
220
+ )}`;
221
+ for (let i = 0; i < displayLines.length; i++) {
222
+ const isLast = i === displayLines.length - 1 && remaining === 0;
223
+ const branch = isLast ? TREE_END : TREE_MID;
224
+ text += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", displayLines[i])}`;
225
+ }
226
+ if (remaining > 0) {
227
+ text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${remaining} more files`)}`;
228
+ }
229
+ return new Text(text, 0, 0);
230
+ }
231
+
232
+ const fileCount = details?.fileCount ?? 0;
233
+ const truncated = details?.truncated ?? details?.truncation?.truncated ?? false;
234
+ const files = details?.files ?? [];
235
+
236
+ // No matches
237
+ if (fileCount === 0) {
238
+ return new Text(`${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", "No files found")}`, 0, 0);
239
+ }
240
+
241
+ // Build summary
242
+ const icon = theme.fg("success", ICON_SUCCESS);
243
+ let summary = `${fileCount} file${fileCount !== 1 ? "s" : ""}`;
244
+
245
+ if (truncated) {
246
+ summary += theme.fg("warning", " (truncated)");
247
+ }
248
+
249
+ const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
250
+ let text = `${icon} ${theme.fg("toolTitle", "find")} ${theme.fg("dim", summary)}${expandHint}`;
251
+
252
+ // Show file tree if we have files
253
+ if (files.length > 0) {
254
+ const maxFiles = expanded ? files.length : Math.min(files.length, 8);
255
+ for (let i = 0; i < maxFiles; i++) {
256
+ const isLast = i === maxFiles - 1 && (expanded || files.length <= 8);
257
+ const branch = isLast ? TREE_END : TREE_MID;
258
+ text += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", files[i])}`;
259
+ }
260
+
261
+ if (!expanded && files.length > 8) {
262
+ text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${files.length - 8} more files`)}`;
263
+ }
264
+ }
265
+
266
+ return new Text(text, 0, 0);
267
+ },
268
+ };
269
+
270
+ // ============================================================================
271
+ // Notebook Renderer
272
+ // ============================================================================
273
+
274
+ interface NotebookArgs {
275
+ action: string;
276
+ notebookPath: string;
277
+ cellNumber?: number;
278
+ cellType?: string;
279
+ content?: string;
280
+ }
281
+
282
+ const notebookRenderer: ToolRenderer<NotebookArgs, NotebookToolDetails> = {
283
+ renderCall(args, theme) {
284
+ let text = theme.fg("toolTitle", theme.bold("notebook "));
285
+ text += theme.fg("accent", args.action || "?");
286
+
287
+ const meta: string[] = [];
288
+ meta.push(args.notebookPath || "?");
289
+ if (args.cellNumber !== undefined) meta.push(`cell:${args.cellNumber}`);
290
+ if (args.cellType) meta.push(args.cellType);
291
+
292
+ if (meta.length > 0) {
293
+ text += ` ${theme.fg("muted", meta.join(" "))}`;
294
+ }
295
+
296
+ return new Text(text, 0, 0);
297
+ },
298
+
299
+ renderResult(result, _options, theme) {
300
+ const details = result.details;
301
+
302
+ // Error case - check for error in content
303
+ const content = result.content?.[0];
304
+ if (content?.type === "text" && content.text?.startsWith("Error:")) {
305
+ return new Text(`${theme.fg("error", ICON_ERROR)} ${theme.fg("error", content.text)}`, 0, 0);
306
+ }
307
+
308
+ const action = details?.action ?? "edit";
309
+ const cellIndex = details?.cellIndex;
310
+ const cellType = details?.cellType;
311
+ const totalCells = details?.totalCells;
312
+
313
+ // Build summary
314
+ const icon = theme.fg("success", ICON_SUCCESS);
315
+ let summary: string;
316
+
317
+ switch (action) {
318
+ case "insert":
319
+ summary = `Inserted ${cellType || "cell"} at index ${cellIndex}`;
320
+ break;
321
+ case "delete":
322
+ summary = `Deleted cell at index ${cellIndex}`;
323
+ break;
324
+ default:
325
+ summary = `Edited ${cellType || "cell"} at index ${cellIndex}`;
326
+ }
327
+
328
+ if (totalCells !== undefined) {
329
+ summary += ` (${totalCells} total)`;
330
+ }
331
+
332
+ return new Text(`${icon} ${theme.fg("toolTitle", "notebook")} ${theme.fg("dim", summary)}`, 0, 0);
333
+ },
334
+ };
335
+
336
+ // ============================================================================
337
+ // Ask Renderer
338
+ // ============================================================================
339
+
340
+ interface AskArgs {
341
+ question: string;
342
+ options?: Array<{ label: string }>;
343
+ multi?: boolean;
344
+ }
345
+
346
+ const askRenderer: ToolRenderer<AskArgs, AskToolDetails> = {
347
+ renderCall(args, theme) {
348
+ if (!args.question) {
349
+ return new Text(theme.fg("error", "ask: no question provided"), 0, 0);
350
+ }
351
+
352
+ const multiTag = args.multi ? theme.fg("muted", " [multi-select]") : "";
353
+ let text = theme.fg("toolTitle", "? ") + theme.fg("accent", args.question) + multiTag;
354
+
355
+ if (args.options?.length) {
356
+ for (const opt of args.options) {
357
+ text += `\n${theme.fg("dim", " ○ ")}${theme.fg("muted", opt.label)}`;
358
+ }
359
+ text += `\n${theme.fg("dim", " ○ ")}${theme.fg("muted", "Other (custom input)")}`;
360
+ }
361
+
362
+ return new Text(text, 0, 0);
363
+ },
364
+
365
+ renderResult(result, _opts, theme) {
366
+ const { details } = result;
367
+ if (!details) {
368
+ const txt = result.content[0];
369
+ return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
370
+ }
371
+
372
+ let text = theme.fg("toolTitle", "? ") + theme.fg("accent", details.question);
373
+
374
+ if (details.customInput) {
375
+ // Custom input provided
376
+ text += `\n${theme.fg("dim", " ⎿ ")}${theme.fg("success", details.customInput)}`;
377
+ } else if (details.selectedOptions.length > 0) {
378
+ // Show only selected options
379
+ const selected = details.selectedOptions;
380
+ if (selected.length === 1) {
381
+ text += `\n${theme.fg("dim", " ⎿ ")}${theme.fg("success", selected[0])}`;
382
+ } else {
383
+ // Multiple selections - tree format
384
+ for (let i = 0; i < selected.length; i++) {
385
+ const isLast = i === selected.length - 1;
386
+ const branch = isLast ? TREE_END : TREE_MID;
387
+ text += `\n${theme.fg("dim", ` ${branch} `)}${theme.fg("success", selected[i])}`;
388
+ }
389
+ }
390
+ } else {
391
+ text += `\n${theme.fg("dim", " ⎿ ")}${theme.fg("warning", "Cancelled")}`;
392
+ }
393
+
394
+ return new Text(text, 0, 0);
395
+ },
396
+ };
397
+
398
+ // ============================================================================
399
+ // Export
400
+ // ============================================================================
401
+
402
+ // ============================================================================
403
+ // LSP Renderer
404
+ // ============================================================================
405
+
406
+ interface LspArgs {
407
+ action: string;
408
+ file?: string;
409
+ files?: string[];
410
+ line?: number;
411
+ column?: number;
412
+ }
413
+
414
+ const lspRenderer: ToolRenderer<LspArgs, LspToolDetails> = {
415
+ renderCall: renderLspCall,
416
+ renderResult: renderLspResult,
417
+ };
418
+
419
+ // ============================================================================
420
+ // Task Renderer
421
+ // ============================================================================
422
+
423
+ const taskRenderer: ToolRenderer<any, TaskToolDetails> = {
424
+ renderCall: renderTaskCall,
425
+ renderResult: renderTaskResult,
426
+ };
427
+
428
+ // ============================================================================
429
+ // Ls Renderer
430
+ // ============================================================================
431
+
432
+ interface LsArgs {
433
+ path?: string;
434
+ limit?: number;
435
+ }
436
+
437
+ const lsRenderer: ToolRenderer<LsArgs, LsToolDetails> = {
438
+ renderCall(args, theme) {
439
+ let text = theme.fg("toolTitle", theme.bold("ls "));
440
+ text += theme.fg("accent", args.path || ".");
441
+ if (args.limit !== undefined) {
442
+ text += ` ${theme.fg("muted", `(limit ${args.limit})`)}`;
443
+ }
444
+ return new Text(text, 0, 0);
445
+ },
446
+
447
+ renderResult(result, { expanded }, theme) {
448
+ const details = result.details;
449
+ const textContent = result.content?.find((c: any) => c.type === "text")?.text;
450
+
451
+ if (!textContent || textContent.trim() === "") {
452
+ return new Text(`${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", "Empty directory")}`, 0, 0);
453
+ }
454
+
455
+ const entries = textContent.split("\n").filter((l: string) => l.trim());
456
+ const dirs = entries.filter((e: string) => e.endsWith("/"));
457
+ const files = entries.filter((e: string) => !e.endsWith("/"));
458
+
459
+ const truncated = details?.truncation?.truncated || details?.entryLimitReached;
460
+ const icon = truncated ? theme.fg("warning", ICON_WARNING) : theme.fg("success", ICON_SUCCESS);
461
+
462
+ let summary = `${dirs.length} dir${dirs.length !== 1 ? "s" : ""}, ${files.length} file${
463
+ files.length !== 1 ? "s" : ""
464
+ }`;
465
+ if (truncated) {
466
+ summary += theme.fg("warning", " (truncated)");
467
+ }
468
+
469
+ const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
470
+ let text = `${icon} ${theme.fg("toolTitle", "ls")} ${theme.fg("dim", summary)}${expandHint}`;
471
+
472
+ const maxEntries = expanded ? entries.length : Math.min(entries.length, 12);
473
+ for (let i = 0; i < maxEntries; i++) {
474
+ const entry = entries[i];
475
+ const isLast = i === maxEntries - 1 && (expanded || entries.length <= 12);
476
+ const branch = isLast ? TREE_END : TREE_MID;
477
+ const isDir = entry.endsWith("/");
478
+ const color = isDir ? "accent" : "toolOutput";
479
+ text += `\n ${theme.fg("dim", branch)} ${theme.fg(color, entry)}`;
480
+ }
481
+
482
+ if (!expanded && entries.length > 12) {
483
+ text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${entries.length - 12} more entries`)}`;
484
+ }
485
+
486
+ return new Text(text, 0, 0);
487
+ },
488
+ };
489
+
490
+ // ============================================================================
491
+ // Web Fetch Renderer
492
+ // ============================================================================
493
+
494
+ interface WebFetchArgs {
495
+ url: string;
496
+ timeout?: number;
497
+ raw?: boolean;
498
+ }
499
+
500
+ const webFetchRenderer: ToolRenderer<WebFetchArgs, WebFetchToolDetails> = {
501
+ renderCall: renderWebFetchCall,
502
+ renderResult: renderWebFetchResult,
503
+ };
504
+
505
+ // ============================================================================
506
+ // Web Search Renderer
507
+ // ============================================================================
508
+
509
+ interface WebSearchArgs {
510
+ query: string;
511
+ provider?: string;
512
+ [key: string]: unknown;
513
+ }
514
+
515
+ const webSearchRenderer: ToolRenderer<WebSearchArgs, WebSearchRenderDetails> = {
516
+ renderCall: renderWebSearchCall,
517
+ renderResult: renderWebSearchResult,
518
+ };
519
+
520
+ // ============================================================================
521
+ // Export
522
+ // ============================================================================
523
+
524
+ export const toolRenderers: Record<
525
+ string,
526
+ {
527
+ renderCall: (args: any, theme: Theme) => Component;
528
+ renderResult: (result: any, options: RenderResultOptions, theme: Theme) => Component;
529
+ }
530
+ > = {
531
+ ask: askRenderer,
532
+ grep: grepRenderer,
533
+ find: findRenderer,
534
+ notebook: notebookRenderer,
535
+ ls: lsRenderer,
536
+ lsp: lspRenderer,
537
+ task: taskRenderer,
538
+ web_fetch: webFetchRenderer,
539
+ web_search: webSearchRenderer,
540
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Bundled agent definitions.
3
+ *
4
+ * Agents are loaded from .md files in the bundled-agents directory.
5
+ * These serve as defaults when no user/project agents are discovered.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import type { AgentDefinition, AgentSource } from "./types.js";
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const BUNDLED_AGENTS_DIR = path.join(__dirname, "bundled-agents");
15
+
16
+ /**
17
+ * Parse YAML frontmatter from markdown content.
18
+ */
19
+ function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
20
+ const frontmatter: Record<string, string> = {};
21
+ const normalized = content.replace(/\r\n/g, "\n");
22
+
23
+ if (!normalized.startsWith("---")) {
24
+ return { frontmatter, body: normalized };
25
+ }
26
+
27
+ const endIndex = normalized.indexOf("\n---", 3);
28
+ if (endIndex === -1) {
29
+ return { frontmatter, body: normalized };
30
+ }
31
+
32
+ const frontmatterBlock = normalized.slice(4, endIndex);
33
+ const body = normalized.slice(endIndex + 4).trim();
34
+
35
+ for (const line of frontmatterBlock.split("\n")) {
36
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
37
+ if (match) {
38
+ let value = match[2].trim();
39
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
40
+ value = value.slice(1, -1);
41
+ }
42
+ frontmatter[match[1]] = value;
43
+ }
44
+ }
45
+
46
+ return { frontmatter, body };
47
+ }
48
+
49
+ /**
50
+ * Load a single agent from a markdown file.
51
+ */
52
+ function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefinition | null {
53
+ let content: string;
54
+ try {
55
+ content = fs.readFileSync(filePath, "utf-8");
56
+ } catch {
57
+ return null;
58
+ }
59
+
60
+ const { frontmatter, body } = parseFrontmatter(content);
61
+
62
+ if (!frontmatter.name || !frontmatter.description) {
63
+ return null;
64
+ }
65
+
66
+ const tools = frontmatter.tools
67
+ ?.split(",")
68
+ .map((t) => t.trim())
69
+ .filter(Boolean);
70
+
71
+ const recursive =
72
+ frontmatter.recursive === undefined ? false : frontmatter.recursive === "true" || frontmatter.recursive === "1";
73
+
74
+ return {
75
+ name: frontmatter.name,
76
+ description: frontmatter.description,
77
+ tools: tools && tools.length > 0 ? tools : undefined,
78
+ model: frontmatter.model,
79
+ recursive,
80
+ systemPrompt: body,
81
+ source,
82
+ filePath,
83
+ };
84
+ }
85
+
86
+ /** Cache for bundled agents */
87
+ let bundledAgentsCache: AgentDefinition[] | null = null;
88
+
89
+ /**
90
+ * Load all bundled agents from the bundled-agents directory.
91
+ * Results are cached after first load.
92
+ */
93
+ export function loadBundledAgents(): AgentDefinition[] {
94
+ if (bundledAgentsCache !== null) {
95
+ return bundledAgentsCache;
96
+ }
97
+
98
+ const agents: AgentDefinition[] = [];
99
+
100
+ if (!fs.existsSync(BUNDLED_AGENTS_DIR)) {
101
+ bundledAgentsCache = agents;
102
+ return agents;
103
+ }
104
+
105
+ let entries: fs.Dirent[];
106
+ try {
107
+ entries = fs.readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true });
108
+ } catch {
109
+ bundledAgentsCache = agents;
110
+ return agents;
111
+ }
112
+
113
+ for (const entry of entries) {
114
+ if (!entry.name.endsWith(".md")) continue;
115
+
116
+ const filePath = path.join(BUNDLED_AGENTS_DIR, entry.name);
117
+ const agent = loadAgentFromFile(filePath, "bundled");
118
+ if (agent) {
119
+ agents.push(agent);
120
+ }
121
+ }
122
+
123
+ bundledAgentsCache = agents;
124
+ return agents;
125
+ }
126
+
127
+ /**
128
+ * Get a bundled agent by name.
129
+ */
130
+ export function getBundledAgent(name: string): AgentDefinition | undefined {
131
+ return loadBundledAgents().find((a) => a.name === name);
132
+ }
133
+
134
+ /**
135
+ * Get all bundled agents as a map keyed by name.
136
+ */
137
+ export function getBundledAgentsMap(): Map<string, AgentDefinition> {
138
+ const map = new Map<string, AgentDefinition>();
139
+ for (const agent of loadBundledAgents()) {
140
+ map.set(agent.name, agent);
141
+ }
142
+ return map;
143
+ }
144
+
145
+ /**
146
+ * Clear the bundled agents cache (for testing).
147
+ */
148
+ export function clearBundledAgentsCache(): void {
149
+ bundledAgentsCache = null;
150
+ }
151
+
152
+ // Re-export for backward compatibility
153
+ export const BUNDLED_AGENTS = loadBundledAgents;