@oh-my-pi/pi-coding-agent 11.2.3 → 11.4.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 +119 -4
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +41 -13
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +106 -4
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +77 -63
- package/src/prompts/system/plan-mode-active.md +6 -6
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/ask.md +2 -2
- package/src/prompts/tools/gemini-image.md +2 -2
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +1 -1
- package/src/prompts/tools/python.md +3 -3
- package/src/prompts/tools/task.md +7 -1
- package/src/prompts/tools/todo-write.md +2 -2
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +2 -5
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +92 -34
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +42 -26
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
package/src/tools/fetch.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
12
12
|
import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
|
|
13
|
-
import {
|
|
13
|
+
import { renderStatusLine } from "../tui";
|
|
14
|
+
import { CachedOutputBlock } from "../tui/output-block";
|
|
14
15
|
import { ensureTool } from "../utils/tools-manager";
|
|
15
16
|
import { specialHandlers } from "../web/scrapers";
|
|
16
17
|
import type { RenderResult } from "../web/scrapers/types";
|
|
@@ -980,7 +981,6 @@ export function renderFetchResult(
|
|
|
980
981
|
options: RenderResultOptions,
|
|
981
982
|
uiTheme: Theme = theme,
|
|
982
983
|
): Component {
|
|
983
|
-
const { expanded } = options;
|
|
984
984
|
const details = result.details;
|
|
985
985
|
|
|
986
986
|
if (!details) {
|
|
@@ -1031,20 +1031,32 @@ export function renderFetchResult(
|
|
|
1031
1031
|
metadataLines.push(`${uiTheme.fg("muted", "Notes:")} ${details.notes.join("; ")}`);
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const remaining = Math.max(0, contentLines.length - previewLines.length);
|
|
1038
|
-
const contentPreviewLines =
|
|
1039
|
-
previewLines.length > 0 ? previewLines.map(line => uiTheme.fg("dim", line)) : [uiTheme.fg("dim", "(no content)")];
|
|
1040
|
-
if (remaining > 0) {
|
|
1041
|
-
const hint = formatExpandHint(uiTheme, expanded, true);
|
|
1042
|
-
contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
|
|
1043
|
-
}
|
|
1034
|
+
const outputBlock = new CachedOutputBlock();
|
|
1035
|
+
let lastExpanded: boolean | undefined;
|
|
1036
|
+
let contentPreviewLines: string[] | undefined;
|
|
1044
1037
|
|
|
1045
1038
|
return {
|
|
1046
|
-
render: (width: number) =>
|
|
1047
|
-
|
|
1039
|
+
render: (width: number) => {
|
|
1040
|
+
const { expanded } = options;
|
|
1041
|
+
|
|
1042
|
+
if (contentPreviewLines === undefined || lastExpanded !== expanded) {
|
|
1043
|
+
const previewLimit = expanded ? 12 : 3;
|
|
1044
|
+
const previewList = applyListLimit(contentLines, { headLimit: previewLimit });
|
|
1045
|
+
const previewLines = previewList.items.map(line => truncate(line.trimEnd(), 120, "…"));
|
|
1046
|
+
const remaining = Math.max(0, contentLines.length - previewLines.length);
|
|
1047
|
+
contentPreviewLines =
|
|
1048
|
+
previewLines.length > 0
|
|
1049
|
+
? previewLines.map(line => uiTheme.fg("dim", line))
|
|
1050
|
+
: [uiTheme.fg("dim", "(no content)")];
|
|
1051
|
+
if (remaining > 0) {
|
|
1052
|
+
const hint = formatExpandHint(uiTheme, expanded, true);
|
|
1053
|
+
contentPreviewLines.push(uiTheme.fg("muted", `… ${remaining} more lines${hint ? ` ${hint}` : ""}`));
|
|
1054
|
+
}
|
|
1055
|
+
lastExpanded = expanded;
|
|
1056
|
+
outputBlock.invalidate();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return outputBlock.render(
|
|
1048
1060
|
{
|
|
1049
1061
|
header,
|
|
1050
1062
|
state: truncated ? "warning" : "success",
|
|
@@ -1056,8 +1068,13 @@ export function renderFetchResult(
|
|
|
1056
1068
|
applyBg: false,
|
|
1057
1069
|
},
|
|
1058
1070
|
uiTheme,
|
|
1059
|
-
)
|
|
1060
|
-
|
|
1071
|
+
);
|
|
1072
|
+
},
|
|
1073
|
+
invalidate: () => {
|
|
1074
|
+
outputBlock.invalidate();
|
|
1075
|
+
contentPreviewLines = undefined;
|
|
1076
|
+
lastExpanded = undefined;
|
|
1077
|
+
},
|
|
1061
1078
|
};
|
|
1062
1079
|
}
|
|
1063
1080
|
|
package/src/tools/find.ts
CHANGED
|
@@ -11,7 +11,15 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
12
|
import type { Theme } from "../modes/theme/theme";
|
|
13
13
|
import findDescription from "../prompts/tools/find.md" with { type: "text" };
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Ellipsis,
|
|
16
|
+
Hasher,
|
|
17
|
+
type RenderCache,
|
|
18
|
+
renderFileList,
|
|
19
|
+
renderStatusLine,
|
|
20
|
+
renderTreeList,
|
|
21
|
+
truncateToWidth,
|
|
22
|
+
} from "../tui";
|
|
15
23
|
import type { ToolSession } from ".";
|
|
16
24
|
import { applyListLimit } from "./list-limit";
|
|
17
25
|
import type { OutputMeta } from "./output-meta";
|
|
@@ -27,6 +35,8 @@ const findSchema = Type.Object({
|
|
|
27
35
|
limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })),
|
|
28
36
|
});
|
|
29
37
|
|
|
38
|
+
export type FindToolInput = Static<typeof findSchema>;
|
|
39
|
+
|
|
30
40
|
const DEFAULT_LIMIT = 1000;
|
|
31
41
|
const GLOB_TIMEOUT_MS = 5000;
|
|
32
42
|
|
|
@@ -410,7 +420,7 @@ export const findToolRenderer = {
|
|
|
410
420
|
|
|
411
421
|
renderResult(
|
|
412
422
|
result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
|
|
413
|
-
|
|
423
|
+
options: RenderResultOptions,
|
|
414
424
|
uiTheme: Theme,
|
|
415
425
|
args?: FindRenderArgs,
|
|
416
426
|
): Component {
|
|
@@ -444,17 +454,30 @@ export const findToolRenderer = {
|
|
|
444
454
|
},
|
|
445
455
|
uiTheme,
|
|
446
456
|
);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
expanded
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
457
|
+
let cached: RenderCache | undefined;
|
|
458
|
+
return {
|
|
459
|
+
render(width: number): string[] {
|
|
460
|
+
const { expanded } = options;
|
|
461
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
462
|
+
if (cached?.key === key) return cached.lines;
|
|
463
|
+
const listLines = renderTreeList(
|
|
464
|
+
{
|
|
465
|
+
items: lines,
|
|
466
|
+
expanded,
|
|
467
|
+
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
468
|
+
itemType: "file",
|
|
469
|
+
renderItem: line => uiTheme.fg("accent", line),
|
|
470
|
+
},
|
|
471
|
+
uiTheme,
|
|
472
|
+
);
|
|
473
|
+
const result = [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
474
|
+
cached = { key, lines: result };
|
|
475
|
+
return result;
|
|
454
476
|
},
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
477
|
+
invalidate() {
|
|
478
|
+
cached = undefined;
|
|
479
|
+
},
|
|
480
|
+
};
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
const fileCount = details?.fileCount ?? 0;
|
|
@@ -478,15 +501,6 @@ export const findToolRenderer = {
|
|
|
478
501
|
uiTheme,
|
|
479
502
|
);
|
|
480
503
|
|
|
481
|
-
const fileLines = renderFileList(
|
|
482
|
-
{
|
|
483
|
-
files: files.map(entry => ({ path: entry, isDirectory: entry.endsWith("/") })),
|
|
484
|
-
expanded,
|
|
485
|
-
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
486
|
-
},
|
|
487
|
-
uiTheme,
|
|
488
|
-
);
|
|
489
|
-
|
|
490
504
|
const truncationReasons: string[] = [];
|
|
491
505
|
if (details?.resultLimitReached) truncationReasons.push(`limit ${details.resultLimitReached} results`);
|
|
492
506
|
if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
|
|
@@ -499,7 +513,28 @@ export const findToolRenderer = {
|
|
|
499
513
|
extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
|
|
500
514
|
}
|
|
501
515
|
|
|
502
|
-
|
|
516
|
+
let cached: RenderCache | undefined;
|
|
517
|
+
return {
|
|
518
|
+
render(width: number): string[] {
|
|
519
|
+
const { expanded } = options;
|
|
520
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
521
|
+
if (cached?.key === key) return cached.lines;
|
|
522
|
+
const fileLines = renderFileList(
|
|
523
|
+
{
|
|
524
|
+
files: files.map(entry => ({ path: entry, isDirectory: entry.endsWith("/") })),
|
|
525
|
+
expanded,
|
|
526
|
+
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
527
|
+
},
|
|
528
|
+
uiTheme,
|
|
529
|
+
);
|
|
530
|
+
const result = [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
531
|
+
cached = { key, lines: result };
|
|
532
|
+
return result;
|
|
533
|
+
},
|
|
534
|
+
invalidate() {
|
|
535
|
+
cached = undefined;
|
|
536
|
+
},
|
|
537
|
+
};
|
|
503
538
|
},
|
|
504
539
|
mergeCallAndResult: true,
|
|
505
540
|
};
|
package/src/tools/grep.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
12
|
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
|
13
|
-
import { renderStatusLine, renderTreeList } from "../tui";
|
|
13
|
+
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
14
14
|
import type { ToolSession } from ".";
|
|
15
15
|
import type { OutputMeta } from "./output-meta";
|
|
16
16
|
import { resolveToCwd } from "./path-utils";
|
|
@@ -32,6 +32,8 @@ const grepSchema = Type.Object({
|
|
|
32
32
|
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit (default: 0)" })),
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
export type GrepToolInput = Static<typeof grepSchema>;
|
|
36
|
+
|
|
35
37
|
const DEFAULT_MATCH_LIMIT = 100;
|
|
36
38
|
|
|
37
39
|
export interface GrepToolDetails {
|
|
@@ -322,7 +324,7 @@ export const grepToolRenderer = {
|
|
|
322
324
|
|
|
323
325
|
renderResult(
|
|
324
326
|
result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails; isError?: boolean },
|
|
325
|
-
|
|
327
|
+
options: RenderResultOptions,
|
|
326
328
|
uiTheme: Theme,
|
|
327
329
|
args?: GrepRenderArgs,
|
|
328
330
|
): Component {
|
|
@@ -346,17 +348,30 @@ export const grepToolRenderer = {
|
|
|
346
348
|
{ icon: "success", title: "Grep", description, meta: [formatCount("item", lines.length)] },
|
|
347
349
|
uiTheme,
|
|
348
350
|
);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
expanded
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
let cached: RenderCache | undefined;
|
|
352
|
+
return {
|
|
353
|
+
render(width: number): string[] {
|
|
354
|
+
const { expanded } = options;
|
|
355
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
356
|
+
if (cached?.key === key) return cached.lines;
|
|
357
|
+
const listLines = renderTreeList(
|
|
358
|
+
{
|
|
359
|
+
items: lines,
|
|
360
|
+
expanded,
|
|
361
|
+
maxCollapsed: COLLAPSED_TEXT_LIMIT,
|
|
362
|
+
itemType: "item",
|
|
363
|
+
renderItem: line => uiTheme.fg("toolOutput", line),
|
|
364
|
+
},
|
|
365
|
+
uiTheme,
|
|
366
|
+
);
|
|
367
|
+
const result = [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
368
|
+
cached = { key, lines: result };
|
|
369
|
+
return result;
|
|
356
370
|
},
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
371
|
+
invalidate() {
|
|
372
|
+
cached = undefined;
|
|
373
|
+
},
|
|
374
|
+
};
|
|
360
375
|
}
|
|
361
376
|
|
|
362
377
|
const matchCount = details?.matchCount ?? 0;
|
|
@@ -422,18 +437,6 @@ export const grepToolRenderer = {
|
|
|
422
437
|
return count;
|
|
423
438
|
};
|
|
424
439
|
|
|
425
|
-
const maxCollapsed = expanded ? matchGroups.length : getCollapsedMatchLimit(matchGroups, COLLAPSED_TEXT_LIMIT);
|
|
426
|
-
const matchLines = renderTreeList(
|
|
427
|
-
{
|
|
428
|
-
items: matchGroups,
|
|
429
|
-
expanded,
|
|
430
|
-
maxCollapsed,
|
|
431
|
-
itemType: "match",
|
|
432
|
-
renderItem: group => group.map(line => uiTheme.fg("toolOutput", line)),
|
|
433
|
-
},
|
|
434
|
-
uiTheme,
|
|
435
|
-
);
|
|
436
|
-
|
|
437
440
|
const truncationReasons: string[] = [];
|
|
438
441
|
if (limits?.matchLimit) truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
|
|
439
442
|
if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
|
|
@@ -444,7 +447,33 @@ export const grepToolRenderer = {
|
|
|
444
447
|
const extraLines =
|
|
445
448
|
truncationReasons.length > 0 ? [uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)] : [];
|
|
446
449
|
|
|
447
|
-
|
|
450
|
+
let cached: RenderCache | undefined;
|
|
451
|
+
return {
|
|
452
|
+
render(width: number): string[] {
|
|
453
|
+
const { expanded } = options;
|
|
454
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
455
|
+
if (cached?.key === key) return cached.lines;
|
|
456
|
+
const maxCollapsed = expanded
|
|
457
|
+
? matchGroups.length
|
|
458
|
+
: getCollapsedMatchLimit(matchGroups, COLLAPSED_TEXT_LIMIT);
|
|
459
|
+
const matchLines = renderTreeList(
|
|
460
|
+
{
|
|
461
|
+
items: matchGroups,
|
|
462
|
+
expanded,
|
|
463
|
+
maxCollapsed,
|
|
464
|
+
itemType: "match",
|
|
465
|
+
renderItem: group => group.map(line => uiTheme.fg("toolOutput", line)),
|
|
466
|
+
},
|
|
467
|
+
uiTheme,
|
|
468
|
+
);
|
|
469
|
+
const result = [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
470
|
+
cached = { key, lines: result };
|
|
471
|
+
return result;
|
|
472
|
+
},
|
|
473
|
+
invalidate() {
|
|
474
|
+
cached = undefined;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
448
477
|
},
|
|
449
478
|
mergeCallAndResult: true,
|
|
450
479
|
};
|
package/src/tools/index.ts
CHANGED
|
@@ -67,17 +67,17 @@ export {
|
|
|
67
67
|
webSearchLinkedinTool,
|
|
68
68
|
} from "../web/search";
|
|
69
69
|
export { AskTool, type AskToolDetails } from "./ask";
|
|
70
|
-
export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
|
|
70
|
+
export { BashTool, type BashToolDetails, type BashToolInput, type BashToolOptions } from "./bash";
|
|
71
71
|
export { BrowserTool, type BrowserToolDetails } from "./browser";
|
|
72
72
|
export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
|
|
73
73
|
export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
|
|
74
74
|
export { FetchTool, type FetchToolDetails } from "./fetch";
|
|
75
|
-
export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
|
|
75
|
+
export { type FindOperations, FindTool, type FindToolDetails, type FindToolInput, type FindToolOptions } from "./find";
|
|
76
76
|
export { setPreferredImageProvider } from "./gemini-image";
|
|
77
|
-
export { type GrepOperations, GrepTool, type GrepToolDetails, type GrepToolOptions } from "./grep";
|
|
77
|
+
export { type GrepOperations, GrepTool, type GrepToolDetails, type GrepToolInput, type GrepToolOptions } from "./grep";
|
|
78
78
|
export { NotebookTool, type NotebookToolDetails } from "./notebook";
|
|
79
79
|
export { PythonTool, type PythonToolDetails, type PythonToolOptions } from "./python";
|
|
80
|
-
export { ReadTool, type ReadToolDetails } from "./read";
|
|
80
|
+
export { ReadTool, type ReadToolDetails, type ReadToolInput } from "./read";
|
|
81
81
|
export { reportFindingTool, type SubmitReviewDetails } from "./review";
|
|
82
82
|
export { loadSshTool, type SSHToolDetails, SshTool } from "./ssh";
|
|
83
83
|
export { SubmitResultTool } from "./submit-result";
|
|
@@ -92,7 +92,7 @@ export {
|
|
|
92
92
|
truncateLine,
|
|
93
93
|
truncateTail,
|
|
94
94
|
} from "./truncate";
|
|
95
|
-
export { WriteTool, type WriteToolDetails } from "./write";
|
|
95
|
+
export { WriteTool, type WriteToolDetails, type WriteToolInput } from "./write";
|
|
96
96
|
|
|
97
97
|
/** Tool type (AgentTool from pi-ai) */
|
|
98
98
|
export type Tool = AgentTool<any, any, any>;
|
package/src/tools/notebook.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { type Static, Type } from "@sinclair/typebox";
|
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
9
|
import type { ToolSession } from "../sdk";
|
|
10
|
-
import { renderCodeCell, renderStatusLine } from "../tui";
|
|
10
|
+
import { Hasher, type RenderCache, renderCodeCell, renderStatusLine } from "../tui";
|
|
11
11
|
import { resolveToCwd } from "./path-utils";
|
|
12
12
|
import { formatCount, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
13
13
|
|
|
@@ -221,7 +221,7 @@ export const notebookToolRenderer = {
|
|
|
221
221
|
|
|
222
222
|
renderResult(
|
|
223
223
|
result: { content: Array<{ type: string; text?: string }>; details?: NotebookToolDetails },
|
|
224
|
-
|
|
224
|
+
options: RenderResultOptions,
|
|
225
225
|
uiTheme: Theme,
|
|
226
226
|
args?: NotebookRenderArgs,
|
|
227
227
|
): Component {
|
|
@@ -252,9 +252,16 @@ export const notebookToolRenderer = {
|
|
|
252
252
|
|
|
253
253
|
const notebookPath = args?.notebookPath ?? args?.notebook_path;
|
|
254
254
|
const notebookLabel = notebookPath ? `${actionLabel} ${notebookPath}` : "Notebook";
|
|
255
|
+
let cached: RenderCache | undefined;
|
|
256
|
+
|
|
255
257
|
return {
|
|
256
|
-
render: (width: number) =>
|
|
257
|
-
|
|
258
|
+
render: (width: number): string[] => {
|
|
259
|
+
// REACTIVE: read mutable options at render time
|
|
260
|
+
const { expanded } = options;
|
|
261
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
262
|
+
if (cached?.key === key) return cached.lines;
|
|
263
|
+
|
|
264
|
+
const lines = renderCodeCell(
|
|
258
265
|
{
|
|
259
266
|
code: codeText,
|
|
260
267
|
language,
|
|
@@ -266,8 +273,14 @@ export const notebookToolRenderer = {
|
|
|
266
273
|
width,
|
|
267
274
|
},
|
|
268
275
|
uiTheme,
|
|
269
|
-
)
|
|
270
|
-
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
cached = { key, lines };
|
|
279
|
+
return lines;
|
|
280
|
+
},
|
|
281
|
+
invalidate: () => {
|
|
282
|
+
cached = undefined;
|
|
283
|
+
},
|
|
271
284
|
};
|
|
272
285
|
},
|
|
273
286
|
mergeCallAndResult: true,
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -33,8 +33,33 @@ function fileExists(filePath: string): boolean {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function normalizeAtPrefix(filePath: string): string {
|
|
37
|
+
if (!filePath.startsWith("@")) return filePath;
|
|
38
|
+
|
|
39
|
+
const withoutAt = filePath.slice(1);
|
|
40
|
+
|
|
41
|
+
// We only treat a leading "@" as a shorthand for a small set of well-known
|
|
42
|
+
// syntaxes. This avoids mangling literal paths like "@my-file.txt".
|
|
43
|
+
if (
|
|
44
|
+
withoutAt.startsWith("/") ||
|
|
45
|
+
withoutAt === "~" ||
|
|
46
|
+
withoutAt.startsWith("~/") ||
|
|
47
|
+
// Windows absolute paths (drive letters / UNC / root-relative)
|
|
48
|
+
path.win32.isAbsolute(withoutAt) ||
|
|
49
|
+
// Internal URL shorthands
|
|
50
|
+
withoutAt.startsWith("agent://") ||
|
|
51
|
+
withoutAt.startsWith("artifact://") ||
|
|
52
|
+
withoutAt.startsWith("skill://") ||
|
|
53
|
+
withoutAt.startsWith("rule://")
|
|
54
|
+
) {
|
|
55
|
+
return withoutAt;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return filePath;
|
|
59
|
+
}
|
|
60
|
+
|
|
36
61
|
export function expandPath(filePath: string): string {
|
|
37
|
-
const normalized = normalizeUnicodeSpaces(filePath);
|
|
62
|
+
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
|
38
63
|
if (normalized === "~") {
|
|
39
64
|
return os.homedir();
|
|
40
65
|
}
|
package/src/tools/python.ts
CHANGED
|
@@ -832,22 +832,20 @@ export const pythonToolRenderer = {
|
|
|
832
832
|
uiTheme: Theme,
|
|
833
833
|
): Component {
|
|
834
834
|
const ui = new ToolUIKit(uiTheme);
|
|
835
|
-
const { renderContext } = options;
|
|
836
835
|
const details = result.details;
|
|
837
836
|
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
const output = renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
|
|
837
|
+
const output =
|
|
838
|
+
options.renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
|
|
841
839
|
|
|
842
840
|
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
843
841
|
const jsonLines = jsonOutputs.flatMap((value, index) => {
|
|
844
842
|
const header = `JSON output ${index + 1}`;
|
|
845
|
-
const treeLines = renderJsonTree(value, uiTheme, expanded);
|
|
843
|
+
const treeLines = renderJsonTree(value, uiTheme, options.renderContext?.expanded ?? options.expanded);
|
|
846
844
|
return [header, ...treeLines];
|
|
847
845
|
});
|
|
848
846
|
|
|
849
847
|
const truncation = details?.meta?.truncation;
|
|
850
|
-
const timeoutSeconds = renderContext?.timeout;
|
|
848
|
+
const timeoutSeconds = options.renderContext?.timeout;
|
|
851
849
|
const timeoutLine =
|
|
852
850
|
typeof timeoutSeconds === "number"
|
|
853
851
|
? uiTheme.fg("dim", ui.wrapBrackets(`Timeout: ${timeoutSeconds}s`))
|
|
@@ -875,13 +873,12 @@ export const pythonToolRenderer = {
|
|
|
875
873
|
// Cache state following Box pattern
|
|
876
874
|
let cached: { key: string; width: number; result: string[] } | undefined;
|
|
877
875
|
|
|
878
|
-
const buildCacheKey = (spinnerFrame: number | undefined): string => {
|
|
879
|
-
return `${expanded}|${previewLines}|${spinnerFrame}`;
|
|
880
|
-
};
|
|
881
|
-
|
|
882
876
|
return {
|
|
883
877
|
render: (width: number): string[] => {
|
|
884
|
-
|
|
878
|
+
// Read mutable state at render time
|
|
879
|
+
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
880
|
+
const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
|
|
881
|
+
const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
|
|
885
882
|
if (cached && cached.key === key && cached.width === width) {
|
|
886
883
|
return cached.result;
|
|
887
884
|
}
|
|
@@ -951,7 +948,11 @@ export const pythonToolRenderer = {
|
|
|
951
948
|
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
|
|
952
949
|
|
|
953
950
|
const statusEvents = details?.statusEvents ?? [];
|
|
954
|
-
const statusLines = renderStatusEvents(
|
|
951
|
+
const statusLines = renderStatusEvents(
|
|
952
|
+
statusEvents,
|
|
953
|
+
uiTheme,
|
|
954
|
+
options.renderContext?.expanded ?? options.expanded,
|
|
955
|
+
);
|
|
955
956
|
|
|
956
957
|
if (!combinedOutput && statusLines.length === 0) {
|
|
957
958
|
const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
|
|
@@ -965,7 +966,7 @@ export const pythonToolRenderer = {
|
|
|
965
966
|
return new Text(lines.join("\n"), 0, 0);
|
|
966
967
|
}
|
|
967
968
|
|
|
968
|
-
if (expanded) {
|
|
969
|
+
if (options.renderContext?.expanded ?? options.expanded) {
|
|
969
970
|
const styledOutput = combinedOutput
|
|
970
971
|
.split("\n")
|
|
971
972
|
.map(line => uiTheme.fg("toolOutput", line))
|
|
@@ -988,14 +989,18 @@ export const pythonToolRenderer = {
|
|
|
988
989
|
let cachedWidth: number | undefined;
|
|
989
990
|
let cachedLines: string[] | undefined;
|
|
990
991
|
let cachedSkipped: number | undefined;
|
|
992
|
+
let cachedPreviewLines: number | undefined;
|
|
991
993
|
|
|
992
994
|
return {
|
|
993
995
|
render: (width: number): string[] => {
|
|
994
|
-
|
|
996
|
+
// Read mutable state at render time
|
|
997
|
+
const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
|
|
998
|
+
if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
|
|
995
999
|
const result = truncateToVisualLines(textContent, previewLines, width);
|
|
996
1000
|
cachedLines = result.visualLines;
|
|
997
1001
|
cachedSkipped = result.skippedCount;
|
|
998
1002
|
cachedWidth = width;
|
|
1003
|
+
cachedPreviewLines = previewLines;
|
|
999
1004
|
}
|
|
1000
1005
|
const outputLines: string[] = [];
|
|
1001
1006
|
if (cachedSkipped && cachedSkipped > 0) {
|
|
@@ -1025,6 +1030,7 @@ export const pythonToolRenderer = {
|
|
|
1025
1030
|
cachedWidth = undefined;
|
|
1026
1031
|
cachedLines = undefined;
|
|
1027
1032
|
cachedSkipped = undefined;
|
|
1033
|
+
cachedPreviewLines = undefined;
|
|
1028
1034
|
},
|
|
1029
1035
|
};
|
|
1030
1036
|
},
|
package/src/tools/read.ts
CHANGED
|
@@ -7,14 +7,15 @@ import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
11
11
|
import { CONFIG_DIR_NAME } from "../config";
|
|
12
12
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
13
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
14
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
15
15
|
import readDescription from "../prompts/tools/read.md" with { type: "text" };
|
|
16
16
|
import type { ToolSession } from "../sdk";
|
|
17
|
-
import { renderCodeCell,
|
|
17
|
+
import { renderCodeCell, renderStatusLine } from "../tui";
|
|
18
|
+
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
19
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
19
20
|
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
|
|
20
21
|
import { ensureTool } from "../utils/tools-manager";
|
|
@@ -516,6 +517,8 @@ const readSchema = Type.Object({
|
|
|
516
517
|
lines: Type.Optional(Type.Boolean({ description: "Prepend line numbers to output (default: false)" })),
|
|
517
518
|
});
|
|
518
519
|
|
|
520
|
+
export type ReadToolInput = Static<typeof readSchema>;
|
|
521
|
+
|
|
519
522
|
export interface ReadToolDetails {
|
|
520
523
|
truncation?: TruncationResult;
|
|
521
524
|
isDirectory?: boolean;
|
|
@@ -1113,9 +1116,10 @@ export const readToolRenderer = {
|
|
|
1113
1116
|
);
|
|
1114
1117
|
const detailLines = contentText ? contentText.split("\n").map(line => uiTheme.fg("toolOutput", line)) : [];
|
|
1115
1118
|
const lines = [...detailLines, ...warningLines];
|
|
1119
|
+
const outputBlock = new CachedOutputBlock();
|
|
1116
1120
|
return {
|
|
1117
1121
|
render: (width: number) =>
|
|
1118
|
-
|
|
1122
|
+
outputBlock.render(
|
|
1119
1123
|
{
|
|
1120
1124
|
header,
|
|
1121
1125
|
state: "success",
|
|
@@ -1129,7 +1133,7 @@ export const readToolRenderer = {
|
|
|
1129
1133
|
},
|
|
1130
1134
|
uiTheme,
|
|
1131
1135
|
),
|
|
1132
|
-
invalidate: () =>
|
|
1136
|
+
invalidate: () => outputBlock.invalidate(),
|
|
1133
1137
|
};
|
|
1134
1138
|
}
|
|
1135
1139
|
|
|
@@ -1139,9 +1143,12 @@ export const readToolRenderer = {
|
|
|
1139
1143
|
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
|
1140
1144
|
title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
|
|
1141
1145
|
}
|
|
1146
|
+
let cachedWidth: number | undefined;
|
|
1147
|
+
let cachedLines: string[] | undefined;
|
|
1142
1148
|
return {
|
|
1143
|
-
render: (width: number) =>
|
|
1144
|
-
|
|
1149
|
+
render: (width: number) => {
|
|
1150
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
1151
|
+
cachedLines = renderCodeCell(
|
|
1145
1152
|
{
|
|
1146
1153
|
code: contentText,
|
|
1147
1154
|
language: lang,
|
|
@@ -1152,8 +1159,14 @@ export const readToolRenderer = {
|
|
|
1152
1159
|
width,
|
|
1153
1160
|
},
|
|
1154
1161
|
uiTheme,
|
|
1155
|
-
)
|
|
1156
|
-
|
|
1162
|
+
);
|
|
1163
|
+
cachedWidth = width;
|
|
1164
|
+
return cachedLines;
|
|
1165
|
+
},
|
|
1166
|
+
invalidate: () => {
|
|
1167
|
+
cachedWidth = undefined;
|
|
1168
|
+
cachedLines = undefined;
|
|
1169
|
+
},
|
|
1157
1170
|
};
|
|
1158
1171
|
},
|
|
1159
1172
|
mergeCallAndResult: true,
|