@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/cursor.ts
CHANGED
|
@@ -177,16 +177,11 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
177
177
|
|
|
178
178
|
async grep(args: Parameters<NonNullable<ICursorExecHandlers["grep"]>>[0]) {
|
|
179
179
|
const toolCallId = decodeToolCallId(args.toolCallId);
|
|
180
|
+
const grepPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
|
|
180
181
|
const toolResultMessage = await executeTool(this.options, "grep", toolCallId, {
|
|
181
182
|
pattern: args.pattern,
|
|
182
|
-
path:
|
|
183
|
-
|
|
184
|
-
mode: args.outputMode || undefined,
|
|
185
|
-
context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
|
|
186
|
-
ignore_case: args.caseInsensitive || undefined,
|
|
187
|
-
type: args.type || undefined,
|
|
188
|
-
limit: args.headLimit ?? undefined,
|
|
189
|
-
multiline: args.multiline || undefined,
|
|
183
|
+
path: grepPath,
|
|
184
|
+
i: args.caseInsensitive || undefined,
|
|
190
185
|
});
|
|
191
186
|
return toolResultMessage;
|
|
192
187
|
}
|
package/src/debug/system-info.ts
CHANGED
|
@@ -40,8 +40,12 @@ function macosMarketingName(release: string): string | undefined {
|
|
|
40
40
|
|
|
41
41
|
/** Collect system information */
|
|
42
42
|
export async function collectSystemInfo(): Promise<SystemInfo> {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
let cpuModel = "Unknown CPU";
|
|
44
|
+
try {
|
|
45
|
+
cpuModel = os.cpus()[0]?.model ?? cpuModel;
|
|
46
|
+
} catch {
|
|
47
|
+
// Keep debug report collection best-effort when CPU probing fails.
|
|
48
|
+
}
|
|
45
49
|
|
|
46
50
|
// Try to get shell from environment
|
|
47
51
|
const shell = Bun.env.SHELL ?? Bun.env.ComSpec ?? "unknown";
|
package/src/discovery/claude.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
|
|
|
18
18
|
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
|
19
19
|
import { type CustomTool, toolCapability } from "../capability/tool";
|
|
20
20
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
21
|
+
import { settings } from "../config/settings";
|
|
21
22
|
import {
|
|
22
23
|
calculateDepth,
|
|
23
24
|
createSourceMeta,
|
|
@@ -252,48 +253,69 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
|
|
|
252
253
|
// Slash Commands
|
|
253
254
|
// =============================================================================
|
|
254
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Read the Claude command-loading toggles from settings.
|
|
258
|
+
* Falls back to true (current behavior) when settings are not initialized,
|
|
259
|
+
* e.g. inside discovery unit tests that run without Settings.init().
|
|
260
|
+
*/
|
|
261
|
+
function readClaudeCommandToggles(): { enableUser: boolean; enableProject: boolean } {
|
|
262
|
+
try {
|
|
263
|
+
return {
|
|
264
|
+
enableUser: settings.get("commands.enableClaudeUser") ?? true,
|
|
265
|
+
enableProject: settings.get("commands.enableClaudeProject") ?? true,
|
|
266
|
+
};
|
|
267
|
+
} catch {
|
|
268
|
+
return { enableUser: true, enableProject: true };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
255
272
|
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
256
273
|
const items: SlashCommand[] = [];
|
|
257
274
|
const warnings: string[] = [];
|
|
275
|
+
const { enableUser, enableProject } = readClaudeCommandToggles();
|
|
276
|
+
|
|
277
|
+
if (enableUser) {
|
|
278
|
+
const userBase = getUserClaude(ctx);
|
|
279
|
+
const userCommandsDir = path.join(userBase, "commands");
|
|
280
|
+
|
|
281
|
+
const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
|
|
282
|
+
extensions: ["md"],
|
|
283
|
+
transform: (name, content, path, source) => {
|
|
284
|
+
const cmdName = name.replace(/\.md$/, "");
|
|
285
|
+
return {
|
|
286
|
+
name: cmdName,
|
|
287
|
+
path,
|
|
288
|
+
content,
|
|
289
|
+
level: "user",
|
|
290
|
+
_source: source,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
});
|
|
258
294
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
|
|
263
|
-
extensions: ["md"],
|
|
264
|
-
transform: (name, content, path, source) => {
|
|
265
|
-
const cmdName = name.replace(/\.md$/, "");
|
|
266
|
-
return {
|
|
267
|
-
name: cmdName,
|
|
268
|
-
path,
|
|
269
|
-
content,
|
|
270
|
-
level: "user",
|
|
271
|
-
_source: source,
|
|
272
|
-
};
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
items.push(...userResult.items);
|
|
277
|
-
if (userResult.warnings) warnings.push(...userResult.warnings);
|
|
278
|
-
|
|
279
|
-
const projectCommandsDir = path.join(ctx.cwd, CONFIG_DIR, "commands");
|
|
295
|
+
items.push(...userResult.items);
|
|
296
|
+
if (userResult.warnings) warnings.push(...userResult.warnings);
|
|
297
|
+
}
|
|
280
298
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
299
|
+
if (enableProject) {
|
|
300
|
+
const projectCommandsDir = path.join(ctx.cwd, CONFIG_DIR, "commands");
|
|
301
|
+
|
|
302
|
+
const projectResult = await loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
|
|
303
|
+
extensions: ["md"],
|
|
304
|
+
transform: (name, content, path, source) => {
|
|
305
|
+
const cmdName = name.replace(/\.md$/, "");
|
|
306
|
+
return {
|
|
307
|
+
name: cmdName,
|
|
308
|
+
path,
|
|
309
|
+
content,
|
|
310
|
+
level: "project",
|
|
311
|
+
_source: source,
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
});
|
|
294
315
|
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
items.push(...projectResult.items);
|
|
317
|
+
if (projectResult.warnings) warnings.push(...projectResult.warnings);
|
|
318
|
+
}
|
|
297
319
|
|
|
298
320
|
return { items, warnings };
|
|
299
321
|
}
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -221,9 +221,9 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
|
|
221
221
|
|
|
222
222
|
let tools = parseArrayOrCSV(frontmatter.tools)?.map(tool => tool.toLowerCase());
|
|
223
223
|
|
|
224
|
-
// Subagents with explicit tool lists always need
|
|
225
|
-
if (tools && !tools.includes("
|
|
226
|
-
tools = [...tools, "
|
|
224
|
+
// Subagents with explicit tool lists always need yield
|
|
225
|
+
if (tools && !tools.includes("yield")) {
|
|
226
|
+
tools = [...tools, "yield"];
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
// Parse spawns field (array, "*", or CSV)
|
|
@@ -26,6 +26,7 @@ import { type Settings, settingsCapability } from "../capability/settings";
|
|
|
26
26
|
import { type Skill, skillCapability } from "../capability/skill";
|
|
27
27
|
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
28
28
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
29
|
+
import { settings } from "../config/settings";
|
|
29
30
|
|
|
30
31
|
import {
|
|
31
32
|
buildExtensionModuleItems,
|
|
@@ -236,9 +237,26 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
|
|
|
236
237
|
// Slash Commands (commands/)
|
|
237
238
|
// =============================================================================
|
|
238
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Read the OpenCode command-loading toggles from settings.
|
|
242
|
+
* Falls back to true (current behavior) when settings are not initialized,
|
|
243
|
+
* e.g. inside discovery unit tests that run without Settings.init().
|
|
244
|
+
*/
|
|
245
|
+
function readOpencodeCommandToggles(): { enableUser: boolean; enableProject: boolean } {
|
|
246
|
+
try {
|
|
247
|
+
return {
|
|
248
|
+
enableUser: settings.get("commands.enableOpencodeUser") ?? true,
|
|
249
|
+
enableProject: settings.get("commands.enableOpencodeProject") ?? true,
|
|
250
|
+
};
|
|
251
|
+
} catch {
|
|
252
|
+
return { enableUser: true, enableProject: true };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
239
256
|
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
240
|
-
const
|
|
241
|
-
const
|
|
257
|
+
const { enableUser, enableProject } = readOpencodeCommandToggles();
|
|
258
|
+
const userCommandsDir = enableUser ? getUserPath(ctx, "opencode", "commands") : null;
|
|
259
|
+
const projectCommandsDir = enableProject ? getProjectPath(ctx, "opencode", "commands") : null;
|
|
242
260
|
|
|
243
261
|
const transformCommand =
|
|
244
262
|
(level: "user" | "project") => (name: string, content: string, filePath: string, source: SourceMeta) => {
|
package/src/edit/diff.ts
CHANGED
|
@@ -50,17 +50,8 @@ export class ApplyPatchError extends Error {
|
|
|
50
50
|
// Diff String Generation
|
|
51
51
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
52
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
if (lines.length > 1 && lines[lines.length - 1] === "") {
|
|
56
|
-
lines.pop();
|
|
57
|
-
}
|
|
58
|
-
return Math.max(1, lines.length);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, width: number, content: string): string {
|
|
62
|
-
const padded = String(lineNum).padStart(width, " ");
|
|
63
|
-
return `${prefix}${padded}|${content}`;
|
|
53
|
+
function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, content: string): string {
|
|
54
|
+
return `${prefix}${lineNum}|${content}`;
|
|
64
55
|
}
|
|
65
56
|
|
|
66
57
|
/**
|
|
@@ -71,9 +62,6 @@ export function generateDiffString(oldContent: string, newContent: string, conte
|
|
|
71
62
|
const parts = Diff.diffLines(oldContent, newContent);
|
|
72
63
|
const output: string[] = [];
|
|
73
64
|
|
|
74
|
-
const maxLineNum = Math.max(countContentLines(oldContent), countContentLines(newContent));
|
|
75
|
-
const lineNumWidth = String(maxLineNum).length;
|
|
76
|
-
|
|
77
65
|
let oldLineNum = 1;
|
|
78
66
|
let newLineNum = 1;
|
|
79
67
|
let lastWasChange = false;
|
|
@@ -95,10 +83,10 @@ export function generateDiffString(oldContent: string, newContent: string, conte
|
|
|
95
83
|
// Show the change
|
|
96
84
|
for (const line of raw) {
|
|
97
85
|
if (part.added) {
|
|
98
|
-
output.push(formatNumberedDiffLine("+", newLineNum,
|
|
86
|
+
output.push(formatNumberedDiffLine("+", newLineNum, line));
|
|
99
87
|
newLineNum++;
|
|
100
88
|
} else {
|
|
101
|
-
output.push(formatNumberedDiffLine("-", oldLineNum,
|
|
89
|
+
output.push(formatNumberedDiffLine("-", oldLineNum, line));
|
|
102
90
|
oldLineNum++;
|
|
103
91
|
}
|
|
104
92
|
}
|
|
@@ -108,40 +96,57 @@ export function generateDiffString(oldContent: string, newContent: string, conte
|
|
|
108
96
|
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
109
97
|
|
|
110
98
|
if (lastWasChange || nextPartIsChange) {
|
|
111
|
-
|
|
112
|
-
let
|
|
113
|
-
let
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
99
|
+
const contextLimit = Math.max(0, contextLines);
|
|
100
|
+
let leadingSkip = 0;
|
|
101
|
+
let middleSkip = 0;
|
|
102
|
+
let trailingSkip = 0;
|
|
103
|
+
let linesToShow: string[];
|
|
104
|
+
|
|
105
|
+
if (lastWasChange && nextPartIsChange) {
|
|
106
|
+
if (raw.length > contextLimit * 2) {
|
|
107
|
+
const leadingContext = raw.slice(0, contextLimit);
|
|
108
|
+
const trailingContext = raw.slice(raw.length - contextLimit);
|
|
109
|
+
middleSkip = raw.length - leadingContext.length - trailingContext.length;
|
|
110
|
+
linesToShow = [...leadingContext, ...trailingContext];
|
|
111
|
+
} else {
|
|
112
|
+
linesToShow = raw;
|
|
113
|
+
}
|
|
114
|
+
} else if (nextPartIsChange) {
|
|
115
|
+
leadingSkip = Math.max(0, raw.length - contextLimit);
|
|
116
|
+
linesToShow = raw.slice(leadingSkip);
|
|
117
|
+
} else {
|
|
118
|
+
trailingSkip = Math.max(0, raw.length - contextLimit);
|
|
119
|
+
linesToShow = raw.slice(0, contextLimit);
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
newLineNum += skipStart;
|
|
122
|
+
if (leadingSkip > 0) {
|
|
123
|
+
output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
|
|
124
|
+
oldLineNum += leadingSkip;
|
|
125
|
+
newLineNum += leadingSkip;
|
|
132
126
|
}
|
|
133
127
|
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
const firstChunkLength = middleSkip > 0 ? contextLimit : linesToShow.length;
|
|
129
|
+
for (const line of linesToShow.slice(0, firstChunkLength)) {
|
|
130
|
+
output.push(formatNumberedDiffLine(" ", oldLineNum, line));
|
|
136
131
|
oldLineNum++;
|
|
137
132
|
newLineNum++;
|
|
138
133
|
}
|
|
139
134
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
if (middleSkip > 0) {
|
|
136
|
+
output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
|
|
137
|
+
oldLineNum += middleSkip;
|
|
138
|
+
newLineNum += middleSkip;
|
|
139
|
+
for (const line of linesToShow.slice(firstChunkLength)) {
|
|
140
|
+
output.push(formatNumberedDiffLine(" ", oldLineNum, line));
|
|
141
|
+
oldLineNum++;
|
|
142
|
+
newLineNum++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (trailingSkip > 0) {
|
|
147
|
+
output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
|
|
148
|
+
oldLineNum += trailingSkip;
|
|
149
|
+
newLineNum += trailingSkip;
|
|
145
150
|
}
|
|
146
151
|
} else {
|
|
147
152
|
// Skip these context lines entirely
|
|
@@ -184,8 +189,6 @@ export function generateUnifiedDiffString(oldContent: string, newContent: string
|
|
|
184
189
|
const patch = Diff.structuredPatch("", "", oldContent, newContent, "", "", { context: contextLines });
|
|
185
190
|
const output: string[] = [];
|
|
186
191
|
let firstChangedLine: number | undefined;
|
|
187
|
-
const maxLineNum = Math.max(countContentLines(oldContent), countContentLines(newContent));
|
|
188
|
-
const lineNumWidth = String(maxLineNum).length;
|
|
189
192
|
for (const hunk of patch.hunks) {
|
|
190
193
|
output.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
191
194
|
let oldLine = hunk.oldStart;
|
|
@@ -193,18 +196,18 @@ export function generateUnifiedDiffString(oldContent: string, newContent: string
|
|
|
193
196
|
for (const line of hunk.lines) {
|
|
194
197
|
if (line.startsWith("-")) {
|
|
195
198
|
if (firstChangedLine === undefined) firstChangedLine = newLine;
|
|
196
|
-
output.push(formatNumberedDiffLine("-", oldLine,
|
|
199
|
+
output.push(formatNumberedDiffLine("-", oldLine, line.slice(1)));
|
|
197
200
|
oldLine++;
|
|
198
201
|
continue;
|
|
199
202
|
}
|
|
200
203
|
if (line.startsWith("+")) {
|
|
201
204
|
if (firstChangedLine === undefined) firstChangedLine = newLine;
|
|
202
|
-
output.push(formatNumberedDiffLine("+", newLine,
|
|
205
|
+
output.push(formatNumberedDiffLine("+", newLine, line.slice(1)));
|
|
203
206
|
newLine++;
|
|
204
207
|
continue;
|
|
205
208
|
}
|
|
206
209
|
if (line.startsWith(" ")) {
|
|
207
|
-
output.push(formatNumberedDiffLine(" ", oldLine,
|
|
210
|
+
output.push(formatNumberedDiffLine(" ", oldLine, line.slice(1)));
|
|
208
211
|
oldLine++;
|
|
209
212
|
newLine++;
|
|
210
213
|
continue;
|
package/src/edit/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writethroughNoop,
|
|
10
10
|
} from "../lsp";
|
|
11
11
|
import applyPatchDescription from "../prompts/tools/apply-patch.md" with { type: "text" };
|
|
12
|
+
import atomDescription from "../prompts/tools/atom.md" with { type: "text" };
|
|
12
13
|
import chunkEditDescription from "../prompts/tools/chunk-edit.md" with { type: "text" };
|
|
13
14
|
import hashlineDescription from "../prompts/tools/hashline.md" with { type: "text" };
|
|
14
15
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
@@ -17,44 +18,33 @@ import type { ToolSession } from "../tools";
|
|
|
17
18
|
import { VimTool, vimSchema } from "../tools/vim";
|
|
18
19
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
19
20
|
import type { VimToolDetails } from "../vim/types";
|
|
20
|
-
import {
|
|
21
|
-
type ApplyPatchParams,
|
|
22
|
-
applyPatchSchema,
|
|
23
|
-
expandApplyPatchToEntries,
|
|
24
|
-
isApplyPatchParams,
|
|
25
|
-
} from "./modes/apply-patch";
|
|
21
|
+
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
26
22
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
23
|
+
import {
|
|
24
|
+
type AtomParams,
|
|
25
|
+
type AtomToolEdit,
|
|
26
|
+
atomEditParamsSchema,
|
|
27
|
+
executeAtomSingle,
|
|
28
|
+
resolveAtomEntryPaths,
|
|
29
|
+
} from "./modes/atom";
|
|
27
30
|
import {
|
|
28
31
|
type ChunkParams,
|
|
29
32
|
type ChunkToolEdit,
|
|
30
33
|
chunkEditParamsSchema,
|
|
31
34
|
executeChunkSingle,
|
|
32
|
-
isChunkParams,
|
|
33
35
|
parseChunkEditPath,
|
|
34
36
|
resolveAnchorStyle,
|
|
35
37
|
resolveChunkAutoIndent,
|
|
36
38
|
} from "./modes/chunk";
|
|
37
39
|
import {
|
|
38
40
|
executeHashlineSingle,
|
|
41
|
+
HashlineMismatchError,
|
|
39
42
|
type HashlineParams,
|
|
40
43
|
type HashlineToolEdit,
|
|
41
44
|
hashlineEditParamsSchema,
|
|
42
|
-
isHashlineParams,
|
|
43
45
|
} from "./modes/hashline";
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
isPatchParams,
|
|
47
|
-
type PatchEditEntry,
|
|
48
|
-
type PatchParams,
|
|
49
|
-
patchEditSchema,
|
|
50
|
-
} from "./modes/patch";
|
|
51
|
-
import {
|
|
52
|
-
executeReplaceSingle,
|
|
53
|
-
isReplaceParams,
|
|
54
|
-
type ReplaceEditEntry,
|
|
55
|
-
type ReplaceParams,
|
|
56
|
-
replaceEditSchema,
|
|
57
|
-
} from "./modes/replace";
|
|
46
|
+
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
47
|
+
import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
|
|
58
48
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
59
49
|
|
|
60
50
|
export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
@@ -62,30 +52,38 @@ export * from "./apply-patch";
|
|
|
62
52
|
export * from "./diff";
|
|
63
53
|
export * from "./line-hash";
|
|
64
54
|
export * from "./modes/apply-patch";
|
|
55
|
+
export * from "./modes/atom";
|
|
65
56
|
export * from "./modes/chunk";
|
|
66
57
|
export * from "./modes/hashline";
|
|
67
58
|
export * from "./modes/patch";
|
|
68
59
|
export * from "./modes/replace";
|
|
69
60
|
export * from "./normalize";
|
|
70
61
|
export * from "./renderer";
|
|
62
|
+
export * from "./streaming";
|
|
71
63
|
|
|
72
64
|
type TInput =
|
|
73
65
|
| typeof replaceEditSchema
|
|
74
66
|
| typeof patchEditSchema
|
|
75
67
|
| typeof hashlineEditParamsSchema
|
|
68
|
+
| typeof atomEditParamsSchema
|
|
76
69
|
| typeof chunkEditParamsSchema
|
|
77
70
|
| typeof vimSchema
|
|
78
71
|
| typeof applyPatchSchema;
|
|
79
72
|
|
|
80
73
|
type VimParams = Static<typeof vimSchema>;
|
|
81
|
-
type EditParams =
|
|
74
|
+
type EditParams =
|
|
75
|
+
| ReplaceParams
|
|
76
|
+
| PatchParams
|
|
77
|
+
| HashlineParams
|
|
78
|
+
| AtomParams
|
|
79
|
+
| ChunkParams
|
|
80
|
+
| VimParams
|
|
81
|
+
| ApplyPatchParams;
|
|
82
82
|
type EditToolResultDetails = EditToolDetails | VimToolDetails;
|
|
83
83
|
|
|
84
84
|
type EditModeDefinition = {
|
|
85
85
|
description: (session: ToolSession) => string;
|
|
86
86
|
parameters: TInput;
|
|
87
|
-
invalidParamsMessage: string;
|
|
88
|
-
validate: (params: EditParams) => boolean;
|
|
89
87
|
execute: (
|
|
90
88
|
tool: EditTool,
|
|
91
89
|
params: EditParams,
|
|
@@ -108,10 +106,6 @@ function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
|
|
|
108
106
|
return editMode;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
function isVimParams(params: EditParams): params is VimParams {
|
|
112
|
-
return typeof params === "object" && params !== null && "file" in params && typeof params.file === "string";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
109
|
function resolveAllowFuzzy(session: ToolSession, rawValue: string): boolean {
|
|
116
110
|
switch (rawValue) {
|
|
117
111
|
case "true":
|
|
@@ -147,6 +141,25 @@ function createEditWritethrough(session: ToolSession): WritethroughCallback {
|
|
|
147
141
|
return enableLsp ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics }) : writethroughNoop;
|
|
148
142
|
}
|
|
149
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolve per-entry `path` against an optional top-level `path` default.
|
|
146
|
+
* If both are absent on an entry, throws a descriptive error.
|
|
147
|
+
*/
|
|
148
|
+
function resolveEntryPaths<T extends { path?: string }>(
|
|
149
|
+
edits: readonly T[],
|
|
150
|
+
topLevelPath: string | undefined,
|
|
151
|
+
): (T & { path: string })[] {
|
|
152
|
+
return edits.map((edit, i) => {
|
|
153
|
+
const path = (edit && typeof edit.path === "string" && edit.path) || topLevelPath;
|
|
154
|
+
if (!path) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Edit ${i}: missing \`path\`. Provide \`path\` on this edit or supply a top-level \`path\` for the request.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return { ...edit, path };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
150
163
|
/** Group items by a key, preserving insertion order. */
|
|
151
164
|
function groupBy<T, K>(items: T[], key: (item: T) => K): Map<K, T[]> {
|
|
152
165
|
const map = new Map<K, T[]>();
|
|
@@ -202,7 +215,8 @@ async function executePerFile(
|
|
|
202
215
|
if (text) contentTexts.push(text);
|
|
203
216
|
} catch (err) {
|
|
204
217
|
const errorText = err instanceof Error ? err.message : String(err);
|
|
205
|
-
|
|
218
|
+
const displayErrorText = err instanceof HashlineMismatchError ? err.displayMessage : undefined;
|
|
219
|
+
perFileResults.push({ path, diff: "", isError: true, errorText, displayErrorText });
|
|
206
220
|
contentTexts.push(`Error editing ${path}: ${errorText}`);
|
|
207
221
|
}
|
|
208
222
|
|
|
@@ -306,10 +320,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
306
320
|
context?: AgentToolContext,
|
|
307
321
|
): Promise<AgentToolResult<EditToolResultDetails, TInput>> {
|
|
308
322
|
const modeDefinition = this.#getModeDefinition();
|
|
309
|
-
if (!modeDefinition.validate(params)) {
|
|
310
|
-
throw new Error(modeDefinition.invalidParamsMessage);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
323
|
return modeDefinition.execute(this, params, signal, getLspBatchRequest(context?.toolCall), onUpdate);
|
|
314
324
|
}
|
|
315
325
|
|
|
@@ -322,9 +332,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
322
332
|
chunkAutoIndent: resolveChunkAutoIndent(),
|
|
323
333
|
}),
|
|
324
334
|
parameters: chunkEditParamsSchema,
|
|
325
|
-
invalidParamsMessage:
|
|
326
|
-
"Invalid edit parameters for chunk mode. Expected `{ edits: [{ path: 'file:selector', ...op }, ...] }` with at least one edit. Each edit needs a `path`; supply one of `write` (string content; pass an empty string or omit it together with `replace`/`insert` to delete the chunk), `replace: { old, new }`, or `insert: { loc, body }`.",
|
|
327
|
-
validate: isChunkParams,
|
|
328
335
|
execute: (
|
|
329
336
|
tool: EditTool,
|
|
330
337
|
params: EditParams,
|
|
@@ -332,8 +339,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
332
339
|
batchRequest: LspBatchRequest | undefined,
|
|
333
340
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
334
341
|
) => {
|
|
335
|
-
const { edits } = params as ChunkParams;
|
|
336
|
-
const
|
|
342
|
+
const { edits, path: topPath } = params as ChunkParams & { path?: string };
|
|
343
|
+
const resolved = resolveEntryPaths(edits as ChunkToolEdit[], topPath);
|
|
344
|
+
const byFile = groupBy(resolved, (e: ChunkToolEdit) => parseChunkEditPath(e.path).filePath);
|
|
337
345
|
const entries = [...byFile.entries()].map(([filePath, fileEdits]) => ({
|
|
338
346
|
path: filePath,
|
|
339
347
|
run: (br: LspBatchRequest | undefined) =>
|
|
@@ -353,8 +361,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
353
361
|
patch: {
|
|
354
362
|
description: () => prompt.render(patchDescription),
|
|
355
363
|
parameters: patchEditSchema,
|
|
356
|
-
invalidParamsMessage: "Invalid edit parameters for patch mode.",
|
|
357
|
-
validate: isPatchParams,
|
|
358
364
|
execute: (
|
|
359
365
|
tool: EditTool,
|
|
360
366
|
params: EditParams,
|
|
@@ -362,8 +368,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
362
368
|
batchRequest: LspBatchRequest | undefined,
|
|
363
369
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
364
370
|
) => {
|
|
365
|
-
const { edits } = params as PatchParams;
|
|
366
|
-
const
|
|
371
|
+
const { edits, path: topPath } = params as PatchParams & { path?: string };
|
|
372
|
+
const resolved = resolveEntryPaths(edits as PatchEditEntry[], topPath);
|
|
373
|
+
const entries = resolved.map(entry => ({
|
|
367
374
|
path: entry.path,
|
|
368
375
|
run: (br: LspBatchRequest | undefined) =>
|
|
369
376
|
executePatchSingle({
|
|
@@ -383,8 +390,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
383
390
|
apply_patch: {
|
|
384
391
|
description: () => prompt.render(applyPatchDescription),
|
|
385
392
|
parameters: applyPatchSchema,
|
|
386
|
-
invalidParamsMessage: "Invalid edit parameters for apply_patch mode.",
|
|
387
|
-
validate: isApplyPatchParams,
|
|
388
393
|
execute: (
|
|
389
394
|
tool: EditTool,
|
|
390
395
|
params: EditParams,
|
|
@@ -393,8 +398,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
393
398
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
394
399
|
) => {
|
|
395
400
|
const entries = expandApplyPatchToEntries(params as ApplyPatchParams);
|
|
396
|
-
const perFile = entries.map(
|
|
397
|
-
path: entry.path
|
|
401
|
+
const perFile = entries.map(entry => ({
|
|
402
|
+
path: entry.path!,
|
|
398
403
|
run: (br: LspBatchRequest | undefined) =>
|
|
399
404
|
executePatchSingle({
|
|
400
405
|
session: tool.session,
|
|
@@ -413,8 +418,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
413
418
|
hashline: {
|
|
414
419
|
description: () => prompt.render(hashlineDescription),
|
|
415
420
|
parameters: hashlineEditParamsSchema,
|
|
416
|
-
invalidParamsMessage: "Invalid edit parameters for hashline mode.",
|
|
417
|
-
validate: isHashlineParams,
|
|
418
421
|
execute: (
|
|
419
422
|
tool: EditTool,
|
|
420
423
|
params: EditParams,
|
|
@@ -422,8 +425,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
422
425
|
batchRequest: LspBatchRequest | undefined,
|
|
423
426
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
424
427
|
) => {
|
|
425
|
-
const { edits } = params as HashlineParams;
|
|
426
|
-
const
|
|
428
|
+
const { edits, path: topPath } = params as HashlineParams & { path?: string };
|
|
429
|
+
const resolved = resolveEntryPaths(edits as HashlineToolEdit[], topPath);
|
|
430
|
+
const byFile = groupBy(resolved, e => e.path);
|
|
427
431
|
const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
|
|
428
432
|
path,
|
|
429
433
|
run: (br: LspBatchRequest | undefined) =>
|
|
@@ -440,11 +444,38 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
440
444
|
return executePerFile(entries, batchRequest, onUpdate);
|
|
441
445
|
},
|
|
442
446
|
},
|
|
447
|
+
atom: {
|
|
448
|
+
description: () => prompt.render(atomDescription),
|
|
449
|
+
parameters: atomEditParamsSchema,
|
|
450
|
+
execute: (
|
|
451
|
+
tool: EditTool,
|
|
452
|
+
params: EditParams,
|
|
453
|
+
signal: AbortSignal | undefined,
|
|
454
|
+
batchRequest: LspBatchRequest | undefined,
|
|
455
|
+
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
456
|
+
) => {
|
|
457
|
+
const { edits, path: topPath } = params as AtomParams & { path?: string };
|
|
458
|
+
const resolved = resolveAtomEntryPaths(edits as AtomToolEdit[], topPath);
|
|
459
|
+
const byFile = groupBy(resolved, e => e.path);
|
|
460
|
+
const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
|
|
461
|
+
path,
|
|
462
|
+
run: (br: LspBatchRequest | undefined) =>
|
|
463
|
+
executeAtomSingle({
|
|
464
|
+
session: tool.session,
|
|
465
|
+
path,
|
|
466
|
+
edits: fileEdits,
|
|
467
|
+
signal,
|
|
468
|
+
batchRequest: br,
|
|
469
|
+
writethrough: tool.#writethrough,
|
|
470
|
+
beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
|
|
471
|
+
}),
|
|
472
|
+
}));
|
|
473
|
+
return executePerFile(entries, batchRequest, onUpdate);
|
|
474
|
+
},
|
|
475
|
+
},
|
|
443
476
|
replace: {
|
|
444
477
|
description: () => prompt.render(replaceDescription),
|
|
445
478
|
parameters: replaceEditSchema,
|
|
446
|
-
invalidParamsMessage: "Invalid edit parameters for replace mode.",
|
|
447
|
-
validate: isReplaceParams,
|
|
448
479
|
execute: (
|
|
449
480
|
tool: EditTool,
|
|
450
481
|
params: EditParams,
|
|
@@ -452,8 +483,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
452
483
|
batchRequest: LspBatchRequest | undefined,
|
|
453
484
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
454
485
|
) => {
|
|
455
|
-
const { edits } = params as ReplaceParams;
|
|
456
|
-
const
|
|
486
|
+
const { edits, path: topPath } = params as ReplaceParams & { path?: string };
|
|
487
|
+
const resolved = resolveEntryPaths(edits as ReplaceEditEntry[], topPath);
|
|
488
|
+
const entries = resolved.map(entry => ({
|
|
457
489
|
path: entry.path,
|
|
458
490
|
run: (br: LspBatchRequest | undefined) =>
|
|
459
491
|
executeReplaceSingle({
|
|
@@ -473,8 +505,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
473
505
|
vim: {
|
|
474
506
|
description: () => this.#vimTool.description,
|
|
475
507
|
parameters: vimSchema,
|
|
476
|
-
invalidParamsMessage: "Invalid edit parameters for vim mode.",
|
|
477
|
-
validate: isVimParams,
|
|
478
508
|
execute: async (
|
|
479
509
|
tool: EditTool,
|
|
480
510
|
params: EditParams,
|