@oh-my-pi/pi-coding-agent 14.5.2 → 14.5.5

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 (69) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/sdk/README.md +1 -1
  4. package/package.json +7 -7
  5. package/src/config/prompt-templates.ts +104 -6
  6. package/src/config/settings-schema.ts +14 -13
  7. package/src/config/settings.ts +1 -1
  8. package/src/cursor.ts +4 -4
  9. package/src/edit/index.ts +111 -109
  10. package/src/edit/line-hash.ts +33 -3
  11. package/src/edit/modes/apply-patch.ts +6 -4
  12. package/src/edit/modes/atom.lark +27 -0
  13. package/src/edit/modes/atom.ts +1094 -642
  14. package/src/edit/modes/hashline.ts +9 -10
  15. package/src/edit/modes/patch.ts +23 -19
  16. package/src/edit/modes/replace.ts +19 -15
  17. package/src/edit/renderer.ts +65 -8
  18. package/src/edit/streaming.ts +47 -77
  19. package/src/extensibility/extensions/types.ts +11 -11
  20. package/src/extensibility/hooks/types.ts +6 -6
  21. package/src/lsp/edits.ts +8 -5
  22. package/src/lsp/index.ts +4 -4
  23. package/src/lsp/utils.ts +13 -43
  24. package/src/mcp/discoverable-tool-metadata.ts +1 -1
  25. package/src/mcp/manager.ts +3 -3
  26. package/src/mcp/tool-bridge.ts +4 -4
  27. package/src/memories/index.ts +1 -1
  28. package/src/modes/acp/acp-event-mapper.ts +1 -1
  29. package/src/modes/components/session-observer-overlay.ts +1 -1
  30. package/src/modes/components/settings-defs.ts +3 -3
  31. package/src/modes/components/tree-selector.ts +2 -2
  32. package/src/modes/controllers/event-controller.ts +12 -0
  33. package/src/modes/utils/ui-helpers.ts +31 -7
  34. package/src/prompts/agents/explore.md +1 -1
  35. package/src/prompts/agents/librarian.md +2 -2
  36. package/src/prompts/agents/plan.md +2 -2
  37. package/src/prompts/agents/reviewer.md +1 -1
  38. package/src/prompts/agents/task.md +2 -2
  39. package/src/prompts/system/plan-mode-active.md +1 -1
  40. package/src/prompts/system/system-prompt.md +34 -31
  41. package/src/prompts/tools/apply-patch.md +0 -2
  42. package/src/prompts/tools/atom.md +88 -97
  43. package/src/prompts/tools/bash.md +7 -4
  44. package/src/prompts/tools/checkpoint.md +1 -1
  45. package/src/prompts/tools/find.md +6 -1
  46. package/src/prompts/tools/hashline.md +10 -11
  47. package/src/prompts/tools/patch.md +13 -13
  48. package/src/prompts/tools/read.md +5 -5
  49. package/src/prompts/tools/replace.md +3 -3
  50. package/src/prompts/tools/{grep.md → search.md} +4 -4
  51. package/src/sdk.ts +19 -9
  52. package/src/session/agent-session.ts +69 -1
  53. package/src/system-prompt.ts +15 -5
  54. package/src/task/executor.ts +5 -0
  55. package/src/task/index.ts +10 -1
  56. package/src/tools/ast-edit.ts +27 -50
  57. package/src/tools/ast-grep.ts +22 -48
  58. package/src/tools/bash.ts +1 -1
  59. package/src/tools/file-recorder.ts +6 -6
  60. package/src/tools/find.ts +11 -13
  61. package/src/tools/grouped-file-output.ts +96 -0
  62. package/src/tools/index.ts +7 -7
  63. package/src/tools/path-utils.ts +31 -4
  64. package/src/tools/read.ts +12 -6
  65. package/src/tools/renderers.ts +2 -2
  66. package/src/tools/{grep.ts → search.ts} +43 -86
  67. package/src/tools/todo-write.ts +0 -1
  68. package/src/tools/write.ts +8 -4
  69. package/src/web/search/index.ts +1 -1
package/src/edit/index.ts CHANGED
@@ -19,13 +19,8 @@ import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit
19
19
  import type { VimToolDetails } from "../vim/types";
20
20
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
21
21
  import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
