@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +107 -8
  2. package/docs/custom-tools.md +3 -3
  3. package/docs/extensions.md +226 -220
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +50 -53
  6. package/examples/custom-tools/README.md +2 -17
  7. package/examples/extensions/README.md +76 -74
  8. package/examples/extensions/todo.ts +2 -5
  9. package/examples/hooks/custom-compaction.ts +2 -4
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +7 -11
  14. package/package.json +6 -6
  15. package/src/cli/args.ts +9 -6
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +16 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/custom-tools/wrapper.ts +0 -1
  25. package/src/core/extensions/index.ts +1 -6
  26. package/src/core/extensions/runner.ts +1 -1
  27. package/src/core/extensions/types.ts +1 -1
  28. package/src/core/extensions/wrapper.ts +1 -8
  29. package/src/core/file-mentions.ts +5 -8
  30. package/src/core/hooks/runner.ts +2 -2
  31. package/src/core/hooks/types.ts +1 -1
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +1 -1
  35. package/src/core/sdk.ts +64 -105
  36. package/src/core/session-manager.ts +18 -22
  37. package/src/core/settings-manager.ts +66 -1
  38. package/src/core/slash-commands.ts +12 -5
  39. package/src/core/system-prompt.ts +49 -36
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/ask.ts +98 -4
  42. package/src/core/tools/bash-interceptor.ts +11 -4
  43. package/src/core/tools/bash.ts +121 -5
  44. package/src/core/tools/context.ts +7 -0
  45. package/src/core/tools/edit-diff.ts +73 -24
  46. package/src/core/tools/edit.ts +221 -34
  47. package/src/core/tools/exa/render.ts +4 -16
  48. package/src/core/tools/find.ts +149 -5
  49. package/src/core/tools/gemini-image.ts +279 -56
  50. package/src/core/tools/git.ts +17 -3
  51. package/src/core/tools/grep.ts +185 -5
  52. package/src/core/tools/index.test.ts +180 -0
  53. package/src/core/tools/index.ts +96 -242
  54. package/src/core/tools/ls.ts +133 -5
  55. package/src/core/tools/lsp/index.ts +32 -29
  56. package/src/core/tools/lsp/render.ts +21 -22
  57. package/src/core/tools/notebook.ts +112 -4
  58. package/src/core/tools/output.ts +175 -15
  59. package/src/core/tools/read.ts +127 -25
  60. package/src/core/tools/render-utils.ts +241 -0
  61. package/src/core/tools/renderers.ts +40 -828
  62. package/src/core/tools/review.ts +26 -25
  63. package/src/core/tools/rulebook.ts +11 -3
  64. package/src/core/tools/task/agents.ts +28 -7
  65. package/src/core/tools/task/discovery.ts +0 -6
  66. package/src/core/tools/task/executor.ts +264 -254
  67. package/src/core/tools/task/index.ts +48 -208
  68. package/src/core/tools/task/render.ts +26 -11
  69. package/src/core/tools/task/types.ts +7 -12
  70. package/src/core/tools/task/worker-protocol.ts +17 -0
  71. package/src/core/tools/task/worker.ts +238 -0
  72. package/src/core/tools/truncate.ts +27 -1
  73. package/src/core/tools/web-fetch.ts +25 -49
  74. package/src/core/tools/web-search/index.ts +132 -46
  75. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  76. package/src/core/tools/web-search/providers/exa.ts +2 -1
  77. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  78. package/src/core/tools/web-search/render.ts +6 -4
  79. package/src/core/tools/web-search/types.ts +13 -0
  80. package/src/core/tools/write.ts +96 -14
  81. package/src/core/voice.ts +1 -1
  82. package/src/discovery/helpers.test.ts +1 -1
  83. package/src/index.ts +5 -16
  84. package/src/main.ts +5 -5
  85. package/src/modes/interactive/components/assistant-message.ts +1 -1
  86. package/src/modes/interactive/components/custom-message.ts +1 -1
  87. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  88. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  89. package/src/modes/interactive/components/footer.ts +1 -1
  90. package/src/modes/interactive/components/hook-message.ts +1 -1
  91. package/src/modes/interactive/components/model-selector.ts +1 -1
  92. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  93. package/src/modes/interactive/components/settings-defs.ts +49 -0
  94. package/src/modes/interactive/components/status-line.ts +1 -1
  95. package/src/modes/interactive/components/tool-execution.ts +93 -538
  96. package/src/modes/interactive/interactive-mode.ts +19 -7
  97. package/src/modes/interactive/theme/theme.ts +4 -4
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +1 -1
  100. package/src/modes/rpc/rpc-types.ts +1 -1
  101. package/src/prompts/system-prompt.md +4 -0
  102. package/src/prompts/task.md +0 -7
  103. package/src/prompts/tools/gemini-image.md +5 -1
  104. package/src/prompts/tools/output.md +6 -2
  105. package/src/prompts/tools/task.md +68 -0
  106. package/src/prompts/tools/web-fetch.md +1 -0
  107. package/src/prompts/tools/web-search.md +2 -0
  108. package/src/utils/image-convert.ts +8 -2
  109. package/src/utils/image-magick.ts +247 -0
  110. package/src/utils/image-resize.ts +53 -13
  111. package/examples/custom-tools/question/index.ts +0 -84
  112. package/examples/custom-tools/subagent/README.md +0 -172
  113. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  114. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  115. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  116. package/examples/custom-tools/subagent/agents.ts +0 -156
  117. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  118. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  119. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  120. package/examples/custom-tools/subagent/index.ts +0 -1002
  121. package/examples/sdk/05-tools.ts +0 -94
  122. package/examples/sdk/12-full-control.ts +0 -95
  123. package/src/prompts/browser.md +0 -71
