@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.
Files changed (200) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/dist/types/cli/auth-gateway-cli.d.ts +1 -1
  3. package/dist/types/cli/file-processor.d.ts +1 -1
  4. package/dist/types/config/settings-schema.d.ts +45 -3
  5. package/dist/types/config/settings.d.ts +1 -1
  6. package/dist/types/debug/raw-sse.d.ts +2 -0
  7. package/dist/types/edit/file-read-cache.d.ts +15 -4
  8. package/dist/types/edit/index.d.ts +3 -8
  9. package/dist/types/edit/renderer.d.ts +1 -2
  10. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  11. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  12. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  13. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  14. package/dist/types/eval/py/executor.d.ts +1 -2
  15. package/dist/types/eval/py/kernel.d.ts +6 -0
  16. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  17. package/dist/types/eval/session-id.d.ts +3 -0
  18. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  19. package/dist/types/hashline/anchors.d.ts +15 -9
  20. package/dist/types/hashline/constants.d.ts +0 -2
  21. package/dist/types/hashline/diff.d.ts +1 -2
  22. package/dist/types/hashline/executor.d.ts +52 -0
  23. package/dist/types/hashline/hash.d.ts +44 -93
  24. package/dist/types/hashline/index.d.ts +2 -1
  25. package/dist/types/hashline/input.d.ts +2 -9
  26. package/dist/types/hashline/recovery.d.ts +3 -9
  27. package/dist/types/hashline/tokenizer.d.ts +91 -0
  28. package/dist/types/hashline/types.d.ts +5 -7
  29. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  30. package/dist/types/modes/types.d.ts +1 -0
  31. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  32. package/dist/types/sdk.d.ts +2 -0
  33. package/dist/types/session/agent-session.d.ts +11 -15
  34. package/dist/types/session/agent-storage.d.ts +11 -10
  35. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  36. package/dist/types/slash-commands/types.d.ts +0 -5
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/task/types.d.ts +8 -0
  39. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  40. package/dist/types/tools/index.d.ts +2 -8
  41. package/dist/types/tools/match-line-format.d.ts +4 -4
  42. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  43. package/dist/types/tools/review.d.ts +13 -0
  44. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  45. package/dist/types/tools/search.d.ts +4 -3
  46. package/dist/types/utils/edit-mode.d.ts +1 -1
  47. package/dist/types/web/kagi.d.ts +4 -2
  48. package/dist/types/web/parallel.d.ts +4 -3
  49. package/dist/types/web/scrapers/types.d.ts +2 -1
  50. package/dist/types/web/search/index.d.ts +12 -4
  51. package/dist/types/web/search/provider.d.ts +2 -1
  52. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  53. package/dist/types/web/search/providers/base.d.ts +34 -2
  54. package/dist/types/web/search/providers/brave.d.ts +8 -1
  55. package/dist/types/web/search/providers/codex.d.ts +13 -9
  56. package/dist/types/web/search/providers/exa.d.ts +10 -1
  57. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  58. package/dist/types/web/search/providers/jina.d.ts +2 -1
  59. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  60. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  61. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  62. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  63. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  64. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  65. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  66. package/dist/types/web/search/providers/utils.d.ts +8 -6
  67. package/dist/types/web/search/providers/zai.d.ts +12 -3
  68. package/package.json +7 -7
  69. package/src/cli/auth-gateway-cli.ts +71 -2
  70. package/src/cli/file-processor.ts +12 -2
  71. package/src/cli.ts +0 -8
  72. package/src/commands/auth-gateway.ts +2 -0
  73. package/src/commands/commit.ts +8 -8
  74. package/src/config/prompt-templates.ts +6 -6
  75. package/src/config/settings-schema.ts +47 -3
  76. package/src/config/settings.ts +5 -5
  77. package/src/debug/raw-sse.ts +68 -3
  78. package/src/edit/file-read-cache.ts +68 -25
  79. package/src/edit/index.ts +6 -37
  80. package/src/edit/renderer.ts +9 -47
  81. package/src/edit/streaming.ts +43 -56
  82. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  83. package/src/eval/js/context-manager.ts +64 -53
  84. package/src/eval/js/shared/local-module-loader.ts +265 -0
  85. package/src/eval/js/shared/prelude.txt +4 -0
  86. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  87. package/src/eval/js/shared/runtime.ts +129 -86
  88. package/src/eval/js/worker-core.ts +23 -38
  89. package/src/eval/py/executor.ts +155 -84
  90. package/src/eval/py/kernel.ts +10 -1
  91. package/src/eval/py/prelude.py +22 -24
  92. package/src/eval/py/runner.py +203 -85
  93. package/src/eval/py/tool-bridge.ts +17 -10
  94. package/src/eval/session-id.ts +8 -0
  95. package/src/exec/bash-executor.ts +27 -16
  96. package/src/extensibility/extensions/runner.ts +0 -1
  97. package/src/extensibility/extensions/types.ts +1 -3
  98. package/src/extensibility/plugins/marketplace/manager.ts +20 -1
  99. package/src/hashline/anchors.ts +56 -65
  100. package/src/hashline/apply.ts +29 -31
  101. package/src/hashline/constants.ts +0 -3
  102. package/src/hashline/diff-preview.ts +4 -5
  103. package/src/hashline/diff.ts +30 -4
  104. package/src/hashline/execute.ts +91 -26
  105. package/src/hashline/executor.ts +239 -0
  106. package/src/hashline/grammar.lark +12 -10
  107. package/src/hashline/hash.ts +69 -114
  108. package/src/hashline/index.ts +2 -1
  109. package/src/hashline/input.ts +48 -41
  110. package/src/hashline/prefixes.ts +21 -11
  111. package/src/hashline/recovery.ts +63 -71
  112. package/src/hashline/stream.ts +2 -2
  113. package/src/hashline/tokenizer.ts +467 -0
  114. package/src/hashline/types.ts +6 -8
  115. package/src/internal-urls/docs-index.generated.ts +9 -8
  116. package/src/lsp/config.ts +87 -22
  117. package/src/modes/components/extensions/types.ts +0 -5
  118. package/src/modes/components/session-observer-overlay.ts +11 -2
  119. package/src/modes/components/tree-selector.ts +10 -2
  120. package/src/modes/controllers/command-controller.ts +1 -3
  121. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  122. package/src/modes/controllers/selector-controller.ts +5 -5
  123. package/src/modes/types.ts +4 -1
  124. package/src/modes/utils/ui-helpers.ts +4 -0
  125. package/src/prompts/agents/explore.md +1 -1
  126. package/src/prompts/tools/ast-edit.md +1 -1
  127. package/src/prompts/tools/ast-grep.md +1 -1
  128. package/src/prompts/tools/eval.md +1 -1
  129. package/src/prompts/tools/hashline.md +73 -94
  130. package/src/prompts/tools/read.md +4 -4
  131. package/src/prompts/tools/search.md +3 -3
  132. package/src/sdk.ts +21 -24
  133. package/src/session/agent-session.ts +59 -66
  134. package/src/session/agent-storage.ts +13 -14
  135. package/src/slash-commands/acp-builtins.ts +3 -3
  136. package/src/slash-commands/types.ts +0 -6
  137. package/src/task/executor.ts +55 -57
  138. package/src/task/index.ts +8 -4
  139. package/src/task/render.ts +53 -1
  140. package/src/task/types.ts +8 -0
  141. package/src/tool-discovery/tool-index.ts +0 -134
  142. package/src/tools/ast-edit.ts +36 -13
  143. package/src/tools/ast-grep.ts +45 -4
  144. package/src/tools/browser/tab-worker.ts +3 -2
  145. package/src/tools/eval.ts +2 -1
  146. package/src/tools/fetch.ts +23 -14
  147. package/src/tools/index.ts +2 -8
  148. package/src/tools/irc.ts +59 -5
  149. package/src/tools/jtd-to-json-schema.ts +5 -1
  150. package/src/tools/match-line-format.ts +5 -7
  151. package/src/tools/output-schema-validator.ts +132 -0
  152. package/src/tools/read.ts +142 -63
  153. package/src/tools/review.ts +23 -0
  154. package/src/tools/search-tool-bm25.ts +3 -30
  155. package/src/tools/search.ts +48 -16
  156. package/src/tools/write.ts +3 -3
  157. package/src/tools/yield.ts +32 -41
  158. package/src/utils/edit-mode.ts +1 -2
  159. package/src/utils/file-mentions.ts +2 -2
  160. package/src/web/kagi.ts +15 -6
  161. package/src/web/parallel.ts +9 -6
  162. package/src/web/scrapers/types.ts +7 -1
  163. package/src/web/scrapers/youtube.ts +13 -7
  164. package/src/web/search/index.ts +37 -11
  165. package/src/web/search/provider.ts +5 -3
  166. package/src/web/search/providers/anthropic.ts +30 -21
  167. package/src/web/search/providers/base.ts +35 -2
  168. package/src/web/search/providers/brave.ts +4 -4
  169. package/src/web/search/providers/codex.ts +118 -89
  170. package/src/web/search/providers/exa.ts +3 -2
  171. package/src/web/search/providers/gemini.ts +58 -155
  172. package/src/web/search/providers/jina.ts +4 -4
  173. package/src/web/search/providers/kagi.ts +17 -11
  174. package/src/web/search/providers/kimi.ts +29 -13
  175. package/src/web/search/providers/parallel.ts +171 -23
  176. package/src/web/search/providers/perplexity.ts +38 -37
  177. package/src/web/search/providers/searxng.ts +3 -1
  178. package/src/web/search/providers/synthetic.ts +16 -19
  179. package/src/web/search/providers/tavily.ts +23 -18
  180. package/src/web/search/providers/utils.ts +11 -17
  181. package/src/web/search/providers/zai.ts +16 -8
  182. package/dist/types/hashline/parser.d.ts +0 -7
  183. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  184. package/dist/types/tools/vim.d.ts +0 -58
  185. package/dist/types/vim/buffer.d.ts +0 -41
  186. package/dist/types/vim/commands.d.ts +0 -6
  187. package/dist/types/vim/engine.d.ts +0 -47
  188. package/dist/types/vim/parser.d.ts +0 -3
  189. package/dist/types/vim/render.d.ts +0 -25
  190. package/dist/types/vim/types.d.ts +0 -182
  191. package/src/hashline/parser.ts +0 -212
  192. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  193. package/src/prompts/tools/vim.md +0 -98
  194. package/src/tools/vim.ts +0 -949
  195. package/src/vim/buffer.ts +0 -309
  196. package/src/vim/commands.ts +0 -382
  197. package/src/vim/engine.ts +0 -2409
  198. package/src/vim/parser.ts +0 -134
  199. package/src/vim/render.ts +0 -252
  200. 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; the cache always reflects what *this* session most recently saw,