22
- import {
23
- type AtomParams,
24
- type AtomToolEdit,
25
- atomEditParamsSchema,
26
- executeAtomSingle,
27
- resolveAtomEntryPaths,
28
- } from "./modes/atom";
22
+ import { type AtomParams, atomEditParamsSchema, executeAtomSingle } from "./modes/atom";
23
+ import atomGrammar from "./modes/atom.lark" with { type: "text" };
29
24
  import {
30
25
  executeHashlineSingle,
31
26
  HashlineMismatchError,
@@ -122,45 +117,11 @@ function createEditWritethrough(session: ToolSession): WritethroughCallback {
122
117
  return enableLsp ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics }) : writethroughNoop;
123
118
  }
124
119
 
125
- /**
126
- * Resolve per-entry `path` against an optional top-level `path` default.
127
- * If both are absent on an entry, throws a descriptive error.
128
- */
129
- function resolveEntryPaths<T extends { path?: string }>(
130
- edits: readonly T[],
131
- topLevelPath: string | undefined,
132
- ): (T & { path: string })[] {
133
- return edits.map((edit, i) => {
134
- const path = (edit && typeof edit.path === "string" && edit.path) || topLevelPath;
135
- if (!path) {
136
- throw new Error(
137
- `Edit ${i}: missing \`path\`. Provide \`path\` on this edit or supply a top-level \`path\` for the request.`,
138
- );
139
- }
140
- return { ...edit, path };
141
- });
142
- }
143
-
144
- /** Group items by a key, preserving insertion order. */
145
- function groupBy<T, K>(items: T[], key: (item: T) => K): Map<K, T[]> {
146
- const map = new Map<K, T[]>();
147
- for (const item of items) {
148
- const k = key(item);
149
- let arr = map.get(k);
150
- if (!arr) {
151
- arr = [];
152
- map.set(k, arr);
153
- }
154
- arr.push(item);
155
- }
156
- return map;
157
- }
158
-
159
- /** Run single-file executors for each file group and aggregate results. */
160
- async function executePerFile(
120
+ /** Run apply_patch file operations and aggregate their multi-file result. */
121
+ async function executeApplyPatchPerFile(
161
122
  fileEntries: {
162
123
  path: string;
163
- run: (batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails, any>>;
124
+ run: (batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails>>;
164
125
  }[],
165
126
  outerBatchRequest: LspBatchRequest | undefined,
166
127
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
@@ -230,6 +191,58 @@ async function executePerFile(
230
191
  };
231
192
  }
232
193
 
194
+ async function executeSinglePathEntries(
195
+ path: string,
196
+ runs: ((batchRequest: LspBatchRequest | undefined) => Promise<AgentToolResult<EditToolDetails>>)[],
197
+ outerBatchRequest: LspBatchRequest | undefined,
198
+ onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
199
+ ): Promise<AgentToolResult<EditToolDetails, TInput>> {
200
+ if (runs.length === 1) {
201
+ return runs[0](outerBatchRequest);
202
+ }
203
+
204
+ const contentTexts: string[] = [];
205
+ const diffTexts: string[] = [];
206
+ let firstChangedLine: number | undefined;
207
+
208
+ for (let i = 0; i < runs.length; i++) {
209
+ const isLast = i === runs.length - 1;
210
+ const batchRequest: LspBatchRequest | undefined = outerBatchRequest
211
+ ? { id: outerBatchRequest.id, flush: isLast && outerBatchRequest.flush }
212
+ : undefined;
213
+
214
+ try {
215
+ const result = await runs[i](batchRequest);
216
+ const details = result.details;
217
+ if (details?.diff) diffTexts.push(details.diff);
218
+ firstChangedLine ??= details?.firstChangedLine;
219
+ const text = result.content?.find(c => c.type === "text")?.text ?? "";
220
+ if (text) contentTexts.push(text);
221
+ } catch (err) {
222
+ const errorText = err instanceof Error ? err.message : String(err);
223
+ contentTexts.push(`Error editing ${path}: ${errorText}`);
224
+ }
225
+
226
+ if (!isLast && onUpdate) {
227
+ onUpdate({
228
+ content: [{ type: "text", text: contentTexts.join("\n") }],
229
+ details: {
230
+ diff: diffTexts.join("\n"),
231
+ firstChangedLine,
232
+ },
233
+ });
234
+ }
235
+ }
236
+
237
+ return {
238
+ content: [{ type: "text", text: contentTexts.join("\n") }],
239
+ details: {
240
+ diff: diffTexts.join("\n"),
241
+ firstChangedLine,
242
+ },
243
+ };
244
+ }
245
+
233
246
  export class EditTool implements AgentTool<TInput> {
234
247
  readonly name = "edit";
235
248
  readonly label = "Edit";
@@ -278,8 +291,9 @@ export class EditTool implements AgentTool<TInput> {
278
291
  * and fall back to emitting a JSON function tool from `parameters`.
279
292
  */
280
293
  get customFormat(): { syntax: "lark"; definition: string } | undefined {
281
- if (this.mode !== "apply_patch") return undefined;
282
- return { syntax: "lark", definition: applyPatchGrammar };
294
+ if (this.mode === "apply_patch") return { syntax: "lark", definition: applyPatchGrammar };
295
+ if (this.mode === "atom") return { syntax: "lark", definition: atomGrammar };
296
+ return undefined;
283
297
  }
284
298
 
285
299
  /**
@@ -316,13 +330,12 @@ export class EditTool implements AgentTool<TInput> {
316
330
  batchRequest: LspBatchRequest | undefined,
317
331
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
318
332
  ) => {
319
- const { edits, path: topPath } = params as PatchParams & { path?: string };
320
- const resolved = resolveEntryPaths(edits as PatchEditEntry[], topPath);
321
- const entries = resolved.map(entry => ({
322
- path: entry.path,
323
- run: (br: LspBatchRequest | undefined) =>
333
+ const { edits, path } = params as PatchParams;
334
+ const runs = (edits as PatchEditEntry[]).map(
335
+ entry => (br: LspBatchRequest | undefined) =>
324
336
  executePatchSingle({
325
337
  session: tool.session,
338
+ path,
326
339
  params: entry,
327
340
  signal,
328
341
  batchRequest: br,
@@ -331,8 +344,8 @@ export class EditTool implements AgentTool<TInput> {
331
344
  writethrough: tool.#writethrough,
332
345
  beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
333
346
  }),
334
- }));
335
- return executePerFile(entries, batchRequest, onUpdate);
347
+ );
348
+ return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
336
349
  },
337
350
  },
338
351
  apply_patch: {
@@ -346,21 +359,25 @@ export class EditTool implements AgentTool<TInput> {
346
359
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
347
360
  ) => {
348
361
  const entries = expandApplyPatchToEntries(params as ApplyPatchParams);
349
- const perFile = entries.map(entry => ({
350
- path: entry.path!,
351
- run: (br: LspBatchRequest | undefined) =>
352
- executePatchSingle({
353
- session: tool.session,
354
- params: entry,
355
- signal,
356
- batchRequest: br,
357
- allowFuzzy: tool.#allowFuzzy,
358
- fuzzyThreshold: tool.#fuzzyThreshold,
359
- writethrough: tool.#writethrough,
360
- beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
361
- }),
362
- }));
363
- return executePerFile(perFile, batchRequest, onUpdate);
362
+ const perFile = entries.map(entry => {
363
+ const { path, ...patchParams } = entry;
364
+ return {
365
+ path,
366
+ run: (br: LspBatchRequest | undefined) =>
367
+ executePatchSingle({
368
+ session: tool.session,
369
+ path,
370
+ params: patchParams,
371
+ signal,
372
+ batchRequest: br,
373
+ allowFuzzy: tool.#allowFuzzy,
374
+ fuzzyThreshold: tool.#fuzzyThreshold,
375
+ writethrough: tool.#writethrough,
376
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
377
+ }),
378
+ };
379
+ });
380
+ return executeApplyPatchPerFile(perFile, batchRequest, onUpdate);
364
381
  },
365
382
  },
366
383
  hashline: {
@@ -371,25 +388,18 @@ export class EditTool implements AgentTool<TInput> {
371
388
  params: EditParams,
372
389
  signal: AbortSignal | undefined,
373
390
  batchRequest: LspBatchRequest | undefined,
374
- onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
391
+ _onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
375
392
  ) => {
376
- const { edits, path: topPath } = params as HashlineParams & { path?: string };
377
- const resolved = resolveEntryPaths(edits as HashlineToolEdit[], topPath);
378
- const byFile = groupBy(resolved, e => e.path);
379
- const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
393
+ const { edits, path } = params as HashlineParams;
394
+ return executeHashlineSingle({
395
+ session: tool.session,
380
396
  path,
381
- run: (br: LspBatchRequest | undefined) =>
382
- executeHashlineSingle({
383
- session: tool.session,
384
- path,
385
- edits: fileEdits,
386
- signal,
387
- batchRequest: br,
388
- writethrough: tool.#writethrough,
389
- beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
390
- }),
391
- }));
392
- return executePerFile(entries, batchRequest, onUpdate);
397
+ edits: edits as HashlineToolEdit[],
398
+ signal,
399
+ batchRequest,
400
+ writethrough: tool.#writethrough,
401
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
402
+ });
393
403
  },
394
404
  },
395
405
  atom: {
@@ -400,25 +410,18 @@ export class EditTool implements AgentTool<TInput> {
400
410
  params: EditParams,
401
411
  signal: AbortSignal | undefined,
402
412
  batchRequest: LspBatchRequest | undefined,
403
- onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
413
+ _onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
404
414
  ) => {
405
- const { edits, path: topPath } = params as AtomParams & { path?: string };
406
- const resolved = resolveAtomEntryPaths(edits as AtomToolEdit[], topPath);
407
- const byFile = groupBy(resolved, e => e.path);
408
- const entries = [...byFile.entries()].map(([path, fileEdits]) => ({
415
+ const { input, path } = params as AtomParams & { path?: string };
416
+ return executeAtomSingle({
417
+ session: tool.session,
418
+ input,
409
419
  path,
410
- run: (br: LspBatchRequest | undefined) =>
411
- executeAtomSingle({
412
- session: tool.session,
413
- path,
414
- edits: fileEdits,
415
- signal,
416
- batchRequest: br,
417
- writethrough: tool.#writethrough,
418
- beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
419
- }),
420
- }));
421
- return executePerFile(entries, batchRequest, onUpdate);
420
+ signal,
421
+ batchRequest,
422
+ writethrough: tool.#writethrough,
423
+ beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
424
+ });
422
425
  },
423
426
  },
424
427
  replace: {
@@ -431,13 +434,12 @@ export class EditTool implements AgentTool<TInput> {
431
434
  batchRequest: LspBatchRequest | undefined,
432
435
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
433
436
  ) => {
434
- const { edits, path: topPath } = params as ReplaceParams & { path?: string };
435
- const resolved = resolveEntryPaths(edits as ReplaceEditEntry[], topPath);
436
- const entries = resolved.map(entry => ({
437
- path: entry.path,
438
- run: (br: LspBatchRequest | undefined) =>
437
+ const { edits, path } = params as ReplaceParams;
438
+ const runs = (edits as ReplaceEditEntry[]).map(
439
+ entry => (br: LspBatchRequest | undefined) =>
439
440
  executeReplaceSingle({
440
441
  session: tool.session,
442
+ path,
441
443
  params: entry,
442
444
  signal,
443
445
  batchRequest: br,
@@ -446,8 +448,8 @@ export class EditTool implements AgentTool<TInput> {
446
448
  writethrough: tool.#writethrough,
447
449
  beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
448
450
  }),
449
- }));
450
- return executePerFile(entries, batchRequest, onUpdate);
451
+ );
452
+ return executeSinglePathEntries(path, runs, batchRequest, onUpdate);
451
453
  },
452
454
  },
453
455
  vim: {
@@ -676,18 +676,48 @@ export const HASHLINE_BIGRAM_RE_SRC = `(?:${HASHLINE_BIGRAMS.join("|")})`;
676
676
  export const HASHLINE_CONTENT_SEPARATOR = "|";
677
677
 
678
678
  const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
679
+ const RE_STRUCTURAL_STRIP = /[\s{}]/g;
680
+
681
+ /**
682
+ * Bigram returned for lines that contain only whitespace and `{`/`}`.
683
+ * Picks the English ordinal suffix for the line number (`1` → `st`,
684
+ * `2` → `nd`, `3` → `rd`, `11`/`12`/`13` → `th`, else `th`) so the
685
+ * line digits + bigram BPE-merge into a single ordinal token (`1st`, `42nd`,
686
+ * `100th`, …). Brace-only lines therefore cost one token for the whole
687
+ * `LINE+ID` anchor instead of two.
688
+ */
689
+ function structuralBigram(line: number): string {
690
+ const mod100 = line % 100;
691
+ if (mod100 >= 11 && mod100 <= 13) return "th";
692
+ switch (line % 10) {
693
+ case 1:
694
+ return "st";
695
+ case 2:
696
+ return "nd";
697
+ case 3:
698
+ return "rd";
699
+ default:
700
+ return "th";
701
+ }
702
+ }
679
703
 
680
704
  /**
681
705
  * Compute a short BPE-bigram hash of a single line.
682
706
  *
683
707
  * Uses xxHash32 on a trailing-whitespace-trimmed, CR-stripped line, mapped into
684
- * {@link HASHLINE_BIGRAMS} via modulo. For lines containing no alphanumeric
685
- * characters (only punctuation/symbols/whitespace), the line number is mixed in
686
- * to reduce hash collisions. The line input should not include a trailing newline.
708
+ * {@link HASHLINE_BIGRAMS} via modulo. Lines that contain only whitespace and
709
+ * `{`/`}` collapse to an ordinal-suffix bigram (see {@link structuralBigram})
710
+ * so brace-only structure shares one merged ordinal token. For other lines
711
+ * containing no alphanumeric characters, the line number is mixed in to reduce hash collisions.
712
+ * The line input should not include a trailing newline.
687
713
  */
688
714
  export function computeLineHash(idx: number, line: string): string {
689
715
  line = line.replace(/\r/g, "").trimEnd();
690
716
 
717
+ if (line.replace(RE_STRUCTURAL_STRIP, "").length === 0) {
718
+ return structuralBigram(idx);
719
+ }
720
+
691
721
  let seed = 0;
692
722
  if (!RE_SIGNIFICANT.test(line)) {
693
723
  seed = idx;
@@ -22,17 +22,19 @@ export const applyPatchSchema = Type.Object({
22
22
 
23
23
  export type ApplyPatchParams = Static<typeof applyPatchSchema>;
24
24
 
25
+ export type ApplyPatchEntry = PatchEditEntry & { path: string };
26
+
25
27
  /**
26
28
  * Parse the envelope and lower each hunk to a `PatchEditEntry` so it can
27
29
  * be routed through `executePatchSingle`.
28
30
  */
29
- export function expandApplyPatchToEntries(params: ApplyPatchParams): PatchEditEntry[] {
31
+ export function expandApplyPatchToEntries(params: ApplyPatchParams): ApplyPatchEntry[] {
30
32
  const hunks = parseApplyPatch(params.input);
31
33
  if (hunks.length === 0) {
32
34
  throw new ApplyPatchError("No files were modified.");
33
35
  }
34
36
  return hunks.map(
35
- (h): PatchEditEntry => ({
37
+ (h): ApplyPatchEntry => ({
36
38
  path: h.path,
37
39
  op: h.op,
38
40
  rename: h.rename,
@@ -41,10 +43,10 @@ export function expandApplyPatchToEntries(params: ApplyPatchParams): PatchEditEn
41
43
  );
42
44
  }
43
45
 
44
- export function expandApplyPatchToPreviewEntries(params: ApplyPatchParams): PatchEditEntry[] {
46
+ export function expandApplyPatchToPreviewEntries(params: ApplyPatchParams): ApplyPatchEntry[] {
45
47
  const hunks = parseApplyPatchStreaming(params.input);
46
48
  return hunks.map(
47
- (h): PatchEditEntry => ({
49
+ (h): ApplyPatchEntry => ({
48
50
  path: h.path,
49
51
  op: h.op,
50
52
  rename: h.rename,
@@ -0,0 +1,27 @@
1
+ %import common.LF
2
+
3
+ start: file_section+
4
+
5
+ file_section: file_header (line_change | whole_file_change)
6
+ file_header: "---" filename LF
7
+
8
+ filename: /(.+)/
9
+
10
+ line_change: line* mutation_line line*
11
+ line: insert_line | delete_line | set_line | move_line | blank
12
+ mutation_line: insert_line | delete_line | set_line
13
+
14
+ whole_file_change: blank* whole_file_line blank*
15
+ whole_file_line: remove_file | move_file
16
+ remove_file: "!rm" LF
17
+ move_file: "!mv" WS destination LF
18
+ destination: /(?:[^ \t\r\n]+|"[^"\r\n]+"|'[^'\r\n]+')/
19
+
20
+ insert_line: "+" /(.*)/ LF
21
+ delete_line: "-" LID LF
22
+ set_line: LID "=" /(.*)/ LF
23
+ move_line: ("@" LID | "$" | "^") LF
24
+
25
+ LID: /[1-9][0-9]*[a-z]{2}/
26
+ WS: /[ \t]+/
27
+ blank: LF