@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -8
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +50 -53
- package/examples/custom-tools/README.md +2 -17
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +2 -4
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/README.md +7 -11
- package/package.json +6 -6
- package/src/cli/args.ts +9 -6
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +16 -5
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/custom-tools/wrapper.ts +0 -1
- package/src/core/extensions/index.ts +1 -6
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -8
- package/src/core/file-mentions.ts +5 -8
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +1 -1
- package/src/core/sdk.ts +64 -105
- package/src/core/session-manager.ts +18 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +49 -36
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +98 -4
- package/src/core/tools/bash-interceptor.ts +11 -4
- package/src/core/tools/bash.ts +121 -5
- package/src/core/tools/context.ts +7 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +221 -34
- package/src/core/tools/exa/render.ts +4 -16
- package/src/core/tools/find.ts +149 -5
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +17 -3
- package/src/core/tools/grep.ts +185 -5
- package/src/core/tools/index.test.ts +180 -0
- package/src/core/tools/index.ts +96 -242
- package/src/core/tools/ls.ts +133 -5
- package/src/core/tools/lsp/index.ts +32 -29
- package/src/core/tools/lsp/render.ts +21 -22
- package/src/core/tools/notebook.ts +112 -4
- package/src/core/tools/output.ts +175 -15
- package/src/core/tools/read.ts +127 -25
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -25
- package/src/core/tools/rulebook.ts +11 -3
- package/src/core/tools/task/agents.ts +28 -7
- package/src/core/tools/task/discovery.ts +0 -6
- package/src/core/tools/task/executor.ts +264 -254
- package/src/core/tools/task/index.ts +48 -208
- package/src/core/tools/task/render.ts +26 -11
- package/src/core/tools/task/types.ts +7 -12
- package/src/core/tools/task/worker-protocol.ts +17 -0
- package/src/core/tools/task/worker.ts +238 -0
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +25 -49
- package/src/core/tools/web-search/index.ts +132 -46
- package/src/core/tools/web-search/providers/anthropic.ts +7 -2
- package/src/core/tools/web-search/providers/exa.ts +2 -1
- package/src/core/tools/web-search/providers/perplexity.ts +6 -1
- package/src/core/tools/web-search/render.ts +6 -4
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +96 -14
- package/src/core/voice.ts +1 -1
- package/src/discovery/helpers.test.ts +1 -1
- package/src/index.ts +5 -16
- package/src/main.ts +5 -5
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +49 -0
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +93 -538
- package/src/modes/interactive/interactive-mode.ts +19 -7
- package/src/modes/interactive/theme/theme.ts +4 -4
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/task.md +0 -7
- package/src/prompts/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +6 -2
- package/src/prompts/tools/task.md +68 -0
- package/src/prompts/tools/web-fetch.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/utils/image-convert.ts +8 -2
- package/src/utils/image-magick.ts +247 -0
- package/src/utils/image-resize.ts +53 -13
- package/examples/custom-tools/question/index.ts +0 -84
- package/examples/custom-tools/subagent/README.md +0 -172
- package/examples/custom-tools/subagent/agents/planner.md +0 -37
- package/examples/custom-tools/subagent/agents/scout.md +0 -50
- package/examples/custom-tools/subagent/agents/worker.md +0 -24
- package/examples/custom-tools/subagent/agents.ts +0 -156
- package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
- package/examples/custom-tools/subagent/commands/implement.md +0 -10
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
- package/examples/custom-tools/subagent/index.ts +0 -1002
- package/examples/sdk/05-tools.ts +0 -94
- package/examples/sdk/12-full-control.ts +0 -95
- package/src/prompts/browser.md +0 -71
package/src/core/tools/output.ts
CHANGED
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import type { TextContent } from "@mariozechner/pi-ai";
|
|
9
10
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import type {
|
|
11
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
12
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
11
13
|
import { Type } from "@sinclair/typebox";
|
|
14
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
12
15
|
import outputDescription from "../../prompts/tools/output.md" with { type: "text" };
|
|
13
|
-
import type {
|
|
16
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
17
|
+
import type { ToolSession } from "./index";
|
|
18
|
+
import {
|
|
19
|
+
formatCount,
|
|
20
|
+
formatEmptyMessage,
|
|
21
|
+
formatExpandHint,
|
|
22
|
+
formatMeta,
|
|
23
|
+
formatMoreItems,
|
|
24
|
+
TRUNCATE_LENGTHS,
|
|
25
|
+
truncate,
|
|
26
|
+
} from "./render-utils";
|
|
14
27
|
import { getArtifactsDir } from "./task/artifacts";
|
|
15
28
|
|
|
16
29
|
const outputSchema = Type.Object({
|
|
@@ -23,6 +36,18 @@ const outputSchema = Type.Object({
|
|
|
23
36
|
description: "Output format: raw (default), json (structured), stripped (no ANSI)",
|
|
24
37
|
}),
|
|
25
38
|
),
|
|
39
|
+
offset: Type.Optional(
|
|
40
|
+
Type.Number({
|
|
41
|
+
description: "Line number to start reading from (1-indexed)",
|
|
42
|
+
minimum: 1,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
limit: Type.Optional(
|
|
46
|
+
Type.Number({
|
|
47
|
+
description: "Maximum number of lines to read",
|
|
48
|
+
minimum: 1,
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
26
51
|
});
|
|
27
52
|
|
|
28
53
|
/** Metadata for a single output file */
|
|
@@ -31,6 +56,12 @@ interface OutputProvenance {
|
|
|
31
56
|
index: number;
|
|
32
57
|
}
|
|
33
58
|
|
|
59
|
+
interface OutputRange {
|
|
60
|
+
startLine: number;
|
|
61
|
+
endLine: number;
|
|
62
|
+
totalLines: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
34
65
|
interface OutputEntry {
|
|
35
66
|
id: string;
|
|
36
67
|
path: string;
|
|
@@ -38,6 +69,7 @@ interface OutputEntry {
|
|
|
38
69
|
charCount: number;
|
|
39
70
|
provenance?: OutputProvenance;
|
|
40
71
|
previewLines?: string[];
|
|
72
|
+
range?: OutputRange;
|
|
41
73
|
}
|
|
42
74
|
|
|
43
75
|
export interface OutputToolDetails {
|
|
@@ -88,10 +120,7 @@ function extractPreviewLines(content: string, maxLines: number): string[] {
|
|
|
88
120
|
return preview;
|
|
89
121
|
}
|
|
90
122
|
|
|
91
|
-
export function createOutputTool(
|
|
92
|
-
_cwd: string,
|
|
93
|
-
sessionContext?: SessionContext,
|
|
94
|
-
): AgentTool<typeof outputSchema, OutputToolDetails> {
|
|
123
|
+
export function createOutputTool(session: ToolSession): AgentTool<typeof outputSchema, OutputToolDetails> {
|
|
95
124
|
return {
|
|
96
125
|
name: "output",
|
|
97
126
|
label: "Output",
|
|
@@ -99,9 +128,9 @@ export function createOutputTool(
|
|
|
99
128
|
parameters: outputSchema,
|
|
100
129
|
execute: async (
|
|
101
130
|
_toolCallId: string,
|
|
102
|
-
params: { ids: string[]; format?: "raw" | "json" | "stripped" },
|
|
131
|
+
params: { ids: string[]; format?: "raw" | "json" | "stripped"; offset?: number; limit?: number },
|
|
103
132
|
): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
|
|
104
|
-
const sessionFile =
|
|
133
|
+
const sessionFile = session.getSessionFile();
|
|
105
134
|
|
|
106
135
|
if (!sessionFile) {
|
|
107
136
|
return {
|
|
@@ -131,15 +160,37 @@ export function createOutputTool(
|
|
|
131
160
|
continue;
|
|
132
161
|
}
|
|
133
162
|
|
|
134
|
-
const
|
|
135
|
-
|
|
163
|
+
const rawContent = fs.readFileSync(outputPath, "utf-8");
|
|
164
|
+
const rawLines = rawContent.split("\n");
|
|
165
|
+
const totalLines = rawLines.length;
|
|
166
|
+
const totalChars = rawContent.length;
|
|
167
|
+
|
|
168
|
+
let selectedContent = rawContent;
|
|
169
|
+
let range: OutputRange | undefined;
|
|
170
|
+
|
|
171
|
+
if (params.offset !== undefined || params.limit !== undefined) {
|
|
172
|
+
const startLine = Math.max(1, params.offset ?? 1);
|
|
173
|
+
if (startLine > totalLines) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Offset ${params.offset ?? startLine} is beyond end of output (${totalLines} lines) for ${id}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const effectiveLimit = params.limit ?? totalLines - startLine + 1;
|
|
179
|
+
const endLine = Math.min(totalLines, startLine + effectiveLimit - 1);
|
|
180
|
+
const selectedLines = rawLines.slice(startLine - 1, endLine);
|
|
181
|
+
selectedContent = selectedLines.join("\n");
|
|
182
|
+
range = { startLine, endLine, totalLines };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
outputContentById.set(id, selectedContent);
|
|
136
186
|
outputs.push({
|
|
137
187
|
id,
|
|
138
188
|
path: outputPath,
|
|
139
|
-
lineCount:
|
|
140
|
-
charCount:
|
|
189
|
+
lineCount: totalLines,
|
|
190
|
+
charCount: totalChars,
|
|
141
191
|
provenance: parseOutputProvenance(id),
|
|
142
|
-
previewLines: extractPreviewLines(
|
|
192
|
+
previewLines: extractPreviewLines(selectedContent, 4),
|
|
193
|
+
range,
|
|
143
194
|
});
|
|
144
195
|
}
|
|
145
196
|
|
|
@@ -167,6 +218,7 @@ export function createOutputTool(
|
|
|
167
218
|
charCount: o.charCount,
|
|
168
219
|
provenance: o.provenance,
|
|
169
220
|
previewLines: o.previewLines,
|
|
221
|
+
range: o.range,
|
|
170
222
|
content: outputContentById.get(o.id) ?? "",
|
|
171
223
|
}));
|
|
172
224
|
contentText = JSON.stringify(jsonData, null, 2);
|
|
@@ -177,6 +229,10 @@ export function createOutputTool(
|
|
|
177
229
|
if (format === "stripped") {
|
|
178
230
|
content = stripAnsi(content);
|
|
179
231
|
}
|
|
232
|
+
if (o.range && o.range.endLine < o.range.totalLines) {
|
|
233
|
+
const nextOffset = o.range.endLine + 1;
|
|
234
|
+
content += `\n\n[Showing lines ${o.range.startLine}-${o.range.endLine} of ${o.range.totalLines}. Use offset=${nextOffset} to continue]`;
|
|
235
|
+
}
|
|
180
236
|
// Add header for multiple outputs
|
|
181
237
|
if (outputs.length > 1) {
|
|
182
238
|
return `=== ${o.id} (${o.lineCount} lines, ${formatBytes(o.charCount)}) ===\n${content}`;
|
|
@@ -194,5 +250,109 @@ export function createOutputTool(
|
|
|
194
250
|
};
|
|
195
251
|
}
|
|
196
252
|
|
|
197
|
-
|
|
198
|
-
|
|
253
|
+
// =============================================================================
|
|
254
|
+
// TUI Renderer
|
|
255
|
+
// =============================================================================
|
|
256
|
+
|
|
257
|
+
interface OutputRenderArgs {
|
|
258
|
+
ids: string[];
|
|
259
|
+
format?: "raw" | "json" | "stripped";
|
|
260
|
+
offset?: number;
|
|
261
|
+
limit?: number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type OutputEntryItem = OutputToolDetails["outputs"][number];
|
|
265
|
+
|
|
266
|
+
function formatOutputMeta(entry: OutputEntryItem, uiTheme: Theme): string {
|
|
267
|
+
const metaParts: string[] = [];
|
|
268
|
+
if (entry.range) {
|
|
269
|
+
metaParts.push(`lines ${entry.range.startLine}-${entry.range.endLine} of ${entry.range.totalLines}`);
|
|
270
|
+
} else {
|
|
271
|
+
metaParts.push(formatCount("line", entry.lineCount));
|
|
272
|
+
}
|
|
273
|
+
metaParts.push(formatBytes(entry.charCount));
|
|
274
|
+
if (entry.provenance) {
|
|
275
|
+
metaParts.push(`agent ${entry.provenance.agent}(${entry.provenance.index})`);
|
|
276
|
+
}
|
|
277
|
+
return uiTheme.fg("dim", metaParts.join(uiTheme.sep.dot));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const outputToolRenderer = {
|
|
281
|
+
renderCall(args: OutputRenderArgs, uiTheme: Theme): Component {
|
|
282
|
+
const ids = args.ids?.join(", ") ?? "?";
|
|
283
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Output"));
|
|
284
|
+
let text = `${label} ${uiTheme.fg("accent", ids)}`;
|
|
285
|
+
|
|
286
|
+
const meta: string[] = [];
|
|
287
|
+
if (args.format && args.format !== "raw") meta.push(`format:${args.format}`);
|
|
288
|
+
if (args.offset !== undefined) meta.push(`offset:${args.offset}`);
|
|
289
|
+
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
290
|
+
text += formatMeta(meta, uiTheme);
|
|
291
|
+
|
|
292
|
+
return new Text(text, 0, 0);
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
renderResult(
|
|
296
|
+
result: { content: Array<{ type: string; text?: string }>; details?: OutputToolDetails },
|
|
297
|
+
{ expanded }: RenderResultOptions,
|
|
298
|
+
uiTheme: Theme,
|
|
299
|
+
): Component {
|
|
300
|
+
const details = result.details;
|
|
301
|
+
|
|
302
|
+
if (details?.notFound?.length) {
|
|
303
|
+
const icon = uiTheme.styledSymbol("status.error", "error");
|
|
304
|
+
let text = `${icon} ${uiTheme.fg("error", `Error: Not found: ${details.notFound.join(", ")}`)}`;
|
|
305
|
+
if (details.availableIds?.length) {
|
|
306
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", `Available: ${details.availableIds.join(", ")}`)}`;
|
|
307
|
+
} else {
|
|
308
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "No outputs available in current session")}`;
|
|
309
|
+
}
|
|
310
|
+
return new Text(text, 0, 0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const outputs = details?.outputs ?? [];
|
|
314
|
+
|
|
315
|
+
if (outputs.length === 0) {
|
|
316
|
+
const textContent = result.content?.find((c) => c.type === "text")?.text;
|
|
317
|
+
return new Text(formatEmptyMessage(textContent || "No outputs", uiTheme), 0, 0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
321
|
+
const summary = `read ${formatCount("output", outputs.length)}`;
|
|
322
|
+
const previewLimit = expanded ? 3 : 1;
|
|
323
|
+
const maxOutputs = expanded ? outputs.length : Math.min(outputs.length, 5);
|
|
324
|
+
const hasMoreOutputs = outputs.length > maxOutputs;
|
|
325
|
+
const hasMorePreview = outputs.some((o) => (o.previewLines?.length ?? 0) > previewLimit);
|
|
326
|
+
const expandHint = formatExpandHint(expanded, hasMoreOutputs || hasMorePreview, uiTheme);
|
|
327
|
+
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < maxOutputs; i++) {
|
|
330
|
+
const o = outputs[i];
|
|
331
|
+
const isLast = i === maxOutputs - 1 && !hasMoreOutputs;
|
|
332
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
333
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", o.id)} ${formatOutputMeta(o, uiTheme)}`;
|
|
334
|
+
|
|
335
|
+
const previewLines = o.previewLines ?? [];
|
|
336
|
+
const shownPreview = previewLines.slice(0, previewLimit);
|
|
337
|
+
if (shownPreview.length > 0) {
|
|
338
|
+
const childPrefix = isLast ? " " : ` ${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
|
|
339
|
+
for (const line of shownPreview) {
|
|
340
|
+
const previewText = truncate(line, TRUNCATE_LENGTHS.CONTENT, uiTheme.format.ellipsis);
|
|
341
|
+
text += `\n${childPrefix}${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
342
|
+
"muted",
|
|
343
|
+
"preview:",
|
|
344
|
+
)} ${uiTheme.fg("toolOutput", previewText)}`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (hasMoreOutputs) {
|
|
350
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
351
|
+
"muted",
|
|
352
|
+
formatMoreItems(outputs.length - maxOutputs, "output", uiTheme),
|
|
353
|
+
)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return new Text(text, 0, 0);
|
|
357
|
+
},
|
|
358
|
+
};
|
package/src/core/tools/read.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
3
4
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
-
import type {
|
|
5
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
7
|
import { Type } from "@sinclair/typebox";
|
|
6
8
|
import { globSync } from "glob";
|
|
9
|
+
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme";
|
|
7
10
|
import readDescription from "../../prompts/tools/read.md" with { type: "text" };
|
|
8
11
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
|
|
9
12
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
10
13
|
import { ensureTool } from "../../utils/tools-manager";
|
|
14
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
15
|
+
import type { ToolSession } from "../sdk";
|
|
11
16
|
import { untilAborted } from "../utils";
|
|
12
17
|
import { createLsTool } from "./ls";
|
|
13
18
|
import { resolveReadPath, resolveToCwd } from "./path-utils";
|
|
14
|
-
import {
|
|
19
|
+
import { replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_MAX_BYTES,
|
|
22
|
+
DEFAULT_MAX_LINES,
|
|
23
|
+
formatSize,
|
|
24
|
+
type TruncationResult,
|
|
25
|
+
truncateHead,
|
|
26
|
+
truncateStringToBytesFromStart,
|
|
27
|
+
} from "./truncate";
|
|
15
28
|
|
|
16
29
|
// Document types convertible via markitdown
|
|
17
30
|
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
@@ -328,14 +341,9 @@ export interface ReadToolDetails {
|
|
|
328
341
|
redirectedTo?: "ls";
|
|
329
342
|
}
|
|
330
343
|
|
|
331
|
-
export
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
|
|
337
|
-
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
338
|
-
const lsTool = createLsTool(cwd);
|
|
344
|
+
export function createReadTool(session: ToolSession): AgentTool<typeof readSchema> {
|
|
345
|
+
const autoResizeImages = session.settings?.getImageAutoResize() ?? true;
|
|
346
|
+
const lsTool = createLsTool(session);
|
|
339
347
|
return {
|
|
340
348
|
name: "read",
|
|
341
349
|
label: "Read",
|
|
@@ -346,7 +354,7 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
|
|
|
346
354
|
{ path: readPath, offset, limit }: { path: string; offset?: number; limit?: number },
|
|
347
355
|
signal?: AbortSignal,
|
|
348
356
|
) => {
|
|
349
|
-
const absolutePath = resolveReadPath(readPath, cwd);
|
|
357
|
+
const absolutePath = resolveReadPath(readPath, session.cwd);
|
|
350
358
|
|
|
351
359
|
return untilAborted(signal, async () => {
|
|
352
360
|
let isDirectory = false;
|
|
@@ -366,14 +374,12 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
|
|
|
366
374
|
}
|
|
367
375
|
} catch (error) {
|
|
368
376
|
if (isNotFoundError(error)) {
|
|
369
|
-
const suggestions = await findReadPathSuggestions(readPath, cwd);
|
|
377
|
+
const suggestions = await findReadPathSuggestions(readPath, session.cwd);
|
|
370
378
|
let message = `File not found: ${readPath}`;
|
|
371
379
|
|
|
372
380
|
if (suggestions?.suggestions.length) {
|
|
373
381
|
const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
|
|
374
|
-
message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions
|
|
375
|
-
.map((match) => `- ${match}`)
|
|
376
|
-
.join("\n")}`;
|
|
382
|
+
message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map((match) => `- ${match}`).join("\n")}`;
|
|
377
383
|
if (suggestions.truncated) {
|
|
378
384
|
message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
|
|
379
385
|
}
|
|
@@ -450,9 +456,7 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
|
|
|
450
456
|
let outputText = truncation.content;
|
|
451
457
|
|
|
452
458
|
if (truncation.truncated) {
|
|
453
|
-
outputText += `\n\n[Document converted via markitdown. Output truncated to $formatSize(
|
|
454
|
-
DEFAULT_MAX_BYTES,
|
|
455
|
-
)]`;
|
|
459
|
+
outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(DEFAULT_MAX_BYTES)}]`;
|
|
456
460
|
details = { truncation };
|
|
457
461
|
}
|
|
458
462
|
|
|
@@ -498,11 +502,21 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
|
|
|
498
502
|
let outputText: string;
|
|
499
503
|
|
|
500
504
|
if (truncation.firstLineExceedsLimit) {
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
505
|
+
const firstLine = allLines[startLine] ?? "";
|
|
506
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
507
|
+
const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
|
|
508
|
+
const shownSize = formatSize(snippet.bytes);
|
|
509
|
+
|
|
510
|
+
outputText = snippet.text;
|
|
511
|
+
if (outputText.length > 0) {
|
|
512
|
+
outputText += `\n\n[Line ${startLineDisplay} is ${formatSize(
|
|
513
|
+
firstLineBytes,
|
|
514
|
+
)}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Showing first ${shownSize} of the line.]`;
|
|
515
|
+
} else {
|
|
516
|
+
outputText = `[Line ${startLineDisplay} is ${formatSize(
|
|
517
|
+
firstLineBytes,
|
|
518
|
+
)}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
519
|
+
}
|
|
506
520
|
details = { truncation };
|
|
507
521
|
} else if (truncation.truncated) {
|
|
508
522
|
// Truncation occurred - build actionable notice
|
|
@@ -540,5 +554,93 @@ export function createReadTool(cwd: string, options?: ReadToolOptions): AgentToo
|
|
|
540
554
|
};
|
|
541
555
|
}
|
|
542
556
|
|
|
543
|
-
|
|
544
|
-
|
|
557
|
+
// =============================================================================
|
|
558
|
+
// TUI Renderer
|
|
559
|
+
// =============================================================================
|
|
560
|
+
|
|
561
|
+
interface ReadRenderArgs {
|
|
562
|
+
path?: string;
|
|
563
|
+
file_path?: string;
|
|
564
|
+
offset?: number;
|
|
565
|
+
limit?: number;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "tiff"]);
|
|
569
|
+
const BINARY_EXTENSIONS = new Set(["pdf", "zip", "tar", "gz", "exe", "dll", "so", "dylib", "wasm"]);
|
|
570
|
+
|
|
571
|
+
function getFileType(filePath: string): "image" | "binary" | "text" {
|
|
572
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
573
|
+
if (!ext) return "text";
|
|
574
|
+
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
575
|
+
if (BINARY_EXTENSIONS.has(ext)) return "binary";
|
|
576
|
+
return "text";
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export const readToolRenderer = {
|
|
580
|
+
renderCall(args: ReadRenderArgs, uiTheme: Theme): Component {
|
|
581
|
+
const rawPath = args.file_path || args.path || "";
|
|
582
|
+
const filePath = shortenPath(rawPath);
|
|
583
|
+
const offset = args.offset;
|
|
584
|
+
const limit = args.limit;
|
|
585
|
+
|
|
586
|
+
let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", uiTheme.format.ellipsis);
|
|
587
|
+
if (offset !== undefined || limit !== undefined) {
|
|
588
|
+
const startLine = offset ?? 1;
|
|
589
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
590
|
+
pathDisplay += uiTheme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const text = `${uiTheme.fg("toolTitle", uiTheme.bold("Read"))} ${pathDisplay}`;
|
|
594
|
+
return new Text(text, 0, 0);
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
renderResult(
|
|
598
|
+
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
|
|
599
|
+
{ expanded }: RenderResultOptions,
|
|
600
|
+
uiTheme: Theme,
|
|
601
|
+
args?: ReadRenderArgs,
|
|
602
|
+
): Component {
|
|
603
|
+
const rawPath = args?.file_path || args?.path || "";
|
|
604
|
+
const fileType = getFileType(rawPath);
|
|
605
|
+
const details = result.details;
|
|
606
|
+
const lines: string[] = [];
|
|
607
|
+
|
|
608
|
+
const output = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
609
|
+
|
|
610
|
+
if (fileType === "image") {
|
|
611
|
+
lines.push(uiTheme.fg("muted", "Image rendered below"));
|
|
612
|
+
} else if (fileType === "binary") {
|
|
613
|
+
// Binary files just show the header from renderCall
|
|
614
|
+
} else {
|
|
615
|
+
// Text file
|
|
616
|
+
const lang = getLanguageFromPath(rawPath);
|
|
617
|
+
const contentLines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
|
|
618
|
+
|
|
619
|
+
if (expanded) {
|
|
620
|
+
lines.push(
|
|
621
|
+
...contentLines.map((line: string) =>
|
|
622
|
+
lang ? replaceTabs(line) : uiTheme.fg("toolOutput", replaceTabs(line)),
|
|
623
|
+
),
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
lines.push(uiTheme.fg("dim", `${uiTheme.nav.expand} Ctrl+O to show content`));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Truncation warning
|
|
630
|
+
const truncation = details?.truncation;
|
|
631
|
+
if (truncation?.truncated) {
|
|
632
|
+
let warning: string;
|
|
633
|
+
if (truncation.firstLineExceedsLimit) {
|
|
634
|
+
warning = `First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
|
|
635
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
636
|
+
warning = `Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)`;
|
|
637
|
+
} else {
|
|
638
|
+
warning = `Truncated: ${truncation.outputLines} lines (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`;
|
|
639
|
+
}
|
|
640
|
+
lines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
645
|
+
},
|
|
646
|
+
};
|