@@ -1,13 +1,14 @@
1
1
  export { type AskToolDetails, askTool, createAskTool } from "./ask";
2
- export { type BashToolDetails, bashTool, createBashTool } from "./bash";
3
- export { createEditTool, type EditToolOptions, editTool } from "./edit";
2
+ export { type BashToolDetails, createBashTool } from "./bash";
3
+ export { createEditTool } from "./edit";
4
4
  // Exa MCP tools (22 tools)
5
5
  export { exaTools } from "./exa/index";
6
6
  export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
7
- export { createFindTool, type FindToolDetails, findTool } from "./find";
7
+ export { createFindTool, type FindToolDetails } from "./find";
8
+ export { setPreferredImageProvider } from "./gemini-image";
8
9
  export { createGitTool, type GitToolDetails, gitTool } from "./git";
9
- export { createGrepTool, type GrepToolDetails, grepTool } from "./grep";
10
- export { createLsTool, type LsToolDetails, lsTool } from "./ls";
10
+ export { createGrepTool, type GrepToolDetails } from "./grep";
11
+ export { createLsTool, type LsToolDetails } from "./ls";
11
12
  export {
12
13
  createLspTool,
13
14
  type FileDiagnosticsResult,
@@ -19,19 +20,14 @@ export {
19
20
  lspTool,
20
21
  warmupLspServers,
21
22
  } from "./lsp/index";
22
- export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook";
23
- export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
24
- export { createReadTool, type ReadToolDetails, type ReadToolOptions, readTool } from "./read";
25
- export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
26
- export {
27
- createRulebookTool,
28
- filterRulebookRules,
29
- formatRulesForPrompt,
30
- type RulebookToolDetails,
31
- } from "./rulebook";
23
+ export { createNotebookTool, type NotebookToolDetails } from "./notebook";
24
+ export { createOutputTool, type OutputToolDetails } from "./output";
25
+ export { createReadTool, type ReadToolDetails } from "./read";
26
+ export { reportFindingTool, submitReviewTool } from "./review";
27
+ export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
32
28
  export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
33
29
  export type { TruncationResult } from "./truncate";
34
- export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch";
30
+ export { createWebFetchTool, type WebFetchToolDetails } from "./web-fetch";
35
31
  export {
36
32
  companyWebSearchTools,
37
33
  createWebSearchTool,
@@ -39,6 +35,7 @@ export {
39
35
  getWebSearchTools,
40
36
  hasExaWebSearch,
41
37
  linkedinWebSearchTools,
38
+ setPreferredWebSearchProvider,
42
39
  type WebSearchProvider,
43
40
  type WebSearchResponse,
44
41
  type WebSearchToolsOptions,
@@ -50,247 +47,104 @@ export {
50
47
  webSearchLinkedinTool,
51
48
  webSearchTool,
52
49
  } from "./web-search/index";
53
- export { createWriteTool, type WriteToolDetails, type WriteToolOptions, writeTool } from "./write";
50
+ export { createWriteTool, type WriteToolDetails } from "./write";
54
51
 
55
52
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
56
- import { askTool, createAskTool } from "./ask";
57
- import { bashTool, createBashTool } from "./bash";
58
- import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
59
- import { createEditTool, editTool } from "./edit";
60
- import { createFindTool, findTool } from "./find";
61
- import { createGitTool, gitTool } from "./git";
62
- import { createGrepTool, grepTool } from "./grep";
63
- import { createLsTool, lsTool } from "./ls";
64
- import { createLspTool, createLspWritethrough, lspTool } from "./lsp/index";
65
- import { createNotebookTool, notebookTool } from "./notebook";
66
- import { createOutputTool, outputTool } from "./output";
67
- import { createReadTool, readTool } from "./read";
68
- import { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
69
- import { createTaskTool, taskTool } from "./task/index";
70
- import { createWebFetchTool, webFetchTool } from "./web-fetch";
71
- import { createWebSearchTool, webSearchTool } from "./web-search/index";
72
- import { createWriteTool, writeTool } from "./write";
53
+ import type { Rule } from "../../capability/rule";
54
+ import type { EventBus } from "../event-bus";
55
+ import { createAskTool } from "./ask";
56
+ import { createBashTool } from "./bash";
57
+ import { createEditTool } from "./edit";
58
+ import { createFindTool } from "./find";
59
+ import { createGitTool } from "./git";
60
+ import { createGrepTool } from "./grep";
61
+ import { createLsTool } from "./ls";
62
+ import { createLspTool } from "./lsp/index";
63
+ import { createNotebookTool } from "./notebook";
64
+ import { createOutputTool } from "./output";
65
+ import { createReadTool } from "./read";
66
+ import { reportFindingTool, submitReviewTool } from "./review";
67
+ import { createRulebookTool } from "./rulebook";
68
+ import { createTaskTool } from "./task/index";
69
+ import { createWebFetchTool } from "./web-fetch";
70
+ import { createWebSearchTool } from "./web-search/index";
71
+ import { createWriteTool } from "./write";
73
72
 
74
73
  /** Tool type (AgentTool from pi-ai) */
75
74
  export type Tool = AgentTool<any, any, any>;
76
75
 
77
- /** Context for tools that need session information */
78
- export interface SessionContext {
76
+ /** Session context for tool factories */
77
+ export interface ToolSession {
78
+ /** Current working directory */
79
+ cwd: string;
80
+ /** Whether UI is available */
81
+ hasUI: boolean;
82
+ /** Rulebook rules */
83
+ rulebookRules: Rule[];
84
+ /** Event bus for tool/extension communication */
85
+ eventBus?: EventBus;
86
+ /** Get session file */
79
87
  getSessionFile: () => string | null;
88
+ /** Get session spawns */
89
+ getSessionSpawns: () => string | null;
90
+ /** Settings manager (optional) */
91
+ settings?: {
92
+ getImageAutoResize(): boolean;
93
+ getLspFormatOnWrite(): boolean;
94
+ getLspDiagnosticsOnWrite(): boolean;
95
+ getLspDiagnosticsOnEdit(): boolean;
96
+ getEditFuzzyMatch(): boolean;
97
+ getGitToolEnabled(): boolean;
98
+ getBashInterceptorEnabled(): boolean;
99
+ };
80
100
  }
81
101
 
82
- /** Options for creating coding tools */
83
- export interface CodingToolsOptions {
84
- /** Whether to fetch LSP diagnostics after write tool writes files (default: true) */
85
- lspDiagnosticsOnWrite?: boolean;
86
- /** Whether to fetch LSP diagnostics after edit tool edits files (default: false) */
87
- lspDiagnosticsOnEdit?: boolean;
88
- /** Whether to format files using LSP after write tool writes (default: true) */
89
- lspFormatOnWrite?: boolean;
90
- /** Whether to accept high-confidence fuzzy matches in edit tool (default: true) */
91
- editFuzzyMatch?: boolean;
92
- /** Whether to auto-resize images to 2000x2000 max in read tool (default: true) */
93
- readAutoResizeImages?: boolean;
94
- /** Set of tool names available to the agent (for cross-tool awareness) */
95
- availableTools?: Set<string>;
96
- }
97
-
98
- // Factory function type
99
- type ToolFactory = (cwd: string, sessionContext?: SessionContext, options?: CodingToolsOptions) => Tool | Promise<Tool>;
100
-
101
- // Tool definitions: static tools and their factory functions
102
- const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
103
- ask: { tool: askTool, create: createAskTool },
104
- read: {
105
- tool: readTool,
106
- create: (cwd, _ctx, options) => createReadTool(cwd, { autoResizeImages: options?.readAutoResizeImages ?? true }),
107
- },
108
- bash: { tool: bashTool, create: createBashTool },
109
- edit: {
110
- tool: editTool,
111
- create: (cwd, _ctx, options) => {
112
- const enableDiagnostics = options?.lspDiagnosticsOnEdit ?? false;
113
- const enableFormat = options?.lspFormatOnWrite ?? true;
114
- const writethrough = createLspWritethrough(cwd, {
115
- enableFormat,
116
- enableDiagnostics,
117
- });
118
- return createEditTool(cwd, { fuzzyMatch: options?.editFuzzyMatch ?? true, writethrough });
119
- },
120
- },
121
- write: {
122
- tool: writeTool,
123
- create: (cwd, _ctx, options) => {
124
- const enableFormat = options?.lspFormatOnWrite ?? true;
125
- const enableDiagnostics = options?.lspDiagnosticsOnWrite ?? true;
126
- const writethrough = createLspWritethrough(cwd, {
127
- enableFormat,
128
- enableDiagnostics,
129
- });
130
- return createWriteTool(cwd, { writethrough });
131
- },
132
- },
133
- grep: { tool: grepTool, create: createGrepTool },
134
- find: { tool: findTool, create: createFindTool },
135
- git: { tool: gitTool, create: createGitTool },
136
- ls: { tool: lsTool, create: createLsTool },
137
- lsp: { tool: lspTool, create: createLspTool },
138
- notebook: { tool: notebookTool, create: createNotebookTool },
139
- output: { tool: outputTool, create: (cwd, ctx) => createOutputTool(cwd, ctx) },
140
- task: { tool: taskTool, create: (cwd, ctx, opts) => createTaskTool(cwd, ctx, opts) },
141
- web_fetch: { tool: webFetchTool, create: createWebFetchTool },
142
- web_search: { tool: webSearchTool, create: createWebSearchTool },
143
- report_finding: { tool: reportFindingTool, create: createReportFindingTool },
144
- submit_review: { tool: submitReviewTool, create: createSubmitReviewTool },
102
+ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
103
+
104
+ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
105
+ ask: createAskTool,
106
+ bash: createBashTool,
107
+ edit: createEditTool,
108
+ find: createFindTool,
109
+ git: createGitTool,
110
+ grep: createGrepTool,
111
+ ls: createLsTool,
112
+ lsp: createLspTool,
113
+ notebook: createNotebookTool,
114
+ output: createOutputTool,
115
+ read: createReadTool,
116
+ rulebook: createRulebookTool,
117
+ task: createTaskTool,
118
+ web_fetch: createWebFetchTool,
119
+ web_search: createWebSearchTool,
120
+ write: createWriteTool,
145
121
  };
146
122
 
147
- export type ToolName = keyof typeof toolDefs;
148
-
149
- // Tools that require UI (excluded when hasUI is false)
150
- const uiToolNames: ToolName[] = ["ask"];
151
-
152
- // Tool sets defined by name (base sets, without UI-only tools)
153
- export const baseCodingToolNames: ToolName[] = [
154
- "read",
155
- "bash",
156
- "edit",
157
- "write",
158
- "grep",
159
- "find",
160
- "git",
161
- "ls",
162
- "lsp",
163
- "notebook",
164
- "output",
165
- "task",
166
- "web_fetch",
167
- "web_search",
168
- ];
169
- const baseReadOnlyToolNames: ToolName[] = ["read", "grep", "find", "ls"];
170
-
171
- // Default tools for full access mode (using process.cwd(), no UI)
172
- export const codingTools: Tool[] = baseCodingToolNames.map((name) => toolDefs[name].tool);
173
-
174
- // Read-only tools for exploration without modification (using process.cwd(), no UI)
175
- export const readOnlyTools: Tool[] = baseReadOnlyToolNames.map((name) => toolDefs[name].tool);
176
-
177
- // All available tools (using process.cwd(), no UI)
178
- export const allTools = Object.fromEntries(Object.entries(toolDefs).map(([name, def]) => [name, def.tool])) as Record<
179
- ToolName,
180
- Tool
181
- >;
182
-
183
- /**
184
- * Create coding tools configured for a specific working directory.
185
- * @param cwd - Working directory for tools
186
- * @param hasUI - Whether UI is available (includes ask tool if true)
187
- * @param sessionContext - Optional session context for tools that need it
188
- * @param options - Options for tool configuration
189
- */
190
- export async function createCodingTools(
191
- cwd: string,
192
- hasUI = false,
193
- sessionContext?: SessionContext,
194
- options?: CodingToolsOptions,
195
- ): Promise<Tool[]> {
196
- const names = hasUI ? [...baseCodingToolNames, ...uiToolNames] : baseCodingToolNames;
197
- const optionsWithTools = { ...options, availableTools: new Set(names) };
198
- return Promise.all(names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools)));
199
- }
200
-
201
- /**
202
- * Create read-only tools configured for a specific working directory.
203
- * @param cwd - Working directory for tools
204
- * @param hasUI - Whether UI is available (includes ask tool if true)
205
- * @param sessionContext - Optional session context for tools that need it
206
- * @param options - Options for tool configuration
207
- */
208
- export async function createReadOnlyTools(
209
- cwd: string,
210
- hasUI = false,
211
- sessionContext?: SessionContext,
212
- options?: CodingToolsOptions,
213
- ): Promise<Tool[]> {
214
- const names = hasUI ? [...baseReadOnlyToolNames, ...uiToolNames] : baseReadOnlyToolNames;
215
- const optionsWithTools = { ...options, availableTools: new Set(names) };
216
- return Promise.all(names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools)));
217
- }
218
-
219
- /**
220
- * Create all tools configured for a specific working directory.
221
- * @param cwd - Working directory for tools
222
- * @param sessionContext - Optional session context for tools that need it
223
- * @param options - Options for tool configuration
224
- */
225
- export async function createAllTools(
226
- cwd: string,
227
- sessionContext?: SessionContext,
228
- options?: CodingToolsOptions,
229
- ): Promise<Record<ToolName, Tool>> {
230
- const names = Object.keys(toolDefs);
231
- const optionsWithTools = { ...options, availableTools: new Set(names) };
232
- const entries = await Promise.all(
233
- Object.entries(toolDefs).map(async ([name, def]) => [
234
- name,
235
- await def.create(cwd, sessionContext, optionsWithTools),
236
- ]),
237
- );
238
- return Object.fromEntries(entries) as Record<ToolName, Tool>;
239
- }
240
-
241
- /**
242
- * Wrap a bash tool with interception that redirects common patterns to specialized tools.
243
- * This helps prevent LLMs from falling back to shell commands when better tools exist.
244
- *
245
- * @param bashTool - The bash tool to wrap
246
- * @param availableTools - Set of tool names that are available (for context-aware blocking)
247
- * @returns Wrapped bash tool with interception
248
- */
249
- export function wrapBashWithInterception(bashTool: Tool, availableTools: Set<string>): Tool {
250
- const originalExecute = bashTool.execute;
251
-
252
- return {
253
- ...bashTool,
254
- execute: async (toolCallId, params, signal, onUpdate, context) => {
255
- const command = (params as { command: string }).command;
256
-
257
- // Check for forbidden patterns
258
- const interception = checkBashInterception(command, availableTools);
259
- if (interception.block) {
260
- throw new Error(interception.message);
261
- }
262
-
263
- // Check for simple ls that should use ls tool
264
- const lsInterception = checkSimpleLsInterception(command, availableTools);
265
- if (lsInterception.block) {
266
- throw new Error(lsInterception.message);
267
- }
123
+ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
124
+ report_finding: () => reportFindingTool,
125
+ submit_review: () => submitReviewTool,
126
+ };
268
127
 
269
- // Pass through to original bash tool
270
- return originalExecute(toolCallId, params, signal, onUpdate, context);
271
- },
272
- };
273
- }
128
+ export type ToolName = keyof typeof BUILTIN_TOOLS;
274
129
 
