@oh-my-pi/pi-coding-agent 15.3.1 → 15.4.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 +119 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +1 -1
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/auth-gateway-cli.ts +71 -2
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/auth-gateway.ts +2 -0
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/extensibility/plugins/marketplace/manager.ts +20 -1
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +87 -22
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +21 -24
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +55 -57
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +53 -1
- package/src/task/types.ts +8 -0
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/jtd-to-json-schema.ts +5 -1
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -63
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -212
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
|
@@ -11,68 +11,101 @@
|
|
|
11
11
|
* Scoped per `ToolSession`: the cache lives on the session object itself, so
|
|
12
12
|
* different sessions never share snapshots and entries get reclaimed when
|
|
13
13
|
* the session goes out of scope. Each session keeps a small LRU window of
|
|
14
|
-
* paths;
|
|
15
|
-
*
|
|
16
|
-
* file itself — the next read after the write refreshes the entry.
|
|
14
|
+
* paths; each path keeps a short ring of recent snapshots so follow-up edits
|
|
15
|
+
* can recover from the agent's own prior writes as well as stale reads.
|
|
17
16
|
*/
|
|
18
17
|
import { LRUCache } from "lru-cache/raw";
|
|
19
18
|
import type { ToolSession } from "../tools";
|
|
20
19
|
|
|
21
20
|
const MAX_PATHS_PER_SESSION = 30;
|
|
21
|
+
const MAX_SNAPSHOTS_PER_PATH = 4;
|
|
22
22
|
|
|
23
23
|
export interface FileReadSnapshot {
|
|
24
24
|
/** 1-indexed line number → exact line content as observed by `read`/`search`. */
|
|
25
25
|
lines: Map<number, string>;
|
|
26
|
+
/** Full normalized text when the read path observed the whole file. */
|
|
27
|
+
fullText?: string;
|
|
28
|
+
/** 4-hex hash of `fullText`, or a sparse snapshot hash supplied by search. */
|
|
29
|
+
fileHash?: string;
|
|
26
30
|
recordedAt: number;
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
interface FileReadSnapshotMetadata {
|
|
34
|
+
fullText?: string;
|
|
35
|
+
fileHash?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
export class FileReadCache {
|
|
30
|
-
#snapshots = new LRUCache<string, FileReadSnapshot>({ max: MAX_PATHS_PER_SESSION });
|
|
39
|
+
#snapshots = new LRUCache<string, FileReadSnapshot[]>({ max: MAX_PATHS_PER_SESSION });
|
|
31
40
|
|
|
32
41
|
/** Look up the most recent snapshot for `absPath`, or `null` if absent. */
|
|
33
42
|
get(absPath: string): FileReadSnapshot | null {
|
|
34
|
-
return this.#snapshots.get(absPath) ?? null;
|
|
43
|
+
return this.#snapshots.get(absPath)?.[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Look up the most recent snapshot for `absPath` whose file hash matches. */
|
|
47
|
+
getByHash(absPath: string, fileHash: string): FileReadSnapshot | null {
|
|
48
|
+
const history = this.#snapshots.get(absPath);
|
|
49
|
+
return history?.find(snapshot => snapshot.fileHash === fileHash) ?? null;
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
/** Record a contiguous run of lines (e.g. from a `read` tool). `startLine` is 1-indexed. */
|
|
38
|
-
recordContiguous(
|
|
39
|
-
|
|
53
|
+
recordContiguous(
|
|
54
|
+
absPath: string,
|
|
55
|
+
startLine: number,
|
|
56
|
+
lines: readonly string[],
|
|
57
|
+
metadata: FileReadSnapshotMetadata = {},
|
|
58
|
+
): void {
|
|
59
|
+
if (lines.length === 0 && metadata.fullText === undefined) return;
|
|
40
60
|
const entries: Array<readonly [number, string]> = lines.map((line, idx) => [startLine + idx, line] as const);
|
|
41
|
-
this.#record(absPath, entries);
|
|
61
|
+
this.#record(absPath, entries, metadata);
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
/** Record sparse `(lineNumber, content)` pairs (e.g. `search` matches plus context). */
|
|
45
|
-
recordSparse(
|
|
65
|
+
recordSparse(
|
|
66
|
+
absPath: string,
|
|
67
|
+
entries: Iterable<readonly [number, string]>,
|
|
68
|
+
metadata: FileReadSnapshotMetadata = {},
|
|
69
|
+
): void {
|
|
46
70
|
const arr = Array.from(entries);
|
|
47
|
-
if (arr.length === 0) return;
|
|
48
|
-
this.#record(absPath, arr);
|
|
71
|
+
if (arr.length === 0 && metadata.fullText === undefined) return;
|
|
72
|
+
this.#record(absPath, arr, metadata);
|
|
49
73
|
}
|
|
50
74
|
|
|
51
|
-
/** Drop the snapshot for a single path. */
|
|
75
|
+
/** Drop the snapshot history for a single path. */
|
|
52
76
|
invalidate(absPath: string): void {
|
|
53
77
|
this.#snapshots.delete(absPath);
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
/** Drop every snapshot. */
|
|
80
|
+
/** Drop every snapshot history. */
|
|
57
81
|
clear(): void {
|
|
58
82
|
this.#snapshots.clear();
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
#record(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
for (const [lineNum, content] of entries)
|
|
71
|
-
|
|
85
|
+
#record(
|
|
86
|
+
absPath: string,
|
|
87
|
+
entries: ReadonlyArray<readonly [number, string]>,
|
|
88
|
+
metadata: FileReadSnapshotMetadata,
|
|
89
|
+
): void {
|
|
90
|
+
const history = this.#snapshots.get(absPath) ?? [];
|
|
91
|
+
const head = history[0];
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (head && !hasConflict(head.lines, entries) && !hasHashConflict(head, metadata)) {
|
|
94
|
+
for (const [lineNum, content] of entries) head.lines.set(lineNum, content);
|
|
95
|
+
if (metadata.fullText !== undefined) head.fullText = metadata.fullText;
|
|
96
|
+
if (metadata.fileHash !== undefined) head.fileHash = metadata.fileHash;
|
|
97
|
+
head.recordedAt = now;
|
|
72
98
|
// `get` above already touched LRU recency for this key.
|
|
73
99
|
return;
|
|
74
100
|
}
|
|
75
|
-
|
|
101
|
+
|
|
102
|
+
const nextSnapshot: FileReadSnapshot = {
|
|
103
|
+
lines: new Map(entries),
|
|
104
|
+
...metadata,
|
|
105
|
+
recordedAt: now,
|
|
106
|
+
};
|
|
107
|
+
const dedupedHistory = history.filter(snapshot => !isSameSnapshotIdentity(snapshot, nextSnapshot));
|
|
108
|
+
this.#snapshots.set(absPath, [nextSnapshot, ...dedupedHistory].slice(0, MAX_SNAPSHOTS_PER_PATH));
|
|
76
109
|
}
|
|
77
110
|
}
|
|
78
111
|
|
|
@@ -84,6 +117,16 @@ function hasConflict(existing: Map<number, string>, incoming: ReadonlyArray<read
|
|
|
84
117
|
return false;
|
|
85
118
|
}
|
|
86
119
|
|
|
120
|
+
function hasHashConflict(existing: FileReadSnapshot, metadata: FileReadSnapshotMetadata): boolean {
|
|
121
|
+
return metadata.fileHash !== undefined && existing.fileHash !== undefined && metadata.fileHash !== existing.fileHash;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isSameSnapshotIdentity(left: FileReadSnapshot, right: FileReadSnapshot): boolean {
|
|
125
|
+
if (left.fileHash !== undefined && right.fileHash !== undefined) return left.fileHash === right.fileHash;
|
|
126
|
+
if (left.fullText !== undefined && right.fullText !== undefined) return left.fullText === right.fullText;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
87
130
|
/**
|
|
88
131
|
* Look up (or lazily create) the file-read cache attached to a session. The
|
|
89
132
|
* cache is stored as `session.fileReadCache` so it lives exactly as long as
|
package/src/edit/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import type * as z from "zod/v4";
|
|
4
3
|
import {
|
|
5
4
|
executeHashlineSingle,
|
|
6
5
|
HashlineMismatchError,
|
|
@@ -21,9 +20,7 @@ import hashlineDescription from "../prompts/tools/hashline.md" with { type: "tex
|
|
|
21
20
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
22
21
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
23
22
|
import type { ToolSession } from "../tools";
|
|
24
|
-
import { VimTool, vimSchema } from "../tools/vim";
|
|
25
23
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
26
|
-
import type { VimToolDetails } from "../vim/types";
|
|
27
24
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
28
25
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
29
26
|
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
@@ -35,7 +32,7 @@ export * from "./apply-patch";
|
|
|
35
32
|
export * from "./diff";
|
|
36
33
|
export * from "./file-read-cache";
|
|
37
34
|
|
|
38
|
-
// Resolve
|
|
35
|
+
// Resolve hashline grammar placeholders from the TypeScript constants.
|
|
39
36
|
const hashlineGrammar = resolveHashlineGrammarPlaceholders(hashlineGrammarTemplate);
|
|
40
37
|
|
|
41
38
|
export * from "../hashline";
|
|
@@ -50,12 +47,9 @@ type TInput =
|
|
|
50
47
|
| typeof replaceEditSchema
|
|
51
48
|
| typeof patchEditSchema
|
|
52
49
|
| typeof hashlineEditParamsSchema
|
|
53
|
-
| typeof vimSchema
|
|
54
50
|
| typeof applyPatchSchema;
|
|
55
51
|
|
|
56
|
-
type
|
|
57
|
-
type EditParams = ReplaceParams | PatchParams | HashlineParams | VimParams | ApplyPatchParams;
|
|
58
|
-
type EditToolResultDetails = EditToolDetails | VimToolDetails;
|
|
52
|
+
type EditParams = ReplaceParams | PatchParams | HashlineParams | ApplyPatchParams;
|
|
59
53
|
|
|
60
54
|
type EditModeDefinition = {
|
|
61
55
|
description: (session: ToolSession) => string;
|
|
@@ -65,8 +59,8 @@ type EditModeDefinition = {
|
|
|
65
59
|
params: EditParams,
|
|
66
60
|
signal: AbortSignal | undefined,
|
|
67
61
|
batchRequest: LspBatchRequest | undefined,
|
|
68
|
-
onUpdate?: (partialResult: AgentToolResult<
|
|
69
|
-
) => Promise<AgentToolResult<
|
|
62
|
+
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
63
|
+
) => Promise<AgentToolResult<EditToolDetails, TInput>>;
|
|
70
64
|
};
|
|
71
65
|
|
|
72
66
|
function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
|
|
@@ -284,7 +278,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
284
278
|
readonly #fuzzyThreshold: number;
|
|
285
279
|
readonly #writethrough: WritethroughCallback;
|
|
286
280
|
readonly #editMode?: EditMode;
|
|
287
|
-
readonly #vimTool: VimTool;
|
|
288
281
|
readonly #pendingDeferredFetches = new Map<string, AbortController>();
|
|
289
282
|
|
|
290
283
|
constructor(private readonly session: ToolSession) {
|
|
@@ -298,7 +291,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
298
291
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
|
299
292
|
this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
|
|
300
293
|
this.#writethrough = createEditWritethrough(session);
|
|
301
|
-
this.#vimTool = new VimTool(session);
|
|
302
294
|
}
|
|
303
295
|
|
|
304
296
|
get mode(): EditMode {
|
|
@@ -341,9 +333,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
341
333
|
_toolCallId: string,
|
|
342
334
|
params: EditParams,
|
|
343
335
|
signal?: AbortSignal,
|
|
344
|
-
onUpdate?: AgentToolUpdateCallback<
|
|
336
|
+
onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
|
|
345
337
|
context?: AgentToolContext,
|
|
346
|
-
): Promise<AgentToolResult<
|
|
338
|
+
): Promise<AgentToolResult<EditToolDetails, TInput>> {
|
|
347
339
|
const modeDefinition = this.#getModeDefinition();
|
|
348
340
|
return modeDefinition.execute(this, params, signal, getLspBatchRequest(context?.toolCall), onUpdate);
|
|
349
341
|
}
|
|
@@ -460,29 +452,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
460
452
|
return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
|
|
461
453
|
},
|
|
462
454
|
},
|
|
463
|
-
vim: {
|
|
464
|
-
description: () => this.#vimTool.description,
|
|
465
|
-
parameters: vimSchema,
|
|
466
|
-
execute: async (
|
|
467
|
-
tool: EditTool,
|
|
468
|
-
params: EditParams,
|
|
469
|
-
signal: AbortSignal | undefined,
|
|
470
|
-
_batchRequest: LspBatchRequest | undefined,
|
|
471
|
-
onUpdate?: (partialResult: AgentToolResult<EditToolResultDetails, TInput>) => void,
|
|
472
|
-
) => {
|
|
473
|
-
const handleUpdate = onUpdate
|
|
474
|
-
? (partialResult: AgentToolResult<VimToolDetails>) => {
|
|
475
|
-
onUpdate(partialResult as AgentToolResult<EditToolResultDetails, TInput>);
|
|
476
|
-
}
|
|
477
|
-
: undefined;
|
|
478
|
-
return (await tool.#vimTool.execute(
|
|
479
|
-
"edit",
|
|
480
|
-
params as VimParams,
|
|
481
|
-
signal,
|
|
482
|
-
handleUpdate,
|
|
483
|
-
)) as AgentToolResult<EditToolResultDetails, TInput>;
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
455
|
}[this.mode];
|
|
487
456
|
}
|
|
488
457
|
|
package/src/edit/renderer.ts
CHANGED
|
@@ -25,10 +25,8 @@ import {
|
|
|
25
25
|
shortenPath,
|
|
26
26
|
truncateDiffByHunk,
|
|
27
27
|
} from "../tools/render-utils";
|
|
28
|
-
import { type VimRenderArgs, vimToolRenderer } from "../tools/vim";
|
|
29
28
|
import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
30
29
|
import type { EditMode } from "../utils/edit-mode";
|
|
31
|
-
import type { VimToolDetails } from "../vim/types";
|
|
32
30
|
import type { DiffError, DiffResult } from "./diff";
|
|
33
31
|
import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
34
32
|
import type { Operation } from "./modes/patch";
|
|
@@ -127,31 +125,6 @@ interface ApplyPatchRenderSummary {
|
|
|
127
125
|
error?: string;
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
function isVimRenderArgs(args: EditRenderArgs | VimRenderArgs): args is VimRenderArgs {
|
|
131
|
-
return (
|
|
132
|
-
typeof args === "object" &&
|
|
133
|
-
args !== null &&
|
|
134
|
-
typeof (args as { file?: unknown }).file === "string" &&
|
|
135
|
-
!("path" in args) &&
|
|
136
|
-
!("file_path" in args) &&
|
|
137
|
-
!("edits" in args)
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function isVimToolDetails(details: unknown): details is VimToolDetails {
|
|
142
|
-
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
const cursor = (details as { cursor?: unknown }).cursor;
|
|
146
|
-
const viewportLines = (details as { viewportLines?: unknown }).viewportLines;
|
|
147
|
-
return (
|
|
148
|
-
typeof (details as { file?: unknown }).file === "string" &&
|
|
149
|
-
typeof cursor === "object" &&
|
|
150
|
-
cursor !== null &&
|
|
151
|
-
Array.isArray(viewportLines)
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
128
|
/** Extended context for edit tool rendering */
|
|
156
129
|
export interface EditRenderContext {
|
|
157
130
|
/** Edit mode resolved by the caller; lets the renderer dispatch without shape-sniffing */
|
|
@@ -332,19 +305,21 @@ const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** E
|
|
|
332
305
|
|
|
333
306
|
function normalizeHashlineInputPreviewPath(rawPath: string): string {
|
|
334
307
|
const trimmed = rawPath.trim();
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
308
|
+
const hashStart = /#[0-9a-f]{4}$/u.exec(trimmed)?.index;
|
|
309
|
+
const withoutHash = hashStart === undefined ? trimmed : trimmed.slice(0, hashStart);
|
|
310
|
+
if (withoutHash.length < 2) return withoutHash;
|
|
311
|
+
const first = withoutHash[0];
|
|
312
|
+
const last = withoutHash[withoutHash.length - 1];
|
|
338
313
|
if ((first === '"' || first === "'") && first === last) {
|
|
339
|
-
return
|
|
314
|
+
return withoutHash.slice(1, -1);
|
|
340
315
|
}
|
|
341
|
-
return
|
|
316
|
+
return withoutHash;
|
|
342
317
|
}
|
|
343
318
|
|
|
344
319
|
function parseHashlineInputPreviewHeader(line: string): string | null {
|
|
345
320
|
if (!line.startsWith(HL_FILE_PREFIX)) return null;
|
|
346
321
|
// Mirror hashline/input.ts: strip every leading file marker so canonical
|
|
347
|
-
//
|
|
322
|
+
// `¶ PATH` headers and stray `¶¶ PATH` / `¶¶¶PATH` runs render clean paths.
|
|
348
323
|
let prefixEnd = 0;
|
|
349
324
|
while (prefixEnd < line.length && line[prefixEnd] === HL_FILE_PREFIX) prefixEnd++;
|
|
350
325
|
const body = line.slice(prefixEnd).trim();
|
|
@@ -460,16 +435,11 @@ export const editToolRenderer = {
|
|
|
460
435
|
mergeCallAndResult: true,
|
|
461
436
|
|
|
462
437
|
renderCall(
|
|
463
|
-
args: EditRenderArgs
|
|
438
|
+
args: EditRenderArgs,
|
|
464
439
|
options: RenderResultOptions & { renderContext?: EditRenderContext },
|
|
465
440
|
uiTheme: Theme,
|
|
466
441
|
): Component {
|
|
467
442
|
const renderContext = options.renderContext;
|
|
468
|
-
// Dispatch on the explicit editMode when available; fall back to the
|
|
469
|
-
// shape probe for legacy call sites that don't thread renderContext.
|
|
470
|
-
if (renderContext?.editMode === "vim" || isVimRenderArgs(args)) {
|
|
471
|
-
return vimToolRenderer.renderCall(args as VimRenderArgs, options, uiTheme);
|
|
472
|
-
}
|
|
473
443
|
|
|
474
444
|
const editArgs = args as EditRenderArgs;
|
|
475
445
|
const hashlineInputSummary = getHashlineInputRenderSummary(editArgs, renderContext?.editMode);
|
|
@@ -514,14 +484,6 @@ export const editToolRenderer = {
|
|
|
514
484
|
uiTheme: Theme,
|
|
515
485
|
args?: EditRenderArgs,
|
|
516
486
|
): Component {
|
|
517
|
-
if (options.renderContext?.editMode === "vim" || isVimToolDetails(result.details)) {
|
|
518
|
-
return vimToolRenderer.renderResult(
|
|
519
|
-
result as { content: Array<{ type: string; text?: string }>; details?: VimToolDetails; isError?: boolean },
|
|
520
|
-
options,
|
|
521
|
-
uiTheme,
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
487
|
const perFileResults = result.details?.perFileResults;
|
|
526
488
|
const totalFiles = args?.edits ? countEditFiles(args.edits) : 0;
|
|
527
489
|
if (perFileResults && (perFileResults.length > 1 || totalFiles > 1)) {
|
package/src/edit/streaming.ts
CHANGED
|
@@ -22,8 +22,7 @@ import {
|
|
|
22
22
|
containsRecognizableHashlineOperations,
|
|
23
23
|
END_PATCH_MARKER,
|
|
24
24
|
type HashlineInputSection,
|
|
25
|
-
|
|
26
|
-
HL_OP_CHARS,
|
|
25
|
+
HashlineTokenizer,
|
|
27
26
|
splitHashlineInputs,
|
|
28
27
|
} from "../hashline";
|
|
29
28
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -78,40 +77,25 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
78
77
|
const STREAMING_FALLBACK_LINES = 12;
|
|
79
78
|
const STREAMING_FALLBACK_WIDTH = 80;
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const trimmed = line.trimEnd();
|
|
87
|
-
let prefixEnd = 0;
|
|
88
|
-
while (prefixEnd < trimmed.length && trimmed[prefixEnd] === HL_FILE_PREFIX) prefixEnd++;
|
|
89
|
-
return trimmed.slice(prefixEnd).trim();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function isHashlineOpLine(line: string): boolean {
|
|
93
|
-
const first = line[0];
|
|
94
|
-
return first !== undefined && HL_OP_CHARS.includes(first);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isHashlineEnvelopeMarkerLine(line: string): boolean {
|
|
98
|
-
const trimmed = line.trimEnd();
|
|
99
|
-
return trimmed === BEGIN_PATCH_MARKER || trimmed === END_PATCH_MARKER || trimmed === ABORT_MARKER;
|
|
100
|
-
}
|
|
80
|
+
// Streaming-preview classification reuses one tokenizer instance for the
|
|
81
|
+
// stateless predicates and `tokenize`/`tokenizeAll` helpers; instances are
|
|
82
|
+
// cheap, but keeping a single module-level reference matches the rest of
|
|
83
|
+
// the hashline package.
|
|
84
|
+
const HASHLINE_TOKENIZER = new HashlineTokenizer();
|
|
101
85
|
|
|
102
86
|
function trimHashlineStreamingSyntax(lines: string[]): string[] {
|
|
103
87
|
let index = lines.findIndex(line => line.trim().length > 0);
|
|
104
88
|
if (index === -1) return [];
|
|
105
89
|
|
|
106
|
-
if (lines[index].
|
|
90
|
+
if (HASHLINE_TOKENIZER.tokenize(lines[index]).kind === "envelope-begin") {
|
|
107
91
|
index++;
|
|
108
92
|
while (index < lines.length && lines[index].trim().length === 0) index++;
|
|
109
93
|
}
|
|
110
|
-
if (index < lines.length &&
|
|
94
|
+
if (index < lines.length && HASHLINE_TOKENIZER.tokenize(lines[index]).kind === "header") {
|
|
111
95
|
index++;
|
|
112
96
|
}
|
|
113
97
|
|
|
114
|
-
return lines.slice(index).filter(line => !
|
|
98
|
+
return lines.slice(index).filter(line => !HASHLINE_TOKENIZER.isEnvelopeMarker(line));
|
|
115
99
|
}
|
|
116
100
|
|
|
117
101
|
function renderHashlineInputFallback(input: string, uiTheme: Theme): string {
|
|
@@ -381,32 +365,51 @@ function buildHashlineNaturalOrderPreviews(
|
|
|
381
365
|
input: string,
|
|
382
366
|
defaultPath: string | undefined,
|
|
383
367
|
): PerFileDiffPreview[] | null {
|
|
384
|
-
const lines = input.split("\n");
|
|
385
368
|
const groups = new Map<string, string[]>();
|
|
386
369
|
let currentPath = defaultPath ?? "";
|
|
387
|
-
const ensure = (
|
|
388
|
-
let bucket = groups.get(
|
|
370
|
+
const ensure = (sectionPath: string): string[] => {
|
|
371
|
+
let bucket = groups.get(sectionPath);
|
|
389
372
|
if (!bucket) {
|
|
390
373
|
bucket = [];
|
|
391
|
-
groups.set(
|
|
374
|
+
groups.set(sectionPath, bucket);
|
|
392
375
|
}
|
|
393
376
|
return bucket;
|
|
394
377
|
};
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
378
|
+
|
|
379
|
+
// Per-call instance: the streaming preview re-runs each tick with the
|
|
380
|
+
// cumulative input, and we need the line counter to start at 1. A
|
|
381
|
+
// dedicated tokenizer keeps the shared HASHLINE_TOKENIZER above free
|
|
382
|
+
// for stateless predicate use elsewhere in this module.
|
|
383
|
+
const streamer = new HashlineTokenizer();
|
|
384
|
+
for (const token of streamer.tokenizeAll(input)) {
|
|
385
|
+
switch (token.kind) {
|
|
386
|
+
case "envelope-begin":
|
|
387
|
+
case "envelope-end":
|
|
388
|
+
case "abort":
|
|
389
|
+
case "op-insert":
|
|
390
|
+
case "op-replace":
|
|
391
|
+
case "op-delete":
|
|
392
|
+
continue;
|
|
393
|
+
case "header":
|
|
394
|
+
currentPath = token.path;
|
|
395
|
+
if (currentPath) ensure(currentPath);
|
|
396
|
+
continue;
|
|
397
|
+
case "blank":
|
|
398
|
+
if (!currentPath) continue;
|
|
399
|
+
ensure(currentPath).push("+");
|
|
400
|
+
continue;
|
|
401
|
+
case "payload":
|
|
402
|
+
if (!currentPath) continue;
|
|
403
|
+
ensure(currentPath).push(`+${token.text}`);
|
|
404
|
+
continue;
|
|
401
405
|
}
|
|
402
|
-
if (isHashlineOpLine(raw) || !currentPath) continue;
|
|
403
|
-
ensure(currentPath).push(`+${raw}`);
|
|
404
406
|
}
|
|
407
|
+
|
|
405
408
|
if (groups.size === 0) return null;
|
|
406
409
|
const previews: PerFileDiffPreview[] = [];
|
|
407
|
-
for (const [
|
|
410
|
+
for (const [sectionPath, body] of groups) {
|
|
408
411
|
if (body.length === 0) continue;
|
|
409
|
-
previews.push({ path, diff: body.join("\n") });
|
|
412
|
+
previews.push({ path: sectionPath, diff: body.join("\n") });
|
|
410
413
|
}
|
|
411
414
|
return previews.length > 0 ? previews : null;
|
|
412
415
|
}
|
|
@@ -431,7 +434,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
431
434
|
sections = splitHashlineInputs(input, { cwd: ctx.cwd, path: args.path });
|
|
432
435
|
} catch {
|
|
433
436
|
// Single-section fallback keeps the original error rendering for the
|
|
434
|
-
// "haven't typed
|
|
437
|
+
// "haven't typed `¶ PATH` yet" case.
|
|
435
438
|
const result = await computeHashlineDiff({ input, path: args.path }, ctx.cwd, {
|
|
436
439
|
autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
|
|
437
440
|
});
|
|
@@ -524,27 +527,11 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
|
|
|
524
527
|
return "";
|
|
525
528
|
},
|
|
526
529
|
};
|
|
527
|
-
|
|
528
|
-
// Vim streaming preview is handled by the existing vimToolRenderer inside
|
|
529
|
-
// edit/renderer.ts. The strategy here is a no-op so the registry is total.
|
|
530
|
-
const vimStrategy: EditStreamingStrategy<unknown> = {
|
|
531
|
-
extractCompleteEdits(args) {
|
|
532
|
-
return args;
|
|
533
|
-
},
|
|
534
|
-
async computeDiffPreview() {
|
|
535
|
-
return null;
|
|
536
|
-
},
|
|
537
|
-
renderStreamingFallback() {
|
|
538
|
-
return "";
|
|
539
|
-
},
|
|
540
|
-
};
|
|
541
|
-
|
|
542
530
|
export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknown>> = {
|
|
543
531
|
replace: replaceStrategy as EditStreamingStrategy<unknown>,
|
|
544
532
|
patch: patchStrategy as EditStreamingStrategy<unknown>,
|
|
545
533
|
hashline: hashlineStrategy as EditStreamingStrategy<unknown>,
|
|
546
534
|
apply_patch: applyPatchStrategy as EditStreamingStrategy<unknown>,
|
|
547
|
-
vim: vimStrategy,
|
|
548
535
|
};
|
|
549
536
|
|
|
550
537
|
export { resolveEditMode };
|