@oh-my-pi/pi-coding-agent 16.1.15 → 16.1.17
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 +78 -0
- package/dist/cli.js +3531 -3812
- package/dist/types/auto-thinking/classifier.d.ts +4 -2
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +22 -7
- package/dist/types/extensibility/plugins/loader.d.ts +16 -9
- package/dist/types/extensibility/tool-event-input.d.ts +2 -0
- package/dist/types/hindsight/content.d.ts +2 -10
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +6 -6
- package/dist/types/session/provider-image-budget.d.ts +3 -0
- package/dist/types/session/session-context.d.ts +6 -5
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/parallel.d.ts +4 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +23 -1
- package/dist/types/tiny/title-client.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/tools-manager.d.ts +2 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/auto-thinking/classifier.ts +7 -2
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/profile-alias.ts +38 -7
- package/src/cli/session-picker.ts +17 -3
- package/src/cli/usage-cli.ts +5 -1
- package/src/commands/launch.ts +3 -3
- package/src/config/model-discovery.ts +59 -8
- package/src/config/model-registry.ts +74 -3
- package/src/config/settings-schema.ts +13 -1
- package/src/discovery/omp-extension-roots.ts +1 -3
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/extensibility/extensions/wrapper.ts +3 -2
- package/src/extensibility/hooks/tool-wrapper.ts +4 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +40 -8
- package/src/extensibility/plugins/loader.ts +71 -23
- package/src/extensibility/plugins/marketplace/manager.ts +134 -0
- package/src/extensibility/tool-event-input.ts +57 -0
- package/src/hindsight/content.ts +21 -12
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/tool-bridge.ts +27 -2
- package/src/mnemopi/state.ts +5 -2
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/agent-transcript-viewer.ts +193 -41
- package/src/modes/components/chat-transcript-builder.ts +6 -0
- package/src/modes/components/custom-editor.test.ts +18 -1
- package/src/modes/components/custom-editor.ts +77 -45
- package/src/modes/components/hook-editor.ts +15 -2
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +52 -8
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/welcome.ts +12 -14
- package/src/modes/controllers/command-controller.ts +21 -5
- package/src/modes/controllers/input-controller.ts +115 -3
- package/src/modes/controllers/selector-controller.ts +19 -1
- package/src/modes/interactive-mode.ts +3 -3
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +16 -0
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +8 -10
- package/src/session/agent-session.ts +422 -97
- package/src/session/provider-image-budget.ts +86 -0
- package/src/session/session-context.ts +14 -7
- package/src/session/session-storage.ts +24 -2
- package/src/session/snapcompact-inline.ts +19 -3
- package/src/slash-commands/builtin-registry.ts +0 -22
- package/src/slash-commands/helpers/usage-report.ts +9 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/parallel.ts +6 -1
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +29 -2
- package/src/tiny/title-client.ts +75 -21
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/tools-manager.ts +67 -10
- package/src/web/search/providers/exa.ts +85 -1
package/src/tools/eval.ts
CHANGED
|
@@ -38,8 +38,6 @@ const EVAL_LANGUAGE_NAME: Record<EvalLanguageToken, string> = {
|
|
|
38
38
|
rb: "Ruby",
|
|
39
39
|
jl: "Julia",
|
|
40
40
|
};
|
|
41
|
-
const EVAL_CELLS_DESCRIPTION =
|
|
42
|
-
"cells executed in order. State persists within each language across cells and tool calls.";
|
|
43
41
|
|
|
44
42
|
/** Join names as an English "or" list: ["A"]→"A", ["A","B"]→"A or B", 3+→"A, B, or C". */
|
|
45
43
|
function joinWithOr(items: readonly string[]): string {
|
|
@@ -56,12 +54,12 @@ function describeCodeField(langs: readonly EvalLanguageToken[]): string {
|
|
|
56
54
|
const replLangs = langs.filter(lang => lang === "rb" || lang === "jl");
|
|
57
55
|
// No persistent REPL backends → keep the original py/js phrasing verbatim so the
|
|
58
56
|
// default (rb/jl off) wire schema stays byte-identical to the pre-feature one.
|
|
59
|
-
if (replLangs.length === 0) return "
|
|
57
|
+
if (replLangs.length === 0) return "code to run in this eval call, verbatim. Use top-level await freely.";
|
|
60
58
|
const awaitLangs = langs.filter(lang => lang === "py" || lang === "js");
|
|
61
59
|
const clauses: string[] = [];
|
|
62
60
|
if (awaitLangs.length > 0) clauses.push(`Top-level \`await\` is available in ${awaitLangs.join("/")}`);
|
|
63
61
|
clauses.push(`${replLangs.join("/")} auto-display the last expression like a REPL`);
|
|
64
|
-
return `
|
|
62
|
+
return `code to run in this eval call, verbatim. ${clauses.join("; ")}.`;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
/** One-line discovery summary listing the runtimes available this session. */
|
|
@@ -86,29 +84,24 @@ function enabledEvalLanguages(backends: EvalBackendsAllowance): EvalLanguageToke
|
|
|
86
84
|
|
|
87
85
|
const evalCellCommonFields = {
|
|
88
86
|
"title?": type("string").describe('short label shown in transcript (e.g. "imports", "load config")'),
|
|
89
|
-
"timeout?": type("number").describe("
|
|
90
|
-
"reset?": type("boolean").describe(
|
|
91
|
-
"wipe this cell's language kernel before running. Other languages are untouched.",
|
|
92
|
-
),
|
|
87
|
+
"timeout?": type("number").describe("timeout for this eval call in seconds"),
|
|
88
|
+
"reset?": type("boolean").describe("wipe this language's kernel before running. Other languages are untouched."),
|
|
93
89
|
};
|
|
94
90
|
|
|
95
91
|
/**
|
|
96
|
-
* Per-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
92
|
+
* Per-call input: a single cell. State persists within a language across
|
|
93
|
+
* separate eval calls and across tool calls, so each call is one logical step
|
|
94
|
+
* and later calls reuse what earlier ones defined. This static schema carries
|
|
95
|
+
* the full language union for typing; {@link buildEvalSchema} narrows the wire
|
|
96
|
+
* copy per session so disabled backends are never advertised to the model.
|
|
100
97
|
*/
|
|
101
|
-
const
|
|
98
|
+
export const evalSchema = type({
|
|
102
99
|
language: type("'py' | 'js' | 'rb' | 'jl'").describe(describeLanguageField(EVAL_LANGUAGE_ORDER)),
|
|
103
|
-
code: type("string").describe(describeCodeField(EVAL_LANGUAGE_ORDER)),
|
|
104
100
|
...evalCellCommonFields,
|
|
105
|
-
|
|
106
|
-
export type EvalCellInput = typeof evalCellSchema.infer;
|
|
107
|
-
|
|
108
|
-
export const evalSchema = type({
|
|
109
|
-
cells: evalCellSchema.array().atLeastLength(1).describe(EVAL_CELLS_DESCRIPTION),
|
|
101
|
+
code: type("string").describe(describeCodeField(EVAL_LANGUAGE_ORDER)),
|
|
110
102
|
});
|
|
111
103
|
export type EvalToolParams = typeof evalSchema.infer;
|
|
104
|
+
export type EvalCellInput = EvalToolParams;
|
|
112
105
|
|
|
113
106
|
/**
|
|
114
107
|
* Build a session-scoped copy of the eval schema whose `language` enum and field
|
|
@@ -118,14 +111,11 @@ export type EvalToolParams = typeof evalSchema.infer;
|
|
|
118
111
|
* {@link evalSchema} (full union) remains the type-level source of truth.
|
|
119
112
|
*/
|
|
120
113
|
function buildEvalSchema(langs: readonly EvalLanguageToken[]): typeof evalSchema {
|
|
121
|
-
const
|
|
114
|
+
const schema = type({
|
|
122
115
|
language: type.enumerated(...langs).describe(describeLanguageField(langs)),
|
|
123
116
|
code: type("string").describe(describeCodeField(langs)),
|
|
124
117
|
...evalCellCommonFields,
|
|
125
118
|
});
|
|
126
|
-
const schema = type({
|
|
127
|
-
cells: cellSchema.array().atLeastLength(1).describe(EVAL_CELLS_DESCRIPTION),
|
|
128
|
-
});
|
|
129
119
|
return schema as unknown as typeof evalSchema;
|
|
130
120
|
}
|
|
131
121
|
|
|
@@ -290,22 +280,15 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
290
280
|
readonly approval = "exec" as const;
|
|
291
281
|
readonly formatApprovalDetails = (args: unknown): string[] => {
|
|
292
282
|
const params = args as Partial<EvalToolParams>;
|
|
293
|
-
const cells = Array.isArray(params.cells) ? params.cells : [];
|
|
294
|
-
const firstCell = cells[0] as Partial<EvalCellInput> | undefined;
|
|
295
|
-
if (!firstCell) return [];
|
|
296
283
|
const language =
|
|
297
|
-
typeof
|
|
298
|
-
const code = typeof
|
|
299
|
-
|
|
300
|
-
if (cells.length > 1) {
|
|
301
|
-
lines.push(`+${cells.length - 1} more cell${cells.length === 2 ? "" : "s"}`);
|
|
302
|
-
}
|
|
303
|
-
return lines;
|
|
284
|
+
typeof params.language === "string" ? formatEvalInputLanguage(params.language) : "javascript (default)";
|
|
285
|
+
const code = typeof params.code === "string" ? params.code : "";
|
|
286
|
+
return [`Language: ${language}`, `Code:\n${truncateForPrompt(code)}`];
|
|
304
287
|
};
|
|
305
288
|
get summary(): string {
|
|
306
289
|
return summarizeEvalLanguages(this.#enabledLanguages());
|
|
307
290
|
}
|
|
308
|
-
readonly loadMode = "
|
|
291
|
+
readonly loadMode = "essential";
|
|
309
292
|
readonly label = "Eval";
|
|
310
293
|
get description(): string {
|
|
311
294
|
if (!this.session) return getEvalToolDescription();
|
|
@@ -320,25 +303,53 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
320
303
|
spawns: spawnsAllowed,
|
|
321
304
|
});
|
|
322
305
|
}
|
|
323
|
-
|
|
306
|
+
/** All reuse-chain examples; the `examples` getter filters by enabled languages. */
|
|
307
|
+
private static readonly ALL_EXAMPLES: readonly ToolExample<typeof evalSchema.infer>[] = [
|
|
324
308
|
{
|
|
309
|
+
caption: "First call — set up once",
|
|
325
310
|
call: {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
311
|
+
language: "py",
|
|
312
|
+
title: "imports",
|
|
313
|
+
code: "import json\nfrom pathlib import Path",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
caption: "Second call — reuse, do NOT re-import",
|
|
318
|
+
call: {
|
|
319
|
+
language: "py",
|
|
320
|
+
title: "load config",
|
|
321
|
+
code: "data = json.loads(read('package.json'))\ndisplay(data)",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
caption: "Third call — reuse the loaded config",
|
|
326
|
+
call: {
|
|
327
|
+
language: "py",
|
|
328
|
+
title: "scan deps",
|
|
329
|
+
code: "display(sorted(data['dependencies']))",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
caption: "Ruby first call — set up once",
|
|
334
|
+
call: {
|
|
335
|
+
language: "rb",
|
|
336
|
+
title: "setup",
|
|
337
|
+
code: "require 'json'\npkg_path = 'package.json'",
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
caption: "Ruby second call — reuse, do NOT re-require",
|
|
342
|
+
call: {
|
|
343
|
+
language: "rb",
|
|
344
|
+
title: "load config",
|
|
345
|
+
code: "pkg = JSON.parse(read(pkg_path))\ndisplay(pkg.keys.sort)",
|
|
339
346
|
},
|
|
340
347
|
},
|
|
341
348
|
];
|
|
349
|
+
get examples(): readonly ToolExample<typeof evalSchema.infer>[] {
|
|
350
|
+
const langs = new Set(this.#enabledLanguages());
|
|
351
|
+
return EvalTool.ALL_EXAMPLES.filter(ex => "call" in ex && langs.has(ex.call.language as EvalLanguageToken));
|
|
352
|
+
}
|
|
342
353
|
get parameters(): typeof evalSchema {
|
|
343
354
|
const langs = this.#enabledLanguages();
|
|
344
355
|
if (langs.length === 0 || langs.length === EVAL_LANGUAGE_ORDER.length) return evalSchema;
|
|
@@ -352,13 +363,9 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
352
363
|
readonly concurrency = "exclusive";
|
|
353
364
|
readonly strict = true;
|
|
354
365
|
readonly intent = (args: Partial<typeof evalSchema.infer>): string | undefined => {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
const title = typeof first.title === "string" ? first.title : undefined;
|
|
359
|
-
const language = typeof first.language === "string" ? formatEvalInputLanguage(first.language) : "javascript";
|
|
360
|
-
const label = title || `running ${language}`;
|
|
361
|
-
return cells.length > 1 ? `${label} (+${cells.length - 1})` : label;
|
|
366
|
+
const title = typeof args.title === "string" ? args.title : undefined;
|
|
367
|
+
const language = typeof args.language === "string" ? formatEvalInputLanguage(args.language) : "javascript";
|
|
368
|
+
return title || `running ${language}`;
|
|
362
369
|
};
|
|
363
370
|
|
|
364
371
|
readonly #proxyExecutor?: EvalProxyExecutor;
|
|
@@ -398,27 +405,25 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
398
405
|
const session = this.session;
|
|
399
406
|
const excludeWebP = webpExclusionForModel(session.getActiveModel?.());
|
|
400
407
|
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
timeoutMs: (cell.timeout ?? 30) * 1000,
|
|
418
|
-
reset: cell.reset ?? false,
|
|
408
|
+
const cellLanguage: EvalLanguage =
|
|
409
|
+
params.language === "py"
|
|
410
|
+
? "python"
|
|
411
|
+
: params.language === "rb"
|
|
412
|
+
? "ruby"
|
|
413
|
+
: params.language === "jl"
|
|
414
|
+
? "julia"
|
|
415
|
+
: "js";
|
|
416
|
+
const resolved = await resolveBackend(session, cellLanguage);
|
|
417
|
+
const cells: ResolvedEvalCell[] = [
|
|
418
|
+
{
|
|
419
|
+
index: 0,
|
|
420
|
+
title: params.title,
|
|
421
|
+
code: params.code,
|
|
422
|
+
timeoutMs: (params.timeout ?? 30) * 1000,
|
|
423
|
+
reset: params.reset ?? false,
|
|
419
424
|
resolved,
|
|
420
|
-
}
|
|
421
|
-
|
|
425
|
+
},
|
|
426
|
+
];
|
|
422
427
|
const languages = uniqueEvalLanguages(cells);
|
|
423
428
|
const notice = detailsNotice(cells);
|
|
424
429
|
const sessionAbortController = new AbortController();
|
|
@@ -453,6 +458,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
453
458
|
status: "pending",
|
|
454
459
|
}));
|
|
455
460
|
const cellOutputs: string[] = [];
|
|
461
|
+
// The cell currently inside backend.execute(). Streamed stdout is
|
|
462
|
+
// appended to its rendered `output` live so a long-running cell (e.g. a
|
|
463
|
+
// sleep loop) shows progress instead of nothing until it returns. A
|
|
464
|
+
// dedicated per-cell tail buffer keeps attribution correct and avoids
|
|
465
|
+
// double-counting against the aggregate `tailBuffer`; on completion the
|
|
466
|
+
// authoritative `cellResult.output` (below) overwrites this live tail.
|
|
467
|
+
let activeLiveCell: { result: EvalCellResult; buf: TailBuffer } | undefined;
|
|
456
468
|
|
|
457
469
|
const appendTail = (text: string) => {
|
|
458
470
|
tailBuffer.append(text);
|
|
@@ -502,6 +514,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
502
514
|
maxColumns: resolveOutputMaxColumns(session.settings),
|
|
503
515
|
onChunk: chunk => {
|
|
504
516
|
appendTail(chunk);
|
|
517
|
+
if (activeLiveCell) {
|
|
518
|
+
activeLiveCell.buf.append(chunk);
|
|
519
|
+
activeLiveCell.result.output = activeLiveCell.buf.text();
|
|
520
|
+
}
|
|
505
521
|
pushUpdate();
|
|
506
522
|
},
|
|
507
523
|
});
|
|
@@ -529,6 +545,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
529
545
|
cellResult.statusEvents = undefined;
|
|
530
546
|
cellResult.exitCode = undefined;
|
|
531
547
|
cellResult.durationMs = undefined;
|
|
548
|
+
activeLiveCell = { result: cellResult, buf: new TailBuffer(DEFAULT_MAX_BYTES * 2) };
|
|
532
549
|
pushUpdate();
|
|
533
550
|
|
|
534
551
|
const startTime = Date.now();
|
|
@@ -562,6 +579,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
562
579
|
});
|
|
563
580
|
} finally {
|
|
564
581
|
idle.dispose();
|
|
582
|
+
activeLiveCell = undefined;
|
|
565
583
|
}
|
|
566
584
|
const durationMs = Date.now() - startTime;
|
|
567
585
|
|
|
@@ -623,24 +641,9 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
623
641
|
cellResult.statusEvents = cellStatusEvents.length > 0 ? cellStatusEvents : undefined;
|
|
624
642
|
cellResult.hasMarkdown = cellHasMarkdown || undefined;
|
|
625
643
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const cellTitle = cell.title ? ` ${cell.title}` : "";
|
|
630
|
-
if (cellOutput) {
|
|
631
|
-
combinedCellOutput = `${cellHeader}${cellTitle}\n${cellOutput}`;
|
|
632
|
-
} else {
|
|
633
|
-
combinedCellOutput = `${cellHeader}${cellTitle} (ok)`;
|
|
634
|
-
}
|
|
635
|
-
cellOutputs.push(combinedCellOutput);
|
|
636
|
-
} else if (cellOutput) {
|
|
637
|
-
combinedCellOutput = cellOutput;
|
|
638
|
-
cellOutputs.push(combinedCellOutput);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (combinedCellOutput) {
|
|
642
|
-
const prefix = cellOutputs.length > 1 ? "\n\n" : "";
|
|
643
|
-
appendTail(`${prefix}${combinedCellOutput}`);
|
|
644
|
+
if (cellOutput) {
|
|
645
|
+
cellOutputs.push(cellOutput);
|
|
646
|
+
appendTail(cellOutput);
|
|
644
647
|
}
|
|
645
648
|
|
|
646
649
|
if (result.cancelled) {
|
|
@@ -648,10 +651,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
648
651
|
pushUpdate();
|
|
649
652
|
const errorMsg = result.output || "Command aborted";
|
|
650
653
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
651
|
-
const outputText =
|
|
652
|
-
cells.length > 1
|
|
653
|
-
? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
|
|
654
|
-
: combinedOutput || errorMsg;
|
|
654
|
+
const outputText = combinedOutput || errorMsg;
|
|
655
655
|
|
|
656
656
|
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
657
657
|
const details: EvalToolDetails = {
|
|
@@ -674,12 +674,9 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
674
674
|
cellResult.status = "error";
|
|
675
675
|
pushUpdate();
|
|
676
676
|
const combinedOutput = cellOutputs.join("\n\n");
|
|
677
|
-
const outputText =
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
: combinedOutput
|
|
681
|
-
? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
|
|
682
|
-
: `Command exited with code ${result.exitCode}`;
|
|
677
|
+
const outputText = combinedOutput
|
|
678
|
+
? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
|
|
679
|
+
: `Command exited with code ${result.exitCode}`;
|
|
683
680
|
|
|
684
681
|
const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
|
|
685
682
|
const details: EvalToolDetails = {
|
package/src/tools/index.ts
CHANGED
|
@@ -377,7 +377,14 @@ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool |
|
|
|
377
377
|
export type BuiltinToolLoadMode = "essential" | "discoverable";
|
|
378
378
|
|
|
379
379
|
/** Default essential tool names when tools.essentialOverride is empty. */
|
|
380
|
-
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
|
|
380
|
+
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
|
|
381
|
+
"read",
|
|
382
|
+
"bash",
|
|
383
|
+
"edit",
|
|
384
|
+
"write",
|
|
385
|
+
"find",
|
|
386
|
+
"eval",
|
|
387
|
+
] as const;
|
|
381
388
|
|
|
382
389
|
/**
|
|
383
390
|
* Resolve the active essential built-in tool names from settings.
|
package/src/tools/read.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
|
8
8
|
import { glob, type SummaryResult, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
9
9
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
11
|
-
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { getRemoteDir, type ImageMetadata, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { type } from "arktype";
|
|
13
13
|
import { LRUCache } from "lru-cache/raw";
|
|
14
14
|
import {
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { normalizeToLF } from "../edit/normalize";
|
|
23
23
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
24
24
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
25
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
25
|
+
import { InternalUrlRouter, resolveLocalUrlToFile } from "../internal-urls";
|
|
26
26
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
27
27
|
import type { InternalUrl } from "../internal-urls/types";
|
|
28
28
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -1112,6 +1112,79 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1112
1112
|
.done();
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* Build content blocks for an on-disk image file: an `inspect_image`
|
|
1117
|
+
* metadata note when inspection is enabled, otherwise the decoded image
|
|
1118
|
+
* block. Shared by the plain-file read path and the `local://` image fast
|
|
1119
|
+
* path so both honor `inspect_image.enabled`, the size cap, and auto-resize
|
|
1120
|
+
* identically. Too-large / unsupported images surface as {@link ToolError}.
|
|
1121
|
+
*/
|
|
1122
|
+
async #loadImageContent(options: {
|
|
1123
|
+
readPath: string;
|
|
1124
|
+
absolutePath: string;
|
|
1125
|
+
mimeType: string;
|
|
1126
|
+
imageMetadata: ImageMetadata | null;
|
|
1127
|
+
fileSize: number;
|
|
1128
|
+
}): Promise<{ content: Array<TextContent | ImageContent>; details: ReadToolDetails; sourcePath: string }> {
|
|
1129
|
+
const { readPath, absolutePath, mimeType, imageMetadata, fileSize } = options;
|
|
1130
|
+
if (this.#inspectImageEnabled) {
|
|
1131
|
+
const outputMime = imageMetadata?.mimeType ?? mimeType;
|
|
1132
|
+
const metadataLines = [
|
|
1133
|
+
"Image metadata:",
|
|
1134
|
+
`- MIME: ${outputMime}`,
|
|
1135
|
+
`- Bytes: ${fileSize} (${formatBytes(fileSize)})`,
|
|
1136
|
+
imageMetadata?.width !== undefined && imageMetadata.height !== undefined
|
|
1137
|
+
? `- Dimensions: ${imageMetadata.width}x${imageMetadata.height}`
|
|
1138
|
+
: "- Dimensions: unknown",
|
|
1139
|
+
imageMetadata?.channels !== undefined ? `- Channels: ${imageMetadata.channels}` : "- Channels: unknown",
|
|
1140
|
+
imageMetadata?.hasAlpha === true
|
|
1141
|
+
? "- Alpha: yes"
|
|
1142
|
+
: imageMetadata?.hasAlpha === false
|
|
1143
|
+
? "- Alpha: no"
|
|
1144
|
+
: "- Alpha: unknown",
|
|
1145
|
+
"",
|
|
1146
|
+
`If you want to analyze the image, call inspect_image with path="${formatPathRelativeToCwd(
|
|
1147
|
+
absolutePath,
|
|
1148
|
+
this.session.cwd,
|
|
1149
|
+
)}" and a question describing what to inspect and the desired output format.`,
|
|
1150
|
+
];
|
|
1151
|
+
return { content: [{ type: "text", text: metadataLines.join("\n") }], details: {}, sourcePath: absolutePath };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (fileSize > MAX_IMAGE_SIZE) {
|
|
1155
|
+
const sizeStr = formatBytes(fileSize);
|
|
1156
|
+
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
1157
|
+
throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
const imageInput = await loadImageInput({
|
|
1161
|
+
path: readPath,
|
|
1162
|
+
cwd: this.session.cwd,
|
|
1163
|
+
autoResize: this.#autoResizeImages,
|
|
1164
|
+
maxBytes: MAX_IMAGE_SIZE,
|
|
1165
|
+
resolvedPath: absolutePath,
|
|
1166
|
+
detectedMimeType: mimeType,
|
|
1167
|
+
excludeWebP: webpExclusionForModel(this.session.getActiveModel?.()),
|
|
1168
|
+
});
|
|
1169
|
+
if (!imageInput) {
|
|
1170
|
+
throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
|
|
1171
|
+
}
|
|
1172
|
+
return {
|
|
1173
|
+
content: [
|
|
1174
|
+
{ type: "text", text: imageInput.textNote },
|
|
1175
|
+
{ type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
|
|
1176
|
+
],
|
|
1177
|
+
details: {},
|
|
1178
|
+
sourcePath: imageInput.resolvedPath,
|
|
1179
|
+
};
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
if (error instanceof ImageInputTooLargeError) {
|
|
1182
|
+
throw new ToolError(error.message);
|
|
1183
|
+
}
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1115
1188
|
#buildInMemoryTextResult(
|
|
1116
1189
|
text: string,
|
|
1117
1190
|
offset: number | undefined,
|
|
@@ -2132,64 +2205,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2132
2205
|
| undefined;
|
|
2133
2206
|
|
|
2134
2207
|
if (mimeType) {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
`- Bytes: ${outputBytes} (${formatBytes(outputBytes)})`,
|
|
2143
|
-
metadata?.width !== undefined && metadata.height !== undefined
|
|
2144
|
-
? `- Dimensions: ${metadata.width}x${metadata.height}`
|
|
2145
|
-
: "- Dimensions: unknown",
|
|
2146
|
-
metadata?.channels !== undefined ? `- Channels: ${metadata.channels}` : "- Channels: unknown",
|
|
2147
|
-
metadata?.hasAlpha === true
|
|
2148
|
-
? "- Alpha: yes"
|
|
2149
|
-
: metadata?.hasAlpha === false
|
|
2150
|
-
? "- Alpha: no"
|
|
2151
|
-
: "- Alpha: unknown",
|
|
2152
|
-
"",
|
|
2153
|
-
`If you want to analyze the image, call inspect_image with path="${formatPathRelativeToCwd(
|
|
2154
|
-
absolutePath,
|
|
2155
|
-
this.session.cwd,
|
|
2156
|
-
)}" and a question describing what to inspect and the desired output format.`,
|
|
2157
|
-
];
|
|
2158
|
-
content = [{ type: "text", text: metadataLines.join("\n") }];
|
|
2159
|
-
details = {};
|
|
2160
|
-
sourcePath = absolutePath;
|
|
2161
|
-
} else {
|
|
2162
|
-
if (fileSize > MAX_IMAGE_SIZE) {
|
|
2163
|
-
const sizeStr = formatBytes(fileSize);
|
|
2164
|
-
const maxStr = formatBytes(MAX_IMAGE_SIZE);
|
|
2165
|
-
throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
|
|
2166
|
-
}
|
|
2167
|
-
try {
|
|
2168
|
-
const imageInput = await loadImageInput({
|
|
2169
|
-
path: readPath,
|
|
2170
|
-
cwd: this.session.cwd,
|
|
2171
|
-
autoResize: this.#autoResizeImages,
|
|
2172
|
-
maxBytes: MAX_IMAGE_SIZE,
|
|
2173
|
-
resolvedPath: absolutePath,
|
|
2174
|
-
detectedMimeType: mimeType,
|
|
2175
|
-
excludeWebP: webpExclusionForModel(this.session.getActiveModel?.()),
|
|
2176
|
-
});
|
|
2177
|
-
if (!imageInput) {
|
|
2178
|
-
throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
|
|
2179
|
-
}
|
|
2180
|
-
content = [
|
|
2181
|
-
{ type: "text", text: imageInput.textNote },
|
|
2182
|
-
{ type: "image", data: imageInput.data, mimeType: imageInput.mimeType },
|
|
2183
|
-
];
|
|
2184
|
-
details = {};
|
|
2185
|
-
sourcePath = imageInput.resolvedPath;
|
|
2186
|
-
} catch (error) {
|
|
2187
|
-
if (error instanceof ImageInputTooLargeError) {
|
|
2188
|
-
throw new ToolError(error.message);
|
|
2189
|
-
}
|
|
2190
|
-
throw error;
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2208
|
+
({ content, details, sourcePath } = await this.#loadImageContent({
|
|
2209
|
+
readPath,
|
|
2210
|
+
absolutePath,
|
|
2211
|
+
mimeType,
|
|
2212
|
+
imageMetadata,
|
|
2213
|
+
fileSize,
|
|
2214
|
+
}));
|
|
2193
2215
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
2194
2216
|
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
|
2195
2217
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
@@ -2727,6 +2749,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2727
2749
|
hasExtraction = hasPathExtraction || hasQueryExtraction;
|
|
2728
2750
|
}
|
|
2729
2751
|
|
|
2752
|
+
// local:// files are real on-disk paths. Detect image files and emit a
|
|
2753
|
+
// decoded image block before the text-only resource contract UTF-8
|
|
2754
|
+
// decodes the binary into mojibake. The fast path returns null for
|
|
2755
|
+
// non-images, directories, listings, or any resolution failure, so the
|
|
2756
|
+
// text path below reproduces the router's not-found / symlink-escape
|
|
2757
|
+
// behavior unchanged.
|
|
2758
|
+
if (scheme === "local") {
|
|
2759
|
+
const imageResult = await this.#tryReadLocalImage(urlMeta, signal);
|
|
2760
|
+
if (imageResult) return imageResult;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2730
2763
|
// Reject line selectors when query extraction is used
|
|
2731
2764
|
if (hasExtraction && parsedSel.kind !== "none" && parsedSel.kind !== "raw") {
|
|
2732
2765
|
throw new ToolError("Cannot combine query extraction with line selectors");
|
|
@@ -2770,6 +2803,49 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2770
2803
|
});
|
|
2771
2804
|
}
|
|
2772
2805
|
|
|
2806
|
+
/**
|
|
2807
|
+
* Fast path for `local://` image files. Resolves the URL to its real
|
|
2808
|
+
* on-disk path with the same realpath + containment checks as
|
|
2809
|
+
* {@link LocalProtocolHandler.resolve} (via {@link resolveLocalUrlToFile}),
|
|
2810
|
+
* and — only when the target is a genuine image — emits a decoded image
|
|
2811
|
+
* block. Returns null for non-images, directories, listings, or any
|
|
2812
|
+
* resolution failure (not-found, symlink escape) so the caller falls back to
|
|
2813
|
+
* normal text resolution, which reproduces the router's errors. Errors from
|
|
2814
|
+
* a confirmed image (too large / unsupported) propagate rather than
|
|
2815
|
+
* degrading into a corrupted text read.
|
|
2816
|
+
*/
|
|
2817
|
+
async #tryReadLocalImage(url: InternalUrl, signal?: AbortSignal): Promise<AgentToolResult<ReadToolDetails> | null> {
|
|
2818
|
+
let file: { path: string; size: number } | null;
|
|
2819
|
+
try {
|
|
2820
|
+
file = await resolveLocalUrlToFile(url, {
|
|
2821
|
+
cwd: this.session.cwd,
|
|
2822
|
+
settings: this.session.settings,
|
|
2823
|
+
signal,
|
|
2824
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
2825
|
+
});
|
|
2826
|
+
} catch {
|
|
2827
|
+
// Not found / containment escape / no session — let the text path
|
|
2828
|
+
// surface the router's canonical error.
|
|
2829
|
+
return null;
|
|
2830
|
+
}
|
|
2831
|
+
if (!file) return null;
|
|
2832
|
+
|
|
2833
|
+
const imageMetadata = await readImageMetadata(file.path);
|
|
2834
|
+
const mimeType = imageMetadata?.mimeType;
|
|
2835
|
+
if (!mimeType) return null;
|
|
2836
|
+
|
|
2837
|
+
const { content, details, sourcePath } = await this.#loadImageContent({
|
|
2838
|
+
readPath: url.href,
|
|
2839
|
+
absolutePath: file.path,
|
|
2840
|
+
mimeType,
|
|
2841
|
+
imageMetadata,
|
|
2842
|
+
fileSize: file.size,
|
|
2843
|
+
});
|
|
2844
|
+
const resultBuilder = toolResult(details).content(content).sourceInternal(url.href);
|
|
2845
|
+
if (sourcePath) resultBuilder.sourcePath(sourcePath);
|
|
2846
|
+
return resultBuilder.done();
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2773
2849
|
/** Read directory contents as a formatted listing */
|
|
2774
2850
|
async #readDirectory(
|
|
2775
2851
|
absolutePath: string,
|