@mariozechner/pi-coding-agent 0.37.4 → 0.37.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/extensions/index.d.ts +1 -1
  3. package/dist/core/extensions/index.d.ts.map +1 -1
  4. package/dist/core/extensions/index.js.map +1 -1
  5. package/dist/core/extensions/loader.d.ts.map +1 -1
  6. package/dist/core/extensions/loader.js +33 -3
  7. package/dist/core/extensions/loader.js.map +1 -1
  8. package/dist/core/extensions/runner.d.ts +4 -1
  9. package/dist/core/extensions/runner.d.ts.map +1 -1
  10. package/dist/core/extensions/runner.js +3 -0
  11. package/dist/core/extensions/runner.js.map +1 -1
  12. package/dist/core/extensions/types.d.ts +13 -1
  13. package/dist/core/extensions/types.d.ts.map +1 -1
  14. package/dist/core/extensions/types.js.map +1 -1
  15. package/dist/core/system-prompt.d.ts.map +1 -1
  16. package/dist/core/system-prompt.js +1 -1
  17. package/dist/core/system-prompt.js.map +1 -1
  18. package/dist/core/tools/index.d.ts +1 -1
  19. package/dist/core/tools/index.d.ts.map +1 -1
  20. package/dist/core/tools/index.js +1 -0
  21. package/dist/core/tools/index.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/modes/interactive/components/index.d.ts +28 -0
  27. package/dist/modes/interactive/components/index.d.ts.map +1 -0
  28. package/dist/modes/interactive/components/index.js +29 -0
  29. package/dist/modes/interactive/components/index.js.map +1 -0
  30. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  31. package/dist/modes/interactive/interactive-mode.js +9 -0
  32. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  33. package/dist/modes/print-mode.d.ts.map +1 -1
  34. package/dist/modes/print-mode.js +9 -0
  35. package/dist/modes/print-mode.js.map +1 -1
  36. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  37. package/dist/modes/rpc/rpc-mode.js +9 -0
  38. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  39. package/docs/extensions.md +149 -9
  40. package/docs/tui.md +220 -2
  41. package/examples/extensions/README.md +1 -0
  42. package/examples/extensions/preset.ts +398 -0
  43. package/examples/extensions/truncated-tool.ts +192 -0
  44. package/examples/extensions/with-deps/package-lock.json +2 -2
  45. package/examples/extensions/with-deps/package.json +1 -1
  46. package/package.json +4 -4
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Preset Extension
3
+ *
4
+ * Allows defining named presets that configure model, thinking level, tools,
5
+ * and system prompt instructions. Presets are defined in JSON config files
6
+ * and can be activated via CLI flag, /preset command, or Ctrl+Shift+U to cycle.
7
+ *
8
+ * Config files (merged, project takes precedence):
9
+ * - ~/.pi/agent/presets.json (global)
10
+ * - <cwd>/.pi/presets.json (project-local)
11
+ *
12
+ * Example presets.json:
13
+ * ```json
14
+ * {
15
+ * "plan": {
16
+ * "provider": "anthropic",
17
+ * "model": "claude-sonnet-4-5",
18
+ * "thinkingLevel": "high",
19
+ * "tools": ["read", "grep", "find", "ls"],
20
+ * "instructions": "You are in PLANNING MODE. Your job is to deeply understand the problem and create a detailed implementation plan.\n\nRules:\n- DO NOT make any changes. You cannot edit or write files.\n- Read files IN FULL (no offset/limit) to get complete context. Partial reads miss critical details.\n- Explore thoroughly: grep for related code, find similar patterns, understand the architecture.\n- Ask clarifying questions if requirements are ambiguous. Do not assume.\n- Identify risks, edge cases, and dependencies before proposing solutions.\n\nOutput:\n- Create a structured plan with numbered steps.\n- For each step: what to change, why, and potential risks.\n- List files that will be modified.\n- Note any tests that should be added or updated.\n\nWhen done, ask the user if they want you to:\n1. Write the plan to a markdown file (e.g., PLAN.md)\n2. Create a GitHub issue with the plan\n3. Proceed to implementation (they should switch to 'implement' preset)"
21
+ * },
22
+ * "implement": {
23
+ * "provider": "anthropic",
24
+ * "model": "claude-sonnet-4-5",
25
+ * "thinkingLevel": "high",
26
+ * "tools": ["read", "bash", "edit", "write"],
27
+ * "instructions": "You are in IMPLEMENTATION MODE. Your job is to make focused, correct changes.\n\nRules:\n- Keep scope tight. Do exactly what was asked, no more.\n- Read files before editing to understand current state.\n- Make surgical edits. Prefer edit over write for existing files.\n- Explain your reasoning briefly before each change.\n- Run tests or type checks after changes if the project has them (npm test, npm run check, etc.).\n- If you encounter unexpected complexity, STOP and explain the issue rather than hacking around it.\n\nIf no plan exists:\n- Ask clarifying questions before starting.\n- Propose what you'll do and get confirmation for non-trivial changes.\n\nAfter completing changes:\n- Summarize what was done.\n- Note any follow-up work or tests that should be added."
28
+ * }
29
+ * }
30
+ * ```
31
+ *
32
+ * Usage:
33
+ * - `pi --preset plan` - start with plan preset
34
+ * - `/preset` - show selector to switch presets mid-session
35
+ * - `/preset implement` - switch to implement preset directly
36
+ * - `Ctrl+Shift+U` - cycle through presets
37
+ *
38
+ * CLI flags always override preset values.
39
+ */
40
+
41
+ import { existsSync, readFileSync } from "node:fs";
42
+ import { homedir } from "node:os";
43
+ import { join } from "node:path";
44
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
45
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
46
+ import { Container, Key, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
47
+
48
+ // Preset configuration
49
+ interface Preset {
50
+ /** Provider name (e.g., "anthropic", "openai") */
51
+ provider?: string;
52
+ /** Model ID (e.g., "claude-sonnet-4-5") */
53
+ model?: string;
54
+ /** Thinking level */
55
+ thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
56
+ /** Tools to enable (replaces default set) */
57
+ tools?: string[];
58
+ /** Instructions to append to system prompt */
59
+ instructions?: string;
60
+ }
61
+
62
+ interface PresetsConfig {
63
+ [name: string]: Preset;
64
+ }
65
+
66
+ /**
67
+ * Load presets from config files.
68
+ * Project-local presets override global presets with the same name.
69
+ */
70
+ function loadPresets(cwd: string): PresetsConfig {
71
+ const globalPath = join(homedir(), ".pi", "agent", "presets.json");
72
+ const projectPath = join(cwd, ".pi", "presets.json");
73
+
74
+ let globalPresets: PresetsConfig = {};
75
+ let projectPresets: PresetsConfig = {};
76
+
77
+ // Load global presets
78
+ if (existsSync(globalPath)) {
79
+ try {
80
+ const content = readFileSync(globalPath, "utf-8");
81
+ globalPresets = JSON.parse(content);
82
+ } catch (err) {
83
+ console.error(`Failed to load global presets from ${globalPath}: ${err}`);
84
+ }
85
+ }
86
+
87
+ // Load project presets
88
+ if (existsSync(projectPath)) {
89
+ try {
90
+ const content = readFileSync(projectPath, "utf-8");
91
+ projectPresets = JSON.parse(content);
92
+ } catch (err) {
93
+ console.error(`Failed to load project presets from ${projectPath}: ${err}`);
94
+ }
95
+ }
96
+
97
+ // Merge (project overrides global)
98
+ return { ...globalPresets, ...projectPresets };
99
+ }
100
+
101
+ export default function presetExtension(pi: ExtensionAPI) {
102
+ let presets: PresetsConfig = {};
103
+ let activePresetName: string | undefined;
104
+ let activePreset: Preset | undefined;
105
+
106
+ // Register --preset CLI flag
107
+ pi.registerFlag("preset", {
108
+ description: "Preset configuration to use",
109
+ type: "string",
110
+ });
111
+
112
+ /**
113
+ * Apply a preset configuration.
114
+ */
115
+ async function applyPreset(name: string, preset: Preset, ctx: ExtensionContext): Promise<boolean> {
116
+ // Apply model if specified
117
+ if (preset.provider && preset.model) {
118
+ const model = ctx.modelRegistry.find(preset.provider, preset.model);
119
+ if (model) {
120
+ const success = await pi.setModel(model);
121
+ if (!success) {
122
+ ctx.ui.notify(`Preset "${name}": No API key for ${preset.provider}/${preset.model}`, "warning");
123
+ }
124
+ } else {
125
+ ctx.ui.notify(`Preset "${name}": Model ${preset.provider}/${preset.model} not found`, "warning");
126
+ }
127
+ }
128
+
129
+ // Apply thinking level if specified
130
+ if (preset.thinkingLevel) {
131
+ pi.setThinkingLevel(preset.thinkingLevel);
132
+ }
133
+
134
+ // Apply tools if specified
135
+ if (preset.tools && preset.tools.length > 0) {
136
+ const allTools = pi.getAllTools();
137
+ const validTools = preset.tools.filter((t) => allTools.includes(t));
138
+ const invalidTools = preset.tools.filter((t) => !allTools.includes(t));
139
+
140
+ if (invalidTools.length > 0) {
141
+ ctx.ui.notify(`Preset "${name}": Unknown tools: ${invalidTools.join(", ")}`, "warning");
142
+ }
143
+
144
+ if (validTools.length > 0) {
145
+ pi.setActiveTools(validTools);
146
+ }
147
+ }
148
+
149
+ // Store active preset for system prompt injection
150
+ activePresetName = name;
151
+ activePreset = preset;
152
+
153
+ return true;
154
+ }
155
+
156
+ /**
157
+ * Build description string for a preset.
158
+ */
159
+ function buildPresetDescription(preset: Preset): string {
160
+ const parts: string[] = [];
161
+
162
+ if (preset.provider && preset.model) {
163
+ parts.push(`${preset.provider}/${preset.model}`);
164
+ }
165
+ if (preset.thinkingLevel) {
166
+ parts.push(`thinking:${preset.thinkingLevel}`);
167
+ }
168
+ if (preset.tools) {
169
+ parts.push(`tools:${preset.tools.join(",")}`);
170
+ }
171
+ if (preset.instructions) {
172
+ const truncated =
173
+ preset.instructions.length > 30 ? `${preset.instructions.slice(0, 27)}...` : preset.instructions;
174
+ parts.push(`"${truncated}"`);
175
+ }
176
+
177
+ return parts.join(" | ");
178
+ }
179
+
180
+ /**
181
+ * Show preset selector UI using custom SelectList component.
182
+ */
183
+ async function showPresetSelector(ctx: ExtensionContext): Promise<void> {
184
+ const presetNames = Object.keys(presets);
185
+
186
+ if (presetNames.length === 0) {
187
+ ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning");
188
+ return;
189
+ }
190
+
191
+ // Build select items with descriptions
192
+ const items: SelectItem[] = presetNames.map((name) => {
193
+ const preset = presets[name];
194
+ const isActive = name === activePresetName;
195
+ return {
196
+ value: name,
197
+ label: isActive ? `${name} (active)` : name,
198
+ description: buildPresetDescription(preset),
199
+ };
200
+ });
201
+
202
+ // Add "None" option to clear preset
203
+ items.push({
204
+ value: "(none)",
205
+ label: "(none)",
206
+ description: "Clear active preset, restore defaults",
207
+ });
208
+
209
+ const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
210
+ const container = new Container();
211
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
212
+
213
+ // Header
214
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select Preset"))));
215
+
216
+ // SelectList with themed styling
217
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
218
+ selectedPrefix: (text) => theme.fg("accent", text),
219
+ selectedText: (text) => theme.fg("accent", text),
220
+ description: (text) => theme.fg("muted", text),
221
+ scrollInfo: (text) => theme.fg("dim", text),
222
+ noMatch: (text) => theme.fg("warning", text),
223
+ });
224
+
225
+ selectList.onSelect = (item) => done(item.value);
226
+ selectList.onCancel = () => done(null);
227
+
228
+ container.addChild(selectList);
229
+
230
+ // Footer hint
231
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")));
232
+
233
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
234
+
235
+ return {
236
+ render(width: number) {
237
+ return container.render(width);
238
+ },
239
+ invalidate() {
240
+ container.invalidate();
241
+ },
242
+ handleInput(data: string) {
243
+ selectList.handleInput(data);
244
+ tui.requestRender();
245
+ },
246
+ };
247
+ });
248
+
249
+ if (!result) return;
250
+
251
+ if (result === "(none)") {
252
+ // Clear preset and restore defaults
253
+ activePresetName = undefined;
254
+ activePreset = undefined;
255
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
256
+ ctx.ui.notify("Preset cleared, defaults restored", "info");
257
+ updateStatus(ctx);
258
+ return;
259
+ }
260
+
261
+ const preset = presets[result];
262
+ if (preset) {
263
+ await applyPreset(result, preset, ctx);
264
+ ctx.ui.notify(`Preset "${result}" activated`, "info");
265
+ updateStatus(ctx);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Update status indicator.
271
+ */
272
+ function updateStatus(ctx: ExtensionContext) {
273
+ if (activePresetName) {
274
+ ctx.ui.setStatus("preset", ctx.ui.theme.fg("accent", `preset:${activePresetName}`));
275
+ } else {
276
+ ctx.ui.setStatus("preset", undefined);
277
+ }
278
+ }
279
+
280
+ function getPresetOrder(): string[] {
281
+ return Object.keys(presets).sort();
282
+ }
283
+
284
+ async function cyclePreset(ctx: ExtensionContext): Promise<void> {
285
+ const presetNames = getPresetOrder();
286
+ if (presetNames.length === 0) {
287
+ ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning");
288
+ return;
289
+ }
290
+
291
+ const cycleList = ["(none)", ...presetNames];
292
+ const currentName = activePresetName ?? "(none)";
293
+ const currentIndex = cycleList.indexOf(currentName);
294
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % cycleList.length;
295
+ const nextName = cycleList[nextIndex];
296
+
297
+ if (nextName === "(none)") {
298
+ activePresetName = undefined;
299
+ activePreset = undefined;
300
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
301
+ ctx.ui.notify("Preset cleared, defaults restored", "info");
302
+ updateStatus(ctx);
303
+ return;
304
+ }
305
+
306
+ const preset = presets[nextName];
307
+ if (!preset) return;
308
+
309
+ await applyPreset(nextName, preset, ctx);
310
+ ctx.ui.notify(`Preset "${nextName}" activated`, "info");
311
+ updateStatus(ctx);
312
+ }
313
+
314
+ pi.registerShortcut(Key.ctrlShift("u"), {
315
+ description: "Cycle presets",
316
+ handler: async (ctx) => {
317
+ await cyclePreset(ctx);
318
+ },
319
+ });
320
+
321
+ // Register /preset command
322
+ pi.registerCommand("preset", {
323
+ description: "Switch preset configuration",
324
+ handler: async (args, ctx) => {
325
+ // If preset name provided, apply directly
326
+ if (args?.trim()) {
327
+ const name = args.trim();
328
+ const preset = presets[name];
329
+
330
+ if (!preset) {
331
+ const available = Object.keys(presets).join(", ") || "(none defined)";
332
+ ctx.ui.notify(`Unknown preset "${name}". Available: ${available}`, "error");
333
+ return;
334
+ }
335
+
336
+ await applyPreset(name, preset, ctx);
337
+ ctx.ui.notify(`Preset "${name}" activated`, "info");
338
+ updateStatus(ctx);
339
+ return;
340
+ }
341
+
342
+ // Otherwise show selector
343
+ await showPresetSelector(ctx);
344
+ },
345
+ });
346
+
347
+ // Inject preset instructions into system prompt
348
+ pi.on("before_agent_start", async () => {
349
+ if (activePreset?.instructions) {
350
+ return {
351
+ systemPromptAppend: activePreset.instructions,
352
+ };
353
+ }
354
+ });
355
+
356
+ // Initialize on session start
357
+ pi.on("session_start", async (_event, ctx) => {
358
+ // Load presets from config files
359
+ presets = loadPresets(ctx.cwd);
360
+
361
+ // Check for --preset flag
362
+ const presetFlag = pi.getFlag("preset");
363
+ if (typeof presetFlag === "string" && presetFlag) {
364
+ const preset = presets[presetFlag];
365
+ if (preset) {
366
+ await applyPreset(presetFlag, preset, ctx);
367
+ ctx.ui.notify(`Preset "${presetFlag}" activated`, "info");
368
+ } else {
369
+ const available = Object.keys(presets).join(", ") || "(none defined)";
370
+ ctx.ui.notify(`Unknown preset "${presetFlag}". Available: ${available}`, "warning");
371
+ }
372
+ }
373
+
374
+ // Restore preset from session state
375
+ const entries = ctx.sessionManager.getEntries();
376
+ const presetEntry = entries
377
+ .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "preset-state")
378
+ .pop() as { data?: { name: string } } | undefined;
379
+
380
+ if (presetEntry?.data?.name && !presetFlag) {
381
+ const preset = presets[presetEntry.data.name];
382
+ if (preset) {
383
+ activePresetName = presetEntry.data.name;
384
+ activePreset = preset;
385
+ // Don't re-apply model/tools on restore, just keep the name for instructions
386
+ }
387
+ }
388
+
389
+ updateStatus(ctx);
390
+ });
391
+
392
+ // Persist preset state
393
+ pi.on("turn_start", async () => {
394
+ if (activePresetName) {
395
+ pi.appendEntry("preset-state", { name: activePresetName });
396
+ }
397
+ });
398
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Truncated Tool Example - Demonstrates proper output truncation for custom tools
3
+ *
4
+ * Custom tools MUST truncate their output to avoid overwhelming the LLM context.
5
+ * The built-in limit is 50KB (~10k tokens) and 2000 lines, whichever is hit first.
6
+ *
7
+ * This example shows how to:
8
+ * 1. Use the built-in truncation utilities
9
+ * 2. Write full output to a temp file when truncated
10
+ * 3. Inform the LLM where to find the complete output
11
+ * 4. Custom rendering of tool calls and results
12
+ *
13
+ * The `rg` tool here wraps ripgrep with proper truncation. Compare this to the
14
+ * built-in `grep` tool in src/core/tools/grep.ts for a more complete implementation.
15
+ */
16
+
17
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
+ import {
19
+ DEFAULT_MAX_BYTES,
20
+ DEFAULT_MAX_LINES,
21
+ formatSize,
22
+ type TruncationResult,
23
+ truncateHead,
24
+ } from "@mariozechner/pi-coding-agent";
25
+ import { Text } from "@mariozechner/pi-tui";
26
+ import { Type } from "@sinclair/typebox";
27
+ import { execSync } from "child_process";
28
+ import { mkdtempSync, writeFileSync } from "fs";
29
+ import { tmpdir } from "os";
30
+ import { join } from "path";
31
+
32
+ const RgParams = Type.Object({
33
+ pattern: Type.String({ description: "Search pattern (regex)" }),
34
+ path: Type.Optional(Type.String({ description: "Directory to search (default: current directory)" })),
35
+ glob: Type.Optional(Type.String({ description: "File glob pattern, e.g. '*.ts'" })),
36
+ });
37
+
38
+ interface RgDetails {
39
+ pattern: string;
40
+ path?: string;
41
+ glob?: string;
42
+ matchCount: number;
43
+ truncation?: TruncationResult;
44
+ fullOutputPath?: string;
45
+ }
46
+
47
+ export default function (pi: ExtensionAPI) {
48
+ pi.registerTool({
49
+ name: "rg",
50
+ label: "ripgrep",
51
+ // Document the truncation limits in the tool description so the LLM knows
52
+ description: `Search file contents using ripgrep. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first). If truncated, full output is saved to a temp file.`,
53
+ parameters: RgParams,
54
+
55
+ async execute(_toolCallId, params, _onUpdate, ctx) {
56
+ const { pattern, path: searchPath, glob } = params;
57
+
58
+ // Build the ripgrep command
59
+ const args = ["rg", "--line-number", "--color=never"];
60
+ if (glob) args.push("--glob", glob);
61
+ args.push(pattern);
62
+ args.push(searchPath || ".");
63
+
64
+ let output: string;
65
+ try {
66
+ output = execSync(args.join(" "), {
67
+ cwd: ctx.cwd,
68
+ encoding: "utf-8",
69
+ maxBuffer: 100 * 1024 * 1024, // 100MB buffer to capture full output
70
+ });
71
+ } catch (err: any) {
72
+ // ripgrep exits with 1 when no matches found
73
+ if (err.status === 1) {
74
+ return {
75
+ content: [{ type: "text", text: "No matches found" }],
76
+ details: { pattern, path: searchPath, glob, matchCount: 0 } as RgDetails,
77
+ };
78
+ }
79
+ throw new Error(`ripgrep failed: ${err.message}`);
80
+ }
81
+
82
+ if (!output.trim()) {
83
+ return {
84
+ content: [{ type: "text", text: "No matches found" }],
85
+ details: { pattern, path: searchPath, glob, matchCount: 0 } as RgDetails,
86
+ };
87
+ }
88
+
89
+ // Apply truncation using built-in utilities
90
+ // truncateHead keeps the first N lines/bytes (good for search results)
91
+ // truncateTail keeps the last N lines/bytes (good for logs/command output)
92
+ const truncation = truncateHead(output, {
93
+ maxLines: DEFAULT_MAX_LINES,
94
+ maxBytes: DEFAULT_MAX_BYTES,
95
+ });
96
+
97
+ // Count matches (each non-empty line with a match)
98
+ const matchCount = output.split("\n").filter((line) => line.trim()).length;
99
+
100
+ const details: RgDetails = {
101
+ pattern,
102
+ path: searchPath,
103
+ glob,
104
+ matchCount,
105
+ };
106
+
107
+ let resultText = truncation.content;
108
+
109
+ if (truncation.truncated) {
110
+ // Save full output to a temp file so LLM can access it if needed
111
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-rg-"));
112
+ const tempFile = join(tempDir, "output.txt");
113
+ writeFileSync(tempFile, output);
114
+
115
+ details.truncation = truncation;
116
+ details.fullOutputPath = tempFile;
117
+
118
+ // Add truncation notice - this helps the LLM understand the output is incomplete
119
+ const truncatedLines = truncation.totalLines - truncation.outputLines;
120
+ const truncatedBytes = truncation.totalBytes - truncation.outputBytes;
121
+
122
+ resultText += `\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`;
123
+ resultText += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
124
+ resultText += ` ${truncatedLines} lines (${formatSize(truncatedBytes)}) omitted.`;
125
+ resultText += ` Full output saved to: ${tempFile}]`;
126
+ }
127
+
128
+ return {
129
+ content: [{ type: "text", text: resultText }],
130
+ details,
131
+ };
132
+ },
133
+
134
+ // Custom rendering of the tool call (shown before/during execution)
135
+ renderCall(args, theme) {
136
+ let text = theme.fg("toolTitle", theme.bold("rg "));
137
+ text += theme.fg("accent", `"${args.pattern}"`);
138
+ if (args.path) {
139
+ text += theme.fg("muted", ` in ${args.path}`);
140
+ }
141
+ if (args.glob) {
142
+ text += theme.fg("dim", ` --glob ${args.glob}`);
143
+ }
144
+ return new Text(text, 0, 0);
145
+ },
146
+
147
+ // Custom rendering of the tool result
148
+ renderResult(result, { expanded, isPartial }, theme) {
149
+ const details = result.details as RgDetails | undefined;
150
+
151
+ // Handle streaming/partial results
152
+ if (isPartial) {
153
+ return new Text(theme.fg("warning", "Searching..."), 0, 0);
154
+ }
155
+
156
+ // No matches
157
+ if (!details || details.matchCount === 0) {
158
+ return new Text(theme.fg("dim", "No matches found"), 0, 0);
159
+ }
160
+
161
+ // Build result display
162
+ let text = theme.fg("success", `${details.matchCount} matches`);
163
+
164
+ // Show truncation warning if applicable
165
+ if (details.truncation?.truncated) {
166
+ text += theme.fg("warning", " (truncated)");
167
+ }
168
+
169
+ // In expanded view, show the actual matches
170
+ if (expanded) {
171
+ const content = result.content[0];
172
+ if (content?.type === "text") {
173
+ // Show first 20 lines in expanded view, or all if fewer
174
+ const lines = content.text.split("\n").slice(0, 20);
175
+ for (const line of lines) {
176
+ text += `\n${theme.fg("dim", line)}`;
177
+ }
178
+ if (content.text.split("\n").length > 20) {
179
+ text += `\n${theme.fg("muted", "... (use read tool to see full output)")}`;
180
+ }
181
+ }
182
+
183
+ // Show temp file path if truncated
184
+ if (details.fullOutputPath) {
185
+ text += `\n${theme.fg("dim", `Full output: ${details.fullOutputPath}`)}`;
186
+ }
187
+ }
188
+
189
+ return new Text(text, 0, 0);
190
+ },
191
+ });
192
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "1.1.4",
9
+ "version": "1.1.5",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.1.4",
4
+ "version": "1.1.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.37.4",
3
+ "version": "0.37.5",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -39,9 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@crosscopy/clipboard": "^0.2.8",
42
- "@mariozechner/pi-agent-core": "^0.37.4",
43
- "@mariozechner/pi-ai": "^0.37.4",
44
- "@mariozechner/pi-tui": "^0.37.4",
42
+ "@mariozechner/pi-agent-core": "^0.37.5",
43
+ "@mariozechner/pi-ai": "^0.37.5",
44
+ "@mariozechner/pi-tui": "^0.37.5",
45
45
  "chalk": "^5.5.0",
46
46
  "cli-highlight": "^2.1.11",
47
47
  "diff": "^8.0.2",