@oh-my-pi/pi-coding-agent 16.2.4 → 16.2.6

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 (68) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cli.js +3211 -3231
  3. package/dist/types/cli/bench-cli.d.ts +6 -1
  4. package/dist/types/commands/bench.d.ts +8 -0
  5. package/dist/types/config/model-discovery.d.ts +6 -1
  6. package/dist/types/config/settings-schema.d.ts +1 -1
  7. package/dist/types/edit/index.d.ts +1 -0
  8. package/dist/types/edit/renderer.d.ts +4 -0
  9. package/dist/types/edit/snapshot-details.d.ts +33 -0
  10. package/dist/types/mcp/oauth-flow.d.ts +4 -6
  11. package/dist/types/modes/components/status-line/types.d.ts +10 -0
  12. package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
  13. package/dist/types/modes/theme/theme.d.ts +2 -1
  14. package/dist/types/session/agent-session.d.ts +11 -0
  15. package/dist/types/session/session-manager.d.ts +3 -4
  16. package/dist/types/task/provider-concurrency.d.ts +40 -0
  17. package/dist/types/utils/git.d.ts +11 -0
  18. package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
  19. package/dist/types/web/search/types.d.ts +1 -1
  20. package/package.json +12 -12
  21. package/src/cli/args.ts +32 -1
  22. package/src/cli/bench-cli.ts +89 -21
  23. package/src/cli/web-search-cli.ts +6 -1
  24. package/src/cli/worktree-cli.ts +8 -5
  25. package/src/commands/bench.ts +10 -1
  26. package/src/config/mcp-schema.json +1 -1
  27. package/src/config/model-discovery.ts +98 -20
  28. package/src/config/model-registry.ts +13 -6
  29. package/src/config/settings-schema.ts +5 -2
  30. package/src/edit/hashline/execute.ts +15 -9
  31. package/src/edit/index.ts +19 -6
  32. package/src/edit/modes/patch.ts +3 -2
  33. package/src/edit/modes/replace.ts +3 -2
  34. package/src/edit/renderer.ts +4 -0
  35. package/src/edit/snapshot-details.ts +77 -0
  36. package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
  37. package/src/internal-urls/docs-index.generated.txt +1 -1
  38. package/src/mcp/oauth-flow.ts +10 -8
  39. package/src/mcp/transports/stdio.ts +9 -17
  40. package/src/modes/components/status-line/component.ts +29 -2
  41. package/src/modes/components/status-line/segments.ts +22 -8
  42. package/src/modes/components/status-line/types.ts +7 -0
  43. package/src/modes/controllers/event-controller.ts +17 -8
  44. package/src/modes/controllers/input-controller.ts +1 -1
  45. package/src/modes/controllers/tool-args-reveal.ts +100 -22
  46. package/src/modes/theme/theme.ts +6 -0
  47. package/src/prompts/bench.md +4 -10
  48. package/src/prompts/tools/irc.md +19 -29
  49. package/src/prompts/tools/job.md +8 -14
  50. package/src/prompts/tools/lsp.md +19 -30
  51. package/src/prompts/tools/task.md +42 -62
  52. package/src/sdk.ts +14 -5
  53. package/src/session/agent-session.ts +107 -13
  54. package/src/session/session-listing.ts +9 -8
  55. package/src/session/session-loader.ts +98 -3
  56. package/src/session/session-manager.ts +34 -4
  57. package/src/subprocess/worker-client.ts +12 -4
  58. package/src/task/executor.ts +4 -62
  59. package/src/task/provider-concurrency.ts +100 -0
  60. package/src/task/worktree.ts +13 -4
  61. package/src/task/yield-assembly.ts +27 -39
  62. package/src/tools/path-utils.ts +4 -2
  63. package/src/tools/read.ts +11 -1
  64. package/src/utils/git.ts +13 -0
  65. package/src/web/search/index.ts +14 -8
  66. package/src/web/search/providers/duckduckgo.ts +136 -78
  67. package/src/web/search/providers/gemini.ts +268 -185
  68. package/src/web/search/types.ts +1 -1
