@aliou/pi-dev-kit 0.4.9 → 0.6.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.
- package/README.md +60 -0
- package/package.json +63 -2
- package/src/commands/index.ts +6 -0
- package/src/commands/update.ts +144 -0
- package/src/index.ts +8 -0
- package/src/prompts/setup-demo.md +35 -0
- package/src/skills/demo-setup/SKILL.md +217 -0
- package/src/skills/pi-extension/SKILL.md +154 -0
- package/src/skills/pi-extension/references/additional-apis.md +304 -0
- package/src/skills/pi-extension/references/commands.md +100 -0
- package/src/skills/pi-extension/references/components.md +166 -0
- package/src/skills/pi-extension/references/documentation.md +54 -0
- package/src/skills/pi-extension/references/hooks.md +244 -0
- package/src/skills/pi-extension/references/messages.md +169 -0
- package/src/skills/pi-extension/references/modes.md +156 -0
- package/src/skills/pi-extension/references/providers.md +134 -0
- package/src/skills/pi-extension/references/publish.md +139 -0
- package/src/skills/pi-extension/references/state.md +56 -0
- package/src/skills/pi-extension/references/structure.md +522 -0
- package/src/skills/pi-extension/references/testing.md +183 -0
- package/src/skills/pi-extension/references/tools.md +948 -0
- package/src/tools/changelog-tool.ts +484 -0
- package/src/tools/docs-tool.ts +181 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/package-manager-tool.ts +194 -0
- package/src/tools/utils.ts +38 -0
- package/src/tools/version-tool.ts +70 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
# Tools
|
|
2
|
+
|
|
3
|
+
Tools are functions the LLM can call. They are the primary way extensions add capabilities to pi.
|
|
4
|
+
|
|
5
|
+
## Imports
|
|
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 { getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { Container, Markdown, Text } from "@mariozechner/pi-tui";
|
|
21
|
+
import { type Static, Type } from "@mariozechner/pi-coding-agent";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Registration
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const myTool = {
|
|
28
|
+
name: "my_tool",
|
|
29
|
+
label: "My Tool", // Required: human-readable name for UI
|
|
30
|
+
description: "What this tool does. The LLM reads this to decide when to call it.",
|
|
31
|
+
promptSnippet: "Search for items by query", // One-liner for "Available tools" system prompt
|
|
32
|
+
promptGuidelines: [ // Guideline bullets for "Guidelines" system prompt when active
|
|
33
|
+
"Use this tool when the user asks about search",
|
|
34
|
+
"Prefer specific queries over broad ones",
|
|
35
|
+
],
|
|
36
|
+
parameters: Type.Object({
|
|
37
|
+
query: Type.String({ description: "Search query" }),
|
|
38
|
+
limit: Type.Optional(Type.Number({ description: "Max results", default: 10 })),
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
async execute(
|
|
42
|
+
toolCallId: string,
|
|
43
|
+
params: MyToolParams,
|
|
44
|
+
signal: AbortSignal | undefined,
|
|
45
|
+
onUpdate: AgentToolUpdateCallback<MyToolDetails> | undefined,
|
|
46
|
+
ctx: ExtensionContext,
|
|
47
|
+
): Promise<AgentToolResult<MyToolDetails>> {
|
|
48
|
+
const results = await doSomething(params.query, params.limit);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
51
|
+
details: { results },
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Typed param alias - define once, use everywhere
|
|
57
|
+
type MyToolParams = Static<typeof myTool.parameters>;
|
|
58
|
+
interface MyToolDetails {
|
|
59
|
+
results: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default function (pi: ExtensionAPI) {
|
|
63
|
+
pi.registerTool(myTool);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Tool Definition Fields
|
|
68
|
+
|
|
69
|
+
| Field | Type | Required | Description |
|
|
70
|
+
|-------|------|----------|-------------|
|
|
71
|
+
| `name` | `string` | Yes | Snake_case identifier used in tool calls |
|
|
72
|
+
| `label` | `string` | Yes | Human-readable name shown in UI |
|
|
73
|
+
| `description` | `string` | Yes | What the tool does (LLM reads this) |
|
|
74
|
+
| `parameters` | `TSchema` | Yes | TypeBox schema for arguments |
|
|
75
|
+
| `promptSnippet` | `string` | No | One-liner injected into "Available tools" system prompt. Custom tools without this are omitted from that section. |
|
|
76
|
+
| `promptGuidelines` | `string[]` | No | Guideline bullets appended to "Guidelines" system prompt when this tool is active |
|
|
77
|
+
| `execute` | `function` | Yes | Implementation |
|
|
78
|
+
| `renderCall` | `function` | No | Custom call rendering |
|
|
79
|
+
| `renderResult` | `function` | No | Custom result rendering |
|
|
80
|
+
|
|
81
|
+
## Typed Param Alias
|
|
82
|
+
|
|
83
|
+
Define a type alias at the top of your file instead of repeating `Static<typeof parameters>`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const parameters = Type.Object({
|
|
87
|
+
query: Type.String({ description: "Search query" }),
|
|
88
|
+
limit: Type.Optional(Type.Number({ description: "Max results", default: 10 })),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
type MyToolParams = Static<typeof parameters>;
|
|
92
|
+
// Use MyToolParams everywhere: execute params, renderCall args, context.args, etc.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Execute Signature
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
execute(
|
|
99
|
+
toolCallId: string,
|
|
100
|
+
params: Static<TParams>, // Typed from the parameters schema
|
|
101
|
+
signal: AbortSignal | undefined,
|
|
102
|
+
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
): Promise<AgentToolResult<TDetails>>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Parameter order matters.** The signal comes before onUpdate.
|
|
108
|
+
|
|
109
|
+
Always use optional chaining when calling `onUpdate`:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
onUpdate?.({ output: "partial result", details: { progress: 50 } });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `onUpdate` parameter can be `undefined`. Calling it without optional chaining will throw.
|
|
116
|
+
|
|
117
|
+
## Tool Overrides and Delegation
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Return Value
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
return {
|
|
125
|
+
content: (TextContent | ImageContent)[], // Content blocks sent to the LLM
|
|
126
|
+
details?: TDetails, // Arbitrary data available in the renderer
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `content` is what the LLM sees. Each block is `{ type: "text", text: "..." }` or an image. Keep it structured and concise.
|
|
131
|
+
- `details` is what the renderer sees. Put rich data here for custom display.
|
|
132
|
+
|
|
133
|
+
Common pattern:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
138
|
+
details: { results },
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Error Handling
|
|
143
|
+
|
|
144
|
+
To report a tool call failure, **throw an error**. The framework catches it and produces a result with `isError: true` on the context.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
148
|
+
const result = await fetchData(params.query);
|
|
149
|
+
if (!result) {
|
|
150
|
+
throw new Error("No results found. Try a different query.");
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
154
|
+
details: { result },
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The framework's `createErrorToolResult` sets `details: {}` (empty object, not `undefined`) and puts the error message in `content`:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Framework produces when tool throws:
|
|
163
|
+
{
|
|
164
|
+
content: [{ type: "text", text: errorMessage }],
|
|
165
|
+
details: {}
|
|
166
|
+
}
|
|
167
|
+
// And sets context.isError = true in renderResult
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Error rendering in `renderResult`
|
|
171
|
+
|
|
172
|
+
The framework passes `isError` on the 4th `context` parameter to `renderResult`, but `ToolRenderContext` is not currently exported from the public API. Two practical approaches:
|
|
173
|
+
|
|
174
|
+
**Approach 1: Check for missing expected fields in `details`** (recommended for extensions)
|
|
175
|
+
|
|
176
|
+
When a tool throws, the framework sets `details: {}` (empty object). Check for missing expected fields:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
renderResult(
|
|
180
|
+
result: AgentToolResult<MyToolDetails>,
|
|
181
|
+
options: ToolRenderResultOptions,
|
|
182
|
+
theme: Theme,
|
|
183
|
+
) {
|
|
184
|
+
const { details } = result;
|
|
185
|
+
|
|
186
|
+
// details is {} when tool threw — expected fields are missing
|
|
187
|
+
if (!details?.results) {
|
|
188
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
189
|
+
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Operation failed";
|
|
190
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
191
|
+
}
|
|
192
|
+
// ... normal rendering
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Approach 2: Use the 4th context parameter** (used by native tools)
|
|
197
|
+
|
|
198
|
+
Define a minimal interface locally:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface RenderContext { isError: boolean }
|
|
202
|
+
|
|
203
|
+
renderResult(
|
|
204
|
+
result: AgentToolResult<MyToolDetails>,
|
|
205
|
+
options: ToolRenderResultOptions,
|
|
206
|
+
theme: Theme,
|
|
207
|
+
context: RenderContext,
|
|
208
|
+
) {
|
|
209
|
+
if (context.isError) {
|
|
210
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
211
|
+
const errorMsg = textBlock?.type === "text" ? textBlock.text : "Operation failed";
|
|
212
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
213
|
+
}
|
|
214
|
+
// ... normal rendering
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Both approaches work. Approach 1 is more common in published extensions. Approach 2 is what native tools use.
|
|
219
|
+
|
|
220
|
+
## Parameters Schema
|
|
221
|
+
|
|
222
|
+
Use TypeBox (`Type.*`) for parameter schemas. The LLM sees the schema to know what arguments to provide.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { Type } from "@mariozechner/pi-coding-agent";
|
|
226
|
+
|
|
227
|
+
// Required string
|
|
228
|
+
Type.String({ description: "File path to read" })
|
|
229
|
+
|
|
230
|
+
// Optional with default
|
|
231
|
+
Type.Optional(Type.Number({ description: "Max results", default: 10 }))
|
|
232
|
+
|
|
233
|
+
// Enum (string union)
|
|
234
|
+
Type.StringEnum(["created", "updated", "relevance"], { description: "Sort order" })
|
|
235
|
+
|
|
236
|
+
// Boolean
|
|
237
|
+
Type.Boolean({ description: "Include hidden files" })
|
|
238
|
+
|
|
239
|
+
// Nested object
|
|
240
|
+
Type.Object({
|
|
241
|
+
name: Type.String(),
|
|
242
|
+
value: Type.String(),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Array
|
|
246
|
+
Type.Array(Type.String(), { description: "List of tags" })
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Always provide `description` on parameters. The LLM uses these to understand what to pass.
|
|
250
|
+
|
|
251
|
+
## Streaming Updates
|
|
252
|
+
|
|
253
|
+
Use `onUpdate` to stream partial results while the tool executes. This gives the user feedback during long operations.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
257
|
+
for (const chunk of chunks) {
|
|
258
|
+
const partial = processChunk(chunk);
|
|
259
|
+
onUpdate?.({
|
|
260
|
+
content: [{ type: "text", text: partial }],
|
|
261
|
+
details: { progress: chunk.index / chunks.length },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: finalResult }],
|
|
266
|
+
details: { complete: true },
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Custom Rendering
|
|
272
|
+
|
|
273
|
+
Override how a tool's invocation and result appear in the TUI.
|
|
274
|
+
|
|
275
|
+
### `renderCall` Signature
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
renderCall(args: MyToolParams, theme: Theme): Component
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
A 3rd `context` param is available from the framework but rarely needed in `renderCall`.
|
|
282
|
+
|
|
283
|
+
Use `ToolCallHeader` from `@aliou/pi-utils-ui`:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
renderCall(args: MyToolParams, theme: Theme) {
|
|
287
|
+
return new ToolCallHeader(
|
|
288
|
+
{
|
|
289
|
+
toolName: "My Tool",
|
|
290
|
+
action: "search", // Optional: for multi-action tools
|
|
291
|
+
mainArg: `"${args.query}"`, // Primary thing user scans for
|
|
292
|
+
optionArgs: [`limit=${args.limit ?? 10}`], // Compact key-value pairs
|
|
293
|
+
longArgs: [], // Long text goes here
|
|
294
|
+
showColon: true, // Whether to show colon after tool name
|
|
295
|
+
},
|
|
296
|
+
theme,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `renderResult` Signature
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
renderResult(
|
|
305
|
+
result: AgentToolResult<TDetails>,
|
|
306
|
+
options: ToolRenderResultOptions,
|
|
307
|
+
theme: Theme,
|
|
308
|
+
): Component
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
- `options` has `expanded` and `isPartial`
|
|
312
|
+
- `result.details` is `{}` (empty object) when the tool threw an error
|
|
313
|
+
- A 4th `context` param is available (see Error Handling above) but not required
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
renderResult(
|
|
317
|
+
result: AgentToolResult<MyToolDetails>,
|
|
318
|
+
options: ToolRenderResultOptions,
|
|
319
|
+
theme: Theme,
|
|
320
|
+
) {
|
|
321
|
+
// 1. Handle partial state first with stable message
|
|
322
|
+
if (options.isPartial) {
|
|
323
|
+
return new Text(theme.fg("muted", "MyTool: fetching..."), 0, 0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { details } = result;
|
|
327
|
+
|
|
328
|
+
// 2. Handle errors: details is {} when tool threw
|
|
329
|
+
if (!details?.results) {
|
|
330
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
331
|
+
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Operation failed";
|
|
332
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 3. Build fields for ToolBody (showCollapsed controls collapsed/expanded visibility)
|
|
336
|
+
const items = details.results;
|
|
337
|
+
|
|
338
|
+
const fields: Array<{ label: string; value: string; showCollapsed?: boolean } | Text> = [
|
|
339
|
+
{ label: "Results", value: `${items.length} items`, showCollapsed: true },
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
// 4. Build conditional footer
|
|
343
|
+
const footerItems: Array<{ label: string; value: string }> = [];
|
|
344
|
+
if (items.length > 0) {
|
|
345
|
+
footerItems.push({ label: "count", value: `${items.length}` });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return new ToolBody(
|
|
349
|
+
{
|
|
350
|
+
fields,
|
|
351
|
+
footer: footerItems.length > 0
|
|
352
|
+
? new ToolFooter(theme, { items: footerItems, separator: " | " })
|
|
353
|
+
: undefined,
|
|
354
|
+
includeSpacerBeforeFooter: fields.length > 0,
|
|
355
|
+
},
|
|
356
|
+
options,
|
|
357
|
+
theme,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `renderCall` Design Guide
|
|
363
|
+
|
|
364
|
+
The `process` extension is a good baseline. Its call renderer is deterministic and keeps headers readable.
|
|
365
|
+
|
|
366
|
+
Use this extraction order when building header parts:
|
|
367
|
+
|
|
368
|
+
1. **Action first**: always show action for multi-action tools (`start`, `list`, `kill`, ...).
|
|
369
|
+
2. **Pick one main arg**: choose the single value the user scans for first (name, id, or short command).
|
|
370
|
+
3. **Promote short fields to options**: compact values become option args (`end=true`, `limit=10`).
|
|
371
|
+
4. **Demote long fields to long args**: commands/prompts/instructions move to labeled follow-up lines.
|
|
372
|
+
5. **Keep it stable**: same inputs should produce same ordering and formatting.
|
|
373
|
+
|
|
374
|
+
Implementation pattern:
|
|
375
|
+
- Build `mainArg`, `optionArgs`, `longArgs` first, then pass to `ToolCallHeader`.
|
|
376
|
+
- Quote user-provided names (`"backend"`) when that improves visual parsing.
|
|
377
|
+
- Cap inline length (e.g. 60-80 chars), then spill to `longArgs`.
|
|
378
|
+
|
|
379
|
+
### `renderResult` Guidelines
|
|
380
|
+
|
|
381
|
+
- Handle `isPartial` first with a **stable, tool-scoped message** like `"MyTool: fetching..."`. Do NOT echo streaming content.
|
|
382
|
+
- Handle errors by checking for missing expected fields in `details` (framework sets `details: {}` on throw). Alternatively use the 4th `context` param with `context.isError`.
|
|
383
|
+
- Use `ToolBody` with `showCollapsed: true` fields for collapsed/expanded filtering.
|
|
384
|
+
- Use `ToolFooter` for stats/metadata. Omit when empty: `footer: items.length > 0 ? new ToolFooter(...) : undefined`.
|
|
385
|
+
- Use `includeSpacerBeforeFooter: fields.length > 0` to avoid blank line when body is empty.
|
|
386
|
+
- Remove redundant success footer items — don't show `status: ok` when success is obvious.
|
|
387
|
+
- Use `Container` + `addChild()` for multi-element results instead of string concatenation.
|
|
388
|
+
- Use `Markdown` component for rich markdown content:
|
|
389
|
+
```typescript
|
|
390
|
+
new Markdown(text, 0, 0, getMarkdownTheme(), { color: (t) => theme.fg("toolOutput", t) })
|
|
391
|
+
```
|
|
392
|
+
- Use `keyHint("app.tools.expand", "to expand")` for expand hints.
|
|
393
|
+
- Collapsed result should show **actionable preview** (last N lines, first N items with status), not just a status badge.
|
|
394
|
+
- Humanize error messages with names first: `"Could not get X for "name" (id)"`.
|
|
395
|
+
|
|
396
|
+
## Tool UI Rendering Guidelines
|
|
397
|
+
|
|
398
|
+
When customizing tool rendering, keep call/result UI predictable and scannable.
|
|
399
|
+
|
|
400
|
+
### `renderCall` format
|
|
401
|
+
|
|
402
|
+
Use this line model:
|
|
403
|
+
|
|
404
|
+
- First line: `[Tool Name]: [Action] [Main arg] [Option args]`
|
|
405
|
+
- Additional lines: long args only
|
|
406
|
+
|
|
407
|
+
Guidelines:
|
|
408
|
+
- Tool name should be a human display label (`label`), not a raw internal identifier (`name`).
|
|
409
|
+
- Show `action` only when it adds meaning (multi-action tools like process managers).
|
|
410
|
+
- Main arg should be the primary thing user cares about (query, session id, target id/name).
|
|
411
|
+
- Option args should be compact key-value pairs (`limit=10`, `cwd=/path`).
|
|
412
|
+
- Long text (prompt/task/question/context/instructions) goes to additional lines.
|
|
413
|
+
- Prefer wrapping to preserve full meaning over aggressive truncation.
|
|
414
|
+
- For tools without actions, omit colon suffix after tool name if that reads better in your UI system.
|
|
415
|
+
|
|
416
|
+
### `renderResult` layout
|
|
417
|
+
|
|
418
|
+
- Handle `isPartial` first. Return a short stable loading state with tool-scoped message.
|
|
419
|
+
- Keep the first non-loading line as a status summary (`Found N results`, `Updated 3 files`, `Failed: reason`).
|
|
420
|
+
- Use `expanded` to switch between compact and full output. Compact should show the top few items plus an omission hint.
|
|
421
|
+
- Keep body content focused on state + key output; avoid dumping raw JSON unless it is the actual output.
|
|
422
|
+
- If you render a footer (stats, backend, counts), use `includeSpacerBeforeFooter` to control blank line.
|
|
423
|
+
- Keep footer concise and stable across states.
|
|
424
|
+
- Return `undefined` when custom rendering adds no value; fallback rendering is better than noisy UI.
|
|
425
|
+
|
|
426
|
+
## Tool Call + UI Consistency Contract
|
|
427
|
+
|
|
428
|
+
Use this contract to keep tool UX consistent across extensions:
|
|
429
|
+
|
|
430
|
+
1. **Call line is for scanability**: `renderCall` first line follows `[Tool Name]: [Action] [Main arg] [Option args]`.
|
|
431
|
+
2. **Result detects errors** by checking for missing expected fields in `details` (framework sets `details: {}` on throw).
|
|
432
|
+
3. **Long text moves down**: prompts, instructions, and context go to follow-up lines, not the call header.
|
|
433
|
+
4. **Partial updates use a fixed tool-scoped string**, not echoed streaming content.
|
|
434
|
+
5. **Expanded controls density**: compact view shows summary + subset; expanded view shows full detail.
|
|
435
|
+
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`).
|
|
436
|
+
|
|
437
|
+
Related references:
|
|
438
|
+
- `references/modes.md` for `custom()` fallback behavior and RPC/Print handling.
|
|
439
|
+
- `references/components.md` for interactive component authoring.
|
|
440
|
+
- `references/messages.md` for persistent display via `sendMessage` + `registerMessageRenderer`.
|
|
441
|
+
|
|
442
|
+
## Naming Conventions
|
|
443
|
+
|
|
444
|
+
For extensions wrapping a third-party API, prefix tool names with the API name to avoid conflicts:
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
linkup_web_search
|
|
448
|
+
linkup_web_fetch
|
|
449
|
+
synthetic_web_search
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
For internal/custom tools, no prefix is needed:
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
get_current_time
|
|
456
|
+
processes
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Use snake_case for all tool names.
|
|
460
|
+
|
|
461
|
+
## Abort Signal
|
|
462
|
+
|
|
463
|
+
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.
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
467
|
+
const response = await fetch(url, { signal });
|
|
468
|
+
// If the user cancels, fetch throws an AbortError
|
|
469
|
+
return { content: [{ type: "text", text: await response.text() }], details: {} };
|
|
470
|
+
},
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Pass `signal` to every async operation that supports it: `fetch()` calls, `pi.exec()` calls, SDK clients, etc.
|
|
474
|
+
|
|
475
|
+
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()`:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// In the tool:
|
|
479
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
480
|
+
const result = await client.search({ query: params.query, signal });
|
|
481
|
+
// ...
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// In the client:
|
|
485
|
+
async search(params: { query: string; signal?: AbortSignal }) {
|
|
486
|
+
return this.request("/search", { method: "POST", body: ... }, params.signal);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async request<T>(endpoint: string, options: RequestInit = {}, signal?: AbortSignal) {
|
|
490
|
+
return fetch(`${BASE_URL}${endpoint}`, { ...options, signal, headers: { ... } });
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
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.
|
|
495
|
+
|
|
496
|
+
## Output Truncation
|
|
497
|
+
|
|
498
|
+
For tools that may return large outputs, use `truncateHead()` which returns a `TruncationResult`:
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
import { truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
|
|
502
|
+
import { createWriteStream } from "fs";
|
|
503
|
+
import { tmpdir } from "os";
|
|
504
|
+
import { join } from "path";
|
|
505
|
+
|
|
506
|
+
interface FetchDetails {
|
|
507
|
+
content: string;
|
|
508
|
+
url: string;
|
|
509
|
+
truncated: boolean;
|
|
510
|
+
totalLines: number;
|
|
511
|
+
totalBytes: number;
|
|
512
|
+
outputLines: number;
|
|
513
|
+
outputBytes: number;
|
|
514
|
+
tempFile?: string;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async execute(
|
|
518
|
+
_toolCallId: string,
|
|
519
|
+
params: FetchParams,
|
|
520
|
+
signal: AbortSignal | undefined,
|
|
521
|
+
_onUpdate: undefined,
|
|
522
|
+
_ctx: ExtensionContext,
|
|
523
|
+
): Promise<AgentToolResult<FetchDetails>> {
|
|
524
|
+
const response = await fetch(params.url, { signal });
|
|
525
|
+
const fullContent = await response.text();
|
|
526
|
+
|
|
527
|
+
// truncateHead keeps the tail (most recent content)
|
|
528
|
+
const truncated = truncateHead(fullContent, {
|
|
529
|
+
maxBytes: 50000,
|
|
530
|
+
maxLines: 2000,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const details: FetchDetails = {
|
|
534
|
+
content: truncated.content,
|
|
535
|
+
url: params.url,
|
|
536
|
+
truncated: truncated.truncated,
|
|
537
|
+
totalLines: truncated.totalLines,
|
|
538
|
+
totalBytes: truncated.totalBytes,
|
|
539
|
+
outputLines: truncated.outputLines,
|
|
540
|
+
outputBytes: truncated.outputBytes,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// Write full content to temp file if truncated
|
|
544
|
+
if (truncated.truncated) {
|
|
545
|
+
const tempFile = join(tmpdir(), `web-fetch-${Date.now()}.txt`);
|
|
546
|
+
const stream = createWriteStream(tempFile);
|
|
547
|
+
stream.write(fullContent);
|
|
548
|
+
stream.end();
|
|
549
|
+
details.tempFile = tempFile;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
content: [{ type: "text", text: truncated.content }],
|
|
554
|
+
details,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Truncation Result Fields
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
interface TruncationResult {
|
|
563
|
+
content: string; // The truncated content (or full if not truncated)
|
|
564
|
+
truncated: boolean; // Whether truncation occurred
|
|
565
|
+
totalLines: number; // Original total lines
|
|
566
|
+
totalBytes: number; // Original total bytes
|
|
567
|
+
outputLines: number; // Lines in output
|
|
568
|
+
outputBytes: number; // Bytes in output
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Rendering Truncated Output
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
renderResult(
|
|
576
|
+
result: AgentToolResult<FetchDetails>,
|
|
577
|
+
options: ToolRenderResultOptions,
|
|
578
|
+
theme: Theme,
|
|
579
|
+
) {
|
|
580
|
+
if (options.isPartial) {
|
|
581
|
+
return new Text(theme.fg("muted", "WebFetch: fetching..."), 0, 0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const { details } = result;
|
|
585
|
+
|
|
586
|
+
if (!details?.content) {
|
|
587
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
588
|
+
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Fetch failed";
|
|
589
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
590
|
+
}
|
|
591
|
+
const container = new Container();
|
|
592
|
+
|
|
593
|
+
// Body with content preview
|
|
594
|
+
const fields = [
|
|
595
|
+
{ label: "URL", value: details.url, showCollapsed: true },
|
|
596
|
+
{ label: "Size", value: formatSize(details.totalBytes), showCollapsed: true },
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
container.addChild(new ToolBody(theme, { fields, expanded: options.expanded }));
|
|
600
|
+
|
|
601
|
+
// Footer with truncation info
|
|
602
|
+
const footerItems = [];
|
|
603
|
+
|
|
604
|
+
if (details.truncated) {
|
|
605
|
+
footerItems.push(
|
|
606
|
+
{ label: "showing", value: `${details.outputLines}/${details.totalLines} lines` },
|
|
607
|
+
{ label: "full output", value: details.tempFile ?? "temp file" },
|
|
608
|
+
);
|
|
609
|
+
} else {
|
|
610
|
+
footerItems.push({ label: "lines", value: `${details.totalLines}` });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
container.addChild(
|
|
614
|
+
new ToolFooter(theme, {
|
|
615
|
+
items: footerItems,
|
|
616
|
+
separator: " | ",
|
|
617
|
+
includeSpacerBeforeFooter: fields.length > 0,
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return container;
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Multi-Action Tools
|
|
626
|
+
|
|
627
|
+
For tools with multiple actions (e.g., `start`, `list`, `kill`), organize code for maintainability:
|
|
628
|
+
|
|
629
|
+
### File Structure
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
tools/
|
|
633
|
+
my_tool/
|
|
634
|
+
index.ts // Tool definition, registration, execute switch
|
|
635
|
+
actions/
|
|
636
|
+
start.ts // start action implementation
|
|
637
|
+
list.ts // list action implementation
|
|
638
|
+
kill.ts // kill action implementation
|
|
639
|
+
render.ts // renderCall and renderResult (when complex)
|
|
640
|
+
types.ts // Shared types and param schema
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Action Pattern
|
|
644
|
+
|
|
645
|
+
Each action takes an SDK client and typed params, returns typed results:
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// actions/start.ts
|
|
649
|
+
export interface StartParams {
|
|
650
|
+
name: string;
|
|
651
|
+
command: string;
|
|
652
|
+
cwd?: string;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export interface StartResult {
|
|
656
|
+
sessionId: string;
|
|
657
|
+
pid: number;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export async function start(
|
|
661
|
+
client: MyClient,
|
|
662
|
+
params: StartParams,
|
|
663
|
+
signal?: AbortSignal,
|
|
664
|
+
): Promise<StartResult> {
|
|
665
|
+
return client.startSession({
|
|
666
|
+
name: params.name,
|
|
667
|
+
command: params.command,
|
|
668
|
+
cwd: params.cwd,
|
|
669
|
+
signal,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Execute Delegation
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
// index.ts
|
|
678
|
+
async execute(
|
|
679
|
+
toolCallId: string,
|
|
680
|
+
params: MyToolParams,
|
|
681
|
+
signal: AbortSignal | undefined,
|
|
682
|
+
onUpdate: AgentToolUpdateCallback<MyToolDetails> | undefined,
|
|
683
|
+
ctx: ExtensionContext,
|
|
684
|
+
): Promise<AgentToolResult<MyToolDetails>> {
|
|
685
|
+
switch (params.action) {
|
|
686
|
+
case "start":
|
|
687
|
+
return startAction(client, params, signal, onUpdate, ctx);
|
|
688
|
+
case "list":
|
|
689
|
+
return listAction(client, params, signal);
|
|
690
|
+
case "kill":
|
|
691
|
+
return killAction(client, params, signal);
|
|
692
|
+
default:
|
|
693
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Separate Render Module
|
|
699
|
+
|
|
700
|
+
When rendering is complex, extract to `render.ts`:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
// render.ts
|
|
704
|
+
export function renderCall(args: MyToolParams, theme: Theme) {
|
|
705
|
+
return new ToolCallHeader(
|
|
706
|
+
{
|
|
707
|
+
toolName: "MyTool",
|
|
708
|
+
action: args.action,
|
|
709
|
+
mainArg: getMainArg(args),
|
|
710
|
+
optionArgs: getOptionArgs(args),
|
|
711
|
+
longArgs: getLongArgs(args),
|
|
712
|
+
},
|
|
713
|
+
theme,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function renderResult(
|
|
718
|
+
result: AgentToolResult<MyToolDetails>,
|
|
719
|
+
options: ToolRenderResultOptions,
|
|
720
|
+
theme: Theme,
|
|
721
|
+
) {
|
|
722
|
+
// Complex rendering logic here
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
// index.ts
|
|
728
|
+
import { renderCall, renderResult } from "./render.js";
|
|
729
|
+
|
|
730
|
+
const myTool = {
|
|
731
|
+
// ...
|
|
732
|
+
renderCall,
|
|
733
|
+
renderResult,
|
|
734
|
+
};
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Examples to reference:**
|
|
738
|
+
- `pi-processes`: Multi-action process management with complex rendering
|
|
739
|
+
- `pi-linear`: Multi-action Linear API integration
|
|
740
|
+
|
|
741
|
+
## Full Example
|
|
742
|
+
|
|
743
|
+
Here's a realistic tool demonstrating all the patterns:
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";
|
|
747
|
+
import type {
|
|
748
|
+
AgentToolResult,
|
|
749
|
+
ExtensionAPI,
|
|
750
|
+
ExtensionContext,
|
|
751
|
+
Theme,
|
|
752
|
+
ToolRenderResultOptions,
|
|
753
|
+
} from "@mariozechner/pi-coding-agent";
|
|
754
|
+
import { keyHint, formatSize } from "@mariozechner/pi-coding-agent";
|
|
755
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
756
|
+
import { type Static, Type } from "@mariozechner/pi-coding-agent";
|
|
757
|
+
|
|
758
|
+
// Schema
|
|
759
|
+
const parameters = Type.Object({
|
|
760
|
+
owner: Type.String({ description: "Repository owner" }),
|
|
761
|
+
repo: Type.String({ description: "Repository name" }),
|
|
762
|
+
path: Type.Optional(Type.String({ description: "File or directory path", default: "" })),
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Typed aliases
|
|
766
|
+
type RepoTreeParams = Static<typeof parameters>;
|
|
767
|
+
|
|
768
|
+
interface RepoTreeDetails {
|
|
769
|
+
owner: string;
|
|
770
|
+
repo: string;
|
|
771
|
+
path: string;
|
|
772
|
+
entries: Array<{ name: string; type: "file" | "dir"; size?: number }>;
|
|
773
|
+
truncated: boolean;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Tool definition
|
|
777
|
+
const repoTreeTool = {
|
|
778
|
+
name: "repo_tree",
|
|
779
|
+
label: "Repo Tree",
|
|
780
|
+
description: "List files and directories in a GitHub repository.",
|
|
781
|
+
promptSnippet: "Browse repository file structure",
|
|
782
|
+
promptGuidelines: [
|
|
783
|
+
"Use this to explore repository structure before reading files",
|
|
784
|
+
"Start with root path, then drill down into directories",
|
|
785
|
+
],
|
|
786
|
+
parameters,
|
|
787
|
+
|
|
788
|
+
async execute(
|
|
789
|
+
_toolCallId: string,
|
|
790
|
+
params: RepoTreeParams,
|
|
791
|
+
signal: AbortSignal | undefined,
|
|
792
|
+
_onUpdate: undefined,
|
|
793
|
+
_ctx: ExtensionContext,
|
|
794
|
+
): Promise<AgentToolResult<RepoTreeDetails>> {
|
|
795
|
+
const response = await fetch(
|
|
796
|
+
`https://api.github.com/repos/${params.owner}/${params.repo}/contents/${params.path ?? ""}`,
|
|
797
|
+
{ signal, headers: { Accept: "application/vnd.github.v3+json" } },
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
if (!response.ok) {
|
|
801
|
+
if (response.status === 404) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`Could not find repository "${params.owner}/${params.repo}" or path "${params.path ?? ""}"`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const data = await response.json();
|
|
810
|
+
|
|
811
|
+
// GitHub returns a single object for files, array for directories
|
|
812
|
+
const entries = Array.isArray(data) ? data : [data];
|
|
813
|
+
|
|
814
|
+
const processed = entries.map((entry: any) => ({
|
|
815
|
+
name: entry.name,
|
|
816
|
+
type: entry.type === "dir" ? "dir" as const : "file" as const,
|
|
817
|
+
size: entry.size,
|
|
818
|
+
}));
|
|
819
|
+
|
|
820
|
+
// Sort: directories first, then files, alphabetically within each
|
|
821
|
+
processed.sort((a, b) => {
|
|
822
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
823
|
+
return a.name.localeCompare(b.name);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const truncated = processed.length > 100;
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
content: [
|
|
830
|
+
{
|
|
831
|
+
type: "text",
|
|
832
|
+
text: JSON.stringify(truncated ? processed.slice(0, 100) : processed, null, 2),
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
details: {
|
|
836
|
+
owner: params.owner,
|
|
837
|
+
repo: params.repo,
|
|
838
|
+
path: params.path ?? "",
|
|
839
|
+
entries: processed,
|
|
840
|
+
truncated,
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
renderCall(args: RepoTreeParams, theme: Theme) {
|
|
846
|
+
return new ToolCallHeader(
|
|
847
|
+
{
|
|
848
|
+
toolName: "Repo Tree",
|
|
849
|
+
mainArg: `${args.owner}/${args.repo}`,
|
|
850
|
+
optionArgs: args.path ? [`path=${args.path}`] : [],
|
|
851
|
+
longArgs: [],
|
|
852
|
+
},
|
|
853
|
+
theme,
|
|
854
|
+
);
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
renderResult(
|
|
858
|
+
result: AgentToolResult<RepoTreeDetails>,
|
|
859
|
+
options: ToolRenderResultOptions,
|
|
860
|
+
theme: Theme,
|
|
861
|
+
) {
|
|
862
|
+
// 1. Stable partial message
|
|
863
|
+
if (options.isPartial) {
|
|
864
|
+
return new Text(theme.fg("muted", "RepoTree: fetching..."), 0, 0);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const { details } = result;
|
|
868
|
+
|
|
869
|
+
// 2. Error handling: details is {} when tool threw
|
|
870
|
+
if (!details?.entries) {
|
|
871
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
872
|
+
const errorMsg = (textBlock?.type === "text" && textBlock.text) || "Failed to list repository";
|
|
873
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
874
|
+
}
|
|
875
|
+
const container = new Container();
|
|
876
|
+
const entries = details?.entries ?? [];
|
|
877
|
+
const dirs = entries.filter((e) => e.type === "dir");
|
|
878
|
+
const files = entries.filter((e) => e.type === "file");
|
|
879
|
+
|
|
880
|
+
// 3. Body fields with showCollapsed for collapsed/expanded control
|
|
881
|
+
const fields: Array<{ label: string; value: string; showCollapsed: boolean }> = [
|
|
882
|
+
{
|
|
883
|
+
label: "Location",
|
|
884
|
+
value: `${details.owner}/${details.repo}${details.path ? `/${details.path}` : ""}`,
|
|
885
|
+
showCollapsed: true,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
label: "Contents",
|
|
889
|
+
value: `${dirs.length} dirs, ${files.length} files`,
|
|
890
|
+
showCollapsed: true,
|
|
891
|
+
},
|
|
892
|
+
];
|
|
893
|
+
|
|
894
|
+
container.addChild(new ToolBody(theme, { fields, expanded: options.expanded }));
|
|
895
|
+
|
|
896
|
+
// 4. Entry list (in expanded view)
|
|
897
|
+
if (options.expanded && entries.length > 0) {
|
|
898
|
+
const entryLines = entries
|
|
899
|
+
.slice(0, 50)
|
|
900
|
+
.map((e) => {
|
|
901
|
+
const icon = e.type === "dir" ? "📁" : "📄";
|
|
902
|
+
const size = e.size !== undefined ? ` (${formatSize(e.size)})` : "";
|
|
903
|
+
return `${icon} ${e.name}${size}`;
|
|
904
|
+
})
|
|
905
|
+
.join("\n");
|
|
906
|
+
|
|
907
|
+
container.addChild(new Text(entryLines, 0, 0));
|
|
908
|
+
|
|
909
|
+
if (entries.length > 50) {
|
|
910
|
+
container.addChild(
|
|
911
|
+
new Text(theme.fg("muted", `... and ${entries.length - 50} more`), 0, 0),
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// 5. Conditional footer
|
|
917
|
+
const footerItems = [];
|
|
918
|
+
|
|
919
|
+
if (details?.truncated) {
|
|
920
|
+
footerItems.push({ label: "showing", value: "first 100 entries" });
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (entries.length > 0) {
|
|
924
|
+
footerItems.push({ label: "total", value: `${entries.length} items` });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!options.expanded && entries.length > 5) {
|
|
928
|
+
footerItems.push({ label: "", value: keyHint("app.tools.expand", "to expand") });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (footerItems.length > 0) {
|
|
932
|
+
container.addChild(
|
|
933
|
+
new ToolFooter(theme, {
|
|
934
|
+
items: footerItems,
|
|
935
|
+
separator: " | ",
|
|
936
|
+
includeSpacerBeforeFooter: fields.length > 0,
|
|
937
|
+
}),
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return container;
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
export default function (pi: ExtensionAPI) {
|
|
946
|
+
pi.registerTool(repoTreeTool);
|
|
947
|
+
}
|
|
948
|
+
```
|