275
130
  /**
276
- * Apply bash interception to a set of tools.
277
- * Finds the bash tool and wraps it with interception based on other available tools.
278
- *
279
- * @param tools - Array of tools to process
280
- * @returns Tools with bash interception applied
131
+ * Create tools from BUILTIN_TOOLS registry.
281
132
  */
282
- export function applyBashInterception(tools: Tool[]): Tool[] {
283
- const toolNames = new Set(tools.map((t) => t.name));
284
-
285
- // If bash isn't in the tools, nothing to do
286
- if (!toolNames.has("bash")) {
287
- return tools;
133
+ export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
134
+ const requestedTools = toolNames && toolNames.length > 0 ? toolNames : undefined;
135
+ const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
136
+ const entries = requestedTools
137
+ ? requestedTools
138
+ .filter((name, index) => requestedTools.indexOf(name) === index && name in allTools)
139
+ .map((name) => [name, allTools[name]] as const)
140
+ : Object.entries(BUILTIN_TOOLS);
141
+ const results = await Promise.all(entries.map(([, factory]) => factory(session)));
142
+ const tools = results.filter((t): t is Tool => t !== null);
143
+
144
+ if (requestedTools) {
145
+ const allowed = new Set(requestedTools);
146
+ return tools.filter((tool) => allowed.has(tool.name));
288
147
  }
289
148
 
290
- return tools.map((tool) => {
291
- if (tool.name === "bash") {
292
- return wrapBashWithInterception(tool, toolNames);
293
- }
294
- return tool;
295
- });
149
+ return tools;
296
150
  }
@@ -1,10 +1,25 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import nodePath from "node:path";
3
3
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
+ import type { Component } from "@oh-my-pi/pi-tui";
5
+ import { Text } from "@oh-my-pi/pi-tui";
4
6
  import { Type } from "@sinclair/typebox";
7
+ import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
8
+ import type { RenderResultOptions } from "../custom-tools/types";
5
9
  import { untilAborted } from "../utils";
10
+ import type { ToolSession } from "./index";
6
11
  import { resolveToCwd } from "./path-utils";
7
- import { formatAge } from "./render-utils";
12
+ import {
13
+ formatAge,
14
+ formatBytes,
15
+ formatCount,
16
+ formatEmptyMessage,
17
+ formatExpandHint,
18
+ formatMeta,
19
+ formatMoreItems,
20
+ formatTruncationSuffix,
21
+ PREVIEW_LIMITS,
22
+ } from "./render-utils";
8
23
  import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
9
24
 
10
25
  const lsSchema = Type.Object({
@@ -23,7 +38,7 @@ export interface LsToolDetails {
23
38
  entryLimitReached?: number;
24
39
  }
25
40
 
26
- export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
41
+ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
27
42
  return {
28
43
  name: "ls",
29
44
  label: "Ls",
@@ -35,7 +50,7 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
35
50
  signal?: AbortSignal,
36
51
  ) => {
37
52
  return untilAborted(signal, async () => {
38
- const dirPath = resolveToCwd(path || ".", cwd);
53
+ const dirPath = resolveToCwd(path || ".", session.cwd);
39
54
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
40
55
 
41
56
  // Check if path exists
@@ -145,5 +160,118 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
145
160
  };
146
161
  }
147
162
 
148
- /** Default ls tool using process.cwd() - for backwards compatibility */
149
- export const lsTool = createLsTool(process.cwd());
163
+ // =============================================================================
164
+ // TUI Renderer
165
+ // =============================================================================
166
+
167
+ interface LsRenderArgs {
168
+ path?: string;
169
+ limit?: number;
170
+ }
171
+
172
+ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
173
+
174
+ export const lsToolRenderer = {
175
+ renderCall(args: LsRenderArgs, uiTheme: Theme): Component {
176
+ const label = uiTheme.fg("toolTitle", uiTheme.bold("Ls"));
177
+ let text = `${label} ${uiTheme.fg("accent", args.path || ".")}`;
178
+
179
+ const meta: string[] = [];
180
+ if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
181
+ text += formatMeta(meta, uiTheme);
182
+
183
+ return new Text(text, 0, 0);
184
+ },
185
+
186
+ renderResult(
187
+ result: { content: Array<{ type: string; text?: string }>; details?: LsToolDetails },
188
+ { expanded }: RenderResultOptions,
189
+ uiTheme: Theme,
190
+ ): Component {
191
+ const details = result.details;
192
+ const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
193
+
194
+ if (
195
+ (!textContent || textContent.trim() === "" || textContent.trim() === "(empty directory)") &&
196
+ (!details?.entries || details.entries.length === 0)
197
+ ) {
198
+ return new Text(formatEmptyMessage("Empty directory", uiTheme), 0, 0);
199
+ }
200
+
201
+ let entries: string[] = details?.entries ? [...details.entries] : [];
202
+ if (entries.length === 0) {
203
+ const rawLines = textContent.split("\n").filter((l: string) => l.trim());
204
+ entries = rawLines.filter((line) => !/^\[.*\]$/.test(line.trim()));
205
+ }
206
+
207
+ if (entries.length === 0) {
208
+ return new Text(formatEmptyMessage("Empty directory", uiTheme), 0, 0);
209
+ }
210
+
211
+ let dirCount = details?.dirCount;
212
+ let fileCount = details?.fileCount;
213
+ if (dirCount === undefined || fileCount === undefined) {
214
+ dirCount = 0;
215
+ fileCount = 0;
216
+ for (const entry of entries) {
217
+ if (entry.endsWith("/")) {
218
+ dirCount += 1;
219
+ } else {
220
+ fileCount += 1;
221
+ }
222
+ }
223
+ }
224
+
225
+ const truncated = Boolean(details?.truncation?.truncated || details?.entryLimitReached);
226
+ const icon = truncated
227
+ ? uiTheme.styledSymbol("status.warning", "warning")
228
+ : uiTheme.styledSymbol("status.success", "success");
229
+
230
+ const summaryText = [formatCount("dir", dirCount ?? 0), formatCount("file", fileCount ?? 0)].join(
231
+ uiTheme.sep.dot,
232
+ );
233
+ const maxEntries = expanded ? entries.length : Math.min(entries.length, COLLAPSED_LIST_LIMIT);
234
+ const hasMoreEntries = entries.length > maxEntries;
235
+ const expandHint = formatExpandHint(expanded, hasMoreEntries, uiTheme);
236
+
237
+ let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${expandHint}`;
238
+
239
+ const truncationReasons: string[] = [];
240
+ if (details?.entryLimitReached) {
241
+ truncationReasons.push(`entry limit ${details.entryLimitReached}`);
242
+ }
243
+ if (details?.truncation?.truncated) {
244
+ truncationReasons.push(`output cap ${formatBytes(details.truncation.maxBytes)}`);
245
+ }
246
+
247
+ const hasTruncation = truncationReasons.length > 0;
248
+
249
+ for (let i = 0; i < maxEntries; i++) {
250
+ const entry = entries[i];
251
+ const isLast = i === maxEntries - 1 && !hasMoreEntries && !hasTruncation;
252
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
253
+ const isDir = entry.endsWith("/");
254
+ const entryPath = isDir ? entry.slice(0, -1) : entry;
255
+ const lang = isDir ? undefined : getLanguageFromPath(entryPath);
256
+ const entryIcon = isDir
257
+ ? uiTheme.fg("accent", uiTheme.icon.folder)
258
+ : uiTheme.fg("muted", uiTheme.getLangIcon(lang));
259
+ const entryColor = isDir ? "accent" : "toolOutput";
260
+ text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg(entryColor, entry)}`;
261
+ }
262
+
263
+ if (hasMoreEntries) {
264
+ const moreEntriesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
265
+ text += `\n ${uiTheme.fg("dim", moreEntriesBranch)} ${uiTheme.fg(
266
+ "muted",
267
+ formatMoreItems(entries.length - maxEntries, "entry", uiTheme),
268
+ )}`;
269
+ }
270
+
271
+ if (hasTruncation) {
272
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
273
+ }
274
+
275
+ return new Text(text, 0, 0);
276
+ },
277
+ };