@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -19,6 +19,7 @@ import {
19
19
  } from "@oh-my-pi/pi-tui";
20
20
  import { getSessionsDir } from "@oh-my-pi/pi-utils";
21
21
  import { DynamicBorder } from "../modes/components/dynamic-border";
22
+ import { TranscriptBlock } from "../modes/components/transcript-container";
22
23
  import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
23
24
  import type { InteractiveModeContext } from "../modes/types";
24
25
  import { formatBytes } from "../tools/render-utils";
@@ -150,13 +151,13 @@ export class DebugSelectorComponent extends Container {
150
151
  }
151
152
 
152
153
  // Show message and wait for keypress
153
- this.ctx.chatContainer.addChild(new Spacer(1));
154
- this.ctx.chatContainer.addChild(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
155
- this.ctx.chatContainer.addChild(new Spacer(1));
156
- this.ctx.chatContainer.addChild(
154
+ const block = new TranscriptBlock();
155
+ block.addChild(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
156
+ block.addChild(new Spacer(1));
157
+ block.addChild(
157
158
  new Text(theme.fg("muted", "Reproduce the performance issue, then press Enter to stop profiling."), 1, 0),
158
159
  );
159
- this.ctx.ui.requestRender();
160
+ this.ctx.present(block);
160
161
 
161
162
  // Wait for Enter keypress
162
163
  const { promise, resolve } = Promise.withResolvers<void>();
@@ -201,19 +202,16 @@ export class DebugSelectorComponent extends Container {
201
202
  loader.stop();
202
203
  this.ctx.statusContainer.clear();
203
204
 
204
- this.ctx.chatContainer.addChild(new Spacer(1));
205
- this.ctx.chatContainer.addChild(
206
- new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0),
207
- );
208
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
209
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
205
+ const block = new TranscriptBlock();
206
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0));
207
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
208
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
209
+ this.ctx.present(block);
210
210
  } catch (err) {
211
211
  loader.stop();
212
212
  this.ctx.statusContainer.clear();
213
213
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
214
214
  }
215
-
216
- this.ctx.ui.requestRender();
217
215
  }
218
216
 
219
217
  async #handleWorkReport(): Promise<void> {
@@ -231,15 +229,13 @@ export class DebugSelectorComponent extends Container {
231
229
 
232
230
  openPath(tmpPath);
233
231
 
234
- this.ctx.chatContainer.addChild(new Spacer(1));
235
- this.ctx.chatContainer.addChild(
232
+ this.ctx.present([
233
+ new Spacer(1),
236
234
  new Text(theme.fg("dim", `Opened flamegraph (${workProfile.sampleCount} samples)`), 1, 0),
237
- );
235
+ ]);
238
236
  } catch (err) {
239
237
  this.ctx.showError(`Failed to open profile: ${err instanceof Error ? err.message : String(err)}`);
240
238
  }
241
-
242
- this.ctx.ui.requestRender();
243
239
  }
244
240
 
245
241
  async #handleDumpReport(): Promise<void> {
@@ -262,19 +258,16 @@ export class DebugSelectorComponent extends Container {
262
258
  loader.stop();
263
259
  this.ctx.statusContainer.clear();
264
260
 
265
- this.ctx.chatContainer.addChild(new Spacer(1));
266
- this.ctx.chatContainer.addChild(
267
- new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0),
268
- );
269
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
270
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
261
+ const block = new TranscriptBlock();
262
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0));
263
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
264
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
265
+ this.ctx.present(block);
271
266
  } catch (err) {
272
267
  loader.stop();
273
268
  this.ctx.statusContainer.clear();
274
269
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
275
270
  }
276
-
277
- this.ctx.ui.requestRender();
278
271
  }
279
272
 
280
273
  async #handleMemoryReport(): Promise<void> {
@@ -301,19 +294,16 @@ export class DebugSelectorComponent extends Container {
301
294
  loader.stop();
302
295
  this.ctx.statusContainer.clear();
303
296
 
304
- this.ctx.chatContainer.addChild(new Spacer(1));
305
- this.ctx.chatContainer.addChild(
306
- new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0),
307
- );
308
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
309
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
297
+ const block = new TranscriptBlock();
298
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0));
299
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
300
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
301
+ this.ctx.present(block);
310
302
  } catch (err) {
311
303
  loader.stop();
312
304
  this.ctx.statusContainer.clear();
313
305
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
314
306
  }
315
-
316
- this.ctx.ui.requestRender();
317
307
  }
