@oh-my-pi/pi-coding-agent 16.2.5 → 16.2.7

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 (93) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/dist/cli.js +3089 -3104
  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/service-tier.d.ts +39 -24
  7. package/dist/types/config/settings-schema.d.ts +37 -37
  8. package/dist/types/edit/index.d.ts +1 -0
  9. package/dist/types/edit/renderer.d.ts +4 -0
  10. package/dist/types/edit/snapshot-details.d.ts +33 -0
  11. package/dist/types/mcp/config-writer.d.ts +48 -0
  12. package/dist/types/mcp/oauth-flow.d.ts +4 -6
  13. package/dist/types/mcp/types.d.ts +3 -0
  14. package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
  15. package/dist/types/session/agent-session.d.ts +37 -13
  16. package/dist/types/session/messages.d.ts +1 -1
  17. package/dist/types/session/session-context.d.ts +2 -2
  18. package/dist/types/session/session-entries.d.ts +2 -2
  19. package/dist/types/session/session-manager.d.ts +5 -6
  20. package/dist/types/system-prompt.test.d.ts +1 -0
  21. package/dist/types/task/executor.d.ts +6 -6
  22. package/dist/types/task/types.d.ts +6 -0
  23. package/dist/types/task/worktree.d.ts +32 -6
  24. package/dist/types/tiny/title-client.d.ts +5 -1
  25. package/dist/types/tools/index.d.ts +3 -3
  26. package/dist/types/utils/git.d.ts +17 -0
  27. package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
  28. package/dist/types/web/search/types.d.ts +1 -1
  29. package/package.json +12 -12
  30. package/src/cli/args.ts +32 -1
  31. package/src/cli/bench-cli.ts +97 -22
  32. package/src/cli/tiny-models-cli.ts +18 -4
  33. package/src/cli/web-search-cli.ts +6 -1
  34. package/src/commands/bench.ts +10 -1
  35. package/src/config/mcp-schema.json +11 -2
  36. package/src/config/model-discovery.ts +66 -8
  37. package/src/config/model-registry.ts +13 -6
  38. package/src/config/service-tier.ts +85 -56
  39. package/src/config/settings-schema.ts +42 -36
  40. package/src/config/settings.ts +47 -0
  41. package/src/edit/hashline/execute.ts +15 -9
  42. package/src/edit/index.ts +19 -6
  43. package/src/edit/modes/patch.ts +3 -2
  44. package/src/edit/modes/replace.ts +3 -2
  45. package/src/edit/renderer.ts +4 -0
  46. package/src/edit/snapshot-details.ts +77 -0
  47. package/src/eval/agent-bridge.ts +4 -2
  48. package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
  49. package/src/internal-urls/docs-index.generated.txt +1 -1
  50. package/src/main.ts +1 -1
  51. package/src/mcp/config-writer.ts +121 -0
  52. package/src/mcp/config.ts +10 -6
  53. package/src/mcp/oauth-flow.ts +10 -8
  54. package/src/mcp/transports/stdio.ts +9 -17
  55. package/src/mcp/types.ts +3 -0
  56. package/src/modes/components/extensions/extension-dashboard.ts +46 -0
  57. package/src/modes/components/extensions/state-manager.ts +24 -3
  58. package/src/modes/controllers/event-controller.ts +24 -8
  59. package/src/modes/controllers/tool-args-reveal.ts +100 -22
  60. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  61. package/src/prompts/bench.md +4 -10
  62. package/src/prompts/tools/irc.md +19 -29
  63. package/src/prompts/tools/job.md +8 -14
  64. package/src/prompts/tools/lsp.md +19 -30
  65. package/src/prompts/tools/task.md +42 -62
  66. package/src/sdk.ts +13 -11
  67. package/src/session/agent-session.ts +196 -76
  68. package/src/session/messages.ts +1 -1
  69. package/src/session/session-context.ts +4 -4
  70. package/src/session/session-entries.ts +2 -2
  71. package/src/session/session-listing.ts +9 -8
  72. package/src/session/session-loader.ts +98 -3
  73. package/src/session/session-manager.ts +43 -6
  74. package/src/slash-commands/builtin-registry.ts +2 -10
  75. package/src/subprocess/worker-client.ts +12 -4
  76. package/src/system-prompt.test.ts +158 -0
  77. package/src/system-prompt.ts +69 -26
  78. package/src/task/executor.ts +23 -16
  79. package/src/task/index.ts +7 -5
  80. package/src/task/isolation-runner.ts +15 -1
  81. package/src/task/types.ts +6 -0
  82. package/src/task/worktree.ts +219 -38
  83. package/src/tiny/title-client.ts +19 -13
  84. package/src/tools/index.ts +3 -3
  85. package/src/tools/irc.ts +9 -3
  86. package/src/tools/path-utils.ts +4 -2
  87. package/src/tools/read.ts +28 -28
  88. package/src/utils/file-mentions.ts +10 -1
  89. package/src/utils/git.ts +38 -0
  90. package/src/web/search/index.ts +14 -8
  91. package/src/web/search/providers/duckduckgo.ts +150 -78
  92. package/src/web/search/providers/gemini.ts +268 -185
  93. package/src/web/search/types.ts +1 -1
