@oh-my-pi/pi-coding-agent 3.20.1 → 3.21.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 +69 -9
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +3 -3
- package/examples/custom-tools/README.md +2 -2
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +1 -1
- 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/12-full-control.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +13 -2
- 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/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- 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 +33 -4
- package/src/core/session-manager.ts +11 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +27 -3
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +88 -1
- package/src/core/tools/bash-interceptor.ts +7 -0
- package/src/core/tools/bash.ts +106 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +214 -20
- package/src/core/tools/find.ts +155 -0
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +4 -0
- package/src/core/tools/grep.ts +191 -0
- package/src/core/tools/index.ts +3 -6
- package/src/core/tools/ls.ts +134 -1
- package/src/core/tools/lsp/render.ts +34 -14
- package/src/core/tools/notebook.ts +110 -0
- package/src/core/tools/output.ts +179 -7
- package/src/core/tools/read.ts +122 -9
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -7
- package/src/core/tools/rulebook.ts +3 -1
- package/src/core/tools/task/index.ts +18 -3
- package/src/core/tools/task/render.ts +5 -0
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +23 -15
- package/src/core/tools/web-search/index.ts +130 -45
- 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 +5 -0
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +90 -0
- package/src/core/voice.ts +1 -1
- package/src/main.ts +1 -1
- 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/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/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +4 -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/src/core/tools/ls.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import nodePath from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
|
|
8
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
5
9
|
import { untilAborted } from "../utils";
|
|
6
10
|
import { resolveToCwd } from "./path-utils";
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
formatAge,
|
|
13
|
+
formatBytes,
|
|
14
|
+
formatCount,
|
|
15
|
+
formatEmptyMessage,
|
|
16
|
+
formatExpandHint,
|
|
17
|
+
formatMeta,
|
|
18
|
+
formatMoreItems,
|
|
19
|
+
formatTruncationSuffix,
|
|
20
|
+
PREVIEW_LIMITS,
|
|
21
|
+
} from "./render-utils";
|
|
8
22
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
9
23
|
|
|
10
24
|
const lsSchema = Type.Object({
|
|
@@ -147,3 +161,122 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
|
|
|
147
161
|
|
|
148
162
|
/** Default ls tool using process.cwd() - for backwards compatibility */
|
|
149
163
|
export const lsTool = createLsTool(process.cwd());
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// TUI Renderer
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
interface LsRenderArgs {
|
|
170
|
+
path?: string;
|
|
171
|
+
limit?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
175
|
+
|
|
176
|
+
export const lsToolRenderer = {
|
|
177
|
+
renderCall(args: LsRenderArgs, uiTheme: Theme): Component {
|
|
178
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Ls"));
|
|
179
|
+
let text = `${label} ${uiTheme.fg("accent", args.path || ".")}`;
|
|
180
|
+
|
|
181
|
+
const meta: string[] = [];
|
|
182
|
+
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
183
|
+
text += formatMeta(meta, uiTheme);
|
|
184
|
+
|
|
185
|
+
return new Text(text, 0, 0);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
renderResult(
|
|
189
|
+
result: { content: Array<{ type: string; text?: string }>; details?: LsToolDetails },
|
|
190
|
+
{ expanded }: RenderResultOptions,
|
|
191
|
+
uiTheme: Theme,
|
|
192
|
+
): Component {
|
|
193
|
+
const details = result.details;
|
|
194
|
+
const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
(!textContent || textContent.trim() === "" || textContent.trim() === "(empty directory)") &&
|
|
198
|
+
(!details?.entries || details.entries.length === 0)
|
|
199
|
+
) {
|
|
200
|
+
return new Text(formatEmptyMessage("Empty directory", uiTheme), 0, 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let entries: string[] = details?.entries ? [...details.entries] : [];
|
|
204
|
+
if (entries.length === 0) {
|
|
205
|
+
const rawLines = textContent.split("\n").filter((l: string) => l.trim());
|
|
206
|
+
entries = rawLines.filter((line) => !/^\[.*\]$/.test(line.trim()));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (entries.length === 0) {
|
|
210
|
+
return new Text(formatEmptyMessage("Empty directory", uiTheme), 0, 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let dirCount = details?.dirCount;
|
|
214
|
+
let fileCount = details?.fileCount;
|
|
215
|
+
if (dirCount === undefined || fileCount === undefined) {
|
|
216
|
+
dirCount = 0;
|
|
217
|
+
fileCount = 0;
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
if (entry.endsWith("/")) {
|
|
220
|
+
dirCount += 1;
|
|
221
|
+
} else {
|
|
222
|
+
fileCount += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const truncated = Boolean(details?.truncation?.truncated || details?.entryLimitReached);
|
|
228
|
+
const icon = truncated
|
|
229
|
+
? uiTheme.styledSymbol("status.warning", "warning")
|
|
230
|
+
: uiTheme.styledSymbol("status.success", "success");
|
|
231
|
+
|
|
232
|
+
const summaryText = [formatCount("dir", dirCount ?? 0), formatCount("file", fileCount ?? 0)].join(
|
|
233
|
+
uiTheme.sep.dot,
|
|
234
|
+
);
|
|
235
|
+
const maxEntries = expanded ? entries.length : Math.min(entries.length, COLLAPSED_LIST_LIMIT);
|
|
236
|
+
const hasMoreEntries = entries.length > maxEntries;
|
|
237
|
+
const expandHint = formatExpandHint(expanded, hasMoreEntries, uiTheme);
|
|
238
|
+
|
|
239
|
+
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${expandHint}`;
|
|
240
|
+
|
|
241
|
+
const truncationReasons: string[] = [];
|
|
242
|
+
if (details?.entryLimitReached) {
|
|
243
|
+
truncationReasons.push(`entry limit ${details.entryLimitReached}`);
|
|
244
|
+
}
|
|
245
|
+
if (details?.truncation?.truncated) {
|
|
246
|
+
truncationReasons.push(`output cap ${formatBytes(details.truncation.maxBytes)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasTruncation = truncationReasons.length > 0;
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < maxEntries; i++) {
|
|
252
|
+
const entry = entries[i];
|
|
253
|
+
const isLast = i === maxEntries - 1 && !hasMoreEntries && !hasTruncation;
|
|
254
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
255
|
+
const isDir = entry.endsWith("/");
|
|
256
|
+
const entryPath = isDir ? entry.slice(0, -1) : entry;
|
|
257
|
+
const lang = isDir ? undefined : getLanguageFromPath(entryPath);
|
|
258
|
+
const entryIcon = isDir
|
|
259
|
+
? uiTheme.fg("accent", uiTheme.icon.folder)
|
|
260
|
+
: uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
261
|
+
const entryColor = isDir ? "accent" : "toolOutput";
|
|
262
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg(entryColor, entry)}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (hasMoreEntries) {
|
|
266
|
+
const moreEntriesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
267
|
+
text += `\n ${uiTheme.fg("dim", moreEntriesBranch)} ${uiTheme.fg(
|
|
268
|
+
"muted",
|
|
269
|
+
formatMoreItems(entries.length - maxEntries, "entry", uiTheme),
|
|
270
|
+
)}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (hasTruncation) {
|
|
274
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
275
|
+
"warning",
|
|
276
|
+
`truncated: ${truncationReasons.join(", ")}`,
|
|
277
|
+
)}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return new Text(text, 0, 0);
|
|
281
|
+
},
|
|
282
|
+
};
|
|
@@ -242,7 +242,10 @@ function renderDiagnostics(
|
|
|
242
242
|
`[${item.severity}]`,
|
|
243
243
|
)}`;
|
|
244
244
|
if (item.message) {
|
|
245
|
-
output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg(
|
|
245
|
+
output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg(
|
|
246
|
+
"muted",
|
|
247
|
+
truncate(item.message, TRUNCATE_LENGTHS.LINE, theme.format.ellipsis),
|
|
248
|
+
)}`;
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
251
|
return new Text(output, 0, 0);
|
|
@@ -271,7 +274,10 @@ function renderDiagnostics(
|
|
|
271
274
|
output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)}${message}`;
|
|
272
275
|
}
|
|
273
276
|
if (remaining > 0) {
|
|
274
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
277
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
278
|
+
"muted",
|
|
279
|
+
`${theme.format.ellipsis} ${remaining} more`,
|
|
280
|
+
)}`;
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
return new Text(output, 0, 0);
|
|
@@ -436,10 +442,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
436
442
|
const isLast = isLastSibling(i);
|
|
437
443
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
438
444
|
const detailPrefix = isLast ? " " : `${theme.tree.vertical} `;
|
|
439
|
-
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg(
|
|
440
|
-
"accent",
|
|
441
|
-
sym.name,
|
|
442
|
-
)}`;
|
|
445
|
+
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg("accent", sym.name)}`;
|
|
443
446
|
output += `\n${prefix}${theme.fg("dim", detailPrefix)}${theme.fg("muted", `line ${sym.line}`)}`;
|
|
444
447
|
}
|
|
445
448
|
return new Text(output, 0, 0);
|
|
@@ -454,13 +457,16 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
454
457
|
const sym = topLevel[i];
|
|
455
458
|
const isLast = i === topLevel.length - 1 && topLevelCount <= 3;
|
|
456
459
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
457
|
-
output += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg(
|
|
458
|
-
"
|
|
459
|
-
sym.
|
|
460
|
-
)}
|
|
460
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg("accent", sym.name)} ${theme.fg(
|
|
461
|
+
"muted",
|
|
462
|
+
`line ${sym.line}`,
|
|
463
|
+
)}`;
|
|
461
464
|
}
|
|
462
465
|
if (topLevelCount > 3) {
|
|
463
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
466
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
467
|
+
"muted",
|
|
468
|
+
`${theme.format.ellipsis} ${topLevelCount - 3} more`,
|
|
469
|
+
)}`;
|
|
464
470
|
}
|
|
465
471
|
|
|
466
472
|
return new Text(output, 0, 0);
|
|
@@ -496,17 +502,26 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
496
502
|
|
|
497
503
|
const firstLine = lines[0] || "No output";
|
|
498
504
|
const expandHint = formatExpandHint(false, lines.length > 1, theme);
|
|
499
|
-
let output = `${icon} ${theme.fg(
|
|
505
|
+
let output = `${icon} ${theme.fg(
|
|
506
|
+
"dim",
|
|
507
|
+
truncate(firstLine, TRUNCATE_LENGTHS.TITLE, theme.format.ellipsis),
|
|
508
|
+
)}${expandHint}`;
|
|
500
509
|
|
|
501
510
|
if (lines.length > 1) {
|
|
502
511
|
const previewLines = lines.slice(1, 4);
|
|
503
512
|
for (let i = 0; i < previewLines.length; i++) {
|
|
504
513
|
const isLast = i === previewLines.length - 1 && lines.length <= 4;
|
|
505
514
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
506
|
-
output += `\n ${theme.fg("dim", branch)} ${theme.fg(
|
|
515
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg(
|
|
516
|
+
"dim",
|
|
517
|
+
truncate(previewLines[i].trim(), TRUNCATE_LENGTHS.CONTENT, theme.format.ellipsis),
|
|
518
|
+
)}`;
|
|
507
519
|
}
|
|
508
520
|
if (lines.length > 4) {
|
|
509
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
521
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
522
|
+
"muted",
|
|
523
|
+
formatMoreItems(lines.length - 4, "line", theme),
|
|
524
|
+
)}`;
|
|
510
525
|
}
|
|
511
526
|
}
|
|
512
527
|
|
|
@@ -550,3 +565,8 @@ function severityToColor(severity: string): "error" | "warning" | "accent" | "di
|
|
|
550
565
|
return "dim";
|
|
551
566
|
}
|
|
552
567
|
}
|
|
568
|
+
|
|
569
|
+
export const lspToolRenderer = {
|
|
570
|
+
renderCall,
|
|
571
|
+
renderResult,
|
|
572
|
+
};
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
2
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
6
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
3
7
|
import { untilAborted } from "../utils";
|
|
4
8
|
import { resolveToCwd } from "./path-utils";
|
|
9
|
+
import {
|
|
10
|
+
formatCount,
|
|
11
|
+
formatErrorMessage,
|
|
12
|
+
formatExpandHint,
|
|
13
|
+
formatMeta,
|
|
14
|
+
formatMoreItems,
|
|
15
|
+
PREVIEW_LIMITS,
|
|
16
|
+
} from "./render-utils";
|
|
5
17
|
|
|
6
18
|
const notebookSchema = Type.Object({
|
|
7
19
|
action: Type.Union([Type.Literal("edit"), Type.Literal("insert"), Type.Literal("delete")], {
|
|
@@ -180,3 +192,101 @@ export function createNotebookTool(cwd: string): AgentTool<typeof notebookSchema
|
|
|
180
192
|
|
|
181
193
|
/** Default notebook tool using process.cwd() */
|
|
182
194
|
export const notebookTool = createNotebookTool(process.cwd());
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// TUI Renderer
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
interface NotebookRenderArgs {
|
|
201
|
+
action: string;
|
|
202
|
+
notebookPath: string;
|
|
203
|
+
cellNumber?: number;
|
|
204
|
+
cellType?: string;
|
|
205
|
+
content?: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
209
|
+
|
|
210
|
+
function normalizeCellLines(lines: string[]): string[] {
|
|
211
|
+
return lines.map((line) => (line.endsWith("\n") ? line.slice(0, -1) : line));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderCellPreview(lines: string[], expanded: boolean, uiTheme: Theme): string {
|
|
215
|
+
const normalized = normalizeCellLines(lines);
|
|
216
|
+
if (normalized.length === 0) {
|
|
217
|
+
return `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", "(empty cell)")}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const maxLines = expanded ? normalized.length : Math.min(normalized.length, COLLAPSED_TEXT_LIMIT);
|
|
221
|
+
let text = "";
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < maxLines; i++) {
|
|
224
|
+
const isLast = i === maxLines - 1 && (expanded || normalized.length <= maxLines);
|
|
225
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
226
|
+
const line = normalized[i];
|
|
227
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", line)}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const remaining = normalized.length - maxLines;
|
|
231
|
+
if (remaining > 0) {
|
|
232
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
233
|
+
"muted",
|
|
234
|
+
formatMoreItems(remaining, "line", uiTheme),
|
|
235
|
+
)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return text;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const notebookToolRenderer = {
|
|
242
|
+
renderCall(args: NotebookRenderArgs, uiTheme: Theme): Component {
|
|
243
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Notebook"));
|
|
244
|
+
let text = `${label} ${uiTheme.fg("accent", args.action || "?")}`;
|
|
245
|
+
|
|
246
|
+
const meta: string[] = [];
|
|
247
|
+
meta.push(`in ${args.notebookPath || "?"}`);
|
|
248
|
+
if (args.cellNumber !== undefined) meta.push(`cell:${args.cellNumber}`);
|
|
249
|
+
if (args.cellType) meta.push(`type:${args.cellType}`);
|
|
250
|
+
|
|
251
|
+
text += formatMeta(meta, uiTheme);
|
|
252
|
+
|
|
253
|
+
return new Text(text, 0, 0);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
renderResult(
|
|
257
|
+
result: { content: Array<{ type: string; text?: string }>; details?: NotebookToolDetails },
|
|
258
|
+
{ expanded }: RenderResultOptions,
|
|
259
|
+
uiTheme: Theme,
|
|
260
|
+
): Component {
|
|
261
|
+
const content = result.content?.[0];
|
|
262
|
+
if (content?.type === "text" && content.text?.startsWith("Error:")) {
|
|
263
|
+
return new Text(formatErrorMessage(content.text, uiTheme), 0, 0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const details = result.details;
|
|
267
|
+
const action = details?.action ?? "edit";
|
|
268
|
+
const cellIndex = details?.cellIndex;
|
|
269
|
+
const cellType = details?.cellType;
|
|
270
|
+
const totalCells = details?.totalCells;
|
|
271
|
+
const cellSource = details?.cellSource;
|
|
272
|
+
const lineCount = cellSource?.length;
|
|
273
|
+
const canExpand = cellSource !== undefined && cellSource.length > COLLAPSED_TEXT_LIMIT;
|
|
274
|
+
|
|
275
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
276
|
+
const actionLabel = action === "insert" ? "Inserted" : action === "delete" ? "Deleted" : "Edited";
|
|
277
|
+
const cellLabel = cellType || "cell";
|
|
278
|
+
const summaryParts = [`${actionLabel} ${cellLabel} at index ${cellIndex ?? "?"}`];
|
|
279
|
+
if (lineCount !== undefined) summaryParts.push(formatCount("line", lineCount));
|
|
280
|
+
if (totalCells !== undefined) summaryParts.push(`${totalCells} total`);
|
|
281
|
+
const summaryText = summaryParts.join(uiTheme.sep.dot);
|
|
282
|
+
|
|
283
|
+
const expandHint = formatExpandHint(expanded, canExpand, uiTheme);
|
|
284
|
+
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${expandHint}`;
|
|
285
|
+
|
|
286
|
+
if (cellSource) {
|
|
287
|
+
text += renderCellPreview(cellSource, expanded, uiTheme);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return new Text(text, 0, 0);
|
|
291
|
+
},
|
|
292
|
+
};
|
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" };
|
|
16
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
13
17
|
import type { SessionContext } 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 {
|
|
@@ -99,7 +131,7 @@ export function createOutputTool(
|
|
|
99
131
|
parameters: outputSchema,
|
|
100
132
|
execute: async (
|
|
101
133
|
_toolCallId: string,
|
|
102
|
-
params: { ids: string[]; format?: "raw" | "json" | "stripped" },
|
|
134
|
+
params: { ids: string[]; format?: "raw" | "json" | "stripped"; offset?: number; limit?: number },
|
|
103
135
|
): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
|
|
104
136
|
const sessionFile = sessionContext?.getSessionFile();
|
|
105
137
|
|
|
@@ -131,15 +163,37 @@ export function createOutputTool(
|
|
|
131
163
|
continue;
|
|
132
164
|
}
|
|
133
165
|
|
|
134
|
-
const
|
|
135
|
-
|
|
166
|
+
const rawContent = fs.readFileSync(outputPath, "utf-8");
|
|
167
|
+
const rawLines = rawContent.split("\n");
|
|
168
|
+
const totalLines = rawLines.length;
|
|
169
|
+
const totalChars = rawContent.length;
|
|
170
|
+
|
|
171
|
+
let selectedContent = rawContent;
|
|
172
|
+
let range: OutputRange | undefined;
|
|
173
|
+
|
|
174
|
+
if (params.offset !== undefined || params.limit !== undefined) {
|
|
175
|
+
const startLine = Math.max(1, params.offset ?? 1);
|
|
176
|
+
if (startLine > totalLines) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Offset ${params.offset ?? startLine} is beyond end of output (${totalLines} lines) for ${id}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const effectiveLimit = params.limit ?? totalLines - startLine + 1;
|
|
182
|
+
const endLine = Math.min(totalLines, startLine + effectiveLimit - 1);
|
|
183
|
+
const selectedLines = rawLines.slice(startLine - 1, endLine);
|
|
184
|
+
selectedContent = selectedLines.join("\n");
|
|
185
|
+
range = { startLine, endLine, totalLines };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
outputContentById.set(id, selectedContent);
|
|
136
189
|
outputs.push({
|
|
137
190
|
id,
|
|
138
191
|
path: outputPath,
|
|
139
|
-
lineCount:
|
|
140
|
-
charCount:
|
|
192
|
+
lineCount: totalLines,
|
|
193
|
+
charCount: totalChars,
|
|
141
194
|
provenance: parseOutputProvenance(id),
|
|
142
|
-
previewLines: extractPreviewLines(
|
|
195
|
+
previewLines: extractPreviewLines(selectedContent, 4),
|
|
196
|
+
range,
|
|
143
197
|
});
|
|
144
198
|
}
|
|
145
199
|
|
|
@@ -167,6 +221,7 @@ export function createOutputTool(
|
|
|
167
221
|
charCount: o.charCount,
|
|
168
222
|
provenance: o.provenance,
|
|
169
223
|
previewLines: o.previewLines,
|
|
224
|
+
range: o.range,
|
|
170
225
|
content: outputContentById.get(o.id) ?? "",
|
|
171
226
|
}));
|
|
172
227
|
contentText = JSON.stringify(jsonData, null, 2);
|
|
@@ -177,6 +232,10 @@ export function createOutputTool(
|
|
|
177
232
|
if (format === "stripped") {
|
|
178
233
|
content = stripAnsi(content);
|
|
179
234
|
}
|
|
235
|
+
if (o.range && o.range.endLine < o.range.totalLines) {
|
|
236
|
+
const nextOffset = o.range.endLine + 1;
|
|
237
|
+
content += `\n\n[Showing lines ${o.range.startLine}-${o.range.endLine} of ${o.range.totalLines}. Use offset=${nextOffset} to continue]`;
|
|
238
|
+
}
|
|
180
239
|
// Add header for multiple outputs
|
|
181
240
|
if (outputs.length > 1) {
|
|
182
241
|
return `=== ${o.id} (${o.lineCount} lines, ${formatBytes(o.charCount)}) ===\n${content}`;
|
|
@@ -196,3 +255,116 @@ export function createOutputTool(
|
|
|
196
255
|
|
|
197
256
|
/** Default output tool using process.cwd() - for backwards compatibility */
|
|
198
257
|
export const outputTool = createOutputTool(process.cwd());
|
|
258
|
+
|
|
259
|
+
// =============================================================================
|
|
260
|
+
// TUI Renderer
|
|
261
|
+
// =============================================================================
|
|
262
|
+
|
|
263
|
+
interface OutputRenderArgs {
|
|
264
|
+
ids: string[];
|
|
265
|
+
format?: "raw" | "json" | "stripped";
|
|
266
|
+
offset?: number;
|
|
267
|
+
limit?: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
type OutputEntryItem = OutputToolDetails["outputs"][number];
|
|
271
|
+
|
|
272
|
+
function formatOutputMeta(entry: OutputEntryItem, uiTheme: Theme): string {
|
|
273
|
+
const metaParts: string[] = [];
|
|
274
|
+
if (entry.range) {
|
|
275
|
+
metaParts.push(`lines ${entry.range.startLine}-${entry.range.endLine} of ${entry.range.totalLines}`);
|
|
276
|
+
} else {
|
|
277
|
+
metaParts.push(formatCount("line", entry.lineCount));
|
|
278
|
+
}
|
|
279
|
+
metaParts.push(formatBytes(entry.charCount));
|
|
280
|
+
if (entry.provenance) {
|
|
281
|
+
metaParts.push(`agent ${entry.provenance.agent}(${entry.provenance.index})`);
|
|
282
|
+
}
|
|
283
|
+
return uiTheme.fg("dim", metaParts.join(uiTheme.sep.dot));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const outputToolRenderer = {
|
|
287
|
+
renderCall(args: OutputRenderArgs, uiTheme: Theme): Component {
|
|
288
|
+
const ids = args.ids?.join(", ") ?? "?";
|
|
289
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Output"));
|
|
290
|
+
let text = `${label} ${uiTheme.fg("accent", ids)}`;
|
|
291
|
+
|
|
292
|
+
const meta: string[] = [];
|
|
293
|
+
if (args.format && args.format !== "raw") meta.push(`format:${args.format}`);
|
|
294
|
+
if (args.offset !== undefined) meta.push(`offset:${args.offset}`);
|
|
295
|
+
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
296
|
+
text += formatMeta(meta, uiTheme);
|
|
297
|
+
|
|
298
|
+
return new Text(text, 0, 0);
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
renderResult(
|
|
302
|
+
result: { content: Array<{ type: string; text?: string }>; details?: OutputToolDetails },
|
|
303
|
+
{ expanded }: RenderResultOptions,
|
|
304
|
+
uiTheme: Theme,
|
|
305
|
+
): Component {
|
|
306
|
+
const details = result.details;
|
|
307
|
+
|
|
308
|
+
if (details?.notFound?.length) {
|
|
309
|
+
const icon = uiTheme.styledSymbol("status.error", "error");
|
|
310
|
+
let text = `${icon} ${uiTheme.fg("error", `Error: Not found: ${details.notFound.join(", ")}`)}`;
|
|
311
|
+
if (details.availableIds?.length) {
|
|
312
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
313
|
+
"muted",
|
|
314
|
+
`Available: ${details.availableIds.join(", ")}`,
|
|
315
|
+
)}`;
|
|
316
|
+
} else {
|
|
317
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
318
|
+
"muted",
|
|
319
|
+
"No outputs available in current session",
|
|
320
|
+
)}`;
|
|
321
|
+
}
|
|
322
|
+
return new Text(text, 0, 0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const outputs = details?.outputs ?? [];
|
|
326
|
+
|
|
327
|
+
if (outputs.length === 0) {
|
|
328
|
+
const textContent = result.content?.find((c) => c.type === "text")?.text;
|
|
329
|
+
return new Text(formatEmptyMessage(textContent || "No outputs", uiTheme), 0, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
333
|
+
const summary = `read ${formatCount("output", outputs.length)}`;
|
|
334
|
+
const previewLimit = expanded ? 3 : 1;
|
|
335
|
+
const maxOutputs = expanded ? outputs.length : Math.min(outputs.length, 5);
|
|
336
|
+
const hasMoreOutputs = outputs.length > maxOutputs;
|
|
337
|
+
const hasMorePreview = outputs.some((o) => (o.previewLines?.length ?? 0) > previewLimit);
|
|
338
|
+
const expandHint = formatExpandHint(expanded, hasMoreOutputs || hasMorePreview, uiTheme);
|
|
339
|
+
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
340
|
+
|
|
341
|
+
for (let i = 0; i < maxOutputs; i++) {
|
|
342
|
+
const o = outputs[i];
|
|
343
|
+
const isLast = i === maxOutputs - 1 && !hasMoreOutputs;
|
|
344
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
345
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", o.id)} ${formatOutputMeta(o, uiTheme)}`;
|
|
346
|
+
|
|
347
|
+
const previewLines = o.previewLines ?? [];
|
|
348
|
+
const shownPreview = previewLines.slice(0, previewLimit);
|
|
349
|
+
if (shownPreview.length > 0) {
|
|
350
|
+
const childPrefix = isLast ? " " : ` ${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
|
|
351
|
+
for (const line of shownPreview) {
|
|
352
|
+
const previewText = truncate(line, TRUNCATE_LENGTHS.CONTENT, uiTheme.format.ellipsis);
|
|
353
|
+
text += `\n${childPrefix}${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
354
|
+
"muted",
|
|
355
|
+
"preview:",
|
|
356
|
+
)} ${uiTheme.fg("toolOutput", previewText)}`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (hasMoreOutputs) {
|
|
362
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
363
|
+
"muted",
|
|
364
|
+
formatMoreItems(outputs.length - maxOutputs, "output", uiTheme),
|
|
365
|
+
)}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return new Text(text, 0, 0);
|
|
369
|
+
},
|
|
370
|
+
};
|