package/src/edit/index.ts CHANGED
@@ -25,6 +25,7 @@ import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
25
25
  import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
26
26
  import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
27
27
  import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
28
+ import { pruneOversizedEditSnapshots } from "./snapshot-details";
28
29
  import { EDIT_MODE_STRATEGIES } from "./streaming";
29
30
 
30
31
  export * from "@oh-my-pi/hashline";
@@ -38,6 +39,7 @@ export * from "./modes/patch";
38
39
  export * from "./modes/replace";
39
40
  export * from "./normalize";
40
41
  export * from "./renderer";
42
+ export * from "./snapshot-details";
41
43
  export * from "./streaming";
42
44
 
43
45
  type TInput =
@@ -158,6 +160,7 @@ async function executeApplyPatchPerFile(
158
160
  meta: details?.meta,
159
161
  oldText: details?.oldText,
160
162
  newText: details?.newText,
163
+ snapshotsPruned: details?.snapshotsPruned,
161
164
  });
162
165
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
163
166
  if (text) contentTexts.push(text);
@@ -186,14 +189,14 @@ async function executeApplyPatchPerFile(
186
189
 
187
190
  return {
188
191
  content: [{ type: "text", text: contentTexts.join("\n") }],
189
- details: {
192
+ details: pruneOversizedEditSnapshots({
190
193
  diff: perFileResults
191
194
  .map(r => r.diff)
192
195
  .filter(Boolean)
193
196
  .join("\n"),
194
197
  firstChangedLine: perFileResults.find(r => r.firstChangedLine)?.firstChangedLine,
195
198
  perFileResults,
196
- },
199
+ }),
197
200
  };
198
201
  }
199
202
 
