@oh-my-pi/pi-coding-agent 14.7.3 → 14.7.4
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 +21 -0
- package/package.json +7 -7
- package/src/cli/read-cli.ts +1 -2
- package/src/commands/read.ts +2 -7
- package/src/config/settings-schema.ts +0 -5
- package/src/edit/modes/hashline.ts +40 -19
- package/src/edit/modes/patch.ts +7 -5
- package/src/edit/modes/replace.ts +6 -2
- package/src/edit/notebook.ts +222 -0
- package/src/edit/read-file.ts +7 -0
- package/src/edit/renderer.ts +4 -3
- package/src/edit/streaming.ts +49 -7
- package/src/modes/components/diff.ts +54 -7
- package/src/prompts/agents/designer.md +1 -2
- package/src/prompts/agents/explore.md +2 -5
- package/src/prompts/agents/init.md +1 -4
- package/src/prompts/agents/librarian.md +1 -3
- package/src/prompts/agents/plan.md +7 -8
- package/src/prompts/agents/reviewer.md +1 -2
- package/src/prompts/ci-green-request.md +10 -10
- package/src/prompts/commands/orchestrate.md +48 -0
- package/src/prompts/memories/consolidation.md +10 -10
- package/src/prompts/memories/read-path.md +6 -6
- package/src/prompts/system/agent-creation-architect.md +54 -44
- package/src/prompts/system/custom-system-prompt.md +3 -5
- package/src/prompts/system/eager-todo.md +4 -4
- package/src/prompts/system/handoff-document.md +7 -4
- package/src/prompts/system/plan-mode-active.md +7 -3
- package/src/prompts/system/plan-mode-approved.md +5 -5
- package/src/prompts/system/summarization-system.md +2 -2
- package/src/prompts/system/system-prompt.md +53 -65
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/web-search.md +16 -19
- package/src/prompts/tools/bash.md +8 -8
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/debug.md +3 -1
- package/src/prompts/tools/eval.md +13 -9
- package/src/prompts/tools/hashline.md +4 -2
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/reflect.md +3 -3
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/resolve.md +2 -2
- package/src/prompts/tools/retain.md +3 -2
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +3 -4
- package/src/prompts/tools/task.md +1 -1
- package/src/task/commands.ts +5 -1
- package/src/tools/fetch.ts +6 -7
- package/src/tools/index.ts +0 -4
- package/src/tools/read.ts +18 -7
- package/src/tools/renderers.ts +0 -2
- package/src/tools/write.ts +41 -26
- package/src/tools/notebook.ts +0 -286
package/src/tools/fetch.ts
CHANGED
|
@@ -1220,13 +1220,13 @@ function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: bool
|
|
|
1220
1220
|
|
|
1221
1221
|
async function buildReadUrlCacheEntry(
|
|
1222
1222
|
session: ToolSession,
|
|
1223
|
-
params: { path: string;
|
|
1223
|
+
params: { path: string; raw?: boolean },
|
|
1224
1224
|
signal?: AbortSignal,
|
|
1225
1225
|
options?: { ensureArtifact?: boolean },
|
|
1226
1226
|
): Promise<ReadUrlCacheEntry> {
|
|
1227
|
-
const { path: url,
|
|
1227
|
+
const { path: url, raw = false } = params;
|
|
1228
1228
|
|
|
1229
|
-
const effectiveTimeout = clampTimeout("fetch",
|
|
1229
|
+
const effectiveTimeout = clampTimeout("fetch", 30);
|
|
1230
1230
|
|
|
1231
1231
|
if (signal?.aborted) {
|
|
1232
1232
|
throw new ToolAbortError();
|
|
@@ -1254,7 +1254,7 @@ async function buildReadUrlCacheEntry(
|
|
|
1254
1254
|
|
|
1255
1255
|
export async function loadReadUrlCacheEntry(
|
|
1256
1256
|
session: ToolSession,
|
|
1257
|
-
params: { path: string;
|
|
1257
|
+
params: { path: string; raw?: boolean },
|
|
1258
1258
|
signal?: AbortSignal,
|
|
1259
1259
|
options?: { ensureArtifact?: boolean; preferCached?: boolean },
|
|
1260
1260
|
): Promise<ReadUrlCacheEntry> {
|
|
@@ -1291,7 +1291,7 @@ function buildUrlReadOutput(result: FetchRenderResult, content: string): string
|
|
|
1291
1291
|
|
|
1292
1292
|
export async function executeReadUrl(
|
|
1293
1293
|
session: ToolSession,
|
|
1294
|
-
params: { path: string;
|
|
1294
|
+
params: { path: string; raw?: boolean },
|
|
1295
1295
|
signal?: AbortSignal,
|
|
1296
1296
|
): Promise<AgentToolResult<ReadUrlToolDetails>> {
|
|
1297
1297
|
let cacheEntry = await loadReadUrlCacheEntry(session, params, signal, { preferCached: true });
|
|
@@ -1345,7 +1345,7 @@ function countNonEmptyLines(text: string): number {
|
|
|
1345
1345
|
|
|
1346
1346
|
/** Render URL read call (URL preview) */
|
|
1347
1347
|
export function renderReadUrlCall(
|
|
1348
|
-
args: { path?: string; url?: string;
|
|
1348
|
+
args: { path?: string; url?: string; raw?: boolean },
|
|
1349
1349
|
_options: RenderResultOptions,
|
|
1350
1350
|
uiTheme: Theme = theme,
|
|
1351
1351
|
): Component {
|
|
@@ -1355,7 +1355,6 @@ export function renderReadUrlCall(
|
|
|
1355
1355
|
const description = `${domain}${path ? ` ${path}` : ""}`.trim();
|
|
1356
1356
|
const meta: string[] = [];
|
|
1357
1357
|
if (args.raw) meta.push("raw");
|
|
1358
|
-
if (args.timeout !== undefined) meta.push(`timeout:${args.timeout}s`);
|
|
1359
1358
|
const text = renderStatusLine({ icon: "pending", title: "Read", description, meta }, uiTheme);
|
|
1360
1359
|
return new Text(text, 0, 0);
|
|
1361
1360
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -37,7 +37,6 @@ import { HindsightRetainTool } from "./hindsight-retain";
|
|
|
37
37
|
import { InspectImageTool } from "./inspect-image";
|
|
38
38
|
import { IrcTool } from "./irc";
|
|
39
39
|
import { JobTool } from "./job";
|
|
40
|
-
import { NotebookTool } from "./notebook";
|
|
41
40
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
42
41
|
import { ReadTool } from "./read";
|
|
43
42
|
import { RecipeTool } from "./recipe";
|
|
@@ -80,7 +79,6 @@ export * from "./image-gen";
|
|
|
80
79
|
export * from "./inspect-image";
|
|
81
80
|
export * from "./irc";
|
|
82
81
|
export * from "./job";
|
|
83
|
-
export * from "./notebook";
|
|
84
82
|
export * from "./read";
|
|
85
83
|
export * from "./recipe";
|
|
86
84
|
export * from "./render-mermaid";
|
|
@@ -271,7 +269,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
271
269
|
find: s => new FindTool(s),
|
|
272
270
|
search: s => new SearchTool(s),
|
|
273
271
|
lsp: LspTool.createIf,
|
|
274
|
-
notebook: s => new NotebookTool(s),
|
|
275
272
|
inspect_image: s => new InspectImageTool(s),
|
|
276
273
|
browser: s => new BrowserTool(s),
|
|
277
274
|
checkpoint: CheckpointTool.createIf,
|
|
@@ -430,7 +427,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
430
427
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
431
428
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
432
429
|
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
|
433
|
-
if (name === "notebook") return session.settings.get("notebook.enabled");
|
|
434
430
|
if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
|
|
435
431
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
436
432
|
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
package/src/tools/read.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
11
|
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
12
|
+
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
12
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
14
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
14
15
|
import type { InternalUrl } from "../internal-urls/types";
|
|
@@ -418,7 +419,6 @@ const readSchema = Type.Object({
|
|
|
418
419
|
description: 'path or url; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
|
|
419
420
|
examples: ["src/foo.ts", "src/foo.ts:50-100", "https://example.com:L1-L40"],
|
|
420
421
|
}),
|
|
421
|
-
timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 20 })),
|
|
422
422
|
});
|
|
423
423
|
|
|
424
424
|
export type ReadToolInput = Static<typeof readSchema>;
|
|
@@ -1084,7 +1084,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1084
1084
|
_onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
|
|
1085
1085
|
_toolContext?: AgentToolContext,
|
|
1086
1086
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1087
|
-
let { path: readPath
|
|
1087
|
+
let { path: readPath } = params;
|
|
1088
1088
|
if (readPath.startsWith("file://")) {
|
|
1089
1089
|
readPath = expandPath(readPath);
|
|
1090
1090
|
}
|
|
@@ -1098,7 +1098,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1098
1098
|
if (parsedUrlTarget.offset !== undefined || parsedUrlTarget.limit !== undefined) {
|
|
1099
1099
|
const cached = await loadReadUrlCacheEntry(
|
|
1100
1100
|
this.session,
|
|
1101
|
-
{ path: parsedUrlTarget.path,
|
|
1101
|
+
{ path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
|
|
1102
1102
|
signal,
|
|
1103
1103
|
{
|
|
1104
1104
|
ensureArtifact: true,
|
|
@@ -1111,7 +1111,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1111
1111
|
entityLabel: "URL output",
|
|
1112
1112
|
});
|
|
1113
1113
|
}
|
|
1114
|
-
return executeReadUrl(this.session, { path: parsedUrlTarget.path,
|
|
1114
|
+
return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw }, signal);
|
|
1115
1115
|
}
|
|
1116
1116
|
|
|
1117
1117
|
// Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
|
|
@@ -1196,7 +1196,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1196
1196
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
1197
1197
|
const _hasEditTool = this.session.hasEditTool ?? true;
|
|
1198
1198
|
const _language = getLanguageFromPath(absolutePath);
|
|
1199
|
-
const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext)
|
|
1199
|
+
const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
|
|
1200
1200
|
// Read the file based on type
|
|
1201
1201
|
let content: Array<TextContent | ImageContent> | undefined;
|
|
1202
1202
|
let details: ReadToolDetails = {};
|
|
@@ -1263,8 +1263,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1263
1263
|
throw error;
|
|
1264
1264
|
}
|
|
1265
1265
|
}
|
|
1266
|
+
} else if (isNotebookPath(absolutePath) && parsed.kind !== "raw") {
|
|
1267
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1268
|
+
return this.#buildInMemoryTextResult(
|
|
1269
|
+
await readEditableNotebookText(absolutePath, localReadPath),
|
|
1270
|
+
offset,
|
|
1271
|
+
limit,
|
|
1272
|
+
{
|
|
1273
|
+
details: { resolvedPath: absolutePath },
|
|
1274
|
+
sourcePath: absolutePath,
|
|
1275
|
+
entityLabel: "notebook",
|
|
1276
|
+
},
|
|
1277
|
+
);
|
|
1266
1278
|
} else if (shouldConvertWithMarkit) {
|
|
1267
|
-
// Convert document
|
|
1279
|
+
// Convert document via markit.
|
|
1268
1280
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1269
1281
|
if (result.ok) {
|
|
1270
1282
|
// Apply truncation to converted content
|
|
@@ -1556,7 +1568,6 @@ interface ReadRenderArgs {
|
|
|
1556
1568
|
path?: string;
|
|
1557
1569
|
file_path?: string;
|
|
1558
1570
|
sel?: string;
|
|
1559
|
-
timeout?: number;
|
|
1560
1571
|
// Legacy fields from old schema — tolerated for in-flight tool calls during transition
|
|
1561
1572
|
offset?: number;
|
|
1562
1573
|
limit?: number;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -22,7 +22,6 @@ import { findToolRenderer } from "./find";
|
|
|
22
22
|
import { githubToolRenderer } from "./gh-renderer";
|
|
23
23
|
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
|
24
24
|
import { jobToolRenderer } from "./job";
|
|
25
|
-
import { notebookToolRenderer } from "./notebook";
|
|
26
25
|
import { readToolRenderer } from "./read";
|
|
27
26
|
import { recipeToolRenderer } from "./recipe/render";
|
|
28
27
|
import { resolveToolRenderer } from "./resolve";
|
|
@@ -60,7 +59,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
60
59
|
find: findToolRenderer as ToolRenderer,
|
|
61
60
|
search: searchToolRenderer as ToolRenderer,
|
|
62
61
|
lsp: lspToolRenderer as ToolRenderer,
|
|
63
|
-
notebook: notebookToolRenderer as ToolRenderer,
|
|
64
62
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
65
63
|
read: readToolRenderer as ToolRenderer,
|
|
66
64
|
job: jobToolRenderer as ToolRenderer,
|
package/src/tools/write.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type Static, Type } from "@sinclair/typebox";
|
|
|
9
9
|
import { stripHashlinePrefixes } from "../edit";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
12
|
-
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
12
|
+
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
13
13
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
14
14
|
import type { ToolSession } from "../sdk";
|
|
15
15
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
@@ -168,6 +168,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
168
168
|
readonly concurrency = "exclusive";
|
|
169
169
|
readonly loadMode = "discoverable";
|
|
170
170
|
readonly summary = "Write content to a file (creates or overwrites)";
|
|
171
|
+
readonly intent = (args: Partial<WriteToolInput>) => (args.path ? `writing ${args.path}` : "writing");
|
|
171
172
|
|
|
172
173
|
readonly #writethrough: WritethroughCallback;
|
|
173
174
|
|
|
@@ -523,52 +524,67 @@ function countLines(text: string): number {
|
|
|
523
524
|
return text.split("\n").length;
|
|
524
525
|
}
|
|
525
526
|
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
|
|
530
|
-
}
|
|
531
|
-
return uiTheme.fg("dim", `${icon}`);
|
|
527
|
+
function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
|
|
528
|
+
if (lineCount <= 0) return "";
|
|
529
|
+
return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
|
|
532
530
|
}
|
|
533
531
|
|
|
534
532
|
function normalizeDisplayText(text: string): string {
|
|
535
533
|
return text.replace(/\r/g, "");
|
|
536
534
|
}
|
|
537
535
|
|
|
538
|
-
function formatStreamingContent(content: string, uiTheme: Theme): string {
|
|
536
|
+
function formatStreamingContent(content: string, language: string | undefined, uiTheme: Theme): string {
|
|
539
537
|
if (!content) return "";
|
|
540
538
|
const lines = normalizeDisplayText(content).split("\n");
|
|
541
|
-
const
|
|
542
|
-
const
|
|
539
|
+
const totalLines = lines.length;
|
|
540
|
+
const startIndex = Math.max(0, totalLines - WRITE_STREAMING_PREVIEW_LINES);
|
|
541
|
+
const visibleLines = lines.slice(startIndex);
|
|
542
|
+
const hidden = startIndex;
|
|
543
|
+
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
544
|
+
const lineNumberWidth = String(totalLines).length;
|
|
543
545
|
|
|
544
546
|
let text = "\n\n";
|
|
545
547
|
if (hidden > 0) {
|
|
546
|
-
text += uiTheme.fg("dim", `… (${hidden} earlier
|
|
548
|
+
text += `${uiTheme.fg("dim", `… (${hidden} earlier line${hidden === 1 ? "" : "s"})`)}\n`;
|
|
547
549
|
}
|
|
548
|
-
for (
|
|
549
|
-
|
|
550
|
+
for (let i = 0; i < highlighted.length; i++) {
|
|
551
|
+
const lineNum = startIndex + i + 1;
|
|
552
|
+
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}│`);
|
|
553
|
+
const body = replaceTabs(highlighted[i] ?? "");
|
|
554
|
+
text += ` ${gutter}${body}\n`;
|
|
550
555
|
}
|
|
551
556
|
text += uiTheme.fg("dim", `… (streaming)`);
|
|
552
557
|
return text;
|
|
553
558
|
}
|
|
554
559
|
|
|
555
|
-
function renderContentPreview(
|
|
560
|
+
function renderContentPreview(
|
|
561
|
+
content: string,
|
|
562
|
+
expanded: boolean,
|
|
563
|
+
language: string | undefined,
|
|
564
|
+
uiTheme: Theme,
|
|
565
|
+
): string {
|
|
556
566
|
if (!content) return "";
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
const
|
|
567
|
+
const rawLines = normalizeDisplayText(content).split("\n");
|
|
568
|
+
const totalLines = rawLines.length;
|
|
569
|
+
const maxLines = expanded ? totalLines : Math.min(totalLines, WRITE_PREVIEW_LINES);
|
|
570
|
+
const visibleLines = rawLines.slice(0, maxLines);
|
|
571
|
+
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
572
|
+
const lineNumberWidth = String(maxLines).length;
|
|
573
|
+
const hidden = totalLines - maxLines;
|
|
561
574
|
|
|
562
575
|
let text = "\n\n";
|
|
563
|
-
for (
|
|
564
|
-
|
|
576
|
+
for (let i = 0; i < highlighted.length; i++) {
|
|
577
|
+
const lineNum = i + 1;
|
|
578
|
+
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")}│`);
|
|
579
|
+
const body = replaceTabs(highlighted[i] ?? "");
|
|
580
|
+
text += ` ${gutter}${body}\n`;
|
|
565
581
|
}
|
|
566
582
|
if (!expanded && hidden > 0) {
|
|
567
583
|
const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
|
|
568
584
|
const moreLine = `${formatMoreItems(hidden, "line")}${hint ? ` ${hint}` : ""}`;
|
|
569
585
|
text += uiTheme.fg("dim", moreLine);
|
|
570
586
|
}
|
|
571
|
-
return text;
|
|
587
|
+
return text.trimEnd();
|
|
572
588
|
}
|
|
573
589
|
|
|
574
590
|
export const writeToolRenderer = {
|
|
@@ -588,7 +604,7 @@ export const writeToolRenderer = {
|
|
|
588
604
|
}
|
|
589
605
|
|
|
590
606
|
// Show streaming preview of content (tail)
|
|
591
|
-
text += formatStreamingContent(args.content, uiTheme);
|
|
607
|
+
text += formatStreamingContent(args.content, lang, uiTheme);
|
|
592
608
|
|
|
593
609
|
return new Text(text, 0, 0);
|
|
594
610
|
},
|
|
@@ -606,17 +622,17 @@ export const writeToolRenderer = {
|
|
|
606
622
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
607
623
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
608
624
|
const lineCount = countLines(fileContent);
|
|
625
|
+
const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
|
|
609
626
|
|
|
610
627
|
// Build header with status icon
|
|
611
628
|
const header = renderStatusLine(
|
|
612
629
|
{
|
|
613
630
|
icon: "success",
|
|
614
631
|
title: "Write",
|
|
615
|
-
description: `${langIcon} ${pathDisplay}`,
|
|
632
|
+
description: `${langIcon} ${pathDisplay}${lineSuffix}`,
|
|
616
633
|
},
|
|
617
634
|
uiTheme,
|
|
618
635
|
);
|
|
619
|
-
const metadataLine = formatMetadataLine(lineCount, lang ?? "text", uiTheme);
|
|
620
636
|
const diagnostics = result.details?.diagnostics;
|
|
621
637
|
|
|
622
638
|
let cached: RenderCache | undefined;
|
|
@@ -628,8 +644,7 @@ export const writeToolRenderer = {
|
|
|
628
644
|
if (cached?.key === key) return cached.lines;
|
|
629
645
|
|
|
630
646
|
let text = header;
|
|
631
|
-
text +=
|
|
632
|
-
text += renderContentPreview(fileContent, expanded, uiTheme);
|
|
647
|
+
text += renderContentPreview(fileContent, expanded, lang, uiTheme);
|
|
633
648
|
|
|
634
649
|
if (diagnostics) {
|
|
635
650
|
const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
|
package/src/tools/notebook.ts
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import { type Static, Type } from "@sinclair/typebox";
|
|
7
|
-
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { Theme } from "../modes/theme/theme";
|
|
9
|
-
import type { ToolSession } from "../sdk";
|
|
10
|
-
import { Hasher, type RenderCache, renderCodeCell, renderStatusLine } from "../tui";
|
|
11
|
-
import { resolveToCwd } from "./path-utils";
|
|
12
|
-
import { formatCount, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
13
|
-
|
|
14
|
-
const notebookSchema = Type.Object({
|
|
15
|
-
action: StringEnum(["edit", "insert", "delete"], {
|
|
16
|
-
description: "cell action",
|
|
17
|
-
examples: ["edit", "insert", "delete"],
|
|
18
|
-
}),
|
|
19
|
-
notebook_path: Type.String({ description: "notebook path", examples: ["analysis.ipynb"] }),
|
|
20
|
-
cell_index: Type.Number({ description: "cell index", examples: [0, 1] }),
|
|
21
|
-
content: Type.Optional(Type.String({ description: "new cell content" })),
|
|
22
|
-
cell_type: Type.Optional(
|
|
23
|
-
StringEnum(["code", "markdown"], {
|
|
24
|
-
description: "cell type",
|
|
25
|
-
examples: ["code", "markdown"],
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export interface NotebookToolDetails {
|
|
31
|
-
/** Action performed */
|
|
32
|
-
action: "edit" | "insert" | "delete";
|
|
33
|
-
/** Cell index operated on */
|
|
34
|
-
cellIndex: number;
|
|
35
|
-
/** Cell type */
|
|
36
|
-
cellType?: string;
|
|
37
|
-
/** Total cell count after operation */
|
|
38
|
-
totalCells: number;
|
|
39
|
-
/** Cell content lines after operation (or removed content for delete) */
|
|
40
|
-
cellSource?: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface NotebookCell {
|
|
44
|
-
cell_type: "code" | "markdown" | "raw";
|
|
45
|
-
source: string[];
|
|
46
|
-
metadata: Record<string, unknown>;
|
|
47
|
-
execution_count?: number | null;
|
|
48
|
-
outputs?: unknown[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface Notebook {
|
|
52
|
-
cells: NotebookCell[];
|
|
53
|
-
metadata: Record<string, unknown>;
|
|
54
|
-
nbformat: number;
|
|
55
|
-
nbformat_minor: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function splitIntoLines(content: string): string[] {
|
|
59
|
-
return content.split("\n").map((line, i, arr) => (i < arr.length - 1 ? `${line}\n` : line));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type NotebookParams = Static<typeof notebookSchema>;
|
|
63
|
-
|
|
64
|
-
export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookToolDetails> {
|
|
65
|
-
readonly name = "notebook";
|
|
66
|
-
readonly label = "Notebook";
|
|
67
|
-
readonly loadMode = "discoverable";
|
|
68
|
-
readonly summary = "Read and execute Jupyter notebooks";
|
|
69
|
-
readonly description = "Edit, insert, or delete cells in Jupyter notebooks (.ipynb). cell_index is 0-based.";
|
|
70
|
-
readonly parameters = notebookSchema;
|
|
71
|
-
readonly strict = true;
|
|
72
|
-
readonly concurrency = "exclusive";
|
|
73
|
-
|
|
74
|
-
constructor(private readonly session: ToolSession) {}
|
|
75
|
-
|
|
76
|
-
async execute(
|
|
77
|
-
_toolCallId: string,
|
|
78
|
-
params: NotebookParams,
|
|
79
|
-
signal?: AbortSignal,
|
|
80
|
-
_onUpdate?: AgentToolUpdateCallback<NotebookToolDetails>,
|
|
81
|
-
_context?: AgentToolContext,
|
|
82
|
-
): Promise<AgentToolResult<NotebookToolDetails>> {
|
|
83
|
-
const { action, notebook_path, cell_index, content, cell_type } = params;
|
|
84
|
-
const absolutePath = resolveToCwd(notebook_path, this.session.cwd);
|
|
85
|
-
|
|
86
|
-
return untilAborted(signal, async () => {
|
|
87
|
-
// Read and parse notebook
|
|
88
|
-
let notebook: Notebook;
|
|
89
|
-
try {
|
|
90
|
-
notebook = await Bun.file(absolutePath).json();
|
|
91
|
-
} catch (err) {
|
|
92
|
-
if (isEnoent(err)) throw new Error(`Notebook not found: ${notebook_path}`);
|
|
93
|
-
throw new Error(`Invalid JSON in notebook: ${notebook_path}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Validate notebook structure
|
|
97
|
-
if (!notebook.cells || !Array.isArray(notebook.cells)) {
|
|
98
|
-
throw new Error(`Invalid notebook structure (missing cells array): ${notebook_path}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const cellCount = notebook.cells.length;
|
|
102
|
-
|
|
103
|
-
// Validate cell_index based on action
|
|
104
|
-
if (action === "insert") {
|
|
105
|
-
if (cell_index < 0 || cell_index > cellCount) {
|
|
106
|
-
throw new Error(`Cell index ${cell_index} out of range for insert (0-${cellCount}) in ${notebook_path}`);
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
if (cell_index < 0 || cell_index >= cellCount) {
|
|
110
|
-
throw new Error(`Cell index ${cell_index} out of range (0-${cellCount - 1}) in ${notebook_path}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Validate content for edit/insert
|
|
115
|
-
if ((action === "edit" || action === "insert") && content === undefined) {
|
|
116
|
-
throw new Error(`Content is required for ${action} action`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Perform the action
|
|
120
|
-
let resultMessage: string;
|
|
121
|
-
let finalCellType: string | undefined;
|
|
122
|
-
let cellSource: string[] | undefined;
|
|
123
|
-
|
|
124
|
-
switch (action) {
|
|
125
|
-
case "edit": {
|
|
126
|
-
const sourceLines = splitIntoLines(content!);
|
|
127
|
-
notebook.cells[cell_index].source = sourceLines;
|
|
128
|
-
finalCellType = notebook.cells[cell_index].cell_type;
|
|
129
|
-
cellSource = sourceLines;
|
|
130
|
-
resultMessage = `Replaced cell ${cell_index} (${finalCellType})`;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
case "insert": {
|
|
134
|
-
const sourceLines = splitIntoLines(content!);
|
|
135
|
-
const newCellType = (cell_type as "code" | "markdown") || "code";
|
|
136
|
-
const newCell: NotebookCell = {
|
|
137
|
-
cell_type: newCellType,
|
|
138
|
-
source: sourceLines,
|
|
139
|
-
metadata: {},
|
|
140
|
-
};
|
|
141
|
-
if (newCellType === "code") {
|
|
142
|
-
newCell.execution_count = null;
|
|
143
|
-
newCell.outputs = [];
|
|
144
|
-
}
|
|
145
|
-
notebook.cells.splice(cell_index, 0, newCell);
|
|
146
|
-
finalCellType = newCellType;
|
|
147
|
-
cellSource = sourceLines;
|
|
148
|
-
resultMessage = `Inserted ${newCellType} cell at position ${cell_index}`;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
case "delete": {
|
|
152
|
-
const removedCell = notebook.cells[cell_index];
|
|
153
|
-
finalCellType = removedCell.cell_type;
|
|
154
|
-
cellSource = removedCell.source;
|
|
155
|
-
notebook.cells.splice(cell_index, 1);
|
|
156
|
-
resultMessage = `Deleted cell ${cell_index} (${finalCellType})`;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
default: {
|
|
160
|
-
throw new Error(`Invalid action: ${action}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Write back with single-space indentation
|
|
165
|
-
await Bun.write(absolutePath, JSON.stringify(notebook, null, 1));
|
|
166
|
-
|
|
167
|
-
const newCellCount = notebook.cells.length;
|
|
168
|
-
return {
|
|
169
|
-
content: [
|
|
170
|
-
{
|
|
171
|
-
type: "text",
|
|
172
|
-
text: `${resultMessage}. Notebook now has ${newCellCount} cells.`,
|
|
173
|
-
},
|
|
174
|
-
],
|
|
175
|
-
details: {
|
|
176
|
-
action: action as "edit" | "insert" | "delete",
|
|
177
|
-
cellIndex: cell_index,
|
|
178
|
-
cellType: finalCellType,
|
|
179
|
-
totalCells: newCellCount,
|
|
180
|
-
cellSource,
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// =============================================================================
|
|
188
|
-
// TUI Renderer
|
|
189
|
-
// =============================================================================
|
|
190
|
-
|
|
191
|
-
interface NotebookRenderArgs {
|
|
192
|
-
action: string;
|
|
193
|
-
notebookPath?: string;
|
|
194
|
-
notebook_path?: string;
|
|
195
|
-
cellNumber?: number;
|
|
196
|
-
cell_index?: number;
|
|
197
|
-
cellType?: string;
|
|
198
|
-
cell_type?: string;
|
|
199
|
-
content?: string;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
203
|
-
|
|
204
|
-
export const notebookToolRenderer = {
|
|
205
|
-
renderCall(args: NotebookRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
206
|
-
const meta: string[] = [];
|
|
207
|
-
const notebookPath = args.notebookPath ?? args.notebook_path;
|
|
208
|
-
const cellNumber = args.cellNumber ?? args.cell_index;
|
|
209
|
-
const cellType = args.cellType ?? args.cell_type;
|
|
210
|
-
meta.push(`in ${notebookPath || "?"}`);
|
|
211
|
-
if (cellNumber !== undefined) meta.push(`cell:${cellNumber}`);
|
|
212
|
-
if (cellType) meta.push(`type:${cellType}`);
|
|
213
|
-
|
|
214
|
-
const text = renderStatusLine(
|
|
215
|
-
{ icon: "pending", title: "Notebook", description: args.action || "?", meta },
|
|
216
|
-
uiTheme,
|
|
217
|
-
);
|
|
218
|
-
return new Text(text, 0, 0);
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
renderResult(
|
|
222
|
-
result: { content: Array<{ type: string; text?: string }>; details?: NotebookToolDetails },
|
|
223
|
-
options: RenderResultOptions,
|
|
224
|
-
uiTheme: Theme,
|
|
225
|
-
args?: NotebookRenderArgs,
|
|
226
|
-
): Component {
|
|
227
|
-
const content = result.content?.[0];
|
|
228
|
-
if (content?.type === "text" && content.text?.startsWith("Error:")) {
|
|
229
|
-
const notebookPath = args?.notebookPath ?? args?.notebook_path ?? "?";
|
|
230
|
-
const header = renderStatusLine({ icon: "error", title: "Notebook", description: notebookPath }, uiTheme);
|
|
231
|
-
return new Text([header, formatErrorMessage(content.text, uiTheme)].join("\n"), 0, 0);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const details = result.details;
|
|
235
|
-
const action = details?.action ?? "edit";
|
|
236
|
-
const cellIndex = details?.cellIndex;
|
|
237
|
-
const cellType = details?.cellType;
|
|
238
|
-
const totalCells = details?.totalCells;
|
|
239
|
-
const cellSource = details?.cellSource ?? [];
|
|
240
|
-
const lineCount = cellSource.length;
|
|
241
|
-
|
|
242
|
-
const actionLabel = action === "insert" ? "Inserted" : action === "delete" ? "Deleted" : "Edited";
|
|
243
|
-
const cellLabel = cellType || "cell";
|
|
244
|
-
const summaryParts = [`${actionLabel} ${cellLabel} ${cellIndex ?? "?"}`];
|
|
245
|
-
if (lineCount > 0) summaryParts.push(formatCount("line", lineCount));
|
|
246
|
-
if (totalCells !== undefined) summaryParts.push(`${totalCells} total`);
|
|
247
|
-
|
|
248
|
-
const outputLines = summaryParts.map(part => uiTheme.fg("dim", part));
|
|
249
|
-
const codeText = cellSource.join("");
|
|
250
|
-
const language = cellType === "markdown" ? "markdown" : undefined;
|
|
251
|
-
|
|
252
|
-
const notebookPath = args?.notebookPath ?? args?.notebook_path;
|
|
253
|
-
const notebookLabel = notebookPath ? `${actionLabel} ${notebookPath}` : "Notebook";
|
|
254
|
-
let cached: RenderCache | undefined;
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
render: (width: number): string[] => {
|
|
258
|
-
// REACTIVE: read mutable options at render time
|
|
259
|
-
const { expanded } = options;
|
|
260
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
261
|
-
if (cached?.key === key) return cached.lines;
|
|
262
|
-
|
|
263
|
-
const lines = renderCodeCell(
|
|
264
|
-
{
|
|
265
|
-
code: codeText,
|
|
266
|
-
language,
|
|
267
|
-
title: notebookLabel,
|
|
268
|
-
status: "complete",
|
|
269
|
-
output: outputLines.join("\n"),
|
|
270
|
-
codeMaxLines: expanded ? Number.POSITIVE_INFINITY : COLLAPSED_TEXT_LIMIT,
|
|
271
|
-
expanded,
|
|
272
|
-
width,
|
|
273
|
-
},
|
|
274
|
-
uiTheme,
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
cached = { key, lines };
|
|
278
|
-
return lines;
|
|
279
|
-
},
|
|
280
|
-
invalidate: () => {
|
|
281
|
-
cached = undefined;
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
mergeCallAndResult: true,
|
|
286
|
-
};
|