318
308
 
319
309
  async #handleViewLogs(): Promise<void> {
@@ -365,15 +355,14 @@ export class DebugSelectorComponent extends Container {
365
355
  const info = await collectSystemInfo();
366
356
  const formatted = formatSystemInfo(info);
367
357
 
368
- this.ctx.chatContainer.addChild(new Spacer(1));
369
- this.ctx.chatContainer.addChild(new DynamicBorder());
370
- this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
371
- this.ctx.chatContainer.addChild(new DynamicBorder());
358
+ const block = new TranscriptBlock();
359
+ block.addChild(new DynamicBorder());
360
+ block.addChild(new Text(formatted, 1, 0));
361
+ block.addChild(new DynamicBorder());
362
+ this.ctx.present(block);
372
363
  } catch (err) {
373
364
  this.ctx.showError(`Failed to collect system info: ${err instanceof Error ? err.message : String(err)}`);
374
365
  }
375
-
376
- this.ctx.ui.requestRender();
377
366
  }
378
367
 
379
368
  async #handleViewTerminalState(): Promise<void> {
@@ -384,11 +373,11 @@ export class DebugSelectorComponent extends Container {
384
373
  });
385
374
  const formatted = formatTerminalState(info);
386
375
 
387
- this.ctx.chatContainer.addChild(new Spacer(1));
388
- this.ctx.chatContainer.addChild(new DynamicBorder());
389
- this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
390
- this.ctx.chatContainer.addChild(new DynamicBorder());
391
- this.ctx.ui.requestRender();
376
+ const block = new TranscriptBlock();
377
+ block.addChild(new DynamicBorder());
378
+ block.addChild(new Text(formatted, 1, 0));
379
+ block.addChild(new DynamicBorder());
380
+ this.ctx.present(block);
392
381
  }
393
382
 
394
383
  async #handleViewProtocols(): Promise<void> {
@@ -407,15 +396,14 @@ export class DebugSelectorComponent extends Container {
407
396
  TERMINAL.sendNotification(notification);
408
397
  }
409
398
 
410
- this.ctx.chatContainer.addChild(new Spacer(1));
411
- this.ctx.chatContainer.addChild(
399
+ this.ctx.present([
400
+ new Spacer(1),
412
401
  new ProtocolProbeComponent({
413
402
  image: buildSampleImage(),
414
403
  imageBudget: this.ctx.ui.imageBudget,
415
404
  notificationSuppressed: suppressed,
416
405
  }),
417
- );
418
- this.ctx.ui.requestRender();
406
+ ]);
419
407
  }
420
408
 
421
409
  async #handleTranscriptExport(): Promise<void> {
@@ -487,21 +475,19 @@ export class DebugSelectorComponent extends Container {
487
475
  loader.stop();
488
476
  this.ctx.statusContainer.clear();
489
477
 
490
- this.ctx.chatContainer.addChild(new Spacer(1));
491
- this.ctx.chatContainer.addChild(
478
+ this.ctx.present([
479
+ new Spacer(1),
492
480
  new Text(
493
481
  theme.fg("success", `${theme.status.success} Cleared ${result.removed} artifact directories`),
494
482
  1,
495
483
  0,
496
484
  ),
497
- );
485
+ ]);
498
486
  } catch (err) {
499
487
  loader.stop();
500
488
  this.ctx.statusContainer.clear();
501
489
  this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
502
490
  }
503
-
504
- this.ctx.ui.requestRender();
505
491
  }
506
492
 
507
493
  #getResolvedSettings(): Record<string, unknown> {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { HL_FILE_PREFIX, HL_FILE_SUFFIX } from "@oh-my-pi/hashline";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