@@ -27,6 +27,7 @@ import { ToolError } from "../../tools/tool-errors";
27
27
  import { generateDiffString } from "../diff";
28
28
  import { getFileSnapshotStore } from "../file-snapshot-store";
29
29
  import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
30
+ import { pruneOversizedEditSnapshots } from "../snapshot-details";
30
31
  import { nativeBlockResolver } from "./block-resolver";
31
32
  import { HashlineFilesystem } from "./filesystem";
32
33
  import { hashPatchInput, NOOP_HARD_LIMIT, recordNoopEdit, resetNoopEdit } from "./noop-loop-guard";
@@ -114,17 +115,22 @@ function renderSection(
114
115
  if (result.op === "delete") {
115
116
  const toolResult: AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema> = {
116
117
  content: [{ type: "text", text: `Deleted ${result.path}` }],
117
- details: {
118
+ details: pruneOversizedEditSnapshots({
118
119
  diff: "",
119
120
  op: "delete",
120
121
  path: result.path,
121
122
  oldText: result.before,
122
123
  meta: outputMeta().get(),
123
- },
124
+ }),
124
125
  };
125
126
  return {
126
127
  toolResult,
127
- perFileResult: { path: result.path, diff: "", op: "delete", oldText: result.before },
128
+ perFileResult: pruneOversizedEditSnapshots({
129
+ path: result.path,
130
+ diff: "",
131
+ op: "delete",
132
+ oldText: result.before,
133
+ }),
128
134
  };
129
135
  }
130
136
 
@@ -161,7 +167,7 @@ function renderSection(
161
167
  text: `${result.header}${blockBlock}${moveBlock}${previewBlock}${warningsBlock}`,
162
168
  },
163
169
  ],
164
- details: {
170
+ details: pruneOversizedEditSnapshots({
165
171
  diff: diff.diff,
166
172
  firstChangedLine,
167
173
  diagnostics,
@@ -172,9 +178,9 @@ function renderSection(
172
178
  oldText: result.before,
173
179
  newText: result.after,
174
180
  meta,
175
- },
181
+ }),
176
182
  },
177
- perFileResult: {
183
+ perFileResult: pruneOversizedEditSnapshots({
178
184
  path: result.moveDest ?? result.path,
179
185
  diff: diff.diff,
180
186
  firstChangedLine,
@@ -184,7 +190,7 @@ function renderSection(
184
190
  sourcePath: result.moveDest ? sourcePath : undefined,
185
191
  oldText: result.before,
186
192
  newText: result.after,
187
- },
193
+ }),
188
194
  };
189
195
  }
190
196
 
@@ -263,10 +269,10 @@ export async function executeHashlineSingle(
263
269
  .join("\n\n"),
264
270
  },
265
271
  ],
266
- details: {
272
+ details: pruneOversizedEditSnapshots({
267
273
  diff: rendered.map(r => r.toolResult.details?.diff ?? "").join("\n"),
268
274
  perFileResults: rendered.map(r => r.perFileResult),
269
- },
275
+ }),
270
276
  };
271
277
  }
272
278
 
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
+ }
@@ -405,8 +405,10 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
405
405
  parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
406
406
  parentTelemetry: options.session.getTelemetry?.(),
407
407
  parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
408
- // Live source of truth for `serviceTierSubagent: inherit` (null = explicit none).
409
- parentServiceTier: options.session.getServiceTier ? (options.session.getServiceTier() ?? null) : undefined,
408
+ // Live source of truth for `tier.subagent: inherit` (null = explicit none).
409
+ parentServiceTier: options.session.getServiceTierByFamily
410
+ ? (options.session.getServiceTierByFamily() ?? null)
411
+ : undefined,
410
412
  // Deliberately omit parentEvalSessionId: the parent's Python kernel is
411
413
  // blocked on this bridge call, so sharing the eval session would deadlock
412
414
  // (subagent queues behind the parent's in-flight execution, parent waits
@@ -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 {