@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.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 (158) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/dist/cli.js +869 -825
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/capability/mcp.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/config/keybindings.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +66 -34
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  10. package/dist/types/extensibility/shared-events.d.ts +2 -2
  11. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  12. package/dist/types/internal-urls/index.d.ts +1 -0
  13. package/dist/types/internal-urls/types.d.ts +1 -1
  14. package/dist/types/irc/bus.d.ts +66 -0
  15. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  17. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  18. package/dist/types/mcp/types.d.ts +2 -0
  19. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  21. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  22. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  23. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  25. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  26. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  27. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  28. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  29. package/dist/types/modes/components/welcome.d.ts +3 -9
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  32. package/dist/types/modes/interactive-mode.d.ts +3 -2
  33. package/dist/types/modes/theme/theme.d.ts +3 -1
  34. package/dist/types/modes/types.d.ts +3 -2
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  36. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  37. package/dist/types/registry/agent-registry.d.ts +16 -5
  38. package/dist/types/session/agent-session.d.ts +35 -30
  39. package/dist/types/session/messages.d.ts +2 -4
  40. package/dist/types/session/session-history-format.d.ts +12 -0
  41. package/dist/types/session/session-manager.d.ts +21 -3
  42. package/dist/types/session/streaming-output.d.ts +23 -0
  43. package/dist/types/task/executor.d.ts +11 -2
  44. package/dist/types/task/index.d.ts +11 -4
  45. package/dist/types/task/output-manager.d.ts +0 -7
  46. package/dist/types/task/repair-args.d.ts +8 -7
  47. package/dist/types/task/types.d.ts +55 -51
  48. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  49. package/dist/types/tools/find.d.ts +0 -11
  50. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  51. package/dist/types/tools/index.d.ts +1 -3
  52. package/dist/types/tools/irc.d.ts +76 -38
  53. package/dist/types/tools/job.d.ts +7 -1
  54. package/dist/types/tools/render-utils.d.ts +22 -0
  55. package/examples/extensions/with-deps/package.json +1 -0
  56. package/package.json +11 -10
  57. package/scripts/bundle-dist.ts +28 -19
  58. package/src/async/index.ts +0 -1
  59. package/src/capability/mcp.ts +1 -0
  60. package/src/cli/gallery-cli.ts +6 -5
  61. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  62. package/src/cli/gallery-fixtures/types.ts +5 -0
  63. package/src/cli.ts +20 -6
  64. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  65. package/src/config/keybindings.ts +6 -1
  66. package/src/config/mcp-schema.json +4 -0
  67. package/src/config/settings-schema.ts +68 -41
  68. package/src/config/settings.ts +7 -0
  69. package/src/edit/renderer.ts +96 -46
  70. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  71. package/src/eval/agent-bridge.ts +3 -16
  72. package/src/eval/js/shared/prelude.txt +1 -1
  73. package/src/eval/py/prelude.py +5 -6
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +44 -14
  76. package/src/extensibility/custom-tools/types.ts +2 -2
  77. package/src/extensibility/shared-events.ts +2 -2
  78. package/src/internal-urls/docs-index.generated.ts +9 -9
  79. package/src/internal-urls/history-protocol.ts +113 -0
  80. package/src/internal-urls/index.ts +1 -0
  81. package/src/internal-urls/router.ts +3 -1
  82. package/src/internal-urls/types.ts +1 -1
  83. package/src/irc/bus.ts +292 -0
  84. package/src/main.ts +8 -60
  85. package/src/mcp/manager.ts +3 -0
  86. package/src/mcp/oauth-discovery.ts +27 -2
  87. package/src/mcp/oauth-flow.ts +47 -1
  88. package/src/mcp/transports/stdio.ts +3 -0
  89. package/src/mcp/types.ts +2 -0
  90. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  91. package/src/modes/components/assistant-message.ts +15 -0
  92. package/src/modes/components/btw-panel.ts +5 -1
  93. package/src/modes/components/compaction-summary-message.ts +68 -32
  94. package/src/modes/components/custom-editor.ts +10 -0
  95. package/src/modes/components/mcp-add-wizard.ts +13 -0
  96. package/src/modes/components/settings-selector.ts +2 -0
  97. package/src/modes/components/status-line/component.ts +22 -12
  98. package/src/modes/components/status-line/types.ts +3 -0
  99. package/src/modes/components/tool-execution.ts +31 -1
  100. package/src/modes/components/transcript-container.ts +99 -18
  101. package/src/modes/components/tree-selector.ts +6 -1
  102. package/src/modes/components/ttsr-notification.ts +72 -30
  103. package/src/modes/components/welcome.ts +9 -33
  104. package/src/modes/controllers/event-controller.ts +93 -4
  105. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  106. package/src/modes/controllers/input-controller.ts +18 -2
  107. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  108. package/src/modes/controllers/selector-controller.ts +25 -17
  109. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  110. package/src/modes/interactive-mode.ts +17 -15
  111. package/src/modes/theme/theme.ts +24 -5
  112. package/src/modes/types.ts +3 -5
  113. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  114. package/src/modes/utils/ui-helpers.ts +51 -49
  115. package/src/prompts/system/irc-incoming.md +3 -4
  116. package/src/prompts/system/orchestrate-notice.md +2 -2
  117. package/src/prompts/system/subagent-system-prompt.md +0 -5
  118. package/src/prompts/system/system-prompt.md +1 -0
  119. package/src/prompts/system/workflow-notice.md +2 -2
  120. package/src/prompts/tools/eval.md +3 -3
  121. package/src/prompts/tools/irc.md +29 -19
  122. package/src/prompts/tools/read.md +2 -2
  123. package/src/prompts/tools/task-summary.md +5 -16
  124. package/src/prompts/tools/task.md +43 -29
  125. package/src/registry/agent-lifecycle.ts +218 -0
  126. package/src/registry/agent-registry.ts +16 -5
  127. package/src/sdk.ts +29 -9
  128. package/src/session/agent-session.ts +268 -241
  129. package/src/session/messages.ts +11 -78
  130. package/src/session/session-history-format.ts +246 -0
  131. package/src/session/session-manager.ts +59 -5
  132. package/src/session/streaming-output.ts +60 -0
  133. package/src/task/executor.ts +855 -466
  134. package/src/task/index.ts +723 -794
  135. package/src/task/output-manager.ts +0 -11
  136. package/src/task/render.ts +142 -66
  137. package/src/task/repair-args.ts +21 -9
  138. package/src/task/types.ts +73 -66
  139. package/src/tools/ask.ts +4 -2
  140. package/src/tools/bash.ts +15 -5
  141. package/src/tools/browser/tab-worker.ts +26 -7
  142. package/src/tools/browser.ts +28 -1
  143. package/src/tools/find.ts +2 -27
  144. package/src/tools/grouped-file-output.ts +1 -118
  145. package/src/tools/index.ts +4 -12
  146. package/src/tools/irc.ts +596 -171
  147. package/src/tools/job.ts +41 -7
  148. package/src/tools/read.ts +57 -1
  149. package/src/tools/render-utils.ts +56 -0
  150. package/src/tools/renderers.ts +2 -0
  151. package/src/tools/resolve.ts +4 -1
  152. package/src/tools/write.ts +65 -47
  153. package/src/web/search/providers/anthropic.ts +29 -4
  154. package/dist/types/async/support.d.ts +0 -2
  155. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  156. package/dist/types/task/simple-mode.d.ts +0 -8
  157. package/src/async/support.ts +0 -5
  158. package/src/task/simple-mode.ts +0 -27