@@ -216,6 +219,11 @@ async function executeSinglePathEntries(
216
219
  let firstOldText: string | undefined;
217
220
  let hasLastNewText = false;
218
221
  let lastNewText: string | undefined;
222
+ // Any pruned child invalidates the aggregate snapshot: combining a kept
223
+ // first-entry oldText with a pruned next entry's newText (or vice-versa)
224
+ // would describe a transition the file never made. Suppress aggregate
225
+ // snapshots and stamp the marker so ACP/downstream can degrade cleanly.
226
+ let snapshotsPruned = false;
219
227
 
220
228
  for (let i = 0; i < runs.length; i++) {
221
229
  const isLast = i === runs.length - 1;
@@ -239,6 +247,7 @@ async function executeSinglePathEntries(
239
247
  lastNewText = details.newText;
240
248
  hasLastNewText = true;
241
249
  }
250
+ if (details?.snapshotsPruned) snapshotsPruned = true;
242
251
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
243
252
  if (text) contentTexts.push(text);
244
253
  } catch (err) {
@@ -276,13 +285,17 @@ async function executeSinglePathEntries(
276
285
 
277
286
  return {
278
287
  content: [{ type: "text", text: contentTexts.join("\n") }],
279
- details: {
288
+ details: pruneOversizedEditSnapshots({
280
289
  diff: diffTexts.join("\n"),
281
290
  firstChangedLine,
282
291
  path: metadataPath ?? path,
283
- ...(hasFirstOldText ? { oldText: firstOldText } : {}),
284
- ...(hasLastNewText ? { newText: lastNewText } : {}),
285
- },
292
+ ...(snapshotsPruned
293
+ ? { snapshotsPruned: true as const }
294
+ : {
295
+ ...(hasFirstOldText ? { oldText: firstOldText } : {}),
296
+ ...(hasLastNewText ? { newText: lastNewText } : {}),
297
+ }),
298
+ }),
286
299
  // Any per-entry failure marks the aggregate result as an error so the
287
300
  // renderer takes the error branch instead of falling through to the
288
301
  // streaming-edit preview (which displays the *proposed* diff and looks
@@ -48,6 +48,7 @@ import {
48
48
  } from "../normalize";
49
49
  import { readEditFileText, serializeEditFileText } from "../read-file";
50
50
  import type { EditToolDetails, LspBatchRequest } from "../renderer";
51
+ import { pruneOversizedEditSnapshots } from "../snapshot-details";
51
52
  import {
52
53
  type ContextLineResult,
53
54
  DEFAULT_FUZZY_THRESHOLD,
@@ -1894,7 +1895,7 @@ export async function executePatchSingle(
1894
1895
 
1895
1896
  return {
1896
1897
  content: [{ type: "text", text: resultText }],
1897
- details: {
1898
+ details: pruneOversizedEditSnapshots({
1898
1899
  diff: diffResult.diff,
1899
1900
  // When the patch moves the file, anchor the diff to the destination
1900
1901
  // path. ACP `ToolCallContent.diff.path` comes from this field, and
@@ -1909,6 +1910,6 @@ export async function executePatchSingle(
1909
1910
  meta,
1910
1911
  oldText,
1911
1912
  newText,
1912
- },
1913
+ }),
1913
1914
  };
1914
1915
  }
@@ -24,6 +24,7 @@ import {
24
24
  } from "../normalize";
25
25
  import { readEditFileText, serializeEditFileText } from "../read-file";
26
26
  import type { EditToolDetails, LspBatchRequest } from "../renderer";
27
+ import { pruneOversizedEditSnapshots } from "../snapshot-details";
27
28
 
28
29
  export interface FuzzyMatch {
29
30
  actualText: string;
@@ -1123,7 +1124,7 @@ export async function executeReplaceSingle(
1123
1124
 
1124
1125
  return {
1125
1126
  content: [{ type: "text", text: resultText }],
1126
- details: {
1127
+ details: pruneOversizedEditSnapshots({
1127
1128
  diff: diffResult.diff,
1128
1129
  path: absolutePath,
1129
1130
  firstChangedLine: diffResult.firstChangedLine,
@@ -1131,6 +1132,6 @@ export async function executeReplaceSingle(
1131
1132
  meta,
1132
1133
  oldText: rawContent,
1133
1134
  newText: finalContent,
1134
- },
1135
+ }),
1135
1136
  };
1136
1137
  }
@@ -70,6 +70,8 @@ export interface EditToolPerFileResult {
70
70
  oldText?: string;
71
71
  /** Source-of-truth content after the edit; `undefined` for delete operations. */
72
72
  newText?: string;
73
+ /** True when {@link pruneOversizedEditSnapshots} dropped `oldText`/`newText` from this entry. Aggregators check this to suppress misleading combined snapshots when at least one entry of a multi-entry single-path edit was pruned. */
74
+ snapshotsPruned?: boolean;
73
75
  /** Pre-move source path; set only when the edit moved/renamed the file. The header renders `sourcePath → path`. */
74
76
  sourcePath?: string;
75
77
  }
@@ -95,6 +97,8 @@ export interface EditToolDetails {
95
97
  oldText?: string;
96
98
  /** Source-of-truth content after the edit; `undefined` for delete operations. */
97
99
  newText?: string;
100
+ /** True when {@link pruneOversizedEditSnapshots} dropped `oldText`/`newText` from this entry. Aggregators check this to suppress misleading combined snapshots when at least one entry of a multi-entry single-path edit was pruned. */
101
+ snapshotsPruned?: boolean;
98
102
  /** Pre-move source path; set only when the edit moved/renamed the file. The header renders `sourcePath → path`. */
99
103
  sourcePath?: string;
100
104
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Bound the size of the `oldText` / `newText` snapshots that edit-tool results
3
+ * carry in `details`. These fields hold the full pre/post file content; for
4
+ * large files they balloon the per-turn JSONL line and the session file
5
+ * (300 KB+ each on the cases reported in #3786) without paying for any LLM
6
+ * context (provider serializers send only `content`, never `details`).
7
+ *
8
+ * Only consumer of the raw snapshots is the ACP event mapper, which builds a
9
+ * `diff` ToolCallContent for ACP clients (Zed). When the snapshots are pruned
10
+ * the mapper returns `undefined` for that file and the text content still
11
+ * flows — diff visualization degrades gracefully for over-threshold edits.
12
+ */
13
+
14
+ import type { EditToolDetails, EditToolPerFileResult } from "./renderer";
15
+
16
+ /**
17
+ * Combined `oldText` + `newText` character budget for a single edit-tool
18
+ * result. Applies both per-entry (one file at a time) and as an aggregate
19
+ * across `perFileResults` (so a many-small-files batch can't accumulate
20
+ * unbounded snapshot bytes — see #3787 review).
21
+ *
22
+ * Picked so typical code-file edits keep ACP diff visualization while
23
+ * pathological cases (large generated files, full-file rewrites, or
24
+ * many-file batches) drop the raw snapshots before they hit the
25
+ * session JSONL.
26
+ */
27
+ export const MAX_EDIT_SNAPSHOT_TEXT_CHARS = 32_768;
28
+
29
+ type WithSnapshot = { oldText?: string; newText?: string; snapshotsPruned?: boolean };
30
+
31
+ function pruneSnapshot<T extends WithSnapshot>(details: T): T {
32
+ if ((details.oldText?.length ?? 0) + (details.newText?.length ?? 0) <= MAX_EDIT_SNAPSHOT_TEXT_CHARS) {
33
+ return details;
34
+ }
35
+ const { oldText: _old, newText: _new, ...rest } = details;
36
+ return { ...rest, snapshotsPruned: true } as T;
37
+ }
38
+
39
+ /**
40
+ * Walk `perFileResults` in order with a shared budget. Each per-entry payload
41
+ * is first capped individually by {@link pruneSnapshot}; if its kept bytes
42
+ * would push the running aggregate past the cap, strip and mark this entry
43
+ * too. Early entries get to keep their diff visualization; later entries in
44
+ * a large batch degrade to text-only.
45
+ */
46
+ function capPerFileSnapshots<T extends WithSnapshot>(entries: T[]): T[] {
47
+ let remaining = MAX_EDIT_SNAPSHOT_TEXT_CHARS;
48
+ return entries.map(entry => {
49
+ const perEntry = pruneSnapshot(entry);
50
+ const kept = (perEntry.oldText?.length ?? 0) + (perEntry.newText?.length ?? 0);
51
+ if (kept === 0) return perEntry;
52
+ if (kept <= remaining) {
53
+ remaining -= kept;
54
+ return perEntry;
55
+ }
56
+ const { oldText: _old, newText: _new, ...rest } = perEntry;
57
+ return { ...rest, snapshotsPruned: true } as T;
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Prune oversized `oldText` / `newText` from an edit-tool details payload,
63
+ * recursing into `perFileResults` when present. Per-file overload comes first
64
+ * so the more specific shape (required `path`) wins overload resolution at
65
+ * the per-file call sites.
66
+ */
67
+ export function pruneOversizedEditSnapshots(details: EditToolPerFileResult): EditToolPerFileResult;
68
+ export function pruneOversizedEditSnapshots(details: EditToolDetails): EditToolDetails;
69
+ export function pruneOversizedEditSnapshots(
70
+ details: EditToolDetails | EditToolPerFileResult,
71
+ ): EditToolDetails | EditToolPerFileResult {
72
+ const pruned = pruneSnapshot(details);
73
+ if ("perFileResults" in pruned && pruned.perFileResults) {
74
+ return { ...pruned, perFileResults: capPerFileSnapshots(pruned.perFileResults) };
75
+ }
76
+ return pruned;
77
+ }
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import { isBuiltin } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import * as url from "node:url";
5
- import { isCompiledBinary } from "@oh-my-pi/pi-utils";
5
+ import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
6
6
  import { BUNDLED_PI_REGISTRY_KEYS } from "./legacy-pi-bundled-keys";
7
7
 
8
8
  const IS_COMPILED_BINARY = isCompiledBinary();
@@ -425,7 +425,7 @@ function toImportSpecifier(resolvedPath: string): string {
425
425
  if (isBundledVirtualSpecifier(resolvedPath)) {
426
426
  return resolvedPath;
427
427
  }
428
- return url.pathToFileURL(resolvedPath).href;
428
+ return url.pathToFileURL(stripWindowsExtendedLengthPathPrefix(resolvedPath)).href;
429
429
  }
430
430
 
431
431
  function rewriteLegacyPiImports(source: string): string {