@oh-my-pi/pi-coding-agent 14.2.1 → 14.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 +143 -1
- package/package.json +19 -19
- package/src/autoresearch/prompt.md +1 -1
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +63 -4
- package/src/cursor.ts +3 -8
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/helpers.ts +3 -3
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +87 -57
- package/src/edit/line-hash.ts +735 -19
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +658 -0
- package/src/edit/modes/chunk.ts +144 -78
- package/src/edit/modes/hashline.ts +223 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +112 -143
- package/src/edit/streaming.ts +385 -0
- package/src/exec/bash-executor.ts +58 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +8 -1
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +5 -34
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +112 -105
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/print-mode.ts +8 -0
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +4 -1
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +15 -19
- package/src/prompts/tools/ast-grep.md +18 -24
- package/src/prompts/tools/atom.md +96 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +58 -179
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +8 -8
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +46 -8
- package/src/prompts/tools/read.md +9 -6
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +22 -14
- package/src/session/agent-session.ts +61 -22
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +44 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +21 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +514 -712
- package/src/tools/grep.ts +115 -130
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +9 -8
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -21
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/web/search/providers/codex.ts +129 -6
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -11
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/edit/modes/patch.ts
CHANGED
|
@@ -1577,7 +1577,7 @@ export async function computePatchDiff(
|
|
|
1577
1577
|
}
|
|
1578
1578
|
|
|
1579
1579
|
export const patchEditEntrySchema = Type.Object({
|
|
1580
|
-
path: Type.String({ description: "File path" }),
|
|
1580
|
+
path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
|
|
1581
1581
|
op: Type.Optional(
|
|
1582
1582
|
StringEnum(["create", "delete", "update"], {
|
|
1583
1583
|
description: "Operation (default: update)",
|
|
@@ -1588,6 +1588,7 @@ export const patchEditEntrySchema = Type.Object({
|
|
|
1588
1588
|
});
|
|
1589
1589
|
|
|
1590
1590
|
export const patchEditSchema = Type.Object({
|
|
1591
|
+
path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
|
|
1591
1592
|
edits: Type.Array(patchEditEntrySchema, { description: "Patch operations", minItems: 1 }),
|
|
1592
1593
|
});
|
|
1593
1594
|
|
|
@@ -1605,14 +1606,6 @@ export interface ExecutePatchSingleOptions {
|
|
|
1605
1606
|
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
1606
1607
|
}
|
|
1607
1608
|
|
|
1608
|
-
export function isPatchParams(params: unknown): params is PatchParams {
|
|
1609
|
-
if (typeof params !== "object" || params === null) return false;
|
|
1610
|
-
if (!("edits" in params) || !Array.isArray((params as any).edits)) return false;
|
|
1611
|
-
const first = (params as any).edits[0];
|
|
1612
|
-
if (!first || typeof first !== "object") return false;
|
|
1613
|
-
return "path" in first && !("old_text" in first) && !("new_text" in first);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
1609
|
class LspFileSystem implements FileSystem {
|
|
1617
1610
|
#lastDiagnostics: FileDiagnosticsResult | undefined;
|
|
1618
1611
|
#fileCache: Record<string, Bun.BunFile> = {};
|
|
@@ -1710,6 +1703,9 @@ export async function executePatchSingle(
|
|
|
1710
1703
|
beginDeferredDiagnosticsForPath,
|
|
1711
1704
|
} = options;
|
|
1712
1705
|
const { path, op: rawOp, rename, diff } = params;
|
|
1706
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1707
|
+
throw new Error("patch edit: missing `path`. Provide `path` on the edit or supply a top-level `path`.");
|
|
1708
|
+
}
|
|
1713
1709
|
|
|
1714
1710
|
const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
|
|
1715
1711
|
|
|
@@ -977,13 +977,14 @@ export function findContextLine(
|
|
|
977
977
|
}
|
|
978
978
|
|
|
979
979
|
export const replaceEditEntrySchema = Type.Object({
|
|
980
|
-
path: Type.String({ description: "File path (
|
|
980
|
+
path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
|
|
981
981
|
old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
|
|
982
982
|
new_text: Type.String({ description: "Replacement text" }),
|
|
983
983
|
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
|
|
984
984
|
});
|
|
985
985
|
|
|
986
986
|
export const replaceEditSchema = Type.Object({
|
|
987
|
+
path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
|
|
987
988
|
edits: Type.Array(replaceEditEntrySchema, { description: "Replacements", minItems: 1 }),
|
|
988
989
|
});
|
|
989
990
|
|
|
@@ -1001,13 +1002,6 @@ export interface ExecuteReplaceSingleOptions {
|
|
|
1001
1002
|
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
1002
1003
|
}
|
|
1003
1004
|
|
|
1004
|
-
export function isReplaceParams(params: unknown): params is ReplaceParams {
|
|
1005
|
-
if (typeof params !== "object" || params === null) return false;
|
|
1006
|
-
if (!("edits" in params) || !Array.isArray((params as any).edits)) return false;
|
|
1007
|
-
const first = (params as any).edits[0];
|
|
1008
|
-
return first && typeof first === "object" && "old_text" in first && "new_text" in first;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
1005
|
export async function executeReplaceSingle(
|
|
1012
1006
|
options: ExecuteReplaceSingleOptions,
|
|
1013
1007
|
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
|
|
@@ -1022,6 +1016,9 @@ export async function executeReplaceSingle(
|
|
|
1022
1016
|
beginDeferredDiagnosticsForPath,
|
|
1023
1017
|
} = options;
|
|
1024
1018
|
const { path, old_text, new_text, all } = params;
|
|
1019
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
1020
|
+
throw new Error("replace edit: missing `path`. Provide `path` on the edit or supply a top-level `path`.");
|
|
1021
|
+
}
|
|
1025
1022
|
|
|
1026
1023
|
enforcePlanModeWrite(session, path);
|
|
1027
1024
|
|
|
@@ -1065,9 +1062,7 @@ export async function executeReplaceSingle(
|
|
|
1065
1062
|
}
|
|
1066
1063
|
|
|
1067
1064
|
if (normalizedContent === result.content) {
|
|
1068
|
-
throw new Error(
|
|
1069
|
-
`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
|
1070
|
-
);
|
|
1065
|
+
throw new Error(`Edits to ${path} resulted in no changes being made.`);
|
|
1071
1066
|
}
|
|
1072
1067
|
|
|
1073
1068
|
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
|
package/src/edit/renderer.ts
CHANGED
|
@@ -24,12 +24,12 @@ import {
|
|
|
24
24
|
} from "../tools/render-utils";
|
|
25
25
|
import { type VimRenderArgs, vimToolRenderer } from "../tools/vim";
|
|
26
26
|
import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
27
|
+
import type { EditMode } from "../utils/edit-mode";
|
|
27
28
|
import type { VimToolDetails } from "../vim/types";
|
|
28
29
|
import type { DiffError, DiffResult } from "./diff";
|
|
29
30
|
import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
30
|
-
import { type ChunkToolEdit, parseChunkEditPath } from "./modes/chunk";
|
|
31
|
-
import type { HashlineToolEdit } from "./modes/hashline";
|
|
32
31
|
import type { Operation, PatchEditEntry } from "./modes/patch";
|
|
32
|
+
import type { PerFileDiffPreview } from "./streaming";
|
|
33
33
|
|
|
34
34
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
35
|
// LSP Batching
|
|
@@ -50,6 +50,9 @@ export interface EditToolPerFileResult {
|
|
|
50
50
|
move?: string;
|
|
51
51
|
isError?: boolean;
|
|
52
52
|
errorText?: string;
|
|
53
|
+
/** TUI-friendly error text. When present, rendered to the user instead of `errorText`.
|
|
54
|
+
* Set when the underlying error carries a `displayMessage` (e.g. {@link HashlineMismatchError}). */
|
|
55
|
+
displayErrorText?: string;
|
|
53
56
|
meta?: OutputMeta;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -90,6 +93,7 @@ interface EditRenderArgs {
|
|
|
90
93
|
* Computed preview diff (used when tool args don't include a diff, e.g. hashline mode).
|
|
91
94
|
*/
|
|
92
95
|
previewDiff?: string;
|
|
96
|
+
__partialJson?: string;
|
|
93
97
|
// Hashline / chunk mode fields
|
|
94
98
|
edits?: EditRenderEntry[];
|
|
95
99
|
}
|
|
@@ -133,8 +137,12 @@ function isVimToolDetails(details: unknown): details is VimToolDetails {
|
|
|
133
137
|
|
|
134
138
|
/** Extended context for edit tool rendering */
|
|
135
139
|
export interface EditRenderContext {
|
|
140
|
+
/** Edit mode resolved by the caller; lets the renderer dispatch without shape-sniffing */
|
|
141
|
+
editMode?: EditMode;
|
|
136
142
|
/** Pre-computed diff preview (computed before tool executes) */
|
|
137
143
|
editDiffPreview?: DiffResult | DiffError;
|
|
144
|
+
/** Multi-file streaming diff preview (chunk edits spanning several files) */
|
|
145
|
+
perFileDiffPreview?: PerFileDiffPreview[];
|
|
138
146
|
/** Function to render diff text with syntax highlighting */
|
|
139
147
|
renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
|
|
140
148
|
}
|
|
@@ -142,14 +150,6 @@ export interface EditRenderContext {
|
|
|
142
150
|
const EDIT_STREAMING_PREVIEW_LINES = 12;
|
|
143
151
|
const CALL_TEXT_PREVIEW_LINES = 6;
|
|
144
152
|
const CALL_TEXT_PREVIEW_WIDTH = 80;
|
|
145
|
-
const STREAMING_EDIT_PREVIEW_WIDTH = 120;
|
|
146
|
-
const STREAMING_EDIT_PREVIEW_LIMIT = 4;
|
|
147
|
-
const STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT = 8;
|
|
148
|
-
|
|
149
|
-
interface FormattedStreamingEdit {
|
|
150
|
-
srcLabel: string;
|
|
151
|
-
dst: string;
|
|
152
|
-
}
|
|
153
153
|
|
|
154
154
|
/** Extract file path from an edit entry's path (handles chunk's file:selector format). */
|
|
155
155
|
function filePathFromEditEntry(p: string | undefined): string | undefined {
|
|
@@ -158,6 +158,48 @@ function filePathFromEditEntry(p: string | undefined): string | undefined {
|
|
|
158
158
|
return ci === -1 ? p : p.slice(0, ci);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function decodePartialJsonStringFragment(fragment: string): string {
|
|
162
|
+
let text = fragment;
|
|
163
|
+
const trailingBackslashes = text.match(/\\+$/)?.[0].length ?? 0;
|
|
164
|
+
if (trailingBackslashes % 2 === 1) {
|
|
165
|
+
text = text.slice(0, -1);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(`"${text}"`) as string;
|
|
169
|
+
} catch {
|
|
170
|
+
return text
|
|
171
|
+
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
172
|
+
.replace(/\\(["\\/bfnrt])/g, (_, ch: string) => {
|
|
173
|
+
switch (ch) {
|
|
174
|
+
case "b":
|
|
175
|
+
return "\b";
|
|
176
|
+
case "f":
|
|
177
|
+
return "\f";
|
|
178
|
+
case "n":
|
|
179
|
+
return "\n";
|
|
180
|
+
case "r":
|
|
181
|
+
return "\r";
|
|
182
|
+
case "t":
|
|
183
|
+
return "\t";
|
|
184
|
+
default:
|
|
185
|
+
return ch;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractPartialJsonString(partialJson: string | undefined, key: string): string | undefined {
|
|
192
|
+
if (!partialJson) return undefined;
|
|
193
|
+
const pattern = new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`, "u");
|
|
194
|
+
const match = pattern.exec(partialJson);
|
|
195
|
+
if (!match) return undefined;
|
|
196
|
+
return decodePartialJsonStringFragment(match[1]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getPartialJsonEditPath(args: EditRenderArgs): string | undefined {
|
|
200
|
+
return filePathFromEditEntry(extractPartialJsonString(args.__partialJson, "path"));
|
|
201
|
+
}
|
|
202
|
+
|
|
161
203
|
/** Count distinct file paths in an edits array. */
|
|
162
204
|
function countEditFiles(edits: EditRenderEntry[]): number {
|
|
163
205
|
return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
|
|
@@ -230,133 +272,46 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
|
|
|
230
272
|
return text;
|
|
231
273
|
}
|
|
232
274
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"path" in edit &&
|
|
238
|
-
("write" in edit || "replace" in edit || "insert" in edit)
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function getStreamingEditContent(content: unknown): string {
|
|
243
|
-
if (Array.isArray(content)) {
|
|
244
|
-
return content.join("\n");
|
|
245
|
-
}
|
|
246
|
-
return typeof content === "string" ? content : "";
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function formatHashlineStreamingEdit(edit: Partial<HashlineToolEdit>): FormattedStreamingEdit {
|
|
250
|
-
if (typeof edit !== "object" || !edit) {
|
|
251
|
-
return { srcLabel: "\u2022 (incomplete edit)", dst: "" };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const contentLines = getStreamingEditContent(edit.content);
|
|
255
|
-
const loc = edit.loc;
|
|
256
|
-
|
|
257
|
-
if (loc === "append" || loc === "prepend") {
|
|
258
|
-
return { srcLabel: `\u2022 ${loc} (file-level)`, dst: contentLines };
|
|
259
|
-
}
|
|
260
|
-
if (typeof loc === "object" && loc) {
|
|
261
|
-
if ("range" in loc && typeof loc.range === "object" && loc.range) {
|
|
262
|
-
return { srcLabel: `\u2022 range ${loc.range.pos ?? "?"}\u2026${loc.range.end ?? "?"}`, dst: contentLines };
|
|
263
|
-
}
|
|
264
|
-
if ("line" in loc) {
|
|
265
|
-
return { srcLabel: `\u2022 line ${(loc as { line: string }).line}`, dst: contentLines };
|
|
266
|
-
}
|
|
267
|
-
if ("append" in loc) {
|
|
268
|
-
return { srcLabel: `\u2022 append ${(loc as { append: string }).append}`, dst: contentLines };
|
|
269
|
-
}
|
|
270
|
-
if ("prepend" in loc) {
|
|
271
|
-
return { srcLabel: `\u2022 prepend ${(loc as { prepend: string }).prepend}`, dst: contentLines };
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return { srcLabel: "\u2022 (unknown edit)", dst: contentLines };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function formatChunkStreamingEdit(edit: Partial<ChunkToolEdit>): FormattedStreamingEdit {
|
|
278
|
-
if (typeof edit !== "object" || !edit) {
|
|
279
|
-
return { srcLabel: "\u2022 (incomplete edit)", dst: "" };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const target = edit.path ? (parseChunkEditPath(edit.path).selector ?? edit.path) : "?";
|
|
283
|
-
if (edit.write === null) {
|
|
284
|
-
return { srcLabel: `\u2022 remove ${target}`, dst: "" };
|
|
285
|
-
}
|
|
286
|
-
if (typeof edit.write === "string") {
|
|
287
|
-
return { srcLabel: `\u2022 replace ${target}`, dst: getStreamingEditContent(edit.write) };
|
|
288
|
-
}
|
|
289
|
-
if (typeof edit.replace === "object" && edit.replace) {
|
|
290
|
-
return { srcLabel: `\u2022 replace ${target}`, dst: getStreamingEditContent(edit.replace.new) };
|
|
291
|
-
}
|
|
292
|
-
if (typeof edit.insert === "object" && edit.insert) {
|
|
293
|
-
return { srcLabel: `\u2022 ${edit.insert.loc} ${target}`, dst: getStreamingEditContent(edit.insert.body) };
|
|
275
|
+
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
|
|
276
|
+
const icon = uiTheme.getLangIcon(language);
|
|
277
|
+
if (lineCount !== null) {
|
|
278
|
+
return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
|
|
294
279
|
}
|
|
295
|
-
return
|
|
280
|
+
return uiTheme.fg("dim", `${icon}`);
|
|
296
281
|
}
|
|
297
282
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
text += uiTheme.fg("dim", `[${edits.length} ${label}${edits.length === 1 ? "" : "s"}]`);
|
|
306
|
-
text += "\n";
|
|
307
|
-
let shownEdits = 0;
|
|
308
|
-
let shownDstLines = 0;
|
|
309
|
-
for (const edit of edits) {
|
|
310
|
-
shownEdits++;
|
|
311
|
-
if (shownEdits > STREAMING_EDIT_PREVIEW_LIMIT) break;
|
|
312
|
-
const formatted = formatEdit(edit as never);
|
|
313
|
-
text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(formatted.srcLabel), STREAMING_EDIT_PREVIEW_WIDTH));
|
|
314
|
-
text += "\n";
|
|
315
|
-
if (formatted.dst === "") {
|
|
316
|
-
text += uiTheme.fg("dim", truncateToWidth(" (delete)", STREAMING_EDIT_PREVIEW_WIDTH));
|
|
317
|
-
text += "\n";
|
|
283
|
+
function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme): string {
|
|
284
|
+
const parts: string[] = [];
|
|
285
|
+
for (const preview of previews) {
|
|
286
|
+
if (!preview.diff && !preview.error) continue;
|
|
287
|
+
const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
|
|
288
|
+
if (preview.error) {
|
|
289
|
+
parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error))}`);
|
|
318
290
|
continue;
|
|
319
291
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (shownDstLines > STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT) break;
|
|
323
|
-
text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(`+ ${dstLine}`), STREAMING_EDIT_PREVIEW_WIDTH));
|
|
324
|
-
text += "\n";
|
|
292
|
+
if (preview.diff) {
|
|
293
|
+
parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, "preview")}`);
|
|
325
294
|
}
|
|
326
|
-
if (shownDstLines > STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT) break;
|
|
327
295
|
}
|
|
328
|
-
|
|
329
|
-
text += uiTheme.fg("dim", `\u2026 (${edits.length - STREAMING_EDIT_PREVIEW_LIMIT} more edits)`);
|
|
330
|
-
}
|
|
331
|
-
if (shownDstLines > STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT) {
|
|
332
|
-
text += uiTheme.fg("dim", `\n\u2026 (${shownDstLines - STREAMING_EDIT_PREVIEW_DST_LINE_LIMIT} more dst lines)`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return text.trimEnd();
|
|
296
|
+
return parts.join("");
|
|
336
297
|
}
|
|
337
298
|
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
299
|
+
function getCallPreview(
|
|
300
|
+
args: EditRenderArgs,
|
|
301
|
+
rawPath: string,
|
|
302
|
+
uiTheme: Theme,
|
|
303
|
+
renderContext: EditRenderContext | undefined,
|
|
304
|
+
): string {
|
|
305
|
+
const multi = renderContext?.perFileDiffPreview;
|
|
306
|
+
if (multi && multi.length > 0 && multi.some(p => p.diff || p.error)) {
|
|
307
|
+
return formatMultiFileStreamingDiff(multi, uiTheme);
|
|
342
308
|
}
|
|
343
|
-
return uiTheme.fg("dim", `${icon}`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function getCallPreview(args: EditRenderArgs, rawPath: string, uiTheme: Theme): string {
|
|
347
309
|
if (args.previewDiff) {
|
|
348
310
|
return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
|
|
349
311
|
}
|
|
350
312
|
if (args.diff && args.op) {
|
|
351
313
|
return formatStreamingDiff(args.diff, rawPath, uiTheme);
|
|
352
314
|
}
|
|
353
|
-
if (args.edits && args.edits.length > 0) {
|
|
354
|
-
// Only show hashline/chunk streaming edits — replace/patch use previewDiff above
|
|
355
|
-
const first = args.edits[0];
|
|
356
|
-
if (first && typeof first === "object" && ("loc" in first || isChunkStreamingEdit(first))) {
|
|
357
|
-
return formatStreamingHashlineEdits(args.edits, uiTheme);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
315
|
if (args.diff) {
|
|
361
316
|
return renderPlainTextPreview(args.diff, uiTheme);
|
|
362
317
|
}
|
|
@@ -425,18 +380,18 @@ function wrapEditRendererLine(line: string, width: number): string[] {
|
|
|
425
380
|
const startAnsi = line.match(/^((?:\x1b\[[0-9;]*m)*)/)?.[1] ?? "";
|
|
426
381
|
const bodyWithReset = line.slice(startAnsi.length);
|
|
427
382
|
const body = bodyWithReset.endsWith("\x1b[39m") ? bodyWithReset.slice(0, -"\x1b[39m".length) : bodyWithReset;
|
|
428
|
-
const diffMatch = /^([+\-\s])(\s*\d+)
|
|
383
|
+
const diffMatch = /^([+\-\s])(\s*\d+)([|│])(.*)$/s.exec(body);
|
|
429
384
|
|
|
430
385
|
if (!diffMatch) {
|
|
431
386
|
return wrapTextWithAnsi(line, width);
|
|
432
387
|
}
|
|
433
388
|
|
|
434
|
-
const [, marker, lineNum, content] = diffMatch;
|
|
435
|
-
const prefix = `${marker}${lineNum}
|
|
389
|
+
const [, marker, lineNum, separator, content] = diffMatch;
|
|
390
|
+
const prefix = `${marker}${lineNum}${separator}`;
|
|
436
391
|
const prefixWidth = visibleWidth(prefix);
|
|
437
392
|
const contentWidth = Math.max(1, width - prefixWidth);
|
|
438
|
-
const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}
|
|
439
|
-
const wrappedContent = wrapTextWithAnsi(content, contentWidth);
|
|
393
|
+
const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}${separator}`;
|
|
394
|
+
const wrappedContent = wrapTextWithAnsi(content ?? "", contentWidth);
|
|
440
395
|
|
|
441
396
|
return wrappedContent.map(
|
|
442
397
|
(segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`,
|
|
@@ -446,31 +401,44 @@ function wrapEditRendererLine(line: string, width: number): string[] {
|
|
|
446
401
|
export const editToolRenderer = {
|
|
447
402
|
mergeCallAndResult: true,
|
|
448
403
|
|
|
449
|
-
renderCall(
|
|
450
|
-
|
|
451
|
-
|
|
404
|
+
renderCall(
|
|
405
|
+
args: EditRenderArgs | VimRenderArgs,
|
|
406
|
+
options: RenderResultOptions & { renderContext?: EditRenderContext },
|
|
407
|
+
uiTheme: Theme,
|
|
408
|
+
): Component {
|
|
409
|
+
const renderContext = options.renderContext;
|
|
410
|
+
// Dispatch on the explicit editMode when available; fall back to the
|
|
411
|
+
// shape probe for legacy call sites that don't thread renderContext.
|
|
412
|
+
if (renderContext?.editMode === "vim" || isVimRenderArgs(args)) {
|
|
413
|
+
return vimToolRenderer.renderCall(args as VimRenderArgs, options, uiTheme);
|
|
452
414
|
}
|
|
453
415
|
|
|
454
|
-
const
|
|
416
|
+
const editArgs = args as EditRenderArgs;
|
|
417
|
+
const applyPatchSummary = getApplyPatchRenderSummary(editArgs, options.isPartial);
|
|
455
418
|
const firstApplyPatchEntry = applyPatchSummary?.entries[0];
|
|
456
419
|
// Extract path from first edit entry when top-level path is absent (new schema)
|
|
457
|
-
const firstEdit = Array.isArray(
|
|
420
|
+
const firstEdit = Array.isArray(editArgs.edits) && editArgs.edits.length > 0 ? editArgs.edits[0] : undefined;
|
|
458
421
|
const rawPath =
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
422
|
+
editArgs.file_path ||
|
|
423
|
+
editArgs.path ||
|
|
424
|
+
filePathFromEditEntry(firstEdit?.path) ||
|
|
425
|
+
getPartialJsonEditPath(editArgs) ||
|
|
426
|
+
firstApplyPatchEntry?.path ||
|
|
427
|
+
"";
|
|
428
|
+
const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
|
|
429
|
+
const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op;
|
|
462
430
|
const { description } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
463
431
|
const spinner =
|
|
464
432
|
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
465
433
|
let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
|
|
466
434
|
// Show file count hint for multi-file edits
|
|
467
|
-
const fileCount = Array.isArray(
|
|
468
|
-
? countEditFiles(
|
|
435
|
+
const fileCount = Array.isArray(editArgs.edits)
|
|
436
|
+
? countEditFiles(editArgs.edits)
|
|
469
437
|
: (applyPatchSummary?.entries.length ?? 0);
|
|
470
438
|
if (fileCount > 1) {
|
|
471
439
|
text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
|
|
472
440
|
}
|
|
473
|
-
text += getCallPreview(
|
|
441
|
+
text += getCallPreview(editArgs, rawPath, uiTheme, renderContext);
|
|
474
442
|
if (applyPatchSummary?.error) {
|
|
475
443
|
text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error), CALL_TEXT_PREVIEW_WIDTH))}`;
|
|
476
444
|
}
|
|
@@ -484,7 +452,7 @@ export const editToolRenderer = {
|
|
|
484
452
|
uiTheme: Theme,
|
|
485
453
|
args?: EditRenderArgs,
|
|
486
454
|
): Component {
|
|
487
|
-
if (isVimToolDetails(result.details)) {
|
|
455
|
+
if (options.renderContext?.editMode === "vim" || isVimToolDetails(result.details)) {
|
|
488
456
|
return vimToolRenderer.renderResult(
|
|
489
457
|
result as { content: Array<{ type: string; text?: string }>; details?: VimToolDetails; isError?: boolean },
|
|
490
458
|
options,
|
|
@@ -524,13 +492,14 @@ function renderSingleFileResult(
|
|
|
524
492
|
const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
|
|
525
493
|
const { language } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
526
494
|
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
: "";
|
|
495
|
+
const editTextSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch;
|
|
496
|
+
const metadataLineCount = editTextSource ? countLines(editTextSource) : null;
|
|
497
|
+
const metadataLine = op !== "delete" ? `\n${formatMetadataLine(metadataLineCount, language, uiTheme)}` : "";
|
|
531
498
|
|
|
499
|
+
const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
|
|
532
500
|
const errorText = isError
|
|
533
|
-
?
|
|
501
|
+
? displayErrorText ||
|
|
502
|
+
(details && "errorText" in details && details.errorText) ||
|
|
534
503
|
(result.content?.find(c => c.type === "text")?.text ?? "")
|
|
535
504
|
: "";
|
|
536
505
|
|