@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.0

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 (180) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-be1f2t8h.md} +92 -0
  3. package/dist/cli.js +9986 -9776
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/settings-schema.d.ts +63 -8
  13. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  14. package/dist/types/cursor.d.ts +193 -8
  15. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  16. package/dist/types/edit/hashline/diff.d.ts +9 -1
  17. package/dist/types/edit/index.d.ts +8 -1
  18. package/dist/types/edit/streaming.d.ts +8 -1
  19. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  20. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  21. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  22. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  23. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  24. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  25. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  26. package/dist/types/lsp/client.d.ts +2 -0
  27. package/dist/types/main.d.ts +2 -0
  28. package/dist/types/mcp/manager.d.ts +33 -4
  29. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  30. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  31. package/dist/types/modes/components/session-selector.d.ts +6 -0
  32. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  33. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  34. package/dist/types/modes/components/usage-row.d.ts +2 -0
  35. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  36. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  37. package/dist/types/modes/interactive-mode.d.ts +4 -2
  38. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  39. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  40. package/dist/types/modes/theme/theme.d.ts +12 -1
  41. package/dist/types/modes/types.d.ts +2 -1
  42. package/dist/types/sdk.d.ts +1 -0
  43. package/dist/types/session/agent-session-events.d.ts +2 -0
  44. package/dist/types/session/agent-session-types.d.ts +36 -1
  45. package/dist/types/session/agent-session.d.ts +4 -2
  46. package/dist/types/session/claude-session-store.d.ts +13 -0
  47. package/dist/types/session/codex-session-store.d.ts +14 -0
  48. package/dist/types/session/credential-pin.d.ts +58 -0
  49. package/dist/types/session/foreign-session-import.d.ts +15 -0
  50. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  51. package/dist/types/session/foreign-session-store.d.ts +23 -0
  52. package/dist/types/session/session-advisors.d.ts +47 -2
  53. package/dist/types/session/session-entries.d.ts +19 -1
  54. package/dist/types/session/session-handoff.d.ts +4 -1
  55. package/dist/types/session/session-loader.d.ts +2 -0
  56. package/dist/types/session/session-maintenance.d.ts +1 -1
  57. package/dist/types/session/session-manager.d.ts +21 -0
  58. package/dist/types/session/session-tools.d.ts +25 -1
  59. package/dist/types/session/turn-recovery.d.ts +10 -4
  60. package/dist/types/system-prompt.d.ts +2 -1
  61. package/dist/types/task/executor.d.ts +11 -2
  62. package/dist/types/task/render.d.ts +7 -0
  63. package/dist/types/thinking.d.ts +18 -6
  64. package/dist/types/tools/acp-bridge.d.ts +28 -5
  65. package/dist/types/tools/grep.d.ts +17 -1
  66. package/dist/types/tools/hub/launch.d.ts +5 -1
  67. package/dist/types/tools/index.d.ts +4 -1
  68. package/dist/types/tools/path-utils.d.ts +19 -0
  69. package/dist/types/tools/read.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +2 -1
  71. package/dist/types/utils/changelog.d.ts +19 -0
  72. package/dist/types/utils/git.d.ts +10 -0
  73. package/dist/types/utils/jj.d.ts +11 -4
  74. package/dist/types/web/search/types.d.ts +2 -2
  75. package/package.json +12 -12
  76. package/src/advisor/runtime.ts +138 -12
  77. package/src/advisor/transcript-recorder.ts +56 -0
  78. package/src/auto-thinking/classifier.ts +48 -10
  79. package/src/cli/args.ts +6 -0
  80. package/src/cli/flag-tables.ts +2 -0
  81. package/src/cli/gallery-fixtures/fs.ts +25 -0
  82. package/src/cli/session-picker.ts +34 -17
  83. package/src/cli/ttsr-cli.ts +19 -1
  84. package/src/cli.ts +9 -0
  85. package/src/collab/guest.ts +37 -4
  86. package/src/collab/host.ts +1 -0
  87. package/src/commands/launch.ts +6 -0
  88. package/src/config/model-registry.ts +103 -25
  89. package/src/config/settings-schema.ts +65 -6
  90. package/src/config/settings.ts +25 -0
  91. package/src/cursor-bridge-tools.ts +81 -0
  92. package/src/cursor.ts +463 -12
  93. package/src/edit/edit-clipboard.ts +23 -0
  94. package/src/edit/hashline/diff.ts +49 -10
  95. package/src/edit/hashline/execute.ts +38 -6
  96. package/src/edit/hashline/filesystem.ts +27 -3
  97. package/src/edit/index.ts +12 -2
  98. package/src/edit/renderer.ts +4 -4
  99. package/src/edit/streaming.ts +17 -2
  100. package/src/export/html/template.js +1 -1
  101. package/src/extensibility/extensions/runner.ts +70 -0
  102. package/src/extensibility/extensions/types.ts +30 -0
  103. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  104. package/src/internal-urls/local-protocol.ts +3 -2
  105. package/src/internal-urls/mcp-protocol.ts +4 -1
  106. package/src/launch/terminal-output-worker-client.ts +53 -0
  107. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  108. package/src/launch/terminal-output-worker.ts +23 -0
  109. package/src/lsp/client.ts +18 -3
  110. package/src/lsp/index.ts +4 -0
  111. package/src/lsp/render.ts +1 -1
  112. package/src/main.ts +127 -56
  113. package/src/mcp/manager.ts +120 -18
  114. package/src/modes/acp/acp-agent.ts +15 -9
  115. package/src/modes/components/chat-transcript-builder.ts +30 -11
  116. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  117. package/src/modes/components/read-tool-group.ts +159 -17
  118. package/src/modes/components/session-selector.ts +19 -3
  119. package/src/modes/components/status-line/component.ts +381 -54
  120. package/src/modes/components/tool-execution.ts +78 -31
  121. package/src/modes/components/usage-row.ts +10 -5
  122. package/src/modes/controllers/event-controller.ts +26 -4
  123. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  124. package/src/modes/controllers/selector-controller.ts +94 -36
  125. package/src/modes/controllers/tan-command-controller.ts +14 -0
  126. package/src/modes/interactive-mode.ts +31 -17
  127. package/src/modes/rpc/rpc-client.ts +18 -0
  128. package/src/modes/rpc/rpc-mode.ts +16 -1
  129. package/src/modes/rpc/rpc-types.ts +11 -0
  130. package/src/modes/theme/theme.ts +151 -6
  131. package/src/modes/types.ts +2 -1
  132. package/src/modes/utils/ui-helpers.ts +38 -21
  133. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  134. package/src/prompts/system/plan-mode-active.md +2 -2
  135. package/src/prompts/system/system-prompt.md +3 -0
  136. package/src/prompts/system/xdev-mount-notice.md +1 -1
  137. package/src/prompts/tools/bash.md +15 -17
  138. package/src/prompts/tools/checkpoint.md +1 -1
  139. package/src/prompts/tools/glob.md +7 -6
  140. package/src/prompts/tools/grep.md +7 -6
  141. package/src/sdk.ts +178 -15
  142. package/src/session/agent-session-events.ts +1 -0
  143. package/src/session/agent-session-types.ts +40 -1
  144. package/src/session/agent-session.ts +302 -175
  145. package/src/session/claude-session-store.ts +426 -0
  146. package/src/session/codex-session-store.ts +673 -0
  147. package/src/session/credential-pin.ts +93 -0
  148. package/src/session/foreign-session-import.ts +52 -0
  149. package/src/session/foreign-session-jsonl.ts +29 -0
  150. package/src/session/foreign-session-store.ts +26 -0
  151. package/src/session/model-controls.ts +11 -3
  152. package/src/session/session-advisors.ts +140 -16
  153. package/src/session/session-entries.ts +21 -1
  154. package/src/session/session-handoff.ts +20 -3
  155. package/src/session/session-loader.ts +25 -9
  156. package/src/session/session-maintenance.ts +59 -11
  157. package/src/session/session-manager.ts +61 -0
  158. package/src/session/session-tools.ts +107 -5
  159. package/src/session/settings-stream-fn.ts +1 -0
  160. package/src/session/turn-recovery.ts +97 -51
  161. package/src/slash-commands/builtin-registry.ts +131 -3
  162. package/src/system-prompt.ts +3 -2
  163. package/src/task/executor.ts +17 -4
  164. package/src/task/render.ts +20 -4
  165. package/src/thinking.ts +29 -10
  166. package/src/tools/acp-bridge.ts +52 -8
  167. package/src/tools/browser.ts +5 -0
  168. package/src/tools/checkpoint.ts +0 -13
  169. package/src/tools/grep.ts +64 -8
  170. package/src/tools/hub/launch.ts +10 -15
  171. package/src/tools/index.ts +28 -4
  172. package/src/tools/path-utils.ts +88 -0
  173. package/src/tools/read.ts +3 -0
  174. package/src/tools/write.ts +9 -3
  175. package/src/tools/xdev.ts +39 -14
  176. package/src/utils/changelog.ts +144 -4
  177. package/src/utils/git.ts +61 -14
  178. package/src/utils/jj.ts +19 -13
  179. package/src/web/search/providers/codex.ts +34 -0
  180. package/src/web/search/types.ts +1 -1