- import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
7
+ import { visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
8
8
  import { sanitizeText } from "@oh-my-pi/pi-utils";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { FileDiagnosticsResult } from "../lsp";
@@ -16,7 +16,6 @@ import {
16
16
  formatDiffStats,
17
17
  formatExpandHint,
18
18
  formatStatusIcon,
19
- formatTitle,
20
19
  getDiffStats,
21
20
  getLspBatchRequest,
22
21
  type LspBatchRequest,
@@ -25,7 +24,7 @@ import {
25
24
  shortenPath,
26
25
  truncateDiffByHunk,
27
26
  } from "../tools/render-utils";
28
- import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
27
+ import { fileHyperlink, framedBlock, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
29
28
  import type { EditMode } from "../utils/edit-mode";
30
29
  import type { DiffError, DiffResult } from "./diff";
31
30
  import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
@@ -186,10 +185,14 @@ function getOperationTitle(op: Operation | undefined): string {
186
185
  function formatEditPathDisplay(
187
186
  rawPath: string,
188
187
  uiTheme: Theme,
189
- options?: { rename?: string; firstChangedLine?: number },
188
+ options?: { rename?: string; firstChangedLine?: number; linkPath?: string; renameLinkPath?: string },
190
189
  ): string {
190
+ // `rawPath`/`rename` are shown (cwd-relative) but the OSC 8 link targets the
191
+ // absolute path when known — a relative `rawPath` would yield a `file:///rel`
192
+ // URI that resolves against filesystem root instead of cwd.
193
+ const linkTarget = options?.linkPath || rawPath;
191
194
  let pathDisplay = rawPath
192
- ? fileHyperlink(rawPath, uiTheme.fg("accent", shortenPath(rawPath)))
195
+ ? fileHyperlink(linkTarget, uiTheme.fg("accent", shortenPath(rawPath)))
193
196
  : uiTheme.fg("toolOutput", "…");
194
197
 
195
198
  if (options?.firstChangedLine) {
@@ -197,7 +200,8 @@ function formatEditPathDisplay(
197
200
  }
198
201
 
199
202
  if (options?.rename) {
200
- pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(options.rename, uiTheme.fg("accent", shortenPath(options.rename)))}`;
203
+ const renameTarget = options.renameLinkPath || options.rename;
204
+ pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(renameTarget, uiTheme.fg("accent", shortenPath(options.rename)))}`;
201
205
  }
202
206
 
203
207
  return pathDisplay;
@@ -206,7 +210,7 @@ function formatEditPathDisplay(
206
210
  function formatEditDescription(
207
211
  rawPath: string,
208
212
  uiTheme: Theme,
209
- options?: { rename?: string; firstChangedLine?: number },
213
+ options?: { rename?: string; firstChangedLine?: number; linkPath?: string; renameLinkPath?: string },
210
214
  ): { language: string; description: string } {
211
215
  const language = getLanguageFromPath(rawPath) ?? "text";
212
216
  const icon = uiTheme.fg("muted", uiTheme.getLangIcon(language));
@@ -459,23 +463,31 @@ export const editToolRenderer = {
459
463
  const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
460
464
  const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op;
461
465
  const { description } = formatEditDescription(rawPath, uiTheme, { rename });
462
- const spinner =
463
- options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
464
- let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
465
- // Show file count hint for multi-file edits
466
466
  let fileCount = hashlineInputSummary?.entries.length ?? applyPatchSummary?.entries.length ?? 0;
467
467
  if (Array.isArray(editArgs.edits)) {
468
468
  fileCount = countEditFiles(editArgs.edits);
469
469
  }
470
- if (fileCount > 1) {
471
- text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
472
- }
473
- text += getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded);
474
- if (applyPatchSummary?.error) {
475
- text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
476
- }
477
-
478
- return new Text(text, 0, 0);
470
+ return framedBlock(uiTheme, width => {
471
+ let header = renderStatusLine(
472
+ { icon: "pending", spinnerFrame: options?.spinnerFrame, title: getOperationTitle(op), description },
473
+ uiTheme,
474
+ );
475
+ if (fileCount > 1) header += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
476
+ let body = getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded);
477
+ if (applyPatchSummary?.error) {
478
+ body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), Math.max(1, width - 2)))}`;
479
+ }
480
+ const bodyLines = body ? body.split("\n") : [];
481
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
482
+ return {
483
+ header,
484
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
485
+ state: applyPatchSummary?.error ? "error" : "pending",
486
+ borderColor: applyPatchSummary?.error ? "error" : "borderMuted",
487
+ width,
488
+ contentPaddingLeft: 0,
489
+ };
490
+ });
479
491
  },
480
492
 
481
493
  renderResult(
@@ -525,65 +537,57 @@ function renderSingleFileResult(
525
537
  (result.content?.find(c => c.type === "text")?.text ?? "")
526
538
  : "";
527
539
 
528
- let cached: RenderCache | undefined;
529
-
530
- return {
531
- render(width) {
532
- const { expanded, renderContext } = options;
533
- const editDiffPreview = renderContext?.editDiffPreview;
534
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
535
- const key = new Hasher().bool(expanded).u32(width).digest();
536
- if (cached?.key === key) return cached.lines;
537
-
538
- const firstChangedLine =
539
- (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
540
- (details && !isError ? details.firstChangedLine : undefined);
541
- const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
542
-
543
- // Change stats ride inline on the header next to the path rather than a separate row.
544
- const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
545
- const headerDiff = isError ? undefined : details?.diff || previewDiff;
546
- const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
547
-
548
- const header = renderStatusLine(
549
- {
550
- icon: isError ? "error" : "success",
551
- title: getOperationTitle(op),
552
- description,
553
- },
554
- uiTheme,
540
+ return framedBlock(uiTheme, width => {
541
+ const { expanded, renderContext } = options;
542
+ const editDiffPreview = renderContext?.editDiffPreview;
543
+ const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
544
+
545
+ const firstChangedLine =
546
+ (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
547
+ (details && !isError ? details.firstChangedLine : undefined);
548
+ const linkPath = details && "path" in details ? details.path : undefined;
549
+ const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine, linkPath });
550
+
551
+ // Change stats ride inline on the header bar next to the path.
552
+ const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
553
+ const headerDiff = isError ? undefined : details?.diff || previewDiff;
554
+ const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
555
+ const header =
556
+ renderStatusLine({ icon: isError ? "error" : "success", title: getOperationTitle(op), description }, uiTheme) +
557
+ statsSuffix;
558
+
559
+ let body = "";
560
+ if (isError) {
561
+ if (errorText) body = uiTheme.fg("error", replaceTabs(errorText, rawPath));
562
+ } else if (details?.diff) {
563
+ body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
564
+ } else if (editDiffPreview) {
565
+ if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath));
566
+ else if (editDiffPreview.diff)
567
+ body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
568
+ }
569
+ if (details?.diagnostics) {
570
+ body += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
571
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
555
572
  );
556
- let text = header + statsSuffix;
557
-
558
- if (isError) {
559
- if (errorText) {
560
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText, rawPath))}`;
561
- }
562
- } else if (details?.diff) {
563
- text += renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
564
- } else if (editDiffPreview) {
565
- if ("error" in editDiffPreview) {
566
- text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath))}`;
567
- } else if (editDiffPreview.diff) {
568
- text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
569
- }
570
- }
571
-
572
- if (details?.diagnostics) {
573
- text += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
574
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
575
- );
576
- }
573
+ }
577
574
 
578
- const lines =
579
- width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n");
580
- cached = { key, lines };
581
- return lines;
582
- },
583
- invalidate() {
584
- cached = undefined;
585
- },
586
- };
575
+ // Diff lines self-wrap with a continuation gutter; pre-wrap to the frame's
576
+ // inner width so renderOutputBlock's generic wrap is a no-op. Edit frames
577
+ // use a flush left border because code-frame gutters already provide padding.
578
+ const innerWidth = Math.max(1, width - 2);
579
+ const bodyLines = body.length > 0 ? body.split("\n").flatMap(line => wrapEditRendererLine(line, innerWidth)) : [];
580
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
581
+
582
+ return {
583
+ header,
584
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
585
+ state: isError ? "error" : options.isPartial ? "pending" : "success",
586
+ borderColor: isError ? "error" : "borderMuted",
587
+ width,
588
+ contentPaddingLeft: 0,
589
+ };
590
+ });
587
591
  }
588
592
 
589
593
  function renderMultiFileResult(
@@ -638,7 +642,7 @@ function renderMultiFileResult(
638
642
  },
639
643
  invalidate() {
640
644
  cached = undefined;
641
- for (const c of fileComponents) c.invalidate();
645
+ for (const c of fileComponents) c.invalidate?.();
642
646
  },
643
647
  };
644
648
  }
@@ -4,6 +4,7 @@ import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
4
4
  import * as ai from "@oh-my-pi/pi-ai";
5
5
  import { Effort } from "@oh-my-pi/pi-ai";
6
6
  import { TempDir } from "@oh-my-pi/pi-utils";
7
+ import { $ } from "bun";
7
8
  import type { ModelRegistry } from "../../config/model-registry";
8
9
  import { Settings } from "../../config/settings";
9
10
  import type { ToolSession } from "../../tools";
@@ -13,7 +14,7 @@ import { IdleTimeout } from "../idle-timeout";
13
14
  import { disposeAllVmContexts } from "../js/context-manager";
14
15
  import { executeJs } from "../js/executor";
15
16
  import { runEvalLlm } from "../llm-bridge";
16
- import { disposeAllKernelSessions, executePython } from "../py/executor";
17
+ import { disposeAllKernelSessions, type PythonResult } from "../py/executor";
17
18
 
18
19
  function makeModel(provider: string, id: string, extra: Partial<Model<Api>> = {}): Model<Api> {
19
20
  return {
@@ -57,6 +58,7 @@ function makeSession(opts: SessionOptions = {}): ToolSession {
57
58
  const modelRegistry = {
58
59
  getAvailable: () => opts.available ?? [SMOL, DEFAULT, SLOW],
59
60
  getApiKey: async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey),
61
+ resolver: () => async () => (opts.apiKey === undefined ? "test-key" : opts.apiKey),
60
62
  } as unknown as ModelRegistry;
61
63
  return {
62
64
  settings,
@@ -96,6 +98,77 @@ function assistant(opts: {
96
98
  };
97
99
  }
98
100
 
101
+ async function runPythonLlmInSubprocess(options: { structured: boolean; tempDir: TempDir }): Promise<PythonResult> {
102
+ const repoRoot = path.resolve(import.meta.dir, "../../../..");
103
+ const scriptPath = path.join(options.tempDir.path(), "run-python-llm.ts");
104
+ const resultPath = path.join(options.tempDir.path(), "python-llm-result.json");
105
+ const aiPath = path.resolve(import.meta.dir, "../../../../ai/src/index.ts");
106
+ const executorPath = path.resolve(import.meta.dir, "../py/executor.ts");
107
+ const settingsPath = path.resolve(import.meta.dir, "../../config/settings.ts");
108
+ const code = options.structured
109
+ ? 'import json\nprint(json.dumps(llm("hi", schema={"type": "object"})))'
110
+ : 'print(llm("hi", model="smol"))';
111
+ const responseContent = options.structured
112
+ ? '[{ type: "toolCall", id: "tc-1", name: "respond", arguments: { ok: true } }]'
113
+ : '[{ type: "text", text: "hello from python" }]';
114
+ await Bun.write(
115
+ scriptPath,
116
+ `
117
+ import { vi } from "bun:test";
118
+ import * as ai from ${JSON.stringify(aiPath)};
119
+ import { executePython } from ${JSON.stringify(executorPath)};
120
+ import { Settings } from ${JSON.stringify(settingsPath)};
121
+
122
+ const SMOL = {
123
+ id: "smol",
124
+ name: "smol",
125
+ api: "openai-responses",
126
+ provider: "p",
127
+ baseUrl: "https://example.test/v1",
128
+ reasoning: false,
129
+ input: ["text"],
130
+ cost: { input: 1, output: 1, cacheRead: 0, cacheWrite: 1 },
131
+ contextWindow: 128000,
132
+ maxTokens: 4096,
133
+ };
134
+ const settings = Settings.isolated({ "async.enabled": false, "task.isolation.mode": "none" });
135
+ settings.setModelRole("smol", "p/smol");
136
+ settings.setModelRole("slow", "p/slow");
137
+ const session = {
138
+ settings,
139
+ modelRegistry: {
140
+ getAvailable: () => [SMOL],
141
+ getApiKey: async () => "test-key",
142
+ resolver: () => async () => "test-key",
143
+ },
144
+ getActiveModelString: () => "p/smol",
145
+ };
146
+ vi.spyOn(ai, "completeSimple").mockResolvedValue({
147
+ role: "assistant",
148
+ api: "openai-responses",
149
+ provider: "p",
150
+ model: "smol",
151
+ stopReason: "stop",
152
+ content: ${responseContent},
153
+ });
154
+ const result = await executePython(${JSON.stringify(code)}, {
155
+ cwd: ${JSON.stringify(options.tempDir.path())},
156
+ sessionId: ${JSON.stringify(`py-llm:${options.structured ? "struct" : "plain"}`)},
157
+ sessionFile: ${JSON.stringify(path.join(options.tempDir.path(), "session.jsonl"))},
158
+ toolSession: session,
159
+ kernelMode: "per-call",
160
+ });
161
+ await Bun.write(${JSON.stringify(resultPath)}, JSON.stringify(result));
162
+ process.exit(0);
163
+ `,
164
+ );
165
+ const child = await $`bun ${scriptPath}`.cwd(repoRoot).quiet().nothrow();
166
+ const stdout = child.stdout.toString();
167
+ const stderr = child.stderr.toString();
168
+ if (child.exitCode !== 0) throw new Error(stderr || stdout || `Python llm subprocess exited with ${child.exitCode}`);
169
+ return (await Bun.file(resultPath).json()) as PythonResult;
170
+ }
171
+
99
172
  describe("runEvalLlm", () => {
100
173
  afterEach(() => {
101
174
  vi.restoreAllMocks();
@@ -290,38 +363,24 @@ describe("llm() through eval runtimes", () => {
290
363
  });
291
364
 
292
365
  it("exposes llm() in the Python runtime", async () => {
293
- using tempDir = TempDir.createSync("@omp-eval-llm-py-");
294
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
295
- const sessionId = `py-llm:${crypto.randomUUID()}`;
296
- vi.spyOn(ai, "completeSimple").mockResolvedValue(assistant({ text: "hello from python" }));
297
-
298
- const result = await executePython('print(llm("hi", model="smol"))', {
299
- cwd: tempDir.path(),
300
- sessionId,
301
- sessionFile,
302
- toolSession: makeSession(),
303
- });
304
-
305
- expect(result.exitCode).toBe(0);
306
- expect(result.output.trim()).toBe("hello from python");
366
+ const tempDir = TempDir.createSync("@omp-eval-llm-py-");
367
+ try {
368
+ const result = await runPythonLlmInSubprocess({ structured: false, tempDir });
369
+ expect(result.exitCode).toBe(0);
370
+ expect(result.output.trim()).toBe("hello from python");
371
+ } finally {
372
+ tempDir.removeSync();
373
+ }
307
374
  });
308
375
 
309
376
  it("parses structured llm() output in the Python runtime", async () => {
310
- using tempDir = TempDir.createSync("@omp-eval-llm-py-struct-");
311
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
312
- const sessionId = `py-llm-struct:${crypto.randomUUID()}`;
313
- vi.spyOn(ai, "completeSimple").mockResolvedValue(
314
- assistant({ toolCall: { name: "respond", arguments: { ok: true } } }),
315
- );
316
-
317
- const result = await executePython('import json\nprint(json.dumps(llm("hi", schema={"type": "object"})))', {
318
- cwd: tempDir.path(),
319
- sessionId,
320
- sessionFile,
321
- toolSession: makeSession(),
322
- });
323
-
324
- expect(result.exitCode).toBe(0);
325
- expect(JSON.parse(result.output.trim())).toEqual({ ok: true });
377
+ const tempDir = TempDir.createSync("@omp-eval-llm-py-struct-");
378
+ try {
379
+ const result = await runPythonLlmInSubprocess({ structured: true, tempDir });
380
+ expect(result.exitCode).toBe(0);
381
+ expect(JSON.parse(result.output.trim())).toEqual({ ok: true });
382
+ } finally {
383
+ tempDir.removeSync();
384
+ }
326
385
  });
327
386
  });
@@ -15,6 +15,7 @@ import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent
15
15
  import { type Api, Effort, getSupportedEfforts, type Model, type Tool } from "@oh-my-pi/pi-ai";
16
16
  import * as z from "zod/v4";
17
17
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
18
+
18
19
  import { expandRoleAlias, formatModelString, resolveModelFromString } from "../config/model-resolver";
19
20
  import type { ToolSession } from "../tools";
20
21
  import { ToolError } from "../tools/tool-errors";
@@ -112,8 +113,9 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
112
113
  );
113
114
  }
114
115
 
115
- const apiKey = await options.session.modelRegistry?.getApiKey(model);
116
- if (!apiKey) {
116
+ const registry = options.session.modelRegistry;
117
+ const apiKey = await registry?.getApiKey(model);
118
+ if (!registry || !apiKey) {
117
119
  throw new ToolError(
118
120
  `llm() has no API key for ${formatModelString(model)}. Configure credentials for this provider or choose another tier.`,
119
121
  );
@@ -143,7 +145,10 @@ export async function runEvalLlm(args: unknown, options: EvalLlmBridgeOptions):
143
145
  tools,
144
146
  },
145
147
  {
146
- apiKey,
148
+ apiKey: registry.resolver(model.provider, {
149
+ sessionId: options.session.getSessionId?.() ?? undefined,
150
+ baseUrl: model.baseUrl,
151
+ }),
147
152
  signal: options.signal,
148
153
  reasoning: reasoningForTier(tier, model),
149
154
  toolChoice: schema ? { type: "tool", name: STRUCTURED_TOOL_NAME } : undefined,