@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.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 +47 -2
- package/package.json +8 -8
- package/scripts/build-binary.ts +61 -0
- package/src/autoresearch/helpers.ts +10 -0
- package/src/autoresearch/index.ts +1 -11
- package/src/autoresearch/tools/init-experiment.ts +1 -10
- package/src/autoresearch/tools/log-experiment.ts +1 -11
- package/src/autoresearch/tools/run-experiment.ts +1 -10
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/plugin-cli.ts +23 -45
- package/src/commit/agentic/tools/propose-commit.ts +1 -14
- package/src/commit/agentic/tools/split-commit.ts +1 -15
- package/src/commit/utils.ts +15 -1
- package/src/config/model-registry.ts +3 -3
- package/src/config/prompt-templates.ts +4 -12
- package/src/config/settings-schema.ts +27 -2
- package/src/config/settings.ts +1 -1
- package/src/dap/session.ts +8 -2
- package/src/discovery/claude-plugins.ts +61 -6
- package/src/discovery/codex.ts +2 -15
- package/src/discovery/gemini.ts +2 -15
- package/src/discovery/helpers.ts +40 -1
- package/src/discovery/opencode.ts +2 -15
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +3 -14
- package/src/edit/index.ts +67 -3
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +63 -0
- package/src/edit/modes/chunk.ts +6 -2
- package/src/edit/modes/hashline.ts +3 -3
- package/src/edit/modes/replace.ts +2 -13
- package/src/edit/read-file.ts +18 -0
- package/src/edit/renderer.ts +61 -33
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/runner.ts +11 -29
- package/src/extensibility/utils.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/lsp/client.ts +14 -5
- package/src/lsp/index.ts +53 -10
- package/src/lsp/render.ts +14 -2
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +29 -48
- package/src/memories/index.ts +7 -1
- package/src/modes/acp/acp-agent.ts +3 -16
- package/src/modes/components/model-selector.ts +15 -24
- package/src/modes/components/plugin-settings.ts +16 -5
- package/src/modes/components/read-tool-group.ts +92 -9
- package/src/modes/components/settings-defs.ts +18 -0
- package/src/modes/components/settings-selector.ts +2 -6
- package/src/modes/components/tool-execution.ts +61 -28
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +99 -150
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/print-mode.ts +4 -22
- package/src/modes/rpc/rpc-mode.ts +18 -38
- package/src/modes/shared.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +6 -2
- package/src/plan-mode/approved-plan.ts +5 -4
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/subagent-user-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +208 -243
- package/src/prompts/tools/apply-patch.md +67 -0
- package/src/prompts/tools/ast-edit.md +18 -23
- package/src/prompts/tools/ast-grep.md +25 -32
- package/src/prompts/tools/bash.md +11 -23
- package/src/prompts/tools/debug.md +8 -22
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/grep.md +3 -5
- package/src/prompts/tools/hashline.md +16 -10
- package/src/prompts/tools/python.md +10 -14
- package/src/prompts/tools/read.md +17 -24
- package/src/prompts/tools/task.md +57 -21
- package/src/prompts/tools/todo-write.md +45 -67
- package/src/session/agent-session.ts +4 -4
- package/src/session/session-manager.ts +15 -7
- package/src/session/streaming-output.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +3 -14
- package/src/task/executor.ts +13 -34
- package/src/task/index.ts +82 -18
- package/src/task/simple-mode.ts +27 -0
- package/src/task/template.ts +17 -3
- package/src/task/types.ts +77 -30
- package/src/tools/ask.ts +2 -4
- package/src/tools/ast-edit.ts +41 -17
- package/src/tools/ast-grep.ts +8 -27
- package/src/tools/bash-skill-urls.ts +9 -7
- package/src/tools/bash.ts +66 -24
- package/src/tools/browser.ts +1 -1
- package/src/tools/fetch.ts +1 -14
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +25 -29
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +1 -8
- package/src/tools/gh.ts +6 -13
- package/src/tools/grep.ts +103 -59
- package/src/tools/jtd-to-json-schema.ts +16 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/path-utils.ts +61 -5
- package/src/tools/plan-mode-guard.ts +6 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +38 -6
- package/src/tools/renderers.ts +1 -0
- package/src/tools/resolve.ts +12 -3
- package/src/tools/ssh.ts +3 -11
- package/src/tools/submit-result.ts +1 -13
- package/src/tools/todo-write.ts +137 -103
- package/src/tools/vim.ts +1 -1
- package/src/tools/write.ts +2 -23
- package/src/tui/code-cell.ts +12 -7
- package/src/utils/edit-mode.ts +3 -2
- package/src/utils/git.ts +1 -1
- package/src/vim/engine.ts +41 -58
- package/src/web/scrapers/crates-io.ts +1 -14
- package/src/web/scrapers/types.ts +13 -0
- package/src/web/search/providers/base.ts +13 -0
- package/src/web/search/providers/brave.ts +2 -5
- package/src/web/search/providers/codex.ts +20 -24
- package/src/web/search/providers/gemini.ts +39 -1
- package/src/web/search/providers/jina.ts +2 -5
- package/src/web/search/providers/kagi.ts +3 -8
- package/src/web/search/providers/kimi.ts +3 -7
- package/src/web/search/providers/parallel.ts +3 -8
- package/src/web/search/providers/synthetic.ts +3 -7
- package/src/web/search/providers/tavily.ts +15 -11
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +3 -7
package/src/edit/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type WritethroughDeferredHandle,
|
|
9
9
|
writethroughNoop,
|
|
10
10
|
} from "../lsp";
|
|
11
|
+
import applyPatchDescription from "../prompts/tools/apply-patch.md" with { type: "text" };
|
|
11
12
|
import chunkEditDescription from "../prompts/tools/chunk-edit.md" with { type: "text" };
|
|
12
13
|
import hashlineDescription from "../prompts/tools/hashline.md" with { type: "text" };
|
|
13
14
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
@@ -16,6 +17,13 @@ import type { ToolSession } from "../tools";
|
|
|
16
17
|
import { VimTool, vimSchema } from "../tools/vim";
|
|
17
18
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
18
19
|
import type { VimToolDetails } from "../vim/types";
|
|
20
|
+
import {
|
|
21
|
+
type ApplyPatchParams,
|
|
22
|
+
applyPatchSchema,
|
|
23
|
+
expandApplyPatchToEntries,
|
|
24
|
+
isApplyPatchParams,
|
|
25
|
+
} from "./modes/apply-patch";
|
|
26
|
+
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
19
27
|
import {
|
|
20
28
|
type ChunkParams,
|
|
21
29
|
type ChunkToolEdit,
|
|
@@ -50,8 +58,10 @@ import {
|
|
|
50
58
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
51
59
|
|
|
52
60
|
export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
61
|
+
export * from "./apply-patch";
|
|
53
62
|
export * from "./diff";
|
|
54
63
|
export * from "./line-hash";
|
|
64
|
+
export * from "./modes/apply-patch";
|
|
55
65
|
export * from "./modes/chunk";
|
|
56
66
|
export * from "./modes/hashline";
|
|
57
67
|
export * from "./modes/patch";
|
|
@@ -64,10 +74,11 @@ type TInput =
|
|
|
64
74
|
| typeof patchEditSchema
|
|
65
75
|
| typeof hashlineEditParamsSchema
|
|
66
76
|
| typeof chunkEditParamsSchema
|
|
67
|
-
| typeof vimSchema
|
|
77
|
+
| typeof vimSchema
|
|
78
|
+
| typeof applyPatchSchema;
|
|
68
79
|
|
|
69
80
|
type VimParams = Static<typeof vimSchema>;
|
|
70
|
-
type EditParams = ReplaceParams | PatchParams | HashlineParams | ChunkParams | VimParams;
|
|
81
|
+
type EditParams = ReplaceParams | PatchParams | HashlineParams | ChunkParams | VimParams | ApplyPatchParams;
|
|
71
82
|
type EditToolResultDetails = EditToolDetails | VimToolDetails;
|
|
72
83
|
|
|
73
84
|
type EditModeDefinition = {
|
|
@@ -265,6 +276,28 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
265
276
|
return this.#getModeDefinition().parameters;
|
|
266
277
|
}
|
|
267
278
|
|
|
279
|
+
/**
|
|
280
|
+
* When in `apply_patch` mode, expose the Codex Lark grammar so providers
|
|
281
|
+
* that support OpenAI-style custom tools can emit a grammar-constrained
|
|
282
|
+
* variant. Providers that don't support custom tools ignore this field
|
|
283
|
+
* and fall back to emitting a JSON function tool from `parameters`.
|
|
284
|
+
*/
|
|
285
|
+
get customFormat(): { syntax: "lark"; definition: string } | undefined {
|
|
286
|
+
if (this.mode !== "apply_patch") return undefined;
|
|
287
|
+
return { syntax: "lark", definition: applyPatchGrammar };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Wire-level tool name used when the custom-tool variant is active. GPT-5+
|
|
292
|
+
* is trained on the literal name `apply_patch`; internally this is just a
|
|
293
|
+
* mode of the `edit` tool. The agent-loop dispatcher matches both the
|
|
294
|
+
* internal `name` and `customWireName`, so returned calls route correctly.
|
|
295
|
+
*/
|
|
296
|
+
get customWireName(): string | undefined {
|
|
297
|
+
if (this.mode !== "apply_patch") return undefined;
|
|
298
|
+
return "apply_patch";
|
|
299
|
+
}
|
|
300
|
+
|
|
268
301
|
async execute(
|
|
269
302
|
_toolCallId: string,
|
|
270
303
|
params: EditParams,
|
|
@@ -289,7 +322,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
289
322
|
chunkAutoIndent: resolveChunkAutoIndent(),
|
|
290
323
|
}),
|
|
291
324
|
parameters: chunkEditParamsSchema,
|
|
292
|
-
invalidParamsMessage:
|
|
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 }`.",
|
|
293
327
|
validate: isChunkParams,
|
|
294
328
|
execute: (
|
|
295
329
|
tool: EditTool,
|
|
@@ -346,6 +380,36 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
346
380
|
return executePerFile(entries, batchRequest, onUpdate);
|
|
347
381
|
},
|
|
348
382
|
},
|
|
383
|
+
apply_patch: {
|
|
384
|
+
description: () => prompt.render(applyPatchDescription),
|
|
385
|
+
parameters: applyPatchSchema,
|
|
386
|
+
invalidParamsMessage: "Invalid edit parameters for apply_patch mode.",
|
|
387
|
+
validate: isApplyPatchParams,
|
|
388
|
+
execute: (
|
|
389
|
+
tool: EditTool,
|
|
390
|
+
params: EditParams,
|
|
391
|
+
signal: AbortSignal | undefined,
|
|
392
|
+
batchRequest: LspBatchRequest | undefined,
|
|
393
|
+
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
394
|
+
) => {
|
|
395
|
+
const entries = expandApplyPatchToEntries(params as ApplyPatchParams);
|
|
396
|
+
const perFile = entries.map((entry: PatchEditEntry) => ({
|
|
397
|
+
path: entry.path,
|
|
398
|
+
run: (br: LspBatchRequest | undefined) =>
|
|
399
|
+
executePatchSingle({
|
|
400
|
+
session: tool.session,
|
|
401
|
+
params: entry,
|
|
402
|
+
signal,
|
|
403
|
+
batchRequest: br,
|
|
404
|
+
allowFuzzy: tool.#allowFuzzy,
|
|
405
|
+
fuzzyThreshold: tool.#fuzzyThreshold,
|
|
406
|
+
writethrough: tool.#writethrough,
|
|
407
|
+
beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
|
|
408
|
+
}),
|
|
409
|
+
}));
|
|
410
|
+
return executePerFile(perFile, batchRequest, onUpdate);
|
|
411
|
+
},
|
|
412
|
+
},
|
|
349
413
|
hashline: {
|
|
350
414
|
description: () => prompt.render(hashlineDescription),
|
|
351
415
|
parameters: hashlineEditParamsSchema,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
start: begin_patch hunk+ end_patch
|
|
2
|
+
begin_patch: "*** Begin Patch" LF
|
|
3
|
+
end_patch: "*** End Patch" LF?
|
|
4
|
+
|
|
5
|
+
hunk: add_hunk | delete_hunk | update_hunk
|
|
6
|
+
add_hunk: "*** Add File: " filename LF add_line+
|
|
7
|
+
delete_hunk: "*** Delete File: " filename LF
|
|
8
|
+
update_hunk: "*** Update File: " filename LF change_move? change?
|
|
9
|
+
|
|
10
|
+
filename: /(.+)/
|
|
11
|
+
add_line: "+" /(.*)/ LF -> line
|
|
12
|
+
|
|
13
|
+
change_move: "*** Move to: " filename LF
|
|
14
|
+
change: (change_context | change_line)+ eof_line?
|
|
15
|
+
change_context: ("@@" | "@@ " /(.+)/) LF
|
|
16
|
+
change_line: ("+" | "-" | " ") /(.*)/ LF
|
|
17
|
+
eof_line: "*** End of File" LF
|
|
18
|
+
|
|
19
|
+
%import common.LF
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit mode wrapper for the Codex `apply_patch` envelope format.
|
|
3
|
+
*
|
|
4
|
+
* The mode accepts a single `input` string containing a full
|
|
5
|
+
* `*** Begin Patch ... *** End Patch` block, parses it, and fans out to
|
|
6
|
+
* the existing `executePatchSingle` — so all the machinery (plan mode,
|
|
7
|
+
* LSP writethrough, fs-cache invalidation, diagnostics) is shared with
|
|
8
|
+
* the `patch` mode.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
12
|
+
import { parseApplyPatch, parseApplyPatchStreaming } from "../apply-patch/parser";
|
|
13
|
+
import { ApplyPatchError } from "../diff";
|
|
14
|
+
import type { PatchEditEntry } from "./patch";
|
|
15
|
+
|
|
16
|
+
export const applyPatchSchema = Type.Object({
|
|
17
|
+
input: Type.String({
|
|
18
|
+
description:
|
|
19
|
+
"Full Codex apply_patch envelope, including '*** Begin Patch' and '*** End Patch'. Contains any mix of Add/Delete/Update (with optional Move to) file operations.",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type ApplyPatchParams = Static<typeof applyPatchSchema>;
|
|
24
|
+
|
|
25
|
+
export function isApplyPatchParams(params: unknown): params is ApplyPatchParams {
|
|
26
|
+
return (
|
|
27
|
+
typeof params === "object" &&
|
|
28
|
+
params !== null &&
|
|
29
|
+
"input" in params &&
|
|
30
|
+
typeof (params as { input: unknown }).input === "string"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse the envelope and lower each hunk to a `PatchEditEntry` so it can
|
|
36
|
+
* be routed through `executePatchSingle`.
|
|
37
|
+
*/
|
|
38
|
+
export function expandApplyPatchToEntries(params: ApplyPatchParams): PatchEditEntry[] {
|
|
39
|
+
const hunks = parseApplyPatch(params.input);
|
|
40
|
+
if (hunks.length === 0) {
|
|
41
|
+
throw new ApplyPatchError("No files were modified.");
|
|
42
|
+
}
|
|
43
|
+
return hunks.map(
|
|
44
|
+
(h): PatchEditEntry => ({
|
|
45
|
+
path: h.path,
|
|
46
|
+
op: h.op,
|
|
47
|
+
rename: h.rename,
|
|
48
|
+
diff: h.diff,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function expandApplyPatchToPreviewEntries(params: ApplyPatchParams): PatchEditEntry[] {
|
|
54
|
+
const hunks = parseApplyPatchStreaming(params.input);
|
|
55
|
+
return hunks.map(
|
|
56
|
+
(h): PatchEditEntry => ({
|
|
57
|
+
path: h.path,
|
|
58
|
+
op: h.op,
|
|
59
|
+
rename: h.rename,
|
|
60
|
+
diff: h.diff,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
package/src/edit/modes/chunk.ts
CHANGED
|
@@ -549,8 +549,12 @@ export function isChunkParams(params: unknown): params is ChunkParams {
|
|
|
549
549
|
return false;
|
|
550
550
|
}
|
|
551
551
|
const first = params.edits[0];
|
|
552
|
-
|
|
553
|
-
|
|
552
|
+
// Accept a bare `{ path }` entry: it is interpreted downstream as a chunk
|
|
553
|
+
// delete. Some providers strip `null` values from tool-call JSON, so a
|
|
554
|
+
// documented `{ path, write: null }` delete can arrive here as just
|
|
555
|
+
// `{ path }`. Rejecting that surfaced as a misleading
|
|
556
|
+
// "Invalid edit parameters for chunk mode." error.
|
|
557
|
+
return typeof first === "object" && first !== null && "path" in first;
|
|
554
558
|
}
|
|
555
559
|
|
|
556
560
|
/** Auto-correct indentation for content targeting a body region (`~`) when autoIndent is on.
|
|
@@ -1157,11 +1157,11 @@ export async function computeHashlineDiff(
|
|
|
1157
1157
|
}
|
|
1158
1158
|
> {
|
|
1159
1159
|
const { path, edits, move } = input;
|
|
1160
|
-
const absolutePath = resolveToCwd(path, cwd);
|
|
1161
|
-
const movePath = move ? resolveToCwd(move, cwd) : undefined;
|
|
1162
|
-
const isMoveOnly = Boolean(movePath) && movePath !== absolutePath && edits.length === 0;
|
|
1163
1160
|
|
|
1164
1161
|
try {
|
|
1162
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
1163
|
+
const movePath = move ? resolveToCwd(move, cwd) : undefined;
|
|
1164
|
+
const isMoveOnly = Boolean(movePath) && movePath !== absolutePath && edits.length === 0;
|
|
1165
1165
|
const resolvedEdits = resolveHashlineEditsForDiff(edits);
|
|
1166
1166
|
const file = Bun.file(absolutePath);
|
|
1167
1167
|
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* fallback strategies for finding text in files.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
10
9
|
import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
11
10
|
import type { ToolSession } from "../../tools";
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
restoreLineEndings,
|
|
23
22
|
stripBom,
|
|
24
23
|
} from "../normalize";
|
|
24
|
+
import { readEditFileText } from "../read-file";
|
|
25
25
|
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
26
26
|
|
|
27
27
|
export interface FuzzyMatch {
|
|
@@ -140,17 +140,6 @@ function formatOccurrenceError(path: string, matchOutcome: MatchOutcome): string
|
|
|
140
140
|
return `Found ${matchOutcome.occurrences} occurrences in ${path}${moreMsg}:\n\n${previews}\n\nAdd more context lines to disambiguate.`;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
async function readReplaceFileContent(absolutePath: string, path: string): Promise<string> {
|
|
144
|
-
try {
|
|
145
|
-
return await Bun.file(absolutePath).text();
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (isEnoent(error)) {
|
|
148
|
-
throw new Error(`File not found: ${path}`);
|
|
149
|
-
}
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
143
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
155
144
|
// Constants
|
|
156
145
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1045,7 +1034,7 @@ export async function executeReplaceSingle(
|
|
|
1045
1034
|
}
|
|
1046
1035
|
|
|
1047
1036
|
const absolutePath = resolvePlanPath(session, path);
|
|
1048
|
-
const rawContent = await
|
|
1037
|
+
const rawContent = await readEditFileText(absolutePath, path);
|
|
1049
1038
|
const { bom, text: content } = stripBom(rawContent);
|
|
1050
1039
|
const originalEnding = detectLineEnding(content);
|
|
1051
1040
|
const normalizedContent = normalizeToLF(content);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared file-read helper for edit-mode utilities.
|
|
3
|
+
*
|
|
4
|
+
* Reads a file via Bun and rethrows ENOENT as a user-facing "File not found"
|
|
5
|
+
* error referencing the display path.
|
|
6
|
+
*/
|
|
7
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
8
|
+
|
|
9
|
+
export async function readEditFileText(absolutePath: string, path: string): Promise<string> {
|
|
10
|
+
try {
|
|
11
|
+
return await Bun.file(absolutePath).text();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (isEnoent(error)) {
|
|
14
|
+
throw new Error(`File not found: ${path}`);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/edit/renderer.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Edit tool renderer and LSP batching helpers.
|
|
3
3
|
*/
|
|
4
|
-
import type { ToolCallContext } from "@oh-my-pi/pi-agent-core";
|
|
5
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
5
|
import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
7
6
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -16,6 +15,8 @@ import {
|
|
|
16
15
|
formatStatusIcon,
|
|
17
16
|
formatTitle,
|
|
18
17
|
getDiffStats,
|
|
18
|
+
getLspBatchRequest,
|
|
19
|
+
type LspBatchRequest,
|
|
19
20
|
PREVIEW_LIMITS,
|
|
20
21
|
replaceTabs,
|
|
21
22
|
shortenPath,
|
|
@@ -25,34 +26,16 @@ import { type VimRenderArgs, vimToolRenderer } from "../tools/vim";
|
|
|
25
26
|
import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
26
27
|
import type { VimToolDetails } from "../vim/types";
|
|
27
28
|
import type { DiffError, DiffResult } from "./diff";
|
|
29
|
+
import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
28
30
|
import { type ChunkToolEdit, parseChunkEditPath } from "./modes/chunk";
|
|
29
31
|
import type { HashlineToolEdit } from "./modes/hashline";
|
|
30
|
-
import type { Operation } from "./modes/patch";
|
|
32
|
+
import type { Operation, PatchEditEntry } from "./modes/patch";
|
|
31
33
|
|
|
32
34
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
35
|
// LSP Batching
|
|
34
36
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export interface LspBatchRequest {
|
|
39
|
-
id: string;
|
|
40
|
-
flush: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function getLspBatchRequest(toolCall: ToolCallContext | undefined): LspBatchRequest | undefined {
|
|
44
|
-
if (!toolCall) {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
const hasOtherWrites = toolCall.toolCalls.some(
|
|
48
|
-
(call, index) => index !== toolCall.index && LSP_BATCH_TOOLS.has(call.name),
|
|
49
|
-
);
|
|
50
|
-
if (!hasOtherWrites) {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some(call => LSP_BATCH_TOOLS.has(call.name));
|
|
54
|
-
return { id: toolCall.batchId, flush: !hasLaterWrites };
|
|
55
|
-
}
|
|
38
|
+
export { getLspBatchRequest, type LspBatchRequest };
|
|
56
39
|
|
|
57
40
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
41
|
// Tool Details Types
|
|
@@ -97,6 +80,7 @@ interface EditRenderArgs {
|
|
|
97
80
|
oldText?: string;
|
|
98
81
|
newText?: string;
|
|
99
82
|
patch?: string;
|
|
83
|
+
input?: string;
|
|
100
84
|
all?: boolean;
|
|
101
85
|
// Patch mode fields
|
|
102
86
|
op?: Operation;
|
|
@@ -107,7 +91,19 @@ interface EditRenderArgs {
|
|
|
107
91
|
*/
|
|
108
92
|
previewDiff?: string;
|
|
109
93
|
// Hashline / chunk mode fields
|
|
110
|
-
edits?:
|
|
94
|
+
edits?: EditRenderEntry[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type EditRenderEntry = {
|
|
98
|
+
path?: string;
|
|
99
|
+
rename?: string;
|
|
100
|
+
move?: string;
|
|
101
|
+
op?: Operation;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
interface ApplyPatchRenderSummary {
|
|
105
|
+
entries: PatchEditEntry[];
|
|
106
|
+
error?: string;
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
function isVimRenderArgs(args: EditRenderArgs | VimRenderArgs): args is VimRenderArgs {
|
|
@@ -163,8 +159,8 @@ function filePathFromEditEntry(p: string | undefined): string | undefined {
|
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
/** Count distinct file paths in an edits array. */
|
|
166
|
-
function countEditFiles(edits:
|
|
167
|
-
return new Set(edits.map(
|
|
162
|
+
function countEditFiles(edits: EditRenderEntry[]): number {
|
|
163
|
+
return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
function countLines(text: string): number {
|
|
@@ -370,6 +366,24 @@ function getCallPreview(args: EditRenderArgs, rawPath: string, uiTheme: Theme):
|
|
|
370
366
|
return "";
|
|
371
367
|
}
|
|
372
368
|
|
|
369
|
+
const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** End Patch'";
|
|
370
|
+
|
|
371
|
+
function getApplyPatchRenderSummary(args: EditRenderArgs, isPartial: boolean): ApplyPatchRenderSummary | undefined {
|
|
372
|
+
if (typeof args.input !== "string") {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
return { entries: expandApplyPatchToEntries({ input: args.input }) };
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
380
|
+
if (isPartial && error === MISSING_APPLY_PATCH_END_ERROR) {
|
|
381
|
+
return { entries: expandApplyPatchToPreviewEntries({ input: args.input }) };
|
|
382
|
+
}
|
|
383
|
+
return { entries: [], error };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
373
387
|
function renderDiffSection(
|
|
374
388
|
diff: string,
|
|
375
389
|
rawPath: string,
|
|
@@ -437,21 +451,29 @@ export const editToolRenderer = {
|
|
|
437
451
|
return vimToolRenderer.renderCall(args, options, uiTheme);
|
|
438
452
|
}
|
|
439
453
|
|
|
454
|
+
const applyPatchSummary = getApplyPatchRenderSummary(args, options.isPartial);
|
|
455
|
+
const firstApplyPatchEntry = applyPatchSummary?.entries[0];
|
|
440
456
|
// Extract path from first edit entry when top-level path is absent (new schema)
|
|
441
457
|
const firstEdit = Array.isArray(args.edits) && args.edits.length > 0 ? args.edits[0] : undefined;
|
|
442
|
-
const rawPath =
|
|
443
|
-
|
|
444
|
-
const
|
|
458
|
+
const rawPath =
|
|
459
|
+
args.file_path || args.path || filePathFromEditEntry(firstEdit?.path) || firstApplyPatchEntry?.path || "";
|
|
460
|
+
const rename = args.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
|
|
461
|
+
const op = args.op || firstEdit?.op || firstApplyPatchEntry?.op;
|
|
445
462
|
const { description } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
446
463
|
const spinner =
|
|
447
464
|
options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
|
|
448
465
|
let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
|
|
449
466
|
// Show file count hint for multi-file edits
|
|
450
|
-
const fileCount = Array.isArray(args.edits)
|
|
467
|
+
const fileCount = Array.isArray(args.edits)
|
|
468
|
+
? countEditFiles(args.edits)
|
|
469
|
+
: (applyPatchSummary?.entries.length ?? 0);
|
|
451
470
|
if (fileCount > 1) {
|
|
452
471
|
text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
|
|
453
472
|
}
|
|
454
473
|
text += getCallPreview(args, rawPath, uiTheme);
|
|
474
|
+
if (applyPatchSummary?.error) {
|
|
475
|
+
text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error), CALL_TEXT_PREVIEW_WIDTH))}`;
|
|
476
|
+
}
|
|
455
477
|
|
|
456
478
|
return new Text(text, 0, 0);
|
|
457
479
|
},
|
|
@@ -471,7 +493,7 @@ export const editToolRenderer = {
|
|
|
471
493
|
}
|
|
472
494
|
|
|
473
495
|
const perFileResults = result.details?.perFileResults;
|
|
474
|
-
const totalFiles =
|
|
496
|
+
const totalFiles = args?.edits ? countEditFiles(args.edits) : 0;
|
|
475
497
|
if (perFileResults && (perFileResults.length > 1 || totalFiles > 1)) {
|
|
476
498
|
return renderMultiFileResult(perFileResults, totalFiles, options, uiTheme);
|
|
477
499
|
}
|
|
@@ -491,9 +513,15 @@ function renderSingleFileResult(
|
|
|
491
513
|
): Component {
|
|
492
514
|
const details = result.details;
|
|
493
515
|
const isError = result.isError ?? (details && "isError" in details ? details.isError : false);
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
516
|
+
const firstEdit = args?.edits?.[0];
|
|
517
|
+
const rawPath =
|
|
518
|
+
args?.file_path ||
|
|
519
|
+
args?.path ||
|
|
520
|
+
filePathFromEditEntry(firstEdit?.path) ||
|
|
521
|
+
(details && "path" in details ? details.path : "") ||
|
|
522
|
+
"";
|
|
523
|
+
const op = args?.op || firstEdit?.op || details?.op;
|
|
524
|
+
const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
|
|
497
525
|
const { language } = formatEditDescription(rawPath, uiTheme, { rename });
|
|
498
526
|
|
|
499
527
|
const metadataLine =
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for wiring the `compact` action of an {@link ExtensionContext}.
|
|
3
|
+
*
|
|
4
|
+
* Extension-facing APIs accept `string | CompactOptions`, but `AgentSession.compact`
|
|
5
|
+
* takes two positional arguments `(instructions, options)`. This helper splits the
|
|
6
|
+
* union so the same adapter can be reused by print-mode, rpc-mode, and the executor.
|
|
7
|
+
*/
|
|
8
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { CompactOptions } from "./types";
|
|
10
|
+
|
|
11
|
+
interface CompactableSession {
|
|
12
|
+
compact(instructions?: string, options?: CompactOptions): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runExtensionCompact(
|
|
16
|
+
session: CompactableSession,
|
|
17
|
+
instructionsOrOptions: string | CompactOptions | undefined,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
20
|
+
const options =
|
|
21
|
+
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
22
|
+
await session.compact(instructions, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SetModelCapableSession {
|
|
26
|
+
modelRegistry: { getApiKey(model: Model): Promise<string | undefined> };
|
|
27
|
+
setModel(model: Model): Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper for wiring the `setModel` action of an {@link ExtensionContext}.
|
|
32
|
+
*
|
|
33
|
+
* Returns false when no API key is available for the requested model.
|
|
34
|
+
*/
|
|
35
|
+
export async function runExtensionSetModel(session: SetModelCapableSession, model: Model): Promise<boolean> {
|
|
36
|
+
const key = await session.modelRegistry.getApiKey(model);
|
|
37
|
+
if (!key) return false;
|
|
38
|
+
await session.setModel(model);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -554,53 +554,35 @@ export class ExtensionRunner {
|
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
async emitUserBash(event: UserBashEvent): Promise<UserBashEventResult | undefined> {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
for (const ext of this.extensions) {
|
|
560
|
-
const handlers = ext.handlers.get("user_bash");
|
|
561
|
-
if (!handlers || handlers.length === 0) continue;
|
|
562
|
-
|
|
563
|
-
for (const handler of handlers) {
|
|
564
|
-
try {
|
|
565
|
-
const handlerResult = await handler(event, ctx);
|
|
566
|
-
if (handlerResult) {
|
|
567
|
-
return handlerResult as UserBashEventResult;
|
|
568
|
-
}
|
|
569
|
-
} catch (err) {
|
|
570
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
571
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
572
|
-
this.emitError({
|
|
573
|
-
extensionPath: ext.path,
|
|
574
|
-
event: "user_bash",
|
|
575
|
-
error: message,
|
|
576
|
-
stack,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return undefined;
|
|
557
|
+
return this.emitUserEvent<UserBashEventResult>(event, "user_bash");
|
|
583
558
|
}
|
|
584
559
|
|
|
585
560
|
async emitUserPython(event: UserPythonEvent): Promise<UserPythonEventResult | undefined> {
|
|
561
|
+
return this.emitUserEvent<UserPythonEventResult>(event, "user_python");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private async emitUserEvent<R>(
|
|
565
|
+
event: UserBashEvent | UserPythonEvent,
|
|
566
|
+
eventName: "user_bash" | "user_python",
|
|
567
|
+
): Promise<R | undefined> {
|
|
586
568
|
const ctx = this.createContext();
|
|
587
569
|
|
|
588
570
|
for (const ext of this.extensions) {
|
|
589
|
-
const handlers = ext.handlers.get(
|
|
571
|
+
const handlers = ext.handlers.get(eventName);
|
|
590
572
|
if (!handlers || handlers.length === 0) continue;
|
|
591
573
|
|
|
592
574
|
for (const handler of handlers) {
|
|
593
575
|
try {
|
|
594
576
|
const handlerResult = await handler(event, ctx);
|
|
595
577
|
if (handlerResult) {
|
|
596
|
-
return handlerResult as
|
|
578
|
+
return handlerResult as R;
|
|
597
579
|
}
|
|
598
580
|
} catch (err) {
|
|
599
581
|
const message = err instanceof Error ? err.message : String(err);
|
|
600
582
|
const stack = err instanceof Error ? err.stack : undefined;
|
|
601
583
|
this.emitError({
|
|
602
584
|
extensionPath: ext.path,
|
|
603
|
-
event:
|
|
585
|
+
event: eventName,
|
|
604
586
|
error: message,
|
|
605
587
|
stack,
|
|
606
588
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { theme } from "../modes/theme/theme";
|
|
3
|
-
import { expandPath } from "../tools/path-utils";
|
|
3
|
+
import { expandPath, normalizeLocalScheme } from "../tools/path-utils";
|
|
4
4
|
import type { HookUIContext } from "./hooks/types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -11,6 +11,12 @@ import type { HookUIContext } from "./hooks/types";
|
|
|
11
11
|
*/
|
|
12
12
|
export function resolvePath(filePath: string, cwd: string): string {
|
|
13
13
|
const expanded = expandPath(filePath);
|
|
14
|
+
const expandedAndNormalized = normalizeLocalScheme(expanded);
|
|
15
|
+
if (expandedAndNormalized.startsWith("local://")) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Path "${filePath}" uses internal scheme "local://" and must be resolved through the proper protocol handler, not as a filesystem path.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
14
20
|
if (path.isAbsolute(expanded)) {
|
|
15
21
|
return expanded;
|
|
16
22
|
}
|