@aliou/pi-dev-kit 0.6.5 → 0.7.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.
@@ -1,29 +1,33 @@
1
1
  # Tools
2
2
 
3
- Tools are functions the LLM can call. They are the primary way extensions add capabilities to pi.
3
+ Tools are functions the LLM can call. They are the main way extensions add capabilities to Pi.
4
4
 
5
5
  ## Imports
6
6
 
7
- Use these imports at the top of your tool file:
8
-
9
- ```typescript
10
- import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";
11
- import type {
12
- AgentToolResult,
13
- AgentToolUpdateCallback,
14
- ExtensionAPI,
15
- ExtensionContext,
16
- Theme,
17
- ToolRenderResultOptions,
18
- } from "@mariozechner/pi-coding-agent";
19
- import { defineTool, getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
20
- import { Container, Markdown, Text } from "@mariozechner/pi-tui";
7
+ Use the namespace that matches the target Pi package. Examples use the forward namespace.
8
+
9
+ ```typescript
10
+ import { ToolBody, ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
11
+ import type { AgentToolResult, ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
12
+ import {
13
+ defineTool,
14
+ formatSize,
15
+ getMarkdownTheme,
16
+ keyHint,
17
+ truncateHead,
18
+ truncateTail,
19
+ withFileMutationQueue,
20
+ } from "@earendil-works/pi-coding-agent";
21
+ import { StringEnum } from "@earendil-works/pi-ai";
22
+ import { Container, Markdown, Text } from "@earendil-works/pi-tui";
21
23
  import { type Static, Type } from "typebox";
22
24
  ```
23
25
 
24
- ## Registration
26
+ Use legacy `@mariozechner/*` imports only when the target `@earendil-works/*` package is not available yet.
25
27
 
26
- Tool files are extension entry points. Put the tool registration in `src/tools/index.ts`, export a default function, and list that file in `package.json` `pi.extensions`.
28
+ ## Minimal Tool Entry Point
29
+
30
+ Tool entry points are normal Pi extension entry points. Export a default function and list the file in `package.json` `pi.extensions`.
27
31
 
28
32
  ```typescript
29
33
  const parameters = Type.Object({
@@ -32,29 +36,25 @@ const parameters = Type.Object({
32
36
  });
33
37
 
34
38
  type MyToolParams = Static<typeof parameters>;
39
+
35
40
  interface MyToolDetails {
36
41
  results: string[];
37
42
  }
38
43
 
39
44
  const myTool = defineTool({
40
45
  name: "my_tool",
41
- label: "My Tool", // Required: human-readable name for UI
42
- description: "What this tool does. The LLM reads this to decide when to call it.",
43
- promptSnippet: "Search for items by query", // One-liner for "Available tools" system prompt
44
- promptGuidelines: [ // Guideline bullets appended verbatim to the global "Guidelines" section when this tool is active
45
- "Use my_tool when the user asks about search.",
46
- "Prefer specific queries over broad ones when calling my_tool.",
46
+ label: "My Tool",
47
+ description: "Search for items by query.",
48
+ promptSnippet: "Search for items by query",
49
+ promptGuidelines: [
50
+ "Use my_tool when the user asks to search these items.",
51
+ "Prefer specific queries when calling my_tool.",
47
52
  ],
48
53
  parameters,
49
54
 
50
- async execute(
51
- toolCallId,
52
- params,
53
- signal,
54
- onUpdate,
55
- ctx,
56
- ): Promise<AgentToolResult<MyToolDetails>> {
57
- const results = await doSomething(params.query, params.limit);
55
+ async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<MyToolDetails>> {
56
+ onUpdate?.({ content: [{ type: "text", text: "Searching..." }] });
57
+ const results = await searchItems(params.query, { limit: params.limit, signal });
58
58
  return {
59
59
  content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
60
60
  details: { results },
@@ -62,234 +62,113 @@ const myTool = defineTool({
62
62
  },
63
63
  });
64
64
 
65
- export default async function (pi: ExtensionAPI) {
66
- await configLoader.load();
67
- const config = configLoader.getConfig();
68
- if (!config.enabled) return;
69
-
65
+ export default function toolsExtension(pi: ExtensionAPI) {
70
66
  pi.registerTool(myTool);
71
67
  }
72
68
  ```
73
69
 
74
- ## Tool Definition Fields
75
-
76
- | Field | Type | Required | Description |
77
- |-------|------|----------|-------------|
78
- | `name` | `string` | Yes | Snake_case identifier used in tool calls |
79
- | `label` | `string` | Yes | Human-readable name shown in UI |
80
- | `description` | `string` | Yes | What the tool does (LLM reads this) |
81
- | `parameters` | `TSchema` | Yes | TypeBox schema for arguments |
82
- | `promptSnippet` | `string` | No | One-liner injected into "Available tools" system prompt. Custom tools without this are omitted from that section. |
83
- | `promptGuidelines` | `string[]` | No | Guideline bullets appended verbatim to the global "Guidelines" system prompt section when this tool is active. Write each bullet so it still makes sense standalone. |
84
- | `execute` | `function` | Yes | Implementation |
85
- | `renderCall` | `function` | No | Custom call rendering |
86
- | `renderResult` | `function` | No | Custom result rendering |
70
+ ## `defineTool()`
87
71
 
88
- ## `defineTool()` and Typed Param Alias
72
+ Use `defineTool({...})` for standalone tool objects. It preserves parameter inference from the `parameters` field when the tool is stored in a variable or passed through arrays.
89
73
 
90
- Use `defineTool()` for standalone tool definitions. It infers parameter types from the `parameters` field and preserves them for `execute`, `renderCall`, and `renderResult` so you do not need casts or explicit generic arguments at registration/rendering boundaries.
74
+ Rules:
91
75
 
92
- Inside the `defineTool({...})` object, do not annotate callback parameter types unless TypeScript actually needs help. The contextual type from Pi already provides `toolCallId`, `params`, `signal`, `onUpdate`, `ctx`, `options`, and `theme`. If you want `renderResult` to see a specific `details` shape, annotate the `execute` return type as `Promise<AgentToolResult<MyToolDetails>>`; that is the useful annotation.
76
+ - Do not pass explicit generic arguments to `defineTool`.
77
+ - Do not annotate callback parameters unless TypeScript needs help.
78
+ - Add a `Static<typeof parameters>` alias for helper functions and action modules.
79
+ - If you want `renderResult` to know the `details` shape, annotate `execute` with `Promise<AgentToolResult<MyDetails>>`.
93
80
 
94
- Define a type alias at the top of your file instead of repeating `Static<typeof parameters>`:
81
+ ## Tool Definition Fields
95
82
 
96
- ```typescript
97
- const parameters = Type.Object({
98
- query: Type.String({ description: "Search query" }),
99
- limit: Type.Optional(Type.Number({ description: "Max results", default: 10 })),
100
- });
101
-
102
- type MyToolParams = Static<typeof parameters>;
103
- // Use MyToolParams in helper functions and exported action APIs. Inside defineTool callbacks, prefer inference.
104
- ```
83
+ | Field | Required | Notes |
84
+ |---|---:|---|
85
+ | `name` | Yes | Snake_case identifier used in tool calls. |
86
+ | `label` | Yes | Human-readable display name. |
87
+ | `description` | Yes | Model-facing description. |
88
+ | `parameters` | Yes | TypeBox 1.x schema from `typebox`. |
89
+ | `execute` | Yes | `(toolCallId, params, signal, onUpdate, ctx)`. |
90
+ | `promptSnippet` | No | One-line entry in Available tools. Custom tools are omitted there when absent. |
91
+ | `promptGuidelines` | No | Raw bullets appended to the global Guidelines section. Each bullet must name the tool. |
92
+ | `prepareArguments` | No | Compatibility shim before schema validation. |
93
+ | `executionMode` | No | Use `"sequential"` for shared-state tools that cannot run concurrently. |
94
+ | `renderCall` / `renderResult` | No | Custom TUI component renderers. |
95
+ | `renderShell` | No | Use `"self"` only when the default boxed shell gets in the way. |
105
96
 
106
97
  ## Execute Signature
107
98
 
108
99
  ```typescript
109
- execute(toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<TDetails>>
100
+ async execute(toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<TDetails>>
110
101
  ```
111
102
 
112
- Pi's `ToolDefinition` type is:
113
-
114
- ```typescript
115
- execute(
116
- toolCallId: string,
117
- params: Static<TParams>,
118
- signal: AbortSignal | undefined,
119
- onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
120
- ctx: ExtensionContext,
121
- ): Promise<AgentToolResult<TDetails>>
122
- ```
123
-
124
- You usually do not need to write those parameter types yourself inside `defineTool()`.
125
-
126
- **Parameter order matters.** The signal comes before onUpdate.
103
+ Parameter order matters. `signal` comes before `onUpdate`.
127
104
 
128
- Always use optional chaining when calling `onUpdate`:
105
+ Use optional chaining for updates:
129
106
 
130
107
  ```typescript
131
- onUpdate?.({ output: "partial result", details: { progress: 50 } });
108
+ onUpdate?.({
109
+ content: [{ type: "text", text: "Working..." }],
110
+ details: { progress: 50 },
111
+ });
132
112
  ```
133
113
 
134
- The `onUpdate` parameter can be `undefined`. Calling it without optional chaining will throw.
135
-
136
- ## Tool Overrides and Delegation
137
-
138
- If you override a built-in tool or wrap another tool, audit any delegated `tool.execute(...)` calls during upgrades. These forwarders often pass through `signal`, `onUpdate`, or `ctx` and can silently break when the execute signature changes. Always recheck the delegate call parameter order and include optional parameters that the target tool expects.
139
-
140
- Prompt metadata is not inherited automatically when you override a built-in tool. If the original tool had `promptSnippet` or `promptGuidelines` and you still want that system prompt behavior, define those fields explicitly on the override.
141
-
142
- ## Return Value
114
+ Forward `signal` to all abort-aware work:
143
115
 
144
116
  ```typescript
145
- return {
146
- content: (TextContent | ImageContent)[], // Content blocks sent to the LLM
147
- details?: TDetails, // Arbitrary data available in the renderer
148
- terminate?: boolean, // Optional: skip follow-up LLM call when all finalized results in the batch terminate
149
- };
117
+ const response = await fetch(url, { signal });
118
+ const result = await pi.exec("git", ["status", "--porcelain"], { signal, cwd: ctx.cwd });
150
119
  ```
151
120
 
152
- - `content` is what the LLM sees. Each block is `{ type: "text", text: "..." }` or an image. Keep it structured and concise.
153
- - `details` is what the renderer sees. Put rich data here for custom display.
154
- - `terminate: true` is for final/structured-output tools that should end the turn without an automatic follow-up LLM call. It only applies when every finalized tool result in the current batch also returns `terminate: true`.
121
+ Do not use Node `child_process` for normal commands. Use `pi.exec(command, args, options)` so Pi handles CWD, cancellation, output capture, and lifecycle integration.
155
122
 
156
- Common pattern:
123
+ ## Return Value and Errors
157
124
 
158
125
  ```typescript
159
126
  return {
160
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
161
- details: { results },
127
+ content: [{ type: "text", text: "Text sent to the LLM" }],
128
+ details: { rich: "data for rendering and state" },
129
+ terminate: true, // optional final/structured-output tools only
162
130
  };
163
131
  ```
164
132
 
165
- ## Error Handling
166
-
167
- To report a tool call failure, **throw an error**. The framework catches it and produces a result with `isError: true` on the context.
168
-
169
- ```typescript
170
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
171
- const result = await fetchData(params.query);
172
- if (!result) {
173
- throw new Error("No results found. Try a different query.");
174
- }
175
- return {
176
- content: [{ type: "text", text: JSON.stringify(result) }],
177
- details: { result },
178
- };
179
- },
180
- ```
181
-
182
- The framework's `createErrorToolResult` sets `details: {}` (empty object, not `undefined`) and puts the error message in `content`:
183
-
184
- ```typescript
185
- // Framework produces when tool throws:
186
- {
187
- content: [{ type: "text", text: errorMessage }],
188
- details: {}
189
- }
190
- // And sets context.isError = true in renderResult
191
- ```
192
-
193
- ### Error rendering in `renderResult`
194
-
195
- The framework passes `isError` on the 4th `context` parameter to `renderResult`, but `ToolRenderContext` is not currently exported from the public API. Two practical approaches:
196
-
197
- **Approach 1: Check for missing expected fields in `details`** (recommended for extensions)
198
-
199
- When a tool throws, the framework sets `details: {}` (empty object). Check for missing expected fields:
200
-
201
- ```typescript
202
- renderResult(
203
- result: AgentToolResult<MyToolDetails>,
204
- options: ToolRenderResultOptions,
205
- theme: Theme,
206
- ) {
207
- const { details } = result;
208
-
209
- // details is {} when tool threw — expected fields are missing
210
- if (!details?.results) {
211
- const textBlock = result.content.find((c) => c.type === "text");
212
- const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Operation failed";
213
- return new Text(theme.fg("error", errorMsg), 0, 0);
214
- }
215
- // ... normal rendering
216
- }
217
- ```
218
-
219
- **Approach 2: Use the 4th context parameter** (used by native tools)
220
-
221
- Define a minimal interface locally:
222
-
223
- ```typescript
224
- interface RenderContext { isError: boolean }
225
-
226
- renderResult(
227
- result: AgentToolResult<MyToolDetails>,
228
- options: ToolRenderResultOptions,
229
- theme: Theme,
230
- context: RenderContext,
231
- ) {
232
- if (context.isError) {
233
- const textBlock = result.content.find((c) => c.type === "text");
234
- const errorMsg = textBlock?.type === "text" ? textBlock.text : "Operation failed";
235
- return new Text(theme.fg("error", errorMsg), 0, 0);
236
- }
237
- // ... normal rendering
238
- }
239
- ```
133
+ - `content` is sent to the LLM. Keep it concise and useful.
134
+ - `details` is persisted in the session and used by renderers. Put state snapshots here when the tool mutates extension state.
135
+ - `terminate: true` skips the automatic follow-up LLM call only when every finalized tool in the current batch also terminates.
136
+ - Throw an error to mark a tool result as failed. Returning an object never sets `isError`.
240
137
 
241
- Both approaches work. Approach 1 is more common in published extensions. Approach 2 is what native tools use.
138
+ When a tool throws, Pi creates an error result with `details: {}` and the error message in `content`. Renderers should check for missing expected fields or use the 4th render context's `isError` field.
242
139
 
243
- ## Parameters Schema
140
+ ## Parameters
244
141
 
245
- Use TypeBox 1.x (`Type.*`) from `typebox` for parameter schemas. The LLM sees the schema to know what arguments to provide. Do not import from `@sinclair/typebox` in new extensions.
142
+ Use TypeBox 1.x from `typebox`. Always add useful descriptions.
246
143
 
247
144
  ```typescript
248
- import { Type } from "typebox";
249
-
250
- // Required string
251
- Type.String({ description: "File path to read" })
252
-
253
- // Optional with default
254
- Type.Optional(Type.Number({ description: "Max results", default: 10 }))
255
-
256
- // Enum (string union)
257
- Type.StringEnum(["created", "updated", "relevance"], { description: "Sort order" })
258
-
259
- // Boolean
260
- Type.Boolean({ description: "Include hidden files" })
261
-
262
- // Nested object
263
- Type.Object({
264
- name: Type.String(),
265
- value: Type.String(),
266
- })
267
-
268
- // Array
269
- Type.Array(Type.String(), { description: "List of tags" })
145
+ const parameters = Type.Object({
146
+ path: Type.String({ description: "File path" }),
147
+ includeHidden: Type.Optional(Type.Boolean({ description: "Include hidden files", default: false })),
148
+ sort: Type.Optional(StringEnum(["created", "updated", "relevance"] as const, {
149
+ description: "Sort order",
150
+ })),
151
+ tags: Type.Optional(Type.Array(Type.String({ description: "Tag" }))),
152
+ });
270
153
  ```
271
154
 
272
- Always provide `description` on parameters. The LLM uses these to understand what to pass.
155
+ Use `StringEnum` from `pi-ai` for model-facing string enums. Avoid `Type.Union([Type.Literal(...)])`; it is not compatible with all providers.
273
156
 
274
157
  ## Prompt Metadata
275
158
 
276
- `promptSnippet` and `promptGuidelines` affect different parts of the default system prompt:
277
-
278
- - `promptSnippet` adds a one-line entry to `Available tools`.
279
- - `promptGuidelines` appends raw bullets to the global `Guidelines` section.
159
+ `promptSnippet` and `promptGuidelines` affect different system prompt sections:
280
160
 
281
- Important implications:
161
+ - `promptSnippet` adds one line under Available tools.
162
+ - `promptGuidelines` appends raw bullets to the global Guidelines section when the tool is active.
282
163
 
283
- - `promptGuidelines` bullets are not wrapped with the tool name.
284
- - Write bullets so they still make sense when read out of context.
285
- - Prefer explicit tool names over phrases like `this tool`.
164
+ Because `promptGuidelines` are not grouped under the tool, each bullet must name the exact tool.
286
165
 
287
166
  Good:
288
167
 
289
168
  ```typescript
290
169
  promptGuidelines: [
291
- "Use my_tool to search project docs before broader web research.",
292
- "Prefer specific queries when calling my_tool.",
170
+ "Use repo_tree to inspect repository structure before reading individual files.",
171
+ "Start repo_tree at the root path, then drill into directories as needed.",
293
172
  ]
294
173
  ```
295
174
 
@@ -297,16 +176,16 @@ Weak:
297
176
 
298
177
  ```typescript
299
178
  promptGuidelines: [
300
- "Use this tool for docs.",
301
- "Prefer specific queries.",
179
+ "Use this tool before reading files.",
180
+ "Start at the root path.",
302
181
  ]
303
182
  ```
304
183
 
305
- Use `promptGuidelines` for short, tool-local rules. If the guidance needs cross-tool sequencing, comparisons against several tools, or dynamic config context, use a `before_agent_start` hook instead.
184
+ Use per-tool metadata for simple guidance. Use a `before_agent_start` hook for dynamic config, cross-tool workflows, or complex orchestration.
306
185
 
307
- ## Argument Compatibility and Path Handling
186
+ ## Argument Compatibility
308
187
 
309
- Use `prepareArguments(args)` when you need a compatibility shim before schema validation, for example to support an old parameter shape during a migration.
188
+ Use `prepareArguments(args)` for backward-compatible migrations of old session tool-call shapes. It runs before validation. Keep the public schema strict.
310
189
 
311
190
  ```typescript
312
191
  prepareArguments(args) {
@@ -319,21 +198,18 @@ prepareArguments(args) {
319
198
  }
320
199
  ```
321
200
 
322
- If your custom tool accepts filesystem paths, normalize a leading `@` before resolving the path. Some models include `@` in path arguments, and the built-in file tools already strip it.
201
+ ## Paths and File Mutation
202
+
203
+ If a tool accepts file paths, strip a leading `@` before resolving. Built-in tools do this because models sometimes include `@` from path mentions.
323
204
 
324
205
  ```typescript
325
206
  const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
207
+ const absolutePath = resolve(ctx.cwd, normalizedPath);
326
208
  ```
327
209
 
328
- ## File-Mutating Tools and Concurrency
329
-
330
- Tool calls can run in parallel. If your custom tool mutates files, use `withFileMutationQueue()` so it participates in the same per-file queue as built-in `edit` and `write`.
210
+ If a custom tool mutates files, wrap the whole read-modify-write window in `withFileMutationQueue()` so sibling parallel tool calls cannot overwrite each other.
331
211
 
332
212
  ```typescript
333
- import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
334
- import { mkdir, readFile, writeFile } from "node:fs/promises";
335
- import { dirname, resolve } from "node:path";
336
-
337
213
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
338
214
  const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
339
215
  const absolutePath = resolve(ctx.cwd, normalizedPath);
@@ -343,120 +219,94 @@ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
343
219
  const current = await readFile(absolutePath, "utf8");
344
220
  const next = current.replace(params.oldText, params.newText);
345
221
  await writeFile(absolutePath, next, "utf8");
346
-
347
222
  return {
348
223
  content: [{ type: "text", text: `Updated ${normalizedPath}` }],
349
- details: {},
224
+ details: { path: normalizedPath },
350
225
  };
351
226
  });
352
227
  }
353
228
  ```
354
229
 
355
- Queue the whole read-modify-write window, not just the final write.
356
-
357
- ## Streaming Updates
230
+ ## Concurrency
358
231
 
359
- Use `onUpdate` to stream partial results while the tool executes. This gives the user feedback during long operations.
232
+ Pi runs sibling tool calls in parallel by default. Add `executionMode: "sequential"` when this tool must serialize with sibling tool calls, for example when it mutates shared in-memory state, drives a cursor/game, or depends on strict call order.
360
233
 
361
234
  ```typescript
362
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
363
- for (const chunk of chunks) {
364
- const partial = processChunk(chunk);
365
- onUpdate?.({
366
- content: [{ type: "text", text: partial }],
367
- details: { progress: chunk.index / chunks.length },
368
- });
369
- }
370
- return {
371
- content: [{ type: "text", text: finalResult }],
372
- details: { complete: true },
373
- };
374
- },
235
+ const statefulTool = defineTool({
236
+ name: "game_move",
237
+ label: "Game Move",
238
+ description: "Apply a move to the current game state.",
239
+ executionMode: "sequential",
240
+ parameters,
241
+ async execute(_toolCallId, params) {
242
+ // Mutates shared in-memory state safely.
243
+ },
244
+ });
375
245
  ```
376
246
 
377
- ## Custom Rendering
247
+ Prefer making core operations concurrency-safe. Use sequential mode only when call order is inherently meaningful.
378
248
 
379
- Override how a tool's invocation and result appear in the TUI.
249
+ ## Rendering
380
250
 
381
- ### `renderCall` Signature
251
+ Custom renderers return TUI `Component` objects.
382
252
 
383
- ```typescript
384
- renderCall(args: MyToolParams, theme: Theme): Component
385
- ```
253
+ ### `renderCall`
386
254
 
387
- A 3rd `context` param is available from the framework but rarely needed in `renderCall`.
388
-
389
- Use `ToolCallHeader` from `@aliou/pi-utils-ui`:
255
+ Use `ToolCallHeader` for a stable, scannable call line.
390
256
 
391
257
  ```typescript
392
- renderCall(args: MyToolParams, theme: Theme) {
258
+ renderCall(args, theme) {
393
259
  return new ToolCallHeader(
394
260
  {
395
- toolName: "My Tool",
396
- action: "search", // Optional: for multi-action tools
397
- mainArg: `"${args.query}"`, // Primary thing user scans for
398
- optionArgs: [`limit=${args.limit ?? 10}`], // Compact key-value pairs
399
- longArgs: [], // Long text goes here
400
- showColon: true, // Whether to show colon after tool name
261
+ toolName: "Repo Tree",
262
+ action: args.action,
263
+ mainArg: `${args.owner}/${args.repo}`,
264
+ optionArgs: args.path ? [{ label: "path", value: args.path }] : [],
265
+ longArgs: args.instructions ? [{ label: "instructions", value: args.instructions }] : [],
401
266
  },
402
267
  theme,
403
268
  );
404
269
  }
405
270
  ```
406
271
 
407
- ### `renderResult` Signature
272
+ Header extraction order:
408
273
 
409
- ```typescript
410
- renderResult(
411
- result: AgentToolResult<TDetails>,
412
- options: ToolRenderResultOptions,
413
- theme: Theme,
414
- ): Component
415
- ```
274
+ 1. Action for multi-action tools.
275
+ 2. One main argument users scan for first.
276
+ 3. Short option args.
277
+ 4. Long args on follow-up lines.
278
+
279
+ ### `renderResult`
416
280
 
417
- - `options` has `expanded` and `isPartial`
418
- - `result.details` is `{}` (empty object) when the tool threw an error
419
- - A 4th `context` param is available (see Error Handling above) but not required
281
+ Handle partial state first, then errors, then normal output.
420
282
 
421
283
  ```typescript
422
- renderResult(
423
- result: AgentToolResult<MyToolDetails>,
424
- options: ToolRenderResultOptions,
425
- theme: Theme,
426
- ) {
427
- // 1. Handle partial state first with stable message
284
+ renderResult(result, options, theme) {
428
285
  if (options.isPartial) {
429
- return new Text(theme.fg("muted", "MyTool: fetching..."), 0, 0);
286
+ return new Text(theme.fg("muted", "Repo Tree: fetching..."), 0, 0);
430
287
  }
431
288
 
432
289
  const { details } = result;
433
-
434
- // 2. Handle errors: details is {} when tool threw
435
- if (!details?.results) {
290
+ if (!details?.entries) {
436
291
  const textBlock = result.content.find((c) => c.type === "text");
437
- const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Operation failed";
438
- return new Text(theme.fg("error", errorMsg), 0, 0);
292
+ const message = textBlock?.type === "text" ? textBlock.text : "Repo Tree failed";
293
+ return new Text(theme.fg("error", message), 0, 0);
439
294
  }
440
295
 
441
- // 3. Build fields for ToolBody (showCollapsed controls collapsed/expanded visibility)
442
- const items = details.results;
443
-
444
- const fields: Array<{ label: string; value: string; showCollapsed?: boolean } | Text> = [
445
- { label: "Results", value: `${items.length} items`, showCollapsed: true },
296
+ const fields = [
297
+ { label: "Location", value: `${details.owner}/${details.repo}`, showCollapsed: true },
298
+ { label: "Entries", value: `${details.entries.length}`, showCollapsed: true },
446
299
  ];
447
300
 
448
- // 4. Build conditional footer
449
- const footerItems: Array<{ label: string; value: string }> = [];
450
- if (items.length > 0) {
451
- footerItems.push({ label: "count", value: `${items.length}` });
301
+ const footerItems = [];
302
+ if (!options.expanded && details.entries.length > 5) {
303
+ footerItems.push({ value: keyHint("app.tools.expand", "to expand") });
452
304
  }
453
305
 
454
306
  return new ToolBody(
455
307
  {
456
308
  fields,
457
- footer: footerItems.length > 0
458
- ? new ToolFooter(theme, { items: footerItems, separator: " | " })
459
- : undefined,
309
+ footer: footerItems.length > 0 ? new ToolFooter(theme, { items: footerItems }) : undefined,
460
310
  includeSpacerBeforeFooter: fields.length > 0,
461
311
  },
462
312
  options,
@@ -465,590 +315,115 @@ renderResult(
465
315
  }
466
316
  ```
467
317
 
468
- ### `renderCall` Design Guide
469
-
470
- The `process` extension is a good baseline. Its call renderer is deterministic and keeps headers readable.
471
-
472
- Use this extraction order when building header parts:
473
-
474
- 1. **Action first**: always show action for multi-action tools (`start`, `list`, `kill`, ...).
475
- 2. **Pick one main arg**: choose the single value the user scans for first (name, id, or short command).
476
- 3. **Promote short fields to options**: compact values become option args (`end=true`, `limit=10`).
477
- 4. **Demote long fields to long args**: commands/prompts/instructions move to labeled follow-up lines.
478
- 5. **Keep it stable**: same inputs should produce same ordering and formatting.
479
-
480
- Implementation pattern:
481
- - Build `mainArg`, `optionArgs`, `longArgs` first, then pass to `ToolCallHeader`.
482
- - Quote user-provided names (`"backend"`) when that improves visual parsing.
483
- - Cap inline length (e.g. 60-80 chars), then spill to `longArgs`.
484
-
485
- ### `renderResult` Guidelines
486
-
487
- - Handle `isPartial` first with a **stable, tool-scoped message** like `"MyTool: fetching..."`. Do NOT echo streaming content.
488
- - Handle errors by checking for missing expected fields in `details` (framework sets `details: {}` on throw). Alternatively use the 4th `context` param with `context.isError`.
489
- - Use `ToolBody` with `showCollapsed: true` fields for collapsed/expanded filtering.
490
- - Use `ToolFooter` for stats/metadata. Omit when empty: `footer: items.length > 0 ? new ToolFooter(...) : undefined`.
491
- - Use `includeSpacerBeforeFooter: fields.length > 0` to avoid blank line when body is empty.
492
- - Remove redundant success footer items — don't show `status: ok` when success is obvious.
493
- - Use `Container` + `addChild()` for multi-element results instead of string concatenation.
494
- - Use `Markdown` component for rich markdown content:
495
- ```typescript
496
- new Markdown(text, 0, 0, getMarkdownTheme(), { color: (t) => theme.fg("toolOutput", t) })
497
- ```
498
- - Use `keyHint("app.tools.expand", "to expand")` for expand hints.
499
- - Collapsed result should show **actionable preview** (last N lines, first N items with status), not just a status badge.
500
- - Humanize error messages with names first: `"Could not get X for "name" (id)"`.
501
-
502
- ## Tool UI Rendering Guidelines
503
-
504
- When customizing tool rendering, keep call/result UI predictable and scannable.
505
-
506
- ### `renderCall` format
507
-
508
- Use this line model:
509
-
510
- - First line: `[Tool Name]: [Action] [Main arg] [Option args]`
511
- - Additional lines: long args only
512
-
513
318
  Guidelines:
514
- - Tool name should be a human display label (`label`), not a raw internal identifier (`name`).
515
- - Show `action` only when it adds meaning (multi-action tools like process managers).
516
- - Main arg should be the primary thing user cares about (query, session id, target id/name).
517
- - Option args should be compact key-value pairs (`limit=10`, `cwd=/path`).
518
- - Long text (prompt/task/question/context/instructions) goes to additional lines.
519
- - Prefer wrapping to preserve full meaning over aggressive truncation.
520
- - For tools without actions, omit colon suffix after tool name if that reads better in your UI system.
521
-
522
- ### `renderResult` layout
523
-
524
- - Handle `isPartial` first. Return a short stable loading state with tool-scoped message.
525
- - Keep the first non-loading line as a status summary (`Found N results`, `Updated 3 files`, `Failed: reason`).
526
- - Use `expanded` to switch between compact and full output. Compact should show the top few items plus an omission hint.
527
- - Keep body content focused on state + key output; avoid dumping raw JSON unless it is the actual output.
528
- - If you render a footer (stats, backend, counts), use `includeSpacerBeforeFooter` to control blank line.
529
- - Keep footer concise and stable across states.
530
- - Return `undefined` when custom rendering adds no value; fallback rendering is better than noisy UI.
531
-
532
- ## Tool Call + UI Consistency Contract
533
-
534
- Use this contract to keep tool UX consistent across extensions:
535
-
536
- 1. **Call line is for scanability**: `renderCall` first line follows `[Tool Name]: [Action] [Main arg] [Option args]`.
537
- 2. **Result detects errors** by checking for missing expected fields in `details` (framework sets `details: {}` on throw).
538
- 3. **Long text moves down**: prompts, instructions, and context go to follow-up lines, not the call header.
539
- 4. **Partial updates use a fixed tool-scoped string**, not echoed streaming content.
540
- 5. **Expanded controls density**: compact view shows summary + subset; expanded view shows full detail.
541
- 6. **No mode leaks in tool renderers**: `renderCall`/`renderResult` should not branch on mode. Mode-specific behavior belongs in command/tool logic (`references/modes.md`).
542
-
543
- Related references:
544
- - `references/modes.md` for `custom()` fallback behavior and RPC/Print handling.
545
- - `references/components.md` for interactive component authoring.
546
- - `references/messages.md` for persistent display via `sendMessage` + `registerMessageRenderer`.
547
-
548
- ## Naming Conventions
549
-
550
- For extensions wrapping a third-party API, prefix tool names with the API name to avoid conflicts:
551
-
552
- ```
553
- linkup_web_search
554
- linkup_web_fetch
555
- synthetic_web_search
556
- ```
557
-
558
- For internal/custom tools, no prefix is needed:
559
319
 
560
- ```
561
- get_current_time
562
- processes
563
- ```
564
-
565
- Use snake_case for all tool names.
566
-
567
- ## Abort Signal
568
-
569
- The `signal` parameter lets you cancel long-running operations when the user interrupts (e.g. pressing Escape). If the tool does not forward the signal, the underlying operation keeps running even after the user cancels, wasting resources and API credits.
570
-
571
- ```typescript
572
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
573
- const response = await fetch(url, { signal });
574
- // If the user cancels, fetch throws an AbortError
575
- return { content: [{ type: "text", text: await response.text() }], details: {} };
576
- },
577
- ```
578
-
579
- Pass `signal` to every async operation that supports it: `fetch()` calls, `pi.exec()` calls, SDK clients, etc.
580
-
581
- When wrapping an API client, thread the signal through the entire call chain. The client methods should accept an optional `signal` and forward it to the underlying `fetch()`:
582
-
583
- ```typescript
584
- // In the tool:
585
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
586
- const result = await client.search({ query: params.query, signal });
587
- // ...
588
- }
589
-
590
- // In the client:
591
- async search(params: { query: string; signal?: AbortSignal }) {
592
- return this.request("/search", { method: "POST", body: ... }, params.signal);
593
- }
594
-
595
- private async request<T>(endpoint: string, options: RequestInit = {}, signal?: AbortSignal) {
596
- return fetch(`${BASE_URL}${endpoint}`, { ...options, signal, headers: { ... } });
597
- }
598
- ```
599
-
600
- Do not prefix signal with underscore (`_signal`) unless the tool genuinely cannot use it. A dangling `_signal` is a sign of a missing cancellation path.
320
+ - Partial output uses a fixed tool-scoped message; do not echo streaming content.
321
+ - Collapsed view should show useful preview, not just `ok`.
322
+ - Expanded view can add full details.
323
+ - Omit empty footers.
324
+ - Use `Markdown` with `getMarkdownTheme()` for rich markdown.
325
+ - Use `context.lastComponent` only when reusing a component instance buys real performance.
326
+ - Set `renderShell: "self"` only when the default boxed shell prevents the desired layout.
601
327
 
602
328
  ## Output Truncation
603
329
 
604
- For tools that may return large outputs, use `truncateHead()` which returns a `TruncationResult`:
330
+ Tools must avoid overwhelming model context. Use `truncateHead` for content where the beginning matters and `truncateTail` for logs or command output where the end matters.
605
331
 
606
332
  ```typescript
607
- import { truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
608
- import { createWriteStream } from "fs";
609
- import { tmpdir } from "os";
610
- import { join } from "path";
611
-
612
333
  interface FetchDetails {
613
- content: string;
614
334
  url: string;
335
+ content: string;
615
336
  truncated: boolean;
616
337
  totalLines: number;
617
- totalBytes: number;
618
338
  outputLines: number;
339
+ totalBytes: number;
619
340
  outputBytes: number;
620
341
  tempFile?: string;
621
342
  }
622
343
 
623
- async execute(
624
- _toolCallId: string,
625
- params: FetchParams,
626
- signal: AbortSignal | undefined,
627
- _onUpdate: undefined,
628
- _ctx: ExtensionContext,
629
- ): Promise<AgentToolResult<FetchDetails>> {
344
+ async execute(_toolCallId, params, signal): Promise<AgentToolResult<FetchDetails>> {
630
345
  const response = await fetch(params.url, { signal });
631
346
  const fullContent = await response.text();
632
-
633
- // truncateHead keeps the tail (most recent content)
634
- const truncated = truncateHead(fullContent, {
635
- maxBytes: 50000,
636
- maxLines: 2000,
637
- });
347
+ const truncated = truncateHead(fullContent, { maxBytes: 50_000, maxLines: 2_000 });
638
348
 
639
349
  const details: FetchDetails = {
640
- content: truncated.content,
641
350
  url: params.url,
351
+ content: truncated.content,
642
352
  truncated: truncated.truncated,
643
353
  totalLines: truncated.totalLines,
644
- totalBytes: truncated.totalBytes,
645
354
  outputLines: truncated.outputLines,
355
+ totalBytes: truncated.totalBytes,
646
356
  outputBytes: truncated.outputBytes,
647
357
  };
648
358
 
649
- // Write full content to temp file if truncated
359
+ let text = truncated.content;
650
360
  if (truncated.truncated) {
651
- const tempFile = join(tmpdir(), `web-fetch-${Date.now()}.txt`);
652
- const stream = createWriteStream(tempFile);
653
- stream.write(fullContent);
654
- stream.end();
655
- details.tempFile = tempFile;
361
+ details.tempFile = await writeTempFile(fullContent);
362
+ text += `\n\n[Output truncated: ${truncated.outputLines}/${truncated.totalLines} lines, `;
363
+ text += `${formatSize(truncated.outputBytes)}/${formatSize(truncated.totalBytes)}. `;
364
+ text += `Full output: ${details.tempFile}]`;
656
365
  }
657
366
 
658
- return {
659
- content: [{ type: "text", text: truncated.content }],
660
- details,
661
- };
662
- }
663
- ```
664
-
665
- ### Truncation Result Fields
666
-
667
- ```typescript
668
- interface TruncationResult {
669
- content: string; // The truncated content (or full if not truncated)
670
- truncated: boolean; // Whether truncation occurred
671
- totalLines: number; // Original total lines
672
- totalBytes: number; // Original total bytes
673
- outputLines: number; // Lines in output
674
- outputBytes: number; // Bytes in output
367
+ return { content: [{ type: "text", text }], details };
675
368
  }
676
369
  ```
677
370
 
678
- ### Rendering Truncated Output
679
-
680
- ```typescript
681
- renderResult(
682
- result: AgentToolResult<FetchDetails>,
683
- options: ToolRenderResultOptions,
684
- theme: Theme,
685
- ) {
686
- if (options.isPartial) {
687
- return new Text(theme.fg("muted", "WebFetch: fetching..."), 0, 0);
688
- }
689
-
690
- const { details } = result;
691
-
692
- if (!details?.content) {
693
- const textBlock = result.content.find((c) => c.type === "text");
694
- const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Fetch failed";
695
- return new Text(theme.fg("error", errorMsg), 0, 0);
696
- }
697
- const container = new Container();
698
-
699
- // Body with content preview
700
- const fields = [
701
- { label: "URL", value: details.url, showCollapsed: true },
702
- { label: "Size", value: formatSize(details.totalBytes), showCollapsed: true },
703
- ];
704
-
705
- container.addChild(new ToolBody(theme, { fields, expanded: options.expanded }));
706
-
707
- // Footer with truncation info
708
- const footerItems = [];
709
-
710
- if (details.truncated) {
711
- footerItems.push(
712
- { label: "showing", value: `${details.outputLines}/${details.totalLines} lines` },
713
- { label: "full output", value: details.tempFile ?? "temp file" },
714
- );
715
- } else {
716
- footerItems.push({ label: "lines", value: `${details.totalLines}` });
717
- }
718
-
719
- container.addChild(
720
- new ToolFooter(theme, {
721
- items: footerItems,
722
- separator: " | ",
723
- includeSpacerBeforeFooter: fields.length > 0,
724
- }),
725
- );
726
-
727
- return container;
728
- }
729
- ```
371
+ Always tell the LLM when output was truncated and where full output was saved.
730
372
 
731
373
  ## Multi-Action Tools
732
374
 
733
- For tools with multiple actions (e.g., `start`, `list`, `kill`), organize code for maintainability:
734
-
735
- ### File Structure
375
+ Use a directory when one tool has several actions.
736
376
 
737
377
  ```
738
- tools/
739
- my_tool/
740
- index.ts // Tool definition, registration, execute switch
741
- actions/
742
- start.ts // start action implementation
743
- list.ts // list action implementation
744
- kill.ts // kill action implementation
745
- render.ts // renderCall and renderResult (when complex)
746
- types.ts // Shared types and param schema
378
+ tools/my_tool/
379
+ index.ts
380
+ actions/start.ts
381
+ actions/list.ts
382
+ actions/kill.ts
383
+ render.ts
384
+ types.ts
747
385
  ```
748
386
 
749
- ### Action Pattern
750
-
751
- Each action takes an SDK client and typed params, returns typed results:
387
+ Pattern:
752
388
 
753
- ```typescript
754
- // actions/start.ts
755
- export interface StartParams {
756
- name: string;
757
- command: string;
758
- cwd?: string;
759
- }
389
+ - `types.ts`: parameter schema, `Static` alias, details/result types.
390
+ - `actions/*`: Pi-free or mostly Pi-free action logic.
391
+ - `render.ts`: `renderCall`/`renderResult` when complex.
392
+ - `index.ts`: `defineTool`, execute switch, registration.
760
393
 
761
- export interface StartResult {
762
- sessionId: string;
763
- pid: number;
764
- }
765
-
766
- export async function start(
767
- client: MyClient,
768
- params: StartParams,
769
- signal?: AbortSignal,
770
- ): Promise<StartResult> {
771
- return client.startSession({
772
- name: params.name,
773
- command: params.command,
774
- cwd: params.cwd,
775
- signal,
776
- });
777
- }
778
- ```
779
-
780
- ### Execute Delegation
394
+ Inside the `defineTool` execute callback, inference is enough. If action functions need typed inputs, use the alias from `types.ts`.
781
395
 
782
396
  ```typescript
783
- // index.ts
784
- async execute(
785
- toolCallId: string,
786
- params: MyToolParams,
787
- signal: AbortSignal | undefined,
788
- onUpdate: AgentToolUpdateCallback<MyToolDetails> | undefined,
789
- ctx: ExtensionContext,
790
- ): Promise<AgentToolResult<MyToolDetails>> {
397
+ async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<MyToolDetails>> {
791
398
  switch (params.action) {
792
399
  case "start":
793
400
  return startAction(client, params, signal, onUpdate, ctx);
794
401
  case "list":
795
402
  return listAction(client, params, signal);
796
- case "kill":
797
- return killAction(client, params, signal);
798
403
  default:
799
404
  throw new Error(`Unknown action: ${params.action}`);
800
405
  }
801
406
  }
802
407
  ```
803
408
 
804
- ### Separate Render Module
409
+ ## Naming
805
410
 
806
- When rendering is complex, extract to `render.ts`:
807
-
808
- ```typescript
809
- // render.ts
810
- export function renderCall(args: MyToolParams, theme: Theme) {
811
- return new ToolCallHeader(
812
- {
813
- toolName: "MyTool",
814
- action: args.action,
815
- mainArg: getMainArg(args),
816
- optionArgs: getOptionArgs(args),
817
- longArgs: getLongArgs(args),
818
- },
819
- theme,
820
- );
821
- }
822
-
823
- export function renderResult(
824
- result: AgentToolResult<MyToolDetails>,
825
- options: ToolRenderResultOptions,
826
- theme: Theme,
827
- ) {
828
- // Complex rendering logic here
829
- }
830
- ```
831
-
832
- ```typescript
833
- // index.ts
834
- import { renderCall, renderResult } from "./render.js";
835
-
836
- const myTool = {
837
- // ...
838
- renderCall,
839
- renderResult,
840
- };
841
- ```
411
+ - Use snake_case tool names.
412
+ - Prefix third-party integrations with the API/product name (`linkup_web_search`, `linear_issue`).
413
+ - Internal tools can use direct names (`get_current_time`, `process`).
414
+ - Display names in UI should use `label`, not raw tool IDs.
842
415
 
843
- **Examples to reference:**
844
- - `pi-processes`: Multi-action process management with complex rendering
845
- - `pi-linear`: Multi-action Linear API integration
416
+ ## Checklist
846
417
 
847
- ## Full Example
848
-
849
- Here's a realistic tool demonstrating all the patterns:
850
-
851
- ```typescript
852
- import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";
853
- import type {
854
- AgentToolResult,
855
- ExtensionAPI,
856
- ExtensionContext,
857
- Theme,
858
- ToolRenderResultOptions,
859
- } from "@mariozechner/pi-coding-agent";
860
- import { keyHint, formatSize } from "@mariozechner/pi-coding-agent";
861
- import { Container, Text } from "@mariozechner/pi-tui";
862
- import { type Static, Type } from "typebox";
863
-
864
- // Schema
865
- const parameters = Type.Object({
866
- owner: Type.String({ description: "Repository owner" }),
867
- repo: Type.String({ description: "Repository name" }),
868
- path: Type.Optional(Type.String({ description: "File or directory path", default: "" })),
869
- });
870
-
871
- // Typed aliases
872
- type RepoTreeParams = Static<typeof parameters>;
873
-
874
- interface RepoTreeDetails {
875
- owner: string;
876
- repo: string;
877
- path: string;
878
- entries: Array<{ name: string; type: "file" | "dir"; size?: number }>;
879
- truncated: boolean;
880
- }
881
-
882
- // Tool definition
883
- const repoTreeTool = {
884
- name: "repo_tree",
885
- label: "Repo Tree",
886
- description: "List files and directories in a GitHub repository.",
887
- promptSnippet: "Browse repository file structure",
888
- promptGuidelines: [
889
- "Use repo_tree to explore repository structure before reading files.",
890
- "Start repo_tree at the root path, then drill down into directories.",
891
- ],
892
- parameters,
893
-
894
- async execute(
895
- _toolCallId: string,
896
- params: RepoTreeParams,
897
- signal: AbortSignal | undefined,
898
- _onUpdate: undefined,
899
- _ctx: ExtensionContext,
900
- ): Promise<AgentToolResult<RepoTreeDetails>> {
901
- const response = await fetch(
902
- `https://api.github.com/repos/${params.owner}/${params.repo}/contents/${params.path ?? ""}`,
903
- { signal, headers: { Accept: "application/vnd.github.v3+json" } },
904
- );
905
-
906
- if (!response.ok) {
907
- if (response.status === 404) {
908
- throw new Error(
909
- `Could not find repository "${params.owner}/${params.repo}" or path "${params.path ?? ""}"`,
910
- );
911
- }
912
- throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
913
- }
914
-
915
- const data = await response.json();
916
-
917
- // GitHub returns a single object for files, array for directories
918
- const entries = Array.isArray(data) ? data : [data];
919
-
920
- const processed = entries.map((entry: any) => ({
921
- name: entry.name,
922
- type: entry.type === "dir" ? "dir" as const : "file" as const,
923
- size: entry.size,
924
- }));
925
-
926
- // Sort: directories first, then files, alphabetically within each
927
- processed.sort((a, b) => {
928
- if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
929
- return a.name.localeCompare(b.name);
930
- });
931
-
932
- const truncated = processed.length > 100;
933
-
934
- return {
935
- content: [
936
- {
937
- type: "text",
938
- text: JSON.stringify(truncated ? processed.slice(0, 100) : processed, null, 2),
939
- },
940
- ],
941
- details: {
942
- owner: params.owner,
943
- repo: params.repo,
944
- path: params.path ?? "",
945
- entries: processed,
946
- truncated,
947
- },
948
- };
949
- },
950
-
951
- renderCall(args: RepoTreeParams, theme: Theme) {
952
- return new ToolCallHeader(
953
- {
954
- toolName: "Repo Tree",
955
- mainArg: `${args.owner}/${args.repo}`,
956
- optionArgs: args.path ? [`path=${args.path}`] : [],
957
- longArgs: [],
958
- },
959
- theme,
960
- );
961
- },
962
-
963
- renderResult(
964
- result: AgentToolResult<RepoTreeDetails>,
965
- options: ToolRenderResultOptions,
966
- theme: Theme,
967
- ) {
968
- // 1. Stable partial message
969
- if (options.isPartial) {
970
- return new Text(theme.fg("muted", "RepoTree: fetching..."), 0, 0);
971
- }
972
-
973
- const { details } = result;
974
-
975
- // 2. Error handling: details is {} when tool threw
976
- if (!details?.entries) {
977
- const textBlock = result.content.find((c) => c.type === "text");
978
- const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Failed to list repository";
979
- return new Text(theme.fg("error", errorMsg), 0, 0);
980
- }
981
- const container = new Container();
982
- const entries = details?.entries ?? [];
983
- const dirs = entries.filter((e) => e.type === "dir");
984
- const files = entries.filter((e) => e.type === "file");
985
-
986
- // 3. Body fields with showCollapsed for collapsed/expanded control
987
- const fields: Array<{ label: string; value: string; showCollapsed: boolean }> = [
988
- {
989
- label: "Location",
990
- value: `${details.owner}/${details.repo}${details.path ? `/${details.path}` : ""}`,
991
- showCollapsed: true,
992
- },
993
- {
994
- label: "Contents",
995
- value: `${dirs.length} dirs, ${files.length} files`,
996
- showCollapsed: true,
997
- },
998
- ];
999
-
1000
- container.addChild(new ToolBody(theme, { fields, expanded: options.expanded }));
1001
-
1002
- // 4. Entry list (in expanded view)
1003
- if (options.expanded && entries.length > 0) {
1004
- const entryLines = entries
1005
- .slice(0, 50)
1006
- .map((e) => {
1007
- const icon = e.type === "dir" ? "📁" : "📄";
1008
- const size = e.size !== undefined ? ` (${formatSize(e.size)})` : "";
1009
- return `${icon} ${e.name}${size}`;
1010
- })
1011
- .join("\n");
1012
-
1013
- container.addChild(new Text(entryLines, 0, 0));
1014
-
1015
- if (entries.length > 50) {
1016
- container.addChild(
1017
- new Text(theme.fg("muted", `... and ${entries.length - 50} more`), 0, 0),
1018
- );
1019
- }
1020
- }
1021
-
1022
- // 5. Conditional footer
1023
- const footerItems = [];
1024
-
1025
- if (details?.truncated) {
1026
- footerItems.push({ label: "showing", value: "first 100 entries" });
1027
- }
1028
-
1029
- if (entries.length > 0) {
1030
- footerItems.push({ label: "total", value: `${entries.length} items` });
1031
- }
1032
-
1033
- if (!options.expanded && entries.length > 5) {
1034
- footerItems.push({ label: "", value: keyHint("app.tools.expand", "to expand") });
1035
- }
1036
-
1037
- if (footerItems.length > 0) {
1038
- container.addChild(
1039
- new ToolFooter(theme, {
1040
- items: footerItems,
1041
- separator: " | ",
1042
- includeSpacerBeforeFooter: fields.length > 0,
1043
- }),
1044
- );
1045
- }
1046
-
1047
- return container;
1048
- },
1049
- };
1050
-
1051
- export default function (pi: ExtensionAPI) {
1052
- pi.registerTool(repoTreeTool);
1053
- }
1054
- ```
418
+ - [ ] Tool uses `defineTool({...})` with no explicit generic args.
419
+ - [ ] Params infer from `parameters`; helper APIs use `Static<typeof parameters>` alias.
420
+ - [ ] `label`, `description`, and useful parameter descriptions are present.
421
+ - [ ] `promptGuidelines` bullets name the exact tool.
422
+ - [ ] Execute signature is `(toolCallId, params, signal, onUpdate, ctx)`.
423
+ - [ ] `onUpdate` uses optional chaining.
424
+ - [ ] `signal` is forwarded to async operations.
425
+ - [ ] Errors are thrown, not returned as fake success payloads.
426
+ - [ ] Path tools strip leading `@`; mutating tools use `withFileMutationQueue()`.
427
+ - [ ] Shared-state tools use safe concurrency or `executionMode: "sequential"`.
428
+ - [ ] Large outputs are truncated with a full-output temp file.
429
+ - [ ] Renderers return components, handle partial first, handle errors, and omit empty footers.