@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1
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 +120 -0
- package/package.json +8 -8
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +43 -10
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/commit/model-selection.ts +16 -13
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +242 -45
- package/src/config/model-resolver.ts +282 -65
- package/src/config/settings-schema.ts +27 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +614 -97
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +55 -1
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +6 -2
- package/src/memories/index.ts +7 -6
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +42 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +17 -6
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +16 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +12 -3
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +758 -725
- package/src/session/agent-session.ts +187 -40
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +240 -57
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
- package/src/tools/python.ts +293 -278
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/git.ts +24 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +16 -7
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
package/src/edit/modes/patch.ts
CHANGED
|
@@ -1576,7 +1576,7 @@ export async function computePatchDiff(
|
|
|
1576
1576
|
}
|
|
1577
1577
|
}
|
|
1578
1578
|
|
|
1579
|
-
export const
|
|
1579
|
+
export const patchEditEntrySchema = Type.Object({
|
|
1580
1580
|
path: Type.String({ description: "File path" }),
|
|
1581
1581
|
op: Type.Optional(
|
|
1582
1582
|
StringEnum(["create", "delete", "update"], {
|
|
@@ -1587,11 +1587,16 @@ export const patchEditSchema = Type.Object({
|
|
|
1587
1587
|
diff: Type.Optional(Type.String({ description: "Diff hunks (update) or full content (create)" })),
|
|
1588
1588
|
});
|
|
1589
1589
|
|
|
1590
|
+
export const patchEditSchema = Type.Object({
|
|
1591
|
+
edits: Type.Array(patchEditEntrySchema, { description: "Patch operations", minItems: 1 }),
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
export type PatchEditEntry = Static<typeof patchEditEntrySchema>;
|
|
1590
1595
|
export type PatchParams = Static<typeof patchEditSchema>;
|
|
1591
1596
|
|
|
1592
|
-
interface
|
|
1597
|
+
export interface ExecutePatchSingleOptions {
|
|
1593
1598
|
session: ToolSession;
|
|
1594
|
-
params:
|
|
1599
|
+
params: PatchEditEntry;
|
|
1595
1600
|
signal?: AbortSignal;
|
|
1596
1601
|
batchRequest?: LspBatchRequest;
|
|
1597
1602
|
allowFuzzy: boolean;
|
|
@@ -1601,10 +1606,11 @@ interface ExecutePatchModeOptions {
|
|
|
1601
1606
|
}
|
|
1602
1607
|
|
|
1603
1608
|
export function isPatchParams(params: unknown): params is PatchParams {
|
|
1604
|
-
if (typeof params !== "object" || params === null
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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);
|
|
1608
1614
|
}
|
|
1609
1615
|
|
|
1610
1616
|
class LspFileSystem implements FileSystem {
|
|
@@ -1690,9 +1696,9 @@ function mergeDiagnosticsWithWarnings(
|
|
|
1690
1696
|
};
|
|
1691
1697
|
}
|
|
1692
1698
|
|
|
1693
|
-
export async function
|
|
1694
|
-
options:
|
|
1695
|
-
): Promise<AgentToolResult<EditToolDetails, typeof
|
|
1699
|
+
export async function executePatchSingle(
|
|
1700
|
+
options: ExecutePatchSingleOptions,
|
|
1701
|
+
): Promise<AgentToolResult<EditToolDetails, typeof patchEditEntrySchema>> {
|
|
1696
1702
|
const {
|
|
1697
1703
|
session,
|
|
1698
1704
|
params,
|
|
@@ -987,18 +987,23 @@ export function findContextLine(
|
|
|
987
987
|
return { index: undefined, confidence: bestScore };
|
|
988
988
|
}
|
|
989
989
|
|
|
990
|
-
export const
|
|
990
|
+
export const replaceEditEntrySchema = Type.Object({
|
|
991
991
|
path: Type.String({ description: "File path (relative or absolute)" }),
|
|
992
992
|
old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
|
|
993
993
|
new_text: Type.String({ description: "Replacement text" }),
|
|
994
994
|
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
|
|
995
995
|
});
|
|
996
996
|
|
|
997
|
+
export const replaceEditSchema = Type.Object({
|
|
998
|
+
edits: Type.Array(replaceEditEntrySchema, { description: "Replacements", minItems: 1 }),
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
export type ReplaceEditEntry = Static<typeof replaceEditEntrySchema>;
|
|
997
1002
|
export type ReplaceParams = Static<typeof replaceEditSchema>;
|
|
998
1003
|
|
|
999
|
-
interface
|
|
1004
|
+
export interface ExecuteReplaceSingleOptions {
|
|
1000
1005
|
session: ToolSession;
|
|
1001
|
-
params:
|
|
1006
|
+
params: ReplaceEditEntry;
|
|
1002
1007
|
signal?: AbortSignal;
|
|
1003
1008
|
batchRequest?: LspBatchRequest;
|
|
1004
1009
|
allowFuzzy: boolean;
|
|
@@ -1008,12 +1013,15 @@ interface ExecuteReplaceModeOptions {
|
|
|
1008
1013
|
}
|
|
1009
1014
|
|
|
1010
1015
|
export function isReplaceParams(params: unknown): params is ReplaceParams {
|
|
1011
|
-
|
|
1016
|
+
if (typeof params !== "object" || params === null) return false;
|
|
1017
|
+
if (!("edits" in params) || !Array.isArray((params as any).edits)) return false;
|
|
1018
|
+
const first = (params as any).edits[0];
|
|
1019
|
+
return first && typeof first === "object" && "old_text" in first && "new_text" in first;
|
|
1012
1020
|
}
|
|
1013
1021
|
|
|
1014
|
-
export async function
|
|
1015
|
-
options:
|
|
1016
|
-
): Promise<AgentToolResult<EditToolDetails, typeof
|
|
1022
|
+
export async function executeReplaceSingle(
|
|
1023
|
+
options: ExecuteReplaceSingleOptions,
|
|
1024
|
+
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
|
|
1017
1025
|
const {
|
|
1018
1026
|
session,
|
|
1019
1027
|
params,
|
package/src/edit/renderer.ts
CHANGED
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
shortenPath,
|
|
22
22
|
truncateDiffByHunk,
|
|
23
23
|
} from "../tools/render-utils";
|
|
24
|
+
import { type VimRenderArgs, vimToolRenderer } from "../tools/vim";
|
|
24
25
|
import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
26
|
+
import type { VimToolDetails } from "../vim/types";
|
|
25
27
|
import type { DiffError, DiffResult } from "./diff";
|
|
26
|
-
import type
|
|
28
|
+
import { type ChunkToolEdit, parseChunkEditPath } from "./modes/chunk";
|
|
27
29
|
import type { HashlineToolEdit } from "./modes/hashline";
|
|
28
30
|
import type { Operation } from "./modes/patch";
|
|
29
31
|
|
|
@@ -56,6 +58,18 @@ export function getLspBatchRequest(toolCall: ToolCallContext | undefined): LspBa
|
|
|
56
58
|
// Tool Details Types
|
|
57
59
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
60
|
|
|
61
|
+
export interface EditToolPerFileResult {
|
|
62
|
+
path: string;
|
|
63
|
+
diff: string;
|
|
64
|
+
firstChangedLine?: number;
|
|
65
|
+
diagnostics?: FileDiagnosticsResult;
|
|
66
|
+
op?: Operation;
|
|
67
|
+
move?: string;
|
|
68
|
+
isError?: boolean;
|
|
69
|
+
errorText?: string;
|
|
70
|
+
meta?: OutputMeta;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
export interface EditToolDetails {
|
|
60
74
|
/** Unified diff of the changes made */
|
|
61
75
|
diff: string;
|
|
@@ -69,6 +83,8 @@ export interface EditToolDetails {
|
|
|
69
83
|
move?: string;
|
|
70
84
|
/** Structured output metadata */
|
|
71
85
|
meta?: OutputMeta;
|
|
86
|
+
/** Per-file results (multi-file edits) */
|
|
87
|
+
perFileResults?: EditToolPerFileResult[];
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -94,6 +110,31 @@ interface EditRenderArgs {
|
|
|
94
110
|
edits?: Partial<HashlineToolEdit | ChunkToolEdit>[];
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
function isVimRenderArgs(args: EditRenderArgs | VimRenderArgs): args is VimRenderArgs {
|
|
114
|
+
return (
|
|
115
|
+
typeof args === "object" &&
|
|
116
|
+
args !== null &&
|
|
117
|
+
typeof (args as { file?: unknown }).file === "string" &&
|
|
118
|
+
!("path" in args) &&
|
|
119
|
+
!("file_path" in args) &&
|
|
120
|
+
!("edits" in args)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isVimToolDetails(details: unknown): details is VimToolDetails {
|
|
125
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const cursor = (details as { cursor?: unknown }).cursor;
|
|
129
|
+
const viewportLines = (details as { viewportLines?: unknown }).viewportLines;
|
|
130
|
+
return (
|
|
131
|
+
typeof (details as { file?: unknown }).file === "string" &&
|
|
132
|
+
typeof cursor === "object" &&
|
|
133
|
+
cursor !== null &&
|
|
134
|
+
Array.isArray(viewportLines)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
/** Extended context for edit tool rendering */
|
|
98
139
|
export interface EditRenderContext {
|
|
99
140
|
/** Pre-computed diff preview (computed before tool executes) */
|
|
@@ -114,6 +155,18 @@ interface FormattedStreamingEdit {
|
|
|
114
155
|
dst: string;
|
|
115
156
|
}
|
|
116
157
|
|
|
158
|
+
/** Extract file path from an edit entry's path (handles chunk's file:selector format). */
|
|
159
|
+
function filePathFromEditEntry(p: string | undefined): string | undefined {
|
|
160
|
+
if (!p) return undefined;
|
|
161
|
+
const ci = /^[a-zA-Z]:[/\\]/.test(p) ? p.indexOf(":", 2) : p.indexOf(":");
|
|
162
|
+
return ci === -1 ? p : p.slice(0, ci);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Count distinct file paths in an edits array. */
|
|
166
|
+
function countEditFiles(edits: any[]): number {
|
|
167
|
+
return new Set(edits.map((e: any) => filePathFromEditEntry(e?.path)).filter(Boolean)).size;
|
|
168
|
+
}
|
|
169
|
+
|
|
117
170
|
function countLines(text: string): number {
|
|
118
171
|
if (!text) return 0;
|
|
119
172
|
return text.split("\n").length;
|
|
@@ -182,7 +235,12 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
|
|
|
182
235
|
}
|
|
183
236
|
|
|
184
237
|
function isChunkStreamingEdit(edit: Partial<HashlineToolEdit | ChunkToolEdit>): edit is Partial<ChunkToolEdit> {
|
|
185
|
-
return
|
|
238
|
+
return (
|
|
239
|
+
typeof edit === "object" &&
|
|
240
|
+
edit !== null &&
|
|
241
|
+
"path" in edit &&
|
|
242
|
+
("write" in edit || "replace" in edit || "insert" in edit)
|
|
243
|
+
);
|
|
186
244
|
}
|
|
187
245
|
|
|
188
246
|
function getStreamingEditContent(content: unknown): string {
|
|
@@ -225,25 +283,20 @@ function formatChunkStreamingEdit(edit: Partial<ChunkToolEdit>): FormattedStream
|
|
|
225
283
|
return { srcLabel: "\u2022 (incomplete edit)", dst: "" };
|
|
226
284
|
}
|
|
227
285
|
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
switch (op) {
|
|
233
|
-
case "append":
|
|
234
|
-
return { srcLabel: `\u2022 append ${target}`, dst: contentLines };
|
|
235
|
-
case "prepend":
|
|
236
|
-
return { srcLabel: `\u2022 prepend ${target}`, dst: contentLines };
|
|
237
|
-
case "after":
|
|
238
|
-
return { srcLabel: `\u2022 insert after ${target}`, dst: contentLines };
|
|
239
|
-
case "before":
|
|
240
|
-
return { srcLabel: `\u2022 insert before ${target}`, dst: contentLines };
|
|
241
|
-
default:
|
|
242
|
-
return {
|
|
243
|
-
srcLabel: contentLines.length === 0 ? `\u2022 remove ${target}` : `\u2022 replace ${target}`,
|
|
244
|
-
dst: contentLines,
|
|
245
|
-
};
|
|
286
|
+
const target = edit.path ? (parseChunkEditPath(edit.path).selector ?? edit.path) : "?";
|
|
287
|
+
if (edit.write === null) {
|
|
288
|
+
return { srcLabel: `\u2022 remove ${target}`, dst: "" };
|
|
246
289
|
}
|
|
290
|
+
if (typeof edit.write === "string") {
|
|
291
|
+
return { srcLabel: `\u2022 replace ${target}`, dst: getStreamingEditContent(edit.write) };
|
|
292
|
+
}
|
|
293
|
+
if (typeof edit.replace === "object" && edit.replace) {
|
|
294
|
+
return { srcLabel: `\u2022 replace ${target}`, dst: getStreamingEditContent(edit.replace.new) };
|
|
295
|
+
}
|
|
296
|
+
if (typeof edit.insert === "object" && edit.insert) {
|
|
297
|
+
return { srcLabel: `\u2022 ${edit.insert.loc} ${target}`, dst: getStreamingEditContent(edit.insert.body) };
|
|
298
|
+
}
|
|
299
|
+
return { srcLabel: `\u2022 edit ${target}`, dst: "" };
|
|
247
300
|
}
|
|
248
301
|
|
|
249
302
|
function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit | ChunkToolEdit>[], uiTheme: Theme): string {
|
|
@@ -302,7 +355,11 @@ function getCallPreview(args: EditRenderArgs, rawPath: string, uiTheme: Theme):
|
|
|
302
355
|
return formatStreamingDiff(args.diff, rawPath, uiTheme);
|
|
303
356
|
}
|
|
304
357
|
if (args.edits && args.edits.length > 0) {
|
|
305
|
-
|
|
358
|
+
// Only show hashline/chunk streaming edits — replace/patch use previewDiff above
|
|
359
|
+
const first = args.edits[0];
|
|
360
|
+
if (first && typeof first === "object" && ("loc" in first || isChunkStreamingEdit(first))) {
|
|
361
|
+
return formatStreamingHashlineEdits(args.edits, uiTheme);
|
|
362
|
+
}
|
|
306
363
|
}
|
|
307
364
|
if (args.diff) {
|
|
308
365
|
return renderPlainTextPreview(args.diff, uiTheme);
|
|
@@ -376,11 +433,24 @@ export const editToolRenderer = {
|
|
|
376
433
|
mergeCallAndResult: true,
|
|
377
434
|
|
|
378
435
|
renderCall(args: EditRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
379
|
-
|
|
380
|
-
|
|
436
|
+
if (isVimRenderArgs(args)) {
|
|
437
|
+
return vimToolRenderer.renderCall(args, options, uiTheme);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Extract path from first edit entry when top-level path is absent (new schema)
|
|
441
|
+
const firstEdit = Array.isArray(args.edits) && args.edits.length > 0 ? args.edits[0] : undefined;
|
|
442
|
+
const rawPath = args.file_path || args.path || (firstEdit as any)?.path || "";
|
|
443
|
+
const rename = args.rename || (firstEdit as any)?.rename;
|
|
444
|
+
const op = args.op || (firstEdit as any)?.op;
|
|
445
|
+
const { description } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
381
446
|
const spinner =
|
|
382
447
|
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
383
|
-
let text = `${formatTitle(getOperationTitle(
|
|
448
|
+
let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
|
|
449
|
+
// Show file count hint for multi-file edits
|
|
450
|
+
const fileCount = Array.isArray(args.edits) ? countEditFiles(args.edits as any[]) : 0;
|
|
451
|
+
if (fileCount > 1) {
|
|
452
|
+
text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
|
|
453
|
+
}
|
|
384
454
|
text += getCallPreview(args, rawPath, uiTheme);
|
|
385
455
|
|
|
386
456
|
return new Text(text, 0, 0);
|
|
@@ -392,76 +462,160 @@ export const editToolRenderer = {
|
|
|
392
462
|
uiTheme: Theme,
|
|
393
463
|
args?: EditRenderArgs,
|
|
394
464
|
): Component {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
op !== "delete"
|
|
403
|
-
? `\n${formatMetadataLine(countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? ""), language, uiTheme)}`
|
|
404
|
-
: "";
|
|
405
|
-
|
|
406
|
-
// Pre-compute error text (static)
|
|
407
|
-
const errorText = result.isError ? (result.content?.find(c => c.type === "text")?.text ?? "") : "";
|
|
408
|
-
|
|
409
|
-
let cached: RenderCache | undefined;
|
|
410
|
-
|
|
411
|
-
return {
|
|
412
|
-
render(width) {
|
|
413
|
-
const { expanded, renderContext } = options;
|
|
414
|
-
const editDiffPreview = renderContext?.editDiffPreview;
|
|
415
|
-
const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
|
|
416
|
-
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
417
|
-
if (cached?.key === key) return cached.lines;
|
|
418
|
-
|
|
419
|
-
const firstChangedLine =
|
|
420
|
-
(editDiffPreview && "firstChangedLine" in editDiffPreview
|
|
421
|
-
? editDiffPreview.firstChangedLine
|
|
422
|
-
: undefined) || (result.details && !result.isError ? result.details.firstChangedLine : undefined);
|
|
423
|
-
const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
|
|
424
|
-
|
|
425
|
-
const header = renderStatusLine(
|
|
426
|
-
{
|
|
427
|
-
icon: result.isError ? "error" : "success",
|
|
428
|
-
title: getOperationTitle(op),
|
|
429
|
-
description,
|
|
430
|
-
},
|
|
431
|
-
uiTheme,
|
|
432
|
-
);
|
|
433
|
-
let text = header;
|
|
434
|
-
text += metadataLine;
|
|
435
|
-
|
|
436
|
-
if (result.isError) {
|
|
437
|
-
if (errorText) {
|
|
438
|
-
text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
|
|
439
|
-
}
|
|
440
|
-
} else if (result.details?.diff) {
|
|
441
|
-
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, renderDiffFn);
|
|
442
|
-
} else if (editDiffPreview) {
|
|
443
|
-
if ("error" in editDiffPreview) {
|
|
444
|
-
text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`;
|
|
445
|
-
} else if (editDiffPreview.diff) {
|
|
446
|
-
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Show LSP diagnostics if available
|
|
451
|
-
if (result.details?.diagnostics) {
|
|
452
|
-
text += formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp: string) =>
|
|
453
|
-
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
454
|
-
);
|
|
455
|
-
}
|
|
465
|
+
if (isVimToolDetails(result.details)) {
|
|
466
|
+
return vimToolRenderer.renderResult(
|
|
467
|
+
result as { content: Array<{ type: string; text?: string }>; details?: VimToolDetails; isError?: boolean },
|
|
468
|
+
options,
|
|
469
|
+
uiTheme,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
456
472
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
cached = undefined;
|
|
464
|
-
},
|
|
465
|
-
};
|
|
473
|
+
const perFileResults = result.details?.perFileResults;
|
|
474
|
+
const totalFiles = Array.isArray(args?.edits) ? countEditFiles(args!.edits as any[]) : 0;
|
|
475
|
+
if (perFileResults && (perFileResults.length > 1 || totalFiles > 1)) {
|
|
476
|
+
return renderMultiFileResult(perFileResults, totalFiles, options, uiTheme);
|
|
477
|
+
}
|
|
478
|
+
return renderSingleFileResult(result, options, uiTheme, args);
|
|
466
479
|
},
|
|
467
480
|
};
|
|
481
|
+
|
|
482
|
+
function renderSingleFileResult(
|
|
483
|
+
result: {
|
|
484
|
+
content: Array<{ type: string; text?: string }>;
|
|
485
|
+
details?: EditToolDetails | EditToolPerFileResult;
|
|
486
|
+
isError?: boolean;
|
|
487
|
+
},
|
|
488
|
+
options: RenderResultOptions & { renderContext?: EditRenderContext },
|
|
489
|
+
uiTheme: Theme,
|
|
490
|
+
args?: EditRenderArgs,
|
|
491
|
+
): Component {
|
|
492
|
+
const details = result.details;
|
|
493
|
+
const isError = result.isError ?? (details && "isError" in details ? details.isError : false);
|
|
494
|
+
const rawPath = args?.file_path || args?.path || (details && "path" in details ? details.path : "") || "";
|
|
495
|
+
const op = args?.op || details?.op;
|
|
496
|
+
const rename = args?.rename || details?.move;
|
|
497
|
+
const { language } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
498
|
+
|
|
499
|
+
const metadataLine =
|
|
500
|
+
op !== "delete"
|
|
501
|
+
? `\n${formatMetadataLine(countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? ""), language, uiTheme)}`
|
|
502
|
+
: "";
|
|
503
|
+
|
|
504
|
+
const errorText = isError
|
|
505
|
+
? (details && "errorText" in details && details.errorText) ||
|
|
506
|
+
(result.content?.find(c => c.type === "text")?.text ?? "")
|
|
507
|
+
: "";
|
|
508
|
+
|
|
509
|
+
let cached: RenderCache | undefined;
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
render(width) {
|
|
513
|
+
const { expanded, renderContext } = options;
|
|
514
|
+
const editDiffPreview = renderContext?.editDiffPreview;
|
|
515
|
+
const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
|
|
516
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
517
|
+
if (cached?.key === key) return cached.lines;
|
|
518
|
+
|
|
519
|
+
const firstChangedLine =
|
|
520
|
+
(editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
|
|
521
|
+
(details && !isError ? details.firstChangedLine : undefined);
|
|
522
|
+
const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
|
|
523
|
+
|
|
524
|
+
const header = renderStatusLine(
|
|
525
|
+
{
|
|
526
|
+
icon: isError ? "error" : "success",
|
|
527
|
+
title: getOperationTitle(op),
|
|
528
|
+
description,
|
|
529
|
+
},
|
|
530
|
+
uiTheme,
|
|
531
|
+
);
|
|
532
|
+
let text = header;
|
|
533
|
+
text += metadataLine;
|
|
534
|
+
|
|
535
|
+
if (isError) {
|
|
536
|
+
if (errorText) {
|
|
537
|
+
text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
|
|
538
|
+
}
|
|
539
|
+
} else if (details?.diff) {
|
|
540
|
+
text += renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
|
|
541
|
+
} else if (editDiffPreview) {
|
|
542
|
+
if ("error" in editDiffPreview) {
|
|
543
|
+
text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`;
|
|
544
|
+
} else if (editDiffPreview.diff) {
|
|
545
|
+
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (details?.diagnostics) {
|
|
550
|
+
text += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
|
|
551
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const lines =
|
|
556
|
+
width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n");
|
|
557
|
+
cached = { key, lines };
|
|
558
|
+
return lines;
|
|
559
|
+
},
|
|
560
|
+
invalidate() {
|
|
561
|
+
cached = undefined;
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderMultiFileResult(
|
|
567
|
+
perFileResults: EditToolPerFileResult[],
|
|
568
|
+
totalFiles: number,
|
|
569
|
+
options: RenderResultOptions & { renderContext?: EditRenderContext },
|
|
570
|
+
uiTheme: Theme,
|
|
571
|
+
): Component {
|
|
572
|
+
const fileComponents = perFileResults.map(fileResult =>
|
|
573
|
+
renderSingleFileResult({ content: [], details: fileResult, isError: fileResult.isError }, options, uiTheme),
|
|
574
|
+
);
|
|
575
|
+
const remaining = Math.max(0, totalFiles - perFileResults.length);
|
|
576
|
+
|
|
577
|
+
let cached: RenderCache | undefined;
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
render(width) {
|
|
581
|
+
const key = new Hasher().bool(options.expanded).u32(width).u32(perFileResults.length).u32(remaining).digest();
|
|
582
|
+
if (cached?.key === key) return cached.lines;
|
|
583
|
+
|
|
584
|
+
const allLines: string[] = [];
|
|
585
|
+
for (let i = 0; i < fileComponents.length; i++) {
|
|
586
|
+
if (i > 0) {
|
|
587
|
+
allLines.push("");
|
|
588
|
+
}
|
|
589
|
+
allLines.push(...fileComponents[i].render(width));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Show pending indicator for files still being processed
|
|
593
|
+
if (remaining > 0) {
|
|
594
|
+
if (allLines.length > 0) allLines.push("");
|
|
595
|
+
const spinnerFrame = options.spinnerFrame;
|
|
596
|
+
const spinner = spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, spinnerFrame) : "";
|
|
597
|
+
allLines.push(
|
|
598
|
+
renderStatusLine(
|
|
599
|
+
{
|
|
600
|
+
icon: "pending",
|
|
601
|
+
title: "Edit",
|
|
602
|
+
description: uiTheme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`),
|
|
603
|
+
},
|
|
604
|
+
uiTheme,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
if (spinner) {
|
|
608
|
+
// Replace the pending icon with spinner on the last line
|
|
609
|
+
allLines[allLines.length - 1] = allLines[allLines.length - 1].replace(/^(?:\x1b\[[^m]*m)*./u, spinner);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
cached = { key, lines: allLines };
|
|
614
|
+
return allLines;
|
|
615
|
+
},
|
|
616
|
+
invalidate() {
|
|
617
|
+
cached = undefined;
|
|
618
|
+
for (const c of fileComponents) c.invalidate();
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
@@ -705,6 +705,88 @@
|
|
|
705
705
|
color: var(--error);
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
+
/* Tool renderer extras */
|
|
709
|
+
.tool-meta {
|
|
710
|
+
margin-top: 4px;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.tool-badge {
|
|
714
|
+
display: inline-block;
|
|
715
|
+
padding: 0 6px;
|
|
716
|
+
margin-right: 4px;
|
|
717
|
+
border-radius: 3px;
|
|
718
|
+
background: rgba(255, 255, 255, 0.06);
|
|
719
|
+
color: var(--dim);
|
|
720
|
+
font-size: 11px;
|
|
721
|
+
font-weight: normal;
|
|
722
|
+
vertical-align: baseline;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.tool-pattern {
|
|
726
|
+
color: var(--warning);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.tool-args {
|
|
730
|
+
margin-top: 4px;
|
|
731
|
+
color: var(--toolOutput);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.tool-arg {
|
|
735
|
+
display: block;
|
|
736
|
+
line-height: var(--line-height);
|
|
737
|
+
white-space: pre-wrap;
|
|
738
|
+
word-break: break-word;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.tool-arg-key {
|
|
742
|
+
color: var(--dim);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.tool-arg-val {
|
|
746
|
+
color: var(--text);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.tool-cell {
|
|
750
|
+
margin-top: var(--line-height);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.tool-cell-title {
|
|
754
|
+
color: var(--dim);
|
|
755
|
+
font-size: 11px;
|
|
756
|
+
margin-bottom: 2px;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/* Todo write tree */
|
|
760
|
+
.todo-tree {
|
|
761
|
+
margin-top: var(--line-height);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.todo-phase {
|
|
765
|
+
margin-top: 6px;
|
|
766
|
+
color: var(--accent);
|
|
767
|
+
font-weight: bold;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.todo-task {
|
|
771
|
+
padding-left: 12px;
|
|
772
|
+
line-height: var(--line-height);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.todo-icon {
|
|
776
|
+
display: inline-block;
|
|
777
|
+
width: 14px;
|
|
778
|
+
text-align: center;
|
|
779
|
+
color: var(--dim);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.todo-completed { color: var(--toolDiffAdded); }
|
|
783
|
+
.todo-completed .todo-icon { color: var(--toolDiffAdded); }
|
|
784
|
+
.todo-in_progress { color: var(--warning); }
|
|
785
|
+
.todo-in_progress .todo-icon { color: var(--warning); }
|
|
786
|
+
.todo-abandoned { color: var(--toolDiffRemoved); }
|
|
787
|
+
.todo-abandoned .todo-icon { color: var(--toolDiffRemoved); }
|
|
788
|
+
.todo-pending { color: var(--toolOutput); }
|
|
789
|
+
|
|
708
790
|
/* Images */
|
|
709
791
|
.message-images {
|
|
710
792
|
margin-bottom: 12px;
|