15
- * so it stays correct by construction even when this session writes the
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(absPath: string, startLine: number, lines: readonly string[]): void {
39
- if (lines.length === 0) return;
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(absPath: string, entries: Iterable<readonly [number, string]>): void {
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(absPath: string, entries: ReadonlyArray<readonly [number, string]>): void {
62
- const existing = this.#snapshots.get(absPath);
63
- if (existing && hasConflict(existing.lines, entries)) {
64
- // File content has changed since we last recorded. Drop the stale
65
- // snapshot and start fresh with whatever we just observed.
66
- this.#snapshots.set(absPath, { lines: new Map(entries), recordedAt: Date.now() });
67
- return;
68
- }
69
- if (existing) {
70
- for (const [lineNum, content] of entries) existing.lines.set(lineNum, content);
71
- existing.recordedAt = Date.now();
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
- this.#snapshots.set(absPath, { lines: new Map(entries), recordedAt: Date.now() });
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 the `$HFMT$`, `$HOP_*$`, `$HOP_CHARS$`, and `$HFILE$` placeholders in the hashline Lark grammar.
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 VimParams = z.infer<typeof vimSchema>;
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<EditToolResultDetails, TInput>) => void,
69
- ) => Promise<AgentToolResult<EditToolResultDetails, TInput>>;
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<EditToolResultDetails, TInput>,
336
+ onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>,
345
337
  context?: AgentToolContext,
346
- ): Promise<AgentToolResult<EditToolResultDetails, TInput>> {
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
 
@@ -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
- if (trimmed.length < 2) return trimmed;
336
- const first = trimmed[0];
337
- const last = trimmed[trimmed.length - 1];
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 trimmed.slice(1, -1);
314
+ return withoutHash.slice(1, -1);
340
315
  }
341
- return trimmed;
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
- // PATH` headers and stray `§§ PATH` / `§§§PATH` runs render clean paths.
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 | VimRenderArgs,
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)) {
@@ -22,8 +22,7 @@ import {
22
22
  containsRecognizableHashlineOperations,
23
23
  END_PATCH_MARKER,
24
24
  type HashlineInputSection,
25
- HL_FILE_PREFIX,
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
- function isHashlineHeaderLine(line: string): boolean {
82
- return line.trimEnd().startsWith(HL_FILE_PREFIX);
83
- }
84
-
85
- function parseHashlineHeaderPath(line: string): string {
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].trimEnd() === BEGIN_PATCH_MARKER) {
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 && isHashlineHeaderLine(lines[index])) {
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 => !isHashlineEnvelopeMarkerLine(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 = (path: string): string[] => {
388
- let bucket = groups.get(path);
370
+ const ensure = (sectionPath: string): string[] => {
371
+ let bucket = groups.get(sectionPath);
389
372
  if (!bucket) {
390
373
  bucket = [];
391
- groups.set(path, bucket);
374
+ groups.set(sectionPath, bucket);
392
375
  }
393
376
  return bucket;
394
377
  };
395
- for (const raw of lines) {
396
- if (isHashlineEnvelopeMarkerLine(raw)) continue;
397
- if (isHashlineHeaderLine(raw)) {
398
- currentPath = parseHashlineHeaderPath(raw);
399
- if (currentPath) ensure(currentPath);
400
- continue;
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 [path, body] of groups) {
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 PATH` yet" case.
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 };