@@ -94,6 +94,14 @@ const STARTUP_TIMEOUT_MS = 250;
94
94
  const RECONNECT_BURST_WINDOW_MS = 30_000;
95
95
  const RECONNECT_BURST_LIMIT = 5;
96
96
 
97
+ /**
98
+ * Bounded buffer for notifications received before any listener attaches.
99
+ * Mirrors {@link IrcBus}'s `MAILBOX_CAP` — drop-oldest on overflow. Drained
100
+ * into the first {@link MCPManager.addNotificationListener} subscriber, then
101
+ * cleared; subsequent frames deliver directly to attached listeners.
102
+ */
103
+ const NOTIFICATION_BUFFER_CAP = 100;
104
+
97
105
  function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
98
106
  const tracked: TrackedPromise<T> = { promise, status: "pending" };
99
107
  promise.then(
@@ -194,8 +202,14 @@ export class MCPManager {
194
202
  #sources = new Map<string, SourceMeta>();
195
203
  #authStorage: AuthStorage | null = null;
196
204
  #authHandler?: MCPAuthHandler;
197
- #onNotification?: (serverName: string, method: string, params: unknown) => void;
198
- #onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void;
205
+ #notificationListeners = new Set<(serverName: string, method: string, params: unknown) => void>();
206
+ /**
207
+ * Notifications received before any listener attached, to be drained on
208
+ * the first {@link addNotificationListener} call. Bounded by
209
+ * {@link NOTIFICATION_BUFFER_CAP}, drop-oldest on overflow.
210
+ */
211
+ #pendingNotifications: Array<{ server: string; method: string; params: unknown }> = [];
212
+ #onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void | Promise<void>;
199
213
  #onResourcesChanged?: (serverName: string, uri: string) => void;
200
214
  #onPromptsChanged?: (serverName: string) => void;
201
215
  #notificationsEnabled = false;
@@ -219,16 +233,66 @@ export class MCPManager {
219
233
  ) {}
220
234
 
221
235
  /**
222
- * Set a callback to receive all server notifications.
236
+ * Register a listener for server-initiated MCP notifications.
237
+ *
238
+ * The listener is called for every JSON-RPC notification received from any
239
+ * connected server, AFTER the manager's own handling of known methods
240
+ * (`notifications/tools/list_changed`, `notifications/resources/list_changed`,
241
+ * `notifications/resources/updated`, `notifications/prompts/list_changed`).
242
+ * For list-change methods the internal refresh promise is awaited before
243
+ * fanout, so listeners observe up-to-date manager and tool state. Unknown
244
+ * or server-custom methods are also delivered, letting consumers bridge
245
+ * server-initiated events into session-level behavior (e.g. an extension
246
+ * injecting a steer via `pi.sendMessage`).
247
+ *
248
+ * Notifications received before any listener attached are buffered
249
+ * (bounded FIFO, cap {@link NOTIFICATION_BUFFER_CAP}, drop-oldest) and
250
+ * drained into the first subscriber — matches {@link setOnPromptsChanged}'s
251
+ * replay-on-attach and {@link IrcBus}'s mailbox semantics.
252
+ *
253
+ * Returns an unsubscribe function; call it to remove the listener.
254
+ *
255
+ * Multiple listeners are allowed; each is invoked with independent error
256
+ * isolation — a listener that throws does not prevent other listeners from
257
+ * firing.
223
258
  */
224
- setOnNotification(handler: (serverName: string, method: string, params: unknown) => void): void {
225
- this.#onNotification = handler;
259
+ addNotificationListener(listener: (serverName: string, method: string, params: unknown) => void): () => void {
260
+ const wasEmpty = this.#notificationListeners.size === 0;
261
+ this.#notificationListeners.add(listener);
262
+
263
+ // Drain startup-buffered notifications into the first attaching listener.
264
+ if (wasEmpty && this.#pendingNotifications.length > 0) {
265
+ const pending = this.#pendingNotifications.splice(0);
266
+ for (const frame of pending) {
267
+ try {
268
+ listener(frame.server, frame.method, frame.params);
269
+ } catch (error) {
270
+ logger.debug("MCP notification listener threw during buffered drain", {
271
+ path: `mcp:${frame.server}`,
272
+ method: frame.method,
273
+ error,
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ return () => {
280
+ this.#notificationListeners.delete(listener);
281
+ };
226
282
  }
227
283
 
228
284
  /**
229
285
  * Set a callback to fire when any server's tools change.
286
+ *
287
+ * May return a Promise; if so, {@link refreshServerTools} awaits it so that
288
+ * downstream consumers (e.g. `mcp_notification` listeners for
289
+ * `notifications/tools/list_changed`) observe not just the manager's
290
+ * refreshed tool set but also any session-level rebind driven by the
291
+ * handler (`session.refreshMCPTools`). Other callsites (initial connect,
292
+ * disconnect, reconnect) invoke the handler synchronously — their downstream
293
+ * chains don't need to serialize on the rebind.
230
294
  */
231
- setOnToolsChanged(handler: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void): void {
295
+ setOnToolsChanged(handler: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void | Promise<void>): void {
232
296
  this.#onToolsChanged = handler;
233
297
  }
234
298
 
@@ -488,7 +552,7 @@ export class MCPManager {
488
552
  this.reconnectServer(name, options);
489
553
  const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
490
554
  this.#replaceServerTools(name, customTools);
491
- this.#onToolsChanged?.(this.#tools);
555
+ void this.#onToolsChanged?.(this.#tools);
492
556
  void this.toolCache?.set(name, config, serverTools);
493
557
 
494
558
  onStatus?.({ type: "connected", serverName: name });
@@ -597,7 +661,7 @@ export class MCPManager {
597
661
  sortMCPToolsByName(this.#tools);
598
662
  }
599
663
 
600
- #triggerNotificationRefresh(serverName: string, kind: "tools" | "resources" | "prompts"): void {
664
+ #triggerNotificationRefresh(serverName: string, kind: "tools" | "resources" | "prompts"): Promise<void> {
601
665
  const refresh = (() => {
602
666
  switch (kind) {
603
667
  case "tools":
@@ -608,22 +672,33 @@ export class MCPManager {
608
672
  return this.refreshServerPrompts(serverName);
609
673
  }
610
674
  })();
611
- void refresh.catch(error => {
675
+ return refresh.catch(error => {
612
676
  logger.debug("Failed MCP notification refresh", { path: `mcp:${serverName}`, kind, error });
613
677
  });
614
678
  }
615
- #handleServerNotification(serverName: string, method: string, params: unknown): void {
679
+ async #handleServerNotification(serverName: string, method: string, params: unknown): Promise<void> {
616
680
  logger.debug("MCP notification received", { path: `mcp:${serverName}`, method });
617
681
 
682
+ // Only trigger refresh if the connection is already stored — during the
683
+ // initial connect handshake, notifications may arrive before
684
+ // `#connections.set()` completes, and `refreshServer*` would no-op
685
+ // anyway. Skipping the await in that case preserves arrival order
686
+ // across concurrently-dispatched notifications (an awaited refresh,
687
+ // even a no-op, yields a microtask that lets later frames overtake).
688
+ const connectionKnown = this.#connections.has(serverName);
689
+ let refreshPromise: Promise<void> | undefined;
618
690
  switch (method) {
619
691
  case MCPNotificationMethods.TOOLS_LIST_CHANGED:
620
- this.#triggerNotificationRefresh(serverName, "tools");
692
+ if (connectionKnown) refreshPromise = this.#triggerNotificationRefresh(serverName, "tools");
621
693
  break;
622
694
  case MCPNotificationMethods.RESOURCES_LIST_CHANGED:
623
- this.#triggerNotificationRefresh(serverName, "resources");
695
+ if (connectionKnown) refreshPromise = this.#triggerNotificationRefresh(serverName, "resources");
624
696
  break;
625
697
  case MCPNotificationMethods.RESOURCES_UPDATED: {
626
- const uri = (params as { uri?: string })?.uri;
698
+ const uri =
699
+ params && typeof params === "object" && "uri" in params && typeof params.uri === "string"
700
+ ? params.uri
701
+ : undefined;
627
702
  const subscribed = this.#subscribedResources.get(serverName);
628
703
  if (uri && subscribed?.has(uri)) {
629
704
  this.#onResourcesChanged?.(serverName, uri);
@@ -631,13 +706,40 @@ export class MCPManager {
631
706
  break;
632
707
  }
633
708
  case MCPNotificationMethods.PROMPTS_LIST_CHANGED:
634
- this.#triggerNotificationRefresh(serverName, "prompts");
709
+ if (connectionKnown) refreshPromise = this.#triggerNotificationRefresh(serverName, "prompts");
635
710
  break;
636
711
  default:
637
712
  break;
638
713
  }
639
714
 
640
- this.#onNotification?.(serverName, method, params);
715
+ // Await internal refresh so listeners see the manager's post-refresh
716
+ // state (satisfies the documented "AFTER the manager's own handling"
717
+ // contract on `addNotificationListener` — otherwise an extension acting
718
+ // on `tools/list_changed` could hit stale `getTools()`).
719
+ if (refreshPromise) {
720
+ await refreshPromise;
721
+ }
722
+
723
+ // Buffer for late-attaching subscribers when no listener exists yet.
724
+ if (this.#notificationListeners.size === 0) {
725
+ this.#pendingNotifications.push({ server: serverName, method, params });
726
+ if (this.#pendingNotifications.length > NOTIFICATION_BUFFER_CAP) {
727
+ this.#pendingNotifications.shift();
728
+ }
729
+ return;
730
+ }
731
+
732
+ for (const listener of this.#notificationListeners) {
733
+ try {
734
+ listener(serverName, method, params);
735
+ } catch (error) {
736
+ logger.debug("MCP notification listener threw", {
737
+ path: `mcp:${serverName}`,
738
+ method,
739
+ error,
740
+ });
741
+ }
742
+ }
641
743
  }
642
744
 
643
745
  /** Handle server-to-client JSON-RPC requests (e.g. ping, roots/list). */
@@ -781,7 +883,7 @@ export class MCPManager {
781
883
  // Remove tools from this server and notify consumers
782
884
  const hadTools = this.#tools.some(t => t.mcpServerName === name);
783
885
  this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
784
- if (hadTools) this.#onToolsChanged?.(this.#tools);
886
+ if (hadTools) void this.#onToolsChanged?.(this.#tools);
785
887
 
786
888
  // Notify prompt consumers so stale commands are cleared
787
889
  if (connection?.prompts?.length) this.#onPromptsChanged?.(name);
@@ -1022,7 +1124,7 @@ export class MCPManager {
1022
1124
  const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
1023
1125
  void this.toolCache?.set(name, config, serverTools);
1024
1126
  this.#replaceServerTools(name, customTools);
1025
- this.#onToolsChanged?.(this.#tools);
1127
+ void this.#onToolsChanged?.(this.#tools);
1026
1128
  void this.#loadServerResourcesAndPrompts(name, connection);
1027
1129
  return connection;
1028
1130
  } catch (error) {
@@ -1075,7 +1177,7 @@ export class MCPManager {
1075
1177
 
1076
1178
  // Replace tools from this server
1077
1179
  this.#replaceServerTools(name, customTools);
1078
- this.#onToolsChanged?.(this.#tools);
1180
+ await this.#onToolsChanged?.(this.#tools);
1079
1181
  }
1080
1182
 
1081
1183
  /**
@@ -651,12 +651,17 @@ export class AcpAgent implements Agent {
651
651
  });
652
652
  }
653
653
 
654
- // For `thinking` the lifetime subscription pushes post-bootstrap; only
655
- // push here when it's not yet installed so pre-bootstrap callers still
656
- // see the change without a post-bootstrap duplicate.
657
- const thinkingHandledBySubscription =
658
- params.configId === THINKING_CONFIG_ID && record.lifetimeUnsubscribe !== undefined;
659
- if (!thinkingHandledBySubscription) {
654
+ // For `model`/`thinking`, `#setModelById`/`#setThinkingLevelById` change
655
+ // the session model/thinking level through AgentSession, which now emits
656
+ // a lifetime event (`model_changed`/`thinking_level_changed`) that
657
+ // `#handleLifetimeEvent` turns into a push once the subscription is
658
+ // installed. Only push here when that subscription is not yet
659
+ // installed, so pre-bootstrap callers still see the change without a
660
+ // post-bootstrap duplicate.
661
+ const handledBySubscription =
662
+ (params.configId === THINKING_CONFIG_ID || params.configId === MODEL_CONFIG_ID) &&
663
+ record.lifetimeUnsubscribe !== undefined;
664
+ if (!handledBySubscription) {
660
665
  await this.#pushConfigOptionUpdate(record);
661
666
  }
662
667
  return { configOptions: this.#buildConfigOptions(record.session) };
@@ -1150,14 +1155,15 @@ export class AcpAgent implements Agent {
1150
1155
  }
1151
1156
 
1152
1157
  async #handleLifetimeEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
1153
- if (event.type !== "thinking_level_changed") {
1158
+ if (event.type !== "thinking_level_changed" && event.type !== "model_changed") {
1154
1159
  return;
1155
1160
  }
1156
1161
  try {
1157
1162
  await this.#pushConfigOptionUpdate(record);
1158
1163
  } catch (error) {
1159
- logger.warn("Failed to push thinking-level config_option_update", {
1164
+ logger.warn("Failed to push config_option_update after a lifetime event", {
1160
1165
  sessionId: record.session.sessionId,
1166
+ eventType: event.type,
1161
1167
  error,
1162
1168
  });
1163
1169
  }
@@ -1575,7 +1581,7 @@ export class AcpAgent implements Agent {
1575
1581
  #buildThinkingOptions(session: AgentSession): Array<{ value: string; name: string; description?: string }> {
1576
1582
  return [
1577
1583
  { value: THINKING_OFF, name: "Off" },
1578
- { value: AUTO_THINKING, name: "Auto", description: "Auto-detect per prompt (low–xhigh)" },
1584
+ { value: AUTO_THINKING, name: "Auto", description: "Auto-detect per prompt" },
1579
1585
  ...session.getAvailableThinkingLevels().map(level => ({
1580
1586
  value: level,
1581
1587
  name: level,
@@ -51,7 +51,7 @@ import {
51
51
  import { CustomMessageComponent } from "./custom-message";
52
52
  import { EvalExecutionComponent } from "./eval-execution";
53
53
  import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message";
54
- import { ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group";
54
+ import { groupedReadUsageCallIds, ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group";
55
55
  import { SkillMessageComponent } from "./skill-message";
56
56
  import { ToolExecutionComponent } from "./tool-execution";
57
57
  import { TranscriptContainer } from "./transcript-container";
@@ -86,6 +86,7 @@ export class ChatTranscriptBuilder {
86
86
  #pendingUsageDuration: number | undefined;
87
87
  #pendingUsageTtft: number | undefined;
88
88
  #pendingUsageTimestamp: number | undefined;
89
+ #pendingReadUsageCallIds: string[] | undefined;
89
90
  #lastAssistantUsage: Usage | undefined;
90
91
  #waitingPoll: ToolExecutionComponent | null = null;
91
92
  #todoSnapshot: ToolExecutionComponent | null = null;
@@ -135,6 +136,7 @@ export class ChatTranscriptBuilder {
135
136
  this.#pendingUsageDuration = undefined;
136
137
  this.#pendingUsageTtft = undefined;
137
138
  this.#pendingUsageTimestamp = undefined;
139
+ this.#pendingReadUsageCallIds = undefined;
138
140
  this.#lastAssistantUsage = undefined;
139
141
  this.#waitingPoll = null;
140
142
  this.#todoSnapshot = null;
@@ -194,30 +196,46 @@ export class ChatTranscriptBuilder {
194
196
  return this.#readGroup;
195
197
  }
196
198
 
197
- // The per-turn token-usage row must land below the turn's tool blocks, but
198
- // normal `read` calls only materialize their group in #appendToolResult. Defer
199
- // the row: stash it on the assistant message and flush once the turn's tools
200
- // are placed, sealing the read run so the row sits under it.
199
+ // Defer per-turn metrics until the turn's tool results have materialized.
200
+ // Read-only invisible turns attach the metrics to their shared compact
201
+ // group; every other turn keeps the standalone row below its tool blocks.
201
202
  #flushPendingUsage(): void {
202
203
  if (!this.#pendingUsage) return;
203
- this.#readGroup?.seal();
204
- this.#readGroup = null;
205
- this.container.addChild(
206
- createUsageRowBlock(
204
+ const usageAttached =
205
+ this.#pendingReadUsageCallIds !== undefined &&
206
+ (this.#readGroup?.attachUsage(
207
+ this.#pendingReadUsageCallIds,
207
208
  this.#pendingUsage,
208
209
  this.#pendingUsageDuration,
209
210
  this.#pendingUsageTtft,
210
211
  this.#pendingUsageTimestamp,
211
- ),
212
- );
212
+ ) ??
213
+ false);
214
+ if (!usageAttached) {
215
+ this.#readGroup?.seal();
216
+ this.#readGroup = null;
217
+ this.container.addChild(
218
+ createUsageRowBlock(
219
+ this.#pendingUsage,
220
+ this.#pendingUsageDuration,
221
+ this.#pendingUsageTtft,
222
+ this.#pendingUsageTimestamp,
223
+ ),
224
+ );
225
+ }
213
226
  this.#pendingUsage = undefined;
214
227
  this.#pendingUsageDuration = undefined;
215
228
  this.#pendingUsageTtft = undefined;
216
229
  this.#pendingUsageTimestamp = undefined;
230
+ this.#pendingReadUsageCallIds = undefined;
217
231
  }
218
232
 
219
233
  #appendChatMessage(message: AgentMessage): void {
220
234
  if (message.role !== "toolResult") this.#flushPendingUsage();
235
+ if (message.role !== "assistant" && message.role !== "toolResult") {
236
+ this.#readGroup?.seal();
237
+ this.#readGroup = null;
238
+ }
221
239
  switch (message.role) {
222
240
  case "assistant":
223
241
  this.#appendAssistantMessage(message);
@@ -403,6 +421,7 @@ export class ChatTranscriptBuilder {
403
421
  this.#pendingUsageDuration = message.duration;
404
422
  this.#pendingUsageTtft = message.ttft;
405
423
  this.#pendingUsageTimestamp = message.timestamp;
424
+ this.#pendingReadUsageCallIds = this.#pendingUsage ? groupedReadUsageCallIds(message) : undefined;
406
425
  }
407
426
 
408
427
  #appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {