@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2
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 +142 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -5
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +21 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +8 -3
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +17 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +6 -2
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +54 -21
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +36 -11
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +29 -24
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +106 -43
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +47 -53
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +110 -31
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +22 -6
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +100 -72
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +14 -7
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +164 -109
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +2 -3
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +67 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +170 -126
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +274 -112
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +21 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/modes/workflow.ts +10 -10
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +37 -46
- package/src/session/agent-session.ts +119 -18
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +109 -28
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +76 -38
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +211 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +57 -6
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval.ts +13 -2
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +51 -30
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +10 -3
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +66 -39
- package/src/tools/read.ts +48 -28
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +118 -81
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +83 -64
- package/src/tools/yield.ts +10 -1
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +11 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/codex.ts +37 -8
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
|
@@ -2,8 +2,8 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
2
2
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
4
4
|
import type { Theme } from "../modes/theme/theme";
|
|
5
|
-
import { renderStatusLine } from "../tui";
|
|
6
|
-
import { formatExpandHint, replaceTabs, shortenPath, truncateToWidth } from "./render-utils";
|
|
5
|
+
import { framedBlock, renderStatusLine } from "../tui";
|
|
6
|
+
import { formatErrorDetail, formatExpandHint, replaceTabs, shortenPath, truncateToWidth } from "./render-utils";
|
|
7
7
|
|
|
8
8
|
interface InspectImageRenderArgs {
|
|
9
9
|
path?: string;
|
|
@@ -27,17 +27,21 @@ const INSPECT_OUTPUT_COLLAPSED_LINES = 4;
|
|
|
27
27
|
const INSPECT_OUTPUT_EXPANDED_LINES = 16;
|
|
28
28
|
const INSPECT_OUTPUT_LINE_WIDTH = 120;
|
|
29
29
|
|
|
30
|
+
function questionLine(question: string, uiTheme: Theme): string {
|
|
31
|
+
return `${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(question), INSPECT_QUESTION_PREVIEW_WIDTH))}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
export const inspectImageToolRenderer = {
|
|
31
35
|
renderCall(args: InspectImageRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
32
36
|
const rawPath = args.path ?? "";
|
|
33
37
|
const pathDisplay = rawPath ? shortenPath(rawPath) : "…";
|
|
34
|
-
const header = renderStatusLine({ icon: "pending", title: "Inspect
|
|
38
|
+
const header = renderStatusLine({ icon: "pending", title: "Inspect", description: pathDisplay }, uiTheme);
|
|
35
39
|
const question = args.question?.trim();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
return new Text(`${header}\n${
|
|
40
|
+
// Call is at most a status line plus a one-line question — too small to box.
|
|
41
|
+
// The container renders a lone Text cleanly with no chrome.
|
|
42
|
+
if (!question) return new Text(header, 0, 0);
|
|
43
|
+
const tree = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${questionLine(question, uiTheme)}`;
|
|
44
|
+
return new Text(`${header}\n${tree}`, 0, 0);
|
|
41
45
|
},
|
|
42
46
|
|
|
43
47
|
renderResult(
|
|
@@ -49,55 +53,73 @@ export const inspectImageToolRenderer = {
|
|
|
49
53
|
const details = result.details;
|
|
50
54
|
const rawPath = details?.imagePath ?? args?.path ?? "";
|
|
51
55
|
const pathDisplay = rawPath ? shortenPath(rawPath) : "image";
|
|
52
|
-
const metaParts: string[] = [];
|
|
53
|
-
if (details?.model) metaParts.push(details.model);
|
|
54
|
-
if (details?.mimeType) metaParts.push(details.mimeType);
|
|
55
56
|
const header = renderStatusLine(
|
|
56
57
|
{
|
|
57
58
|
icon: result.isError ? "error" : "success",
|
|
58
|
-
title: "Inspect
|
|
59
|
+
title: "Inspect",
|
|
59
60
|
description: pathDisplay,
|
|
60
61
|
},
|
|
61
62
|
uiTheme,
|
|
62
63
|
);
|
|
63
64
|
|
|
64
|
-
const lines: string[] = [header];
|
|
65
65
|
const question = args?.question?.trim();
|
|
66
|
-
if (question) {
|
|
67
|
-
lines.push(
|
|
68
|
-
` ${uiTheme.fg("dim", uiTheme.tree.branch)} ${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(question), INSPECT_QUESTION_PREVIEW_WIDTH))}`,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
66
|
const outputText = result.content.find(content => content.type === "text")?.text?.trimEnd() ?? "";
|
|
73
|
-
if (!outputText) {
|
|
74
|
-
lines.push(uiTheme.fg("dim", "(no output)"));
|
|
75
|
-
if (metaParts.length > 0) {
|
|
76
|
-
lines.push("");
|
|
77
|
-
lines.push(uiTheme.fg("dim", metaParts.join(" · ")));
|
|
78
|
-
}
|
|
79
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
80
|
-
}
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
if (result.isError) {
|
|
69
|
+
return framedBlock(uiTheme, width => {
|
|
70
|
+
const bodyLines: string[] = [];
|
|
71
|
+
if (question) bodyLines.push(questionLine(question, uiTheme));
|
|
72
|
+
bodyLines.push(formatErrorDetail(outputText || "inspection failed", uiTheme));
|
|
73
|
+
return {
|
|
74
|
+
header,
|
|
75
|
+
sections: [{ lines: bodyLines }],
|
|
76
|
+
state: "error",
|
|
77
|
+
borderColor: "error",
|
|
78
|
+
applyBg: false,
|
|
79
|
+
width,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
84
|
+
const metaParts: string[] = [];
|
|
85
|
+
if (details?.model) metaParts.push(details.model);
|
|
86
|
+
if (details?.mimeType) metaParts.push(details.mimeType);
|
|
87
|
+
const metaLine = metaParts.length > 0 ? uiTheme.fg("dim", metaParts.join(" · ")) : "";
|
|
94
88
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
// No answer text: nothing worth boxing — keep it to a clean status line
|
|
90
|
+
// (plus a trailing meta line, when present).
|
|
91
|
+
if (!outputText) {
|
|
92
|
+
return new Text(metaLine ? `${header}\n${metaLine}` : header, 0, 0);
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
return
|
|
95
|
+
return framedBlock(uiTheme, width => {
|
|
96
|
+
const bodyLines: string[] = [];
|
|
97
|
+
if (question) {
|
|
98
|
+
bodyLines.push(questionLine(question, uiTheme));
|
|
99
|
+
bodyLines.push("");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const outputLines = replaceTabs(outputText).split("\n");
|
|
103
|
+
const maxLines = options.expanded ? INSPECT_OUTPUT_EXPANDED_LINES : INSPECT_OUTPUT_COLLAPSED_LINES;
|
|
104
|
+
for (const line of outputLines.slice(0, maxLines)) {
|
|
105
|
+
bodyLines.push(uiTheme.fg("toolOutput", truncateToWidth(line, INSPECT_OUTPUT_LINE_WIDTH)));
|
|
106
|
+
}
|
|
107
|
+
if (outputLines.length > maxLines) {
|
|
108
|
+
const remaining = outputLines.length - maxLines;
|
|
109
|
+
const hint = formatExpandHint(uiTheme, options.expanded, true);
|
|
110
|
+
bodyLines.push(`${uiTheme.fg("dim", `… ${remaining} more lines`)}${hint ? ` ${hint}` : ""}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
header,
|
|
115
|
+
headerMeta: metaLine || undefined,
|
|
116
|
+
sections: [{ lines: bodyLines }],
|
|
117
|
+
state: "success",
|
|
118
|
+
borderColor: "borderMuted",
|
|
119
|
+
applyBg: false,
|
|
120
|
+
width,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
101
123
|
},
|
|
102
124
|
mergeCallAndResult: true,
|
|
103
125
|
};
|
|
@@ -4,7 +4,8 @@ import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
6
|
import { extractTextContent } from "../commit/utils";
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
import { expandRoleAlias, getModelMatchPreferences, resolveModelFromString } from "../config/model-resolver";
|
|
8
9
|
import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
|
|
9
10
|
import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
|
|
10
11
|
import {
|
|
@@ -71,7 +72,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
71
72
|
throw new ToolError("No models available for inspect_image.");
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const matchPreferences =
|
|
75
|
+
const matchPreferences = getModelMatchPreferences(this.session.settings);
|
|
75
76
|
const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
|
|
76
77
|
if (!pattern) return undefined;
|
|
77
78
|
const expanded = expandRoleAlias(pattern, this.session.settings);
|
|
@@ -136,7 +137,13 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
136
137
|
},
|
|
137
138
|
],
|
|
138
139
|
},
|
|
139
|
-
{
|
|
140
|
+
{
|
|
141
|
+
apiKey: modelRegistry.resolver(model.provider, {
|
|
142
|
+
sessionId: this.session.getSessionId?.() ?? undefined,
|
|
143
|
+
baseUrl: model.baseUrl,
|
|
144
|
+
}),
|
|
145
|
+
signal,
|
|
146
|
+
},
|
|
140
147
|
{ telemetry, oneshotKind: "inspect_image", completeImpl: this.completeImageRequest },
|
|
141
148
|
);
|
|
142
149
|
|
package/src/tools/job.ts
CHANGED
|
@@ -396,7 +396,7 @@ export const jobToolRenderer = {
|
|
|
396
396
|
inline: true,
|
|
397
397
|
|
|
398
398
|
renderCall(args: JobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
399
|
-
const text = renderStatusLine({ icon: "pending", title:
|
|
399
|
+
const text = renderStatusLine({ icon: "pending", title: describeTarget(args) || "Job" }, uiTheme);
|
|
400
400
|
return new Text(text, 0, 0);
|
|
401
401
|
},
|
|
402
402
|
|
|
@@ -410,7 +410,7 @@ export const jobToolRenderer = {
|
|
|
410
410
|
|
|
411
411
|
if (jobs.length === 0) {
|
|
412
412
|
const fallback = result.content?.find(c => c.type === "text")?.text || "No jobs to process";
|
|
413
|
-
const header = renderStatusLine({ icon: "warning", title:
|
|
413
|
+
const header = renderStatusLine({ icon: "warning", title: describeTarget(args) || "Job" }, uiTheme);
|
|
414
414
|
return new Text([header, formatEmptyMessage(fallback, uiTheme)].join("\n"), 0, 0);
|
|
415
415
|
}
|
|
416
416
|
|
|
@@ -433,8 +433,7 @@ export const jobToolRenderer = {
|
|
|
433
433
|
{
|
|
434
434
|
icon: headerIcon,
|
|
435
435
|
spinnerFrame: counts.running > 0 ? options.spinnerFrame : undefined,
|
|
436
|
-
title:
|
|
437
|
-
description,
|
|
436
|
+
title: description,
|
|
438
437
|
meta,
|
|
439
438
|
},
|
|
440
439
|
uiTheme,
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* These keep the transcript terse — one status line plus, for `retain`, one
|
|
6
6
|
* `Remember: …` line per stored item — instead of the generic JSON arg tree,
|
|
7
|
-
* which exploded multi-line memory blobs into an unreadable wall.
|
|
7
|
+
* which exploded multi-line memory blobs into an unreadable wall. The tool
|
|
8
|
+
* container is a transparent passthrough, so these renderers stay frameless:
|
|
9
|
+
* a status line with a couple of dim bullets reads far cleaner than boxing a
|
|
10
|
+
* one-line memory note.
|
|
8
11
|
*/
|
|
9
12
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
13
|
import { Text } from "@oh-my-pi/pi-tui";
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -601,6 +601,23 @@ export function parseSearchPath(filePath: string): ParsedSearchPath {
|
|
|
601
601
|
};
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Async sibling of {@link parseSearchPath} that prefers literal interpretation
|
|
606
|
+
* when a path containing glob metacharacters resolves to an existing entry on
|
|
607
|
+
* disk. Disambiguates Next.js/SvelteKit routes like `apps/[id]/page.tsx` —
|
|
608
|
+
* without this, `[id]` is parsed as a glob character class and silently
|
|
609
|
+
* matches nothing.
|
|
610
|
+
*/
|
|
611
|
+
export async function parseSearchPathPreferringLiteral(filePath: string, cwd: string): Promise<ParsedSearchPath> {
|
|
612
|
+
if (!hasGlobPathChars(filePath) || isInternalUrlPath(filePath)) return parseSearchPath(filePath);
|
|
613
|
+
try {
|
|
614
|
+
await fs.promises.stat(resolveToCwd(filePath, cwd));
|
|
615
|
+
return { basePath: filePath };
|
|
616
|
+
} catch {
|
|
617
|
+
return parseSearchPath(filePath);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
604
621
|
// Parse a find pattern into a base directory path and a glob pattern.
|
|
605
622
|
// Examples:
|
|
606
623
|
// src/app/**/\*.tsx -> { basePath: "src/app", globPattern: "**/*.tsx", hasGlob: true }
|
|
@@ -707,7 +724,7 @@ async function resolveSearchPathItems(
|
|
|
707
724
|
|
|
708
725
|
const parsedItems = await Promise.all(
|
|
709
726
|
pathItems.map(async item => {
|
|
710
|
-
const parsedPath =
|
|
727
|
+
const parsedPath = await parseSearchPathPreferringLiteral(item, cwd);
|
|
711
728
|
const absoluteBasePath = resolveToCwd(parsedPath.basePath, cwd);
|
|
712
729
|
const stat = await fs.promises.stat(absoluteBasePath);
|
|
713
730
|
return { raw: item, parsedPath, absoluteBasePath, stat };
|
|
@@ -946,6 +963,15 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
946
963
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
947
964
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
948
965
|
}
|
|
966
|
+
// External (http/https/ftp/file) URLs are not searchable; route the caller
|
|
967
|
+
// to `read` instead of letting the path-resolver surface a confusing
|
|
968
|
+
// "Path not found" for a slash-stripped URL.
|
|
969
|
+
const externalUrl = rawPaths.find(rawPath => /^(?:https?|ftp|file|ws|wss):\/\//i.test(rawPath));
|
|
970
|
+
if (externalUrl) {
|
|
971
|
+
throw new ToolError(
|
|
972
|
+
`Cannot ${internalUrlAction} external URL: ${externalUrl}. Use \`read\` to fetch web content, then search the returned text.`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
949
975
|
const internalRouter = InternalUrlRouter.instance();
|
|
950
976
|
const resolvedPathInputs: string[] = [];
|
|
951
977
|
const immutableSourcePaths = new Set<string>();
|
|
@@ -989,7 +1015,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
989
1015
|
let multiTargets: ResolvedSearchTarget[] | undefined;
|
|
990
1016
|
let exactFilePaths: string[] | undefined;
|
|
991
1017
|
if (effectivePaths.length === 1) {
|
|
992
|
-
const parsedPath =
|
|
1018
|
+
const parsedPath = await parseSearchPathPreferringLiteral(effectivePaths[0] ?? ".", cwd);
|
|
993
1019
|
searchPath = resolveToCwd(parsedPath.basePath, cwd);
|
|
994
1020
|
globFilter = parsedPath.glob;
|
|
995
1021
|
scopePath = formatPathRelativeToCwd(searchPath, cwd);
|
|
@@ -1,15 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
|
-
import { resolveLocalUrlToPath, resolveVaultUrlToPath } from "../internal-urls";
|
|
3
|
+
import { resolveLocalRoot, resolveLocalUrlToPath, resolveVaultUrlToPath } from "../internal-urls";
|
|
3
4
|
import type { ToolSession } from ".";
|
|
4
5
|
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
5
6
|
import { ToolError } from "./tool-errors";
|
|
6
7
|
|
|
7
8
|
const VAULT_SCHEME_PREFIX = "vault:";
|
|
8
9
|
const LOCAL_SCHEME_PREFIX = "local:";
|
|
9
|
-
const PLAN_ALIAS_FILE = "PLAN.md";
|
|
10
|
-
const LOCAL_PLAN_ALIAS = "local://PLAN.md";
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
/** Resolve the absolute path of the session's `local://` artifact sandbox.
|
|
12
|
+
* Returns `null` when the session has no artifact wiring (e.g. tests). */
|
|
13
|
+
function localSandboxRoot(session: ToolSession): string | null {
|
|
14
|
+
try {
|
|
15
|
+
return path.resolve(
|
|
16
|
+
resolveLocalRoot({
|
|
17
|
+
getArtifactsDir: session.getArtifactsDir,
|
|
18
|
+
getSessionId: session.getSessionId,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when `absolutePath` resolves inside `root` (== root or under it). */
|
|
27
|
+
function isWithinRoot(absolutePath: string, root: string): boolean {
|
|
28
|
+
if (absolutePath === root) return true;
|
|
29
|
+
const sep = `${root}${path.sep}`;
|
|
30
|
+
return absolutePath.startsWith(sep);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when `targetPath` addresses the session-local artifact sandbox.
|
|
34
|
+
* Accepts both `local://…` URLs and absolute paths pointing inside the
|
|
35
|
+
* resolved sandbox root — the latter is what `read local://…` echoes back
|
|
36
|
+
* in the `[path#tag]` header. Those files are not part of the working tree,
|
|
37
|
+
* so plan mode treats them as freely writable scratch/plan space. */
|
|
38
|
+
function targetsLocalSandbox(session: ToolSession, targetPath: string): boolean {
|
|
39
|
+
const normalized = normalizeLocalScheme(targetPath);
|
|
40
|
+
if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) return true;
|
|
41
|
+
if (!path.isAbsolute(normalized)) return false;
|
|
42
|
+
const root = localSandboxRoot(session);
|
|
43
|
+
if (!root) return false;
|
|
44
|
+
// Compare both raw and realpath-normalized forms so that
|
|
45
|
+
// `/tmp/…` vs `/private/tmp/…` (macOS) and other symlink-collapsed
|
|
46
|
+
// roots both resolve to the same sandbox identity.
|
|
47
|
+
const resolved = path.resolve(normalized);
|
|
48
|
+
if (isWithinRoot(resolved, root)) return true;
|
|
49
|
+
try {
|
|
50
|
+
const realRoot = fs.realpathSync.native(root);
|
|
51
|
+
if (isWithinRoot(resolved, realRoot)) return true;
|
|
52
|
+
// `resolved` itself may live in `/tmp/...` while `realRoot` is `/private/tmp/...`;
|
|
53
|
+
// realpath the parent dir of `resolved` so we catch that direction too.
|
|
54
|
+
const realParent = fs.realpathSync.native(path.dirname(resolved));
|
|
55
|
+
return isWithinRoot(path.join(realParent, path.basename(resolved)), realRoot);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a write/edit target to its absolute filesystem path, honoring the
|
|
63
|
+
* `local://` and `vault://` schemes. Plain paths resolve against the session cwd.
|
|
64
|
+
*/
|
|
65
|
+
export function resolvePlanPath(session: ToolSession, targetPath: string): string {
|
|
13
66
|
const normalized = normalizeLocalScheme(targetPath);
|
|
14
67
|
if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) {
|
|
15
68
|
return resolveLocalUrlToPath(normalized, {
|
|
@@ -25,37 +78,12 @@ function resolveRawPath(session: ToolSession, targetPath: string): string {
|
|
|
25
78
|
return resolveToCwd(normalized, session.cwd);
|
|
26
79
|
}
|
|
27
80
|
|
|
28
|
-
function isPlanAliasTarget(session: ToolSession, targetPath: string, resolved: string): boolean {
|
|
29
|
-
const normalized = normalizeLocalScheme(targetPath);
|
|
30
|
-
if (normalized === LOCAL_PLAN_ALIAS) return true;
|
|
31
|
-
return resolved === resolveToCwd(PLAN_ALIAS_FILE, session.cwd);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
81
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* location at `state.planFilePath`. This lets `write` and `edit` accept the
|
|
40
|
-
* habitual plan filename after approval even when the active artifact has a
|
|
41
|
-
* titled path such as `local://APPROVED.md`.
|
|
42
|
-
*
|
|
43
|
-
* Outside plan mode (or when the basename does not match) this is a no-op.
|
|
82
|
+
* Plan mode keeps the working tree read-only while letting the agent draft its
|
|
83
|
+
* plan. Writes and edits to the `local://` artifact sandbox are allowed (that is
|
|
84
|
+
* where the plan and any scratch notes live); anything that would touch the
|
|
85
|
+
* working tree — or rename/delete a file — is rejected.
|
|
44
86
|
*/
|
|
45
|
-
export function resolvePlanPath(session: ToolSession, targetPath: string): string {
|
|
46
|
-
const resolved = resolveRawPath(session, targetPath);
|
|
47
|
-
|
|
48
|
-
const state = session.getPlanModeState?.();
|
|
49
|
-
if (!state?.enabled) return resolved;
|
|
50
|
-
|
|
51
|
-
const planResolved = resolveRawPath(session, state.planFilePath);
|
|
52
|
-
if (resolved === planResolved) return resolved;
|
|
53
|
-
if (isPlanAliasTarget(session, targetPath, resolved)) return planResolved;
|
|
54
|
-
if (path.basename(resolved) !== path.basename(planResolved)) return resolved;
|
|
55
|
-
|
|
56
|
-
return planResolved;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
87
|
export function enforcePlanModeWrite(
|
|
60
88
|
session: ToolSession,
|
|
61
89
|
targetPath: string,
|
|
@@ -64,9 +92,6 @@ export function enforcePlanModeWrite(
|
|
|
64
92
|
const state = session.getPlanModeState?.();
|
|
65
93
|
if (!state?.enabled) return;
|
|
66
94
|
|
|
67
|
-
const resolvedTarget = resolvePlanPath(session, targetPath);
|
|
68
|
-
const resolvedPlan = resolvePlanPath(session, state.planFilePath);
|
|
69
|
-
|
|
70
95
|
if (options?.move) {
|
|
71
96
|
throw new ToolError("Plan mode: renaming files is not allowed.");
|
|
72
97
|
}
|
|
@@ -75,7 +100,9 @@ export function enforcePlanModeWrite(
|
|
|
75
100
|
throw new ToolError("Plan mode: deleting files is not allowed.");
|
|
76
101
|
}
|
|
77
102
|
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
if (targetsLocalSandbox(session, targetPath)) return;
|
|
104
|
+
|
|
105
|
+
throw new ToolError(
|
|
106
|
+
"Plan mode: the working tree is read-only. Write your plan to a local://<slug>-plan.md file instead.",
|
|
107
|
+
);
|
|
81
108
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { getRemoteDir, logger, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import * as z from "zod/v4";
|
|
12
|
-
import { getFileSnapshotStore, recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
12
|
+
import { canonicalSnapshotKey, getFileSnapshotStore, recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
13
13
|
import { normalizeToLF } from "../edit/normalize";
|
|
14
14
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
15
15
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -34,7 +34,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
34
34
|
import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
|
|
35
35
|
import { convertFileWithMarkit } from "../utils/markit";
|
|
36
36
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
37
|
-
import { type ArchiveReader, openArchive, parseArchivePathCandidates } from "./archive-reader";
|
|
37
|
+
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "./archive-reader";
|
|
38
38
|
import {
|
|
39
39
|
type ConflictEntry,
|
|
40
40
|
type ConflictScope,
|
|
@@ -131,7 +131,7 @@ function recordFullHashlineContext(
|
|
|
131
131
|
): HashlineHeaderContext | undefined {
|
|
132
132
|
if (!absolutePath || !path.isAbsolute(absolutePath)) return undefined;
|
|
133
133
|
const normalized = normalizeToLF(fullText);
|
|
134
|
-
const tag = getFileSnapshotStore(session).record(absolutePath, normalized);
|
|
134
|
+
const tag = getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
|
|
135
135
|
return {
|
|
136
136
|
header: formatHashlineHeader(displayPath, tag),
|
|
137
137
|
tag,
|
|
@@ -1154,17 +1154,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1154
1154
|
const limitedEntries = listLimit.items;
|
|
1155
1155
|
const limitMeta = listLimit.meta;
|
|
1156
1156
|
|
|
1157
|
-
|
|
1158
|
-
for (const entry of limitedEntries) {
|
|
1157
|
+
for (let index = 0; index < limitedEntries.length; index++) {
|
|
1159
1158
|
throwIfAborted(signal);
|
|
1160
|
-
if (entry.isDirectory) {
|
|
1161
|
-
results.push(`${entry.name}/`);
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
const sizeSuffix = entry.size > 0 ? ` (${formatBytes(entry.size)})` : "";
|
|
1166
|
-
results.push(`${entry.name}${sizeSuffix}`);
|
|
1167
1159
|
}
|
|
1160
|
+
const results = formatArchiveEntryLines(limitedEntries);
|
|
1168
1161
|
|
|
1169
1162
|
const output = results.length > 0 ? results.join("\n") : "(empty archive directory)";
|
|
1170
1163
|
const text = prependSuffixResolutionNotice(output, details.suffixResolution);
|
|
@@ -1757,15 +1750,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1757
1750
|
// Convert document via markit.
|
|
1758
1751
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1759
1752
|
if (result.ok) {
|
|
1760
|
-
//
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1753
|
+
// Route the converted markdown through the in-memory text builder
|
|
1754
|
+
// so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
|
|
1755
|
+
// raw mode apply against the converted output. Without this,
|
|
1756
|
+
// `file.pdf:50-100` silently returned the head of the document
|
|
1757
|
+
// because only `truncateHead` was being applied.
|
|
1758
|
+
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1759
|
+
return this.#buildInMemoryMultiRangeResult(result.content, parsed.ranges, {
|
|
1760
|
+
details: { resolvedPath: absolutePath },
|
|
1761
|
+
sourcePath: absolutePath,
|
|
1762
|
+
entityLabel: "document",
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1766
|
+
return this.#buildInMemoryTextResult(result.content, offset, limit, {
|
|
1767
|
+
details: { resolvedPath: absolutePath },
|
|
1768
|
+
sourcePath: absolutePath,
|
|
1769
|
+
entityLabel: "document",
|
|
1770
|
+
raw: isRawSelector(parsed),
|
|
1771
|
+
});
|
|
1769
1772
|
} else if (result.error) {
|
|
1770
1773
|
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
1771
1774
|
} else {
|
|
@@ -1951,7 +1954,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1951
1954
|
// full file and any anchor validates while the file is unchanged.
|
|
1952
1955
|
const isWholeFile = offset === undefined && limit === undefined && !wasTruncated;
|
|
1953
1956
|
const tag = isWholeFile
|
|
1954
|
-
? getFileSnapshotStore(this.session).record(
|
|
1957
|
+
? getFileSnapshotStore(this.session).record(
|
|
1958
|
+
canonicalSnapshotKey(absolutePath),
|
|
1959
|
+
normalizeToLF(collectedLines.join("\n")),
|
|
1960
|
+
)
|
|
1955
1961
|
: await recordFileSnapshot(this.session, absolutePath);
|
|
1956
1962
|
if (tag) {
|
|
1957
1963
|
hashContext = hashlineHeaderContext(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag);
|
|
@@ -2337,10 +2343,20 @@ function firstReadSelectorLine(sel: string | undefined): number | undefined {
|
|
|
2337
2343
|
}
|
|
2338
2344
|
}
|
|
2339
2345
|
|
|
2346
|
+
/** Absolute fs path the read result actually resolved to, used as the OSC 8 link
|
|
2347
|
+
* target when the structured `resolvedPath` isn't set (the common plain-file and
|
|
2348
|
+
* image reads only record the path in `meta.source`). URL/internal sources are
|
|
2349
|
+
* not fs paths, so only `type: "path"` qualifies. */
|
|
2350
|
+
function readSourceFsPath(details: ReadToolDetails | undefined): string | undefined {
|
|
2351
|
+
const source = details?.meta?.source;
|
|
2352
|
+
return source?.type === "path" ? source.value : undefined;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2340
2355
|
function formatReadPathLink(
|
|
2341
2356
|
rawPath: string,
|
|
2342
2357
|
options: {
|
|
2343
2358
|
resolvedPath?: string;
|
|
2359
|
+
sourcePath?: string;
|
|
2344
2360
|
suffixResolution?: { from: string; to: string };
|
|
2345
2361
|
offset?: number;
|
|
2346
2362
|
fallbackLabel?: string;
|
|
@@ -2352,7 +2368,7 @@ function formatReadPathLink(
|
|
|
2352
2368
|
const plainDisplayPath = options.suffixResolution
|
|
2353
2369
|
? shortenPath(options.suffixResolution.to)
|
|
2354
2370
|
: shortenPath(basePath || options.resolvedPath || options.fallbackLabel || rawPath);
|
|
2355
|
-
const target = options.resolvedPath ?? tryResolveInternalUrlSync(basePath);
|
|
2371
|
+
const target = options.resolvedPath ?? options.sourcePath ?? tryResolveInternalUrlSync(basePath);
|
|
2356
2372
|
const line = firstReadSelectorLine(split.sel) ?? options.offset;
|
|
2357
2373
|
const linkOptions = line !== undefined ? { line } : undefined;
|
|
2358
2374
|
const displayPath = target ? fileHyperlink(target, plainDisplayPath, linkOptions) : plainDisplayPath;
|
|
@@ -2403,7 +2419,9 @@ export const readToolRenderer = {
|
|
|
2403
2419
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
2404
2420
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
2405
2421
|
const rawPath = args?.file_path || args?.path || "";
|
|
2406
|
-
const filePath =
|
|
2422
|
+
const filePath =
|
|
2423
|
+
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
2424
|
+
shortenPath(rawPath);
|
|
2407
2425
|
let title = filePath ? `Read ${filePath}` : "Read";
|
|
2408
2426
|
if (args?.offset !== undefined || args?.limit !== undefined) {
|
|
2409
2427
|
const startLine = args.offset ?? 1;
|
|
@@ -2454,6 +2472,7 @@ export const readToolRenderer = {
|
|
|
2454
2472
|
const suffix = details?.suffixResolution;
|
|
2455
2473
|
const displayPath = formatReadPathLink(rawPath, {
|
|
2456
2474
|
resolvedPath: details?.resolvedPath,
|
|
2475
|
+
sourcePath: readSourceFsPath(details),
|
|
2457
2476
|
suffixResolution: suffix,
|
|
2458
2477
|
fallbackLabel: "image",
|
|
2459
2478
|
});
|
|
@@ -2486,12 +2505,13 @@ export const readToolRenderer = {
|
|
|
2486
2505
|
}
|
|
2487
2506
|
|
|
2488
2507
|
const suffix = details?.suffixResolution;
|
|
2489
|
-
// resolvedPath is the absolute fs path
|
|
2490
|
-
//
|
|
2491
|
-
//
|
|
2492
|
-
//
|
|
2508
|
+
// resolvedPath is the absolute fs path when a read resolved/corrected the
|
|
2509
|
+
// input (suffix match, internal URL, archive/sqlite/notebook); plain file
|
|
2510
|
+
// reads only record the absolute path in meta.source, so fall back to that
|
|
2511
|
+
// (and then to a sync internal-URL resolver) to keep the title clickable.
|
|
2493
2512
|
const displayPath = formatReadPathLink(rawPath, {
|
|
2494
2513
|
resolvedPath: details?.resolvedPath,
|
|
2514
|
+
sourcePath: readSourceFsPath(details),
|
|
2495
2515
|
suffixResolution: suffix,
|
|
2496
2516
|
offset: args?.offset,
|
|
2497
2517
|
});
|