@@ -1,5 +1,4 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
- import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
2
  import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
4
3
  import {
5
4
  TINY_MODEL_DEVICE_DEFAULT,
@@ -453,6 +452,17 @@ export const SETTINGS_SCHEMA = {
453
452
  description: "Use the session name color for the editor border and status line gap",
454
453
  },
455
454
  },
455
+
456
+ "statusLine.transparent": {
457
+ type: "boolean",
458
+ default: false,
459
+ ui: {
460
+ tab: "appearance",
461
+ label: "Transparent Status Line",
462
+ description:
463
+ "Use the terminal's default background for the status line instead of the theme's `statusLineBg`. Powerline end caps are dropped because they need a contrasting fill to bridge into the surrounding terminal.",
464
+ },
465
+ },
456
466
  "tools.artifactSpillThreshold": {
457
467
  type: "number",
458
468
  default: 50,
@@ -669,7 +679,7 @@ export const SETTINGS_SCHEMA = {
669
679
  ui: {
670
680
  tab: "appearance",
671
681
  label: "Smooth Streaming",
672
- description: "Reveal assistant text smoothly while streamed chunks arrive",
682
+ description: "Reveal assistant text and streamed tool input smoothly while chunks arrive",
673
683
  },
674
684
  },
675
685
 
@@ -1177,13 +1187,13 @@ export const SETTINGS_SCHEMA = {
1177
1187
 
1178
1188
  "compaction.strategy": {
1179
1189
  type: "enum",
1180
- values: ["context-full", "handoff", "shake", "off"] as const,
1190
+ values: ["context-full", "handoff", "shake", "snapcompact", "off"] as const,
1181
1191
  default: "context-full",
1182
1192
  ui: {
1183
1193
  tab: "context",
1184
1194
  label: "Compaction Strategy",
1185
1195
  description:
1186
- "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), or disable auto maintenance (off)",
1196
+ "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), snapcompact (archive history as dense images), or disable auto maintenance (off)",
1187
1197
  options: [
1188
1198
  {
1189
1199
  value: "context-full",
@@ -1196,6 +1206,11 @@ export const SETTINGS_SCHEMA = {
1196
1206
  label: "Shake",
1197
1207
  description: "Drop heavy content (tool results + large blocks) in place; recover via artifact",
1198
1208
  },
1209
+ {
1210
+ value: "snapcompact",
1211
+ label: "Snapcompact",
1212
+ description: "Archive history onto dense bitmap images the model reads back; no LLM call",
1213
+ },
1199
1214
  {
1200
1215
  value: "off",
1201
1216
  label: "Off",
@@ -1326,6 +1341,17 @@ export const SETTINGS_SCHEMA = {
1326
1341
  ],
1327
1342
  },
1328
1343
  },
1344
+
1345
+ "compaction.supersedeReads": {
1346
+ type: "boolean",
1347
+ default: true,
1348
+ ui: {
1349
+ tab: "context",
1350
+ label: "Supersede Stale Reads",
1351
+ description: "Prune older read results when the same file is read again (cache-aware, runs every turn)",
1352
+ },
1353
+ },
1354
+
1329
1355
  // Branch summaries
1330
1356
  "branchSummary.enabled": {
1331
1357
  type: "boolean",
@@ -2289,24 +2315,13 @@ export const SETTINGS_SCHEMA = {
2289
2315
  },
2290
2316
  },
2291
2317
 
2292
- "irc.enabled": {
2293
- type: "boolean",
2294
- default: true,
2295
- ui: {
2296
- tab: "tools",
2297
- label: "IRC",
2298
- description: "Enable agent-to-agent IRC messaging via the irc tool",
2299
- },
2300
- },
2301
-
2302
2318
  "irc.timeoutMs": {
2303
2319
  type: "number",
2304
2320
  default: 120_000,
2305
2321
  ui: {
2306
2322
  tab: "tools",
2307
2323
  label: "IRC Timeout",
2308
- description:
2309
- "Drop IRC messages whose recipient does not respond within this many milliseconds (0 disables the timeout)",
2324
+ description: "Default timeout for irc wait (and send await:true) in milliseconds; 0 disables the timeout",
2310
2325
  options: [
2311
2326
  { value: "0", label: "Disabled" },
2312
2327
  { value: "30000", label: "30 seconds" },
@@ -2497,7 +2512,7 @@ export const SETTINGS_SCHEMA = {
2497
2512
  // Async jobs
2498
2513
  "async.enabled": {
2499
2514
  type: "boolean",
2500
- default: false,
2515
+ default: true,
2501
2516
  ui: {
2502
2517
  tab: "tools",
2503
2518
  label: "Async Execution",
@@ -2747,31 +2762,14 @@ export const SETTINGS_SCHEMA = {
2747
2762
  },
2748
2763
  },
2749
2764
 
2750
- "task.simple": {
2751
- type: "enum",
2752
- values: TASK_SIMPLE_MODES,
2753
- default: "schema-free",
2765
+ "task.batch": {
2766
+ type: "boolean",
2767
+ default: true,
2754
2768
  ui: {
2755
2769
  tab: "tasks",
2756
- label: "Task Input Mode",
2757
- description: "How much shared structure the task tool accepts (default, schema-free, or independent)",
2758
- options: [
2759
- {
2760
- value: "default",
2761
- label: "Default",
2762
- description: "Shared context and custom task schema are available",
2763
- },
2764
- {
2765
- value: "schema-free",
2766
- label: "Schema-free",
2767
- description: "Shared context stays available, but custom task schema is disabled",
2768
- },
2769
- {
2770
- value: "independent",
2771
- label: "Independent",
2772
- description: "No shared context or custom task schema; each task must stand alone",
2773
- },
2774
- ],
2770
+ label: "Batch Task Calls",
2771
+ description:
2772
+ "Switch the task tool to its batch shape: one call carries { agent, context, tasks[] } — one subagent per item (with per-item isolation) and a required shared context prepended to every assignment. With async.enabled=true, each spawn runs as an independent background agent with the normal idle/parked lifecycle; otherwise the call blocks for merged results. Disable to restore the flat single-spawn schema.",
2775
2773
  },
2776
2774
  },
2777
2775
 
@@ -2841,6 +2839,34 @@ export const SETTINGS_SCHEMA = {
2841
2839
  },
2842
2840
  },
2843
2841
 
2842
+ "task.agentIdleTtlMs": {
2843
+ type: "number",
2844
+ default: 420_000,
2845
+ ui: {
2846
+ tab: "tasks",
2847
+ label: "Agent Idle TTL",
2848
+ description:
2849
+ "How long an idle subagent stays live in memory before being parked to disk (ms). Parked agents are revived automatically when messaged or resumed. 0 keeps idle agents live until exit.",
2850
+ },
2851
+ },
2852
+
2853
+ "task.softRequestBudget": {
2854
+ type: "number",
2855
+ default: 90,
2856
+ ui: {
2857
+ tab: "tasks",
2858
+ label: "Soft Subagent Request Budget",
2859
+ description:
2860
+ "Soft per-subagent request budget (assistant requests per run). Crossing it injects one steering notice asking the subagent to wrap up; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled explore/quick_task agents use a lower built-in budget.",
2861
+ options: [
2862
+ { value: "0", label: "Disabled" },
2863
+ { value: "40", label: "40 requests" },
2864
+ { value: "90", label: "90 requests", description: "Default" },
2865
+ { value: "150", label: "150 requests" },
2866
+ ],
2867
+ },
2868
+ },
2869
+
2844
2870
  "task.disabledAgents": {
2845
2871
  type: "array",
2846
2872
  default: [] as string[],
@@ -3353,7 +3379,7 @@ export type TreeFilterMode = SettingValue<"treeFilterMode">;
3353
3379
 
3354
3380
  export interface CompactionSettings {
3355
3381
  enabled: boolean;
3356
- strategy: "context-full" | "handoff" | "shake" | "off";
3382
+ strategy: "context-full" | "handoff" | "shake" | "snapcompact" | "off";
3357
3383
  thresholdPercent: number;
3358
3384
  thresholdTokens: number;
3359
3385
  reserveTokens: number;
@@ -3365,6 +3391,7 @@ export interface CompactionSettings {
3365
3391
  idleEnabled: boolean;
3366
3392
  idleThresholdTokens: number;
3367
3393
  idleTimeoutSeconds: number;
3394
+ supersedeReads: boolean;
3368
3395
  }
3369
3396
 
3370
3397
  export interface ContextPromotionSettings {
@@ -745,6 +745,13 @@ export class Settings {
745
745
  delete isolationObj.enabled;
746
746
  }
747
747
 
748
+ // task.simple: removed — the task tool no longer accepts a per-call
749
+ // schema (workflows drive structured output via eval agent()) and the
750
+ // batch/context shape is gated by task.batch instead.
751
+ if (taskObj && "simple" in taskObj) {
752
+ delete taskObj.simple;
753
+ }
754
+
748
755
  // task.isolation.mode: legacy values from before the pi-iso PAL refactor.
749
756
  // `worktree` was git worktree → now lives under `rcopy`. `fuse-overlay`
750
757
  // and `fuse-projfs` are now the platform-named `overlayfs` / `projfs`
@@ -12,13 +12,17 @@ import { renderDiff as renderDiffColored } from "../modes/components/diff";
12
12
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
13
13
  import type { OutputMeta } from "../tools/output-meta";
14
14
  import {
15
+ cachedRenderedString,
16
+ createRenderedStringCache,
15
17
  formatDiagnostics,
16
18
  formatExpandHint,
17
19
  formatStatusIcon,
18
20
  getDiffStats,
19
21
  getLspBatchRequest,
22
+ invalidateRenderedStringCache,
20
23
  type LspBatchRequest,
21
24
  PREVIEW_LIMITS,
25
+ type RenderedStringCache,
22
26
  replaceTabs,
23
27
  shortenPath,
24
28
  truncateDiffByHunk,
@@ -138,6 +142,26 @@ export interface EditRenderContext {
138
142
  }
139
143
 
140
144
  const EDIT_STREAMING_PREVIEW_LINES = 12;
145
+
146
+ function plainDiffRender(diffText: string): string {
147
+ return diffText;
148
+ }
149
+
150
+ /**
151
+ * Lazily grown per-file preview cache slots: the file count of a streaming
152
+ * multi-file patch is discovered mid-stream, so a fixed-size array would
153
+ * silently bypass caching for late files.
154
+ */
155
+ function previewCacheAt(caches: RenderedStringCache[] | undefined, index: number): RenderedStringCache | undefined {
156
+ if (!caches) return undefined;
157
+ let cache = caches[index];
158
+ if (cache === undefined) {
159
+ cache = createRenderedStringCache();
160
+ caches[index] = cache;
161
+ }
162
+ return cache;
163
+ }
164
+
141
165
  const CALL_TEXT_PREVIEW_LINES = 6;
142
166
  const CALL_TEXT_PREVIEW_WIDTH = 80;
143
167
 
@@ -313,7 +337,6 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
313
337
  }
314
338
  return preview.trimEnd();
315
339
  }
316
-
317
340
  function formatStreamingDiff(
318
341
  diff: string,
319
342
  rawPath: string,
@@ -321,26 +344,30 @@ function formatStreamingDiff(
321
344
  expanded: boolean,
322
345
  label = "streaming",
323
346
  spinnerFrame?: number,
347
+ cache?: RenderedStringCache,
324
348
  ): string {
325
349
  if (!diff) return "";
326
- // Collapsed uses a "Cursor" tail window: pin the last
327
- // EDIT_STREAMING_PREVIEW_LINES rows to the bottom so freshly streamed changes
328
- // stay on screen. The whole-file diff is recomputed on every streamed chunk
329
- // and its Myers alignment is not monotonic in payload length, so a hunk-aware
330
- // window stutters as rows move between hunks. Expanded deliberately lifts that
331
- // cap for the approval-time full view.
332
- const allLines = diff.replace(/\n+$/u, "").split("\n");
333
- const hiddenLines = expanded ? 0 : Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
334
- const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
335
- let text = "\n\n";
336
- if (hiddenLines > 0) {
337
- const hiddenHunks = getDiffStats(allLines.slice(0, hiddenLines).join("\n")).hunks;
338
- const remainder: string[] = [];
339
- if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
340
- remainder.push(`${hiddenLines} more lines`);
341
- text += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
342
- }
343
- text += renderDiffColored(visible.join("\n"), { filePath: rawPath });
350
+ let text = cachedRenderedString(cache, uiTheme, expanded, rawPath, diff, () => {
351
+ // Collapsed uses a "Cursor" tail window: pin the last
352
+ // EDIT_STREAMING_PREVIEW_LINES rows to the bottom so freshly streamed changes
353
+ // stay on screen. The whole-file diff is recomputed on every streamed chunk
354
+ // and its Myers alignment is not monotonic in payload length, so a hunk-aware
355
+ // window stutters as rows move between hunks. Expanded deliberately lifts that
356
+ // cap for the approval-time full view.
357
+ const allLines = diff.replace(/\n+$/u, "").split("\n");
358
+ const hiddenLines = expanded ? 0 : Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
359
+ const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
360
+ let rendered = "\n\n";
361
+ if (hiddenLines > 0) {
362
+ const hiddenHunks = getDiffStats(allLines.slice(0, hiddenLines).join("\n")).hunks;
363
+ const remainder: string[] = [];
364
+ if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
365
+ remainder.push(`${hiddenLines} more lines`);
366
+ rendered += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
367
+ }
368
+ rendered += renderDiffColored(visible.join("\n"), { filePath: rawPath });
369
+ return rendered;
370
+ });
344
371
  // The animated glyph rides this trailing line — inside the transcript's
345
372
  // volatile-tail holdback — never the block header: an animating head row
346
373
  // pins the native-scrollback commit boundary at the top of the block, so a
@@ -360,9 +387,11 @@ function formatMultiFileStreamingDiff(
360
387
  uiTheme: Theme,
361
388
  expanded: boolean,
362
389
  spinnerFrame?: number,
390
+ caches?: RenderedStringCache[],
363
391
  ): string {
364
392
  const parts: string[] = [];
365
- for (const preview of previews) {
393
+ for (let index = 0; index < previews.length; index++) {
394
+ const preview = previews[index]!;
366
395
  if (!preview.diff && !preview.error) continue;
367
396
  const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
368
397
  if (preview.error) {
@@ -373,9 +402,10 @@ function formatMultiFileStreamingDiff(
373
402
  // Only the last file's preview carries the animated streaming glyph;
374
403
  // earlier files have settled and must stay byte-stable so their rows
375
404
  // can commit to native scrollback mid-stream.
376
- const isLast = preview === previews[previews.length - 1];
405
+ const isLast = index === previews.length - 1;
406
+ const cache = previewCacheAt(caches, index);
377
407
  parts.push(
378
- `${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined)}`,
408
+ `${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined, cache)}`,
379
409
  );
380
410
  }
381
411
  }
@@ -389,16 +419,18 @@ function getCallPreview(
389
419
  renderContext: EditRenderContext | undefined,
390
420
  expanded: boolean,
391
421
  spinnerFrame?: number,
422
+ caches?: RenderedStringCache[],
392
423
  ): string {
393
424
  const multi = renderContext?.perFileDiffPreview;
394
425
  if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
395
- return formatMultiFileStreamingDiff(multi, uiTheme, expanded, spinnerFrame);
426
+ return formatMultiFileStreamingDiff(multi, uiTheme, expanded, spinnerFrame, caches);
396
427
  }
428
+ const cache = previewCacheAt(caches, 0);
397
429
  if (args.previewDiff) {
398
- return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview", spinnerFrame);
430
+ return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview", spinnerFrame, cache);
399
431
  }
400
432
  if (args.diff && args.op) {
401
- return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded, "streaming", spinnerFrame);
433
+ return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded, "streaming", spinnerFrame, cache);
402
434
  }
403
435
  if (args.diff) {
404
436
  return renderPlainTextPreview(args.diff, uiTheme, rawPath);
@@ -492,30 +524,32 @@ function formatDiffStatsSuffix(diff: string, uiTheme: Theme): string {
492
524
  ].filter(value => value !== undefined);
493
525
  return ` ${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${stats.join(uiTheme.fg("dim", "/"))}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
494
526
  }
495
-
496
527
  function renderDiffSection(
497
528
  diff: string,
498
529
  rawPath: string,
499
530
  expanded: boolean,
500
531
  uiTheme: Theme,
501
532
  renderDiffFn: (t: string, o?: { filePath?: string }) => string,
533
+ cache?: RenderedStringCache,
502
534
  ): string {
503
- const {
504
- text: truncatedDiff,
505
- hiddenHunks,
506
- hiddenLines,
507
- } = expanded
508
- ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
509
- : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
510
-
511
- let text = `\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
512
- if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
513
- const remainder: string[] = [];
514
- if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
515
- if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
516
- text += uiTheme.fg("toolOutput", `\n… (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`);
517
- }
518
- return text;
535
+ return cachedRenderedString(cache, uiTheme, expanded, rawPath, diff, () => {
536
+ const {
537
+ text: truncatedDiff,
538
+ hiddenHunks,
539
+ hiddenLines,
540
+ } = expanded
541
+ ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
542
+ : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
543
+
544
+ let text = `\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
545
+ if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
546
+ const remainder: string[] = [];
547
+ if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
548
+ if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
549
+ text += uiTheme.fg("toolOutput", `\n… (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`);
550
+ }
551
+ return text;
552
+ });
519
553
  }
520
554
 
521
555
  function wrapEditRendererLine(line: string, width: number): string[] {
@@ -574,6 +608,7 @@ export const editToolRenderer = {
574
608
  if (Array.isArray(editArgs.edits)) {
575
609
  fileCount = countEditFiles(editArgs.edits);
576
610
  }
611
+ const callPreviewCaches: RenderedStringCache[] = [];
577
612
  return framedBlock(uiTheme, width => {
578
613
  // Static pending icon, never the animated glyph: the header is the
579
614
  // head row of the framed block, and native-scrollback commits are
@@ -588,7 +623,15 @@ export const editToolRenderer = {
588
623
  rename,
589
624
  extraSuffix: fileCount > 1 ? uiTheme.fg("dim", ` (+${fileCount - 1} more)`) : undefined,
590
625
  });
591
- let body = getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded, options?.spinnerFrame);
626
+ let body = getCallPreview(
627
+ editArgs,
628
+ rawPath,
629
+ uiTheme,
630
+ renderContext,
631
+ options.expanded,
632
+ options?.spinnerFrame,
633
+ callPreviewCaches,
634
+ );
592
635
  if (applyPatchSummary?.error) {
593
636
  body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), Math.max(1, width - 2)))}`;
594
637
  }
@@ -652,11 +695,18 @@ function renderSingleFileResult(
652
695
  (result.content?.find(c => c.type === "text")?.text ?? "")
653
696
  : "";
654
697
 
698
+ let diffSectionRenderDiffFn: ((t: string, o?: { filePath?: string }) => string) | undefined;
699
+ const diffSectionCache = createRenderedStringCache();
700
+
655
701
  return framedBlock(uiTheme, width => {
656
702
  const { expanded, renderContext } = options;
657
703
  const editDiffPreview = renderContext?.editDiffPreview;
658
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
704
+ const renderDiffFn = renderContext?.renderDiff ?? plainDiffRender;
659
705
 
706
+ if (diffSectionRenderDiffFn !== renderDiffFn) {
707
+ diffSectionRenderDiffFn = renderDiffFn;
708
+ invalidateRenderedStringCache(diffSectionCache);
709
+ }
660
710
  const firstChangedLine =
661
711
  (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
662
712
  (details && !isError ? details.firstChangedLine : undefined);
@@ -681,11 +731,11 @@ function renderSingleFileResult(
681
731
  if (isError) {
682
732
  if (errorText) body = uiTheme.fg("error", replaceTabs(errorText, rawPath));
683
733
  } else if (details?.diff) {
684
- body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
734
+ body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
685
735
  } else if (editDiffPreview) {
686
736
  if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath));
687
737
  else if (editDiffPreview.diff)
688
- body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
738
+ body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
689
739
  }
690
740
  if (details?.diagnostics) {
691
741
  body += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
@@ -99,6 +99,7 @@ function singleResult(options: ExecutorOptions, overrides: Partial<SingleResult>
99
99
  truncated: false,
100
100
  durationMs: 1,
101
101
  tokens: 0,
102
+ requests: 0,
102
103
  ...overrides,
103
104
  };
104
105
  }
@@ -178,7 +179,7 @@ describe("runEvalAgent", () => {
178
179
  expect(runSpy).not.toHaveBeenCalled();
179
180
  });
180
181
 
181
- it("passes the parent execution context and only sets outputSchema when schema is supplied", async () => {
182
+ it("passes parent execution options and only sets outputSchema when schema is supplied", async () => {
182
183
  mockAgents();
183
184
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
184
185
  const abortController = new AbortController();
@@ -186,7 +187,7 @@ describe("runEvalAgent", () => {
186
187
  const session = makeSession({ depth: 2, activeModel: "p/current", modelString: "p/fallback" });
187
188
 
188
189
  await runEvalAgent(
189
- { prompt: " hello ", context: " context ", label: "My Agent", model: "p/override", schema },
190
+ { prompt: " hello ", label: "My Agent", model: "p/override", schema },
190
191
  { session, signal: abortController.signal },
191
192
  );
192
193
  await runEvalAgent({ prompt: "plain" }, { session });
@@ -199,7 +200,6 @@ describe("runEvalAgent", () => {
199
200
  expect(firstOptions.parentActiveModelPattern).toBe("p/current");
200
201
  expect(firstOptions.outputSchema).toBe(schema);
201
202
  expect(firstOptions.assignment).toBe("hello");
202
- expect(firstOptions.context).toBe("context");
203
203
  expect(firstOptions.description).toBe("My Agent");
204
204
  expect(firstOptions.modelOverride).toEqual(["p/override"]);
205
205
  expect(secondOptions.outputSchema).toBeUndefined();
@@ -542,6 +542,7 @@ describe("agent() through eval runtimes", () => {
542
542
  recentOutput: [],
543
543
  toolCount: 0,
544
544
  tokens: 0,
545
+ requests: 0,
545
546
  cost: 0,
546
547
  durationMs: 0,
547
548
  ...overrides,
@@ -674,6 +675,7 @@ describe("agent() through eval runtimes", () => {
674
675
  recentOutput: [],
675
676
  toolCount: i,
676
677
  tokens: 0,
678
+ requests: 0,
677
679
  cost: 0,
678
680
  durationMs: i * 10,
679
681
  });
@@ -34,7 +34,6 @@ const agentArgsSchema = z.object({
34
34
  prompt: z.string().min(1, "prompt must be a non-empty string"),
35
35
  agentType: z.string().min(1).optional(),
36
36
  model: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]).optional(),
37
- context: z.string().optional(),
38
37
  label: z.string().optional(),
39
38
  schema: z.unknown().optional(),
40
39
  });
@@ -43,7 +42,6 @@ interface EvalAgentArgs {
43
42
  prompt: string;
44
43
  agentType?: string;
45
44
  model?: string | string[];
46
- context?: string;
47
45
  label?: string;
48
46
  schema?: unknown;
49
47
  }
@@ -111,7 +109,7 @@ function assertNotPlanMode(session: ToolSession): void {
111
109
  }
112
110
 
113
111
  function renderSubagentPrompt(assignment: string): string {
114
- return prompt.render(subagentUserPromptTemplate, { assignment: assignment.trim(), independentMode: false });
112
+ return prompt.render(subagentUserPromptTemplate, { assignment: assignment.trim() });
115
113
  }
116
114
 
117
115
  function trimToUndefined(value: string | undefined): string | undefined {
@@ -135,20 +133,12 @@ function getOutputManager(session: ToolSession): AgentOutputManager {
135
133
  async function getArtifacts(session: ToolSession): Promise<{
136
134
  sessionFile: string | null;
137
135
  artifactsDir: string;
138
- contextFile?: string;
139
136
  }> {
140
137
  const sessionFile = session.getSessionFile();
141
138
  const sessionArtifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
142
139
  const artifactsDir = sessionArtifactsDir ?? path.join(os.tmpdir(), `omp-eval-agent-${Snowflake.next()}`);
143
140
  await fs.mkdir(artifactsDir, { recursive: true });
144
-
145
- const shouldWriteConversationContext = session.settings.get("irc.enabled") !== true;
146
- const compactContext = shouldWriteConversationContext ? session.getCompactContext?.() : undefined;
147
- if (!compactContext) return { sessionFile, artifactsDir };
148
-
149
- const contextFile = path.join(artifactsDir, "context.md");
150
- await Bun.write(contextFile, compactContext);
151
- return { sessionFile, artifactsDir, contextFile };
141
+ return { sessionFile, artifactsDir };
152
142
  }
153
143
 
154
144
  function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undefined, progress: AgentProgress): void {
@@ -246,11 +236,10 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
246
236
  };
247
237
  const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
248
238
  const mcpManager = options.session.mcpManager ?? MCPManager.instance();
249
- const { sessionFile, artifactsDir, contextFile } = await getArtifacts(options.session);
239
+ const { sessionFile, artifactsDir } = await getArtifacts(options.session);
250
240
  const outputManager = getOutputManager(options.session);
251
241
  const id = await outputManager.allocate(outputIdBase(parsed.label, agentName));
252
242
  const assignment = parsed.prompt.trim();
253
- const context = trimToUndefined(parsed.context);
254
243
  // Suspend eval timeout accounting while the subagent owns control. The
255
244
  // timeout clock restarts once the bridge returns to the cell runtime.
256
245
  const result = await withBridgeTimeoutPause(options.emitStatus, () =>
@@ -259,7 +248,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
259
248
  agent: effectiveAgent,
260
249
  task: renderSubagentPrompt(assignment),
261
250
  assignment,
262
- context,
263
251
  description: trimToUndefined(parsed.label),
264
252
  index: 0,
265
253
  id,
@@ -271,7 +259,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
271
259
  sessionFile,
272
260
  persistArtifacts: Boolean(sessionFile),
273
261
  artifactsDir,
274
- contextFile,
275
262
  // Eval `agent()` subagents are short-lived programmatic helpers (data
276
263
  // collection, structured output, parallel() fan-out). LSP server
277
264
  // cold-start costs tens of seconds and is pure overhead here, so it is
@@ -65,7 +65,7 @@ if (!globalThis.__omp_js_prelude_loaded__) {
65
65
  };
66
66
 
67
67
  const agent = async (prompt, opts, ...rest) => {
68
- const o = optionsArg("agent", opts, rest, "{ agentType, model, context, label, schema }");
68
+ const o = optionsArg("agent", opts, rest, "{ agentType, model, label, schema }");
69
69
  const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...o });
70
70
  const text = res && typeof res === "object" ? res.text : res;
71
71
  return hasOwn(o, "schema") ? JSON.parse(text) : text;
@@ -519,21 +519,20 @@ if "__omp_prelude_loaded__" not in globals():
519
519
  text = res.get("text") if isinstance(res, dict) else res
520
520
  return json.loads(text) if schema is not None else text
521
521
 
522
- def agent(prompt, *, agent_type="task", model=None, context=None, label=None, schema=None):
522
+ def agent(prompt, *, agent_type="task", model=None, label=None, schema=None):
523
523
  """Run a subagent and return its final output.
524
524
 
525
525
  `agent_type` selects the subagent definition (default "task"). Pass
526
- `model` to override that agent's model, `context` for shared background,
527
- `label` for the output artifact id, and `schema` to request structured
528
- JSON output; when `schema` is supplied the parsed object is returned.
526
+ `model` to override that agent's model, `label` for the output artifact
527
+ id, and `schema` to request structured JSON output; when `schema` is
528
+ supplied the parsed object is returned. Share background by writing a
529
+ local:// file and referencing it in the prompt.
529
530
  """
530
531
  args = {"prompt": prompt}
531
532
  if agent_type is not None:
532
533
  args["agentType"] = agent_type
533
534
  if model is not None:
534
535
  args["model"] = model
535
- if context is not None:
536
- args["context"] = context
537
536
  if label is not None:
538
537
  args["label"] = label
539
538
  if schema is not None: