@oh-my-pi/pi-coding-agent 15.12.2 → 15.12.4

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 (231) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/cli.js +1121 -871
  3. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  4. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  5. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  6. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  7. package/dist/types/cli/args.d.ts +0 -1
  8. package/dist/types/cli/models-cli.d.ts +49 -0
  9. package/dist/types/commands/launch.d.ts +0 -3
  10. package/dist/types/commands/models.d.ts +33 -0
  11. package/dist/types/commands/token.d.ts +25 -0
  12. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  13. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  14. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  15. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  18. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  19. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  20. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  21. package/dist/types/commit/changelog/generate.d.ts +1 -1
  22. package/dist/types/commit/shared-llm.d.ts +1 -1
  23. package/dist/types/config/model-registry.d.ts +7 -0
  24. package/dist/types/config/models-config-schema.d.ts +1 -1
  25. package/dist/types/config/settings-schema.d.ts +21 -1
  26. package/dist/types/edit/hashline/params.d.ts +1 -1
  27. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  28. package/dist/types/edit/modes/patch.d.ts +1 -1
  29. package/dist/types/edit/modes/replace.d.ts +1 -1
  30. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  31. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +2 -2
  33. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  34. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  35. package/dist/types/lsp/types.d.ts +1 -1
  36. package/dist/types/mcp/manager.d.ts +8 -0
  37. package/dist/types/mnemopi/config.d.ts +28 -0
  38. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  39. package/dist/types/modes/components/index.d.ts +1 -0
  40. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  41. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  43. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  44. package/dist/types/modes/interactive-mode.d.ts +0 -3
  45. package/dist/types/modes/types.d.ts +0 -5
  46. package/dist/types/session/agent-session.d.ts +14 -33
  47. package/dist/types/session/agent-storage.d.ts +2 -1
  48. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  49. package/dist/types/session/messages.d.ts +8 -10
  50. package/dist/types/session/session-manager.d.ts +15 -0
  51. package/dist/types/session/session-storage.d.ts +5 -0
  52. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  53. package/dist/types/task/types.d.ts +1 -1
  54. package/dist/types/tools/ask.d.ts +1 -1
  55. package/dist/types/tools/ast-edit.d.ts +1 -1
  56. package/dist/types/tools/ast-grep.d.ts +1 -1
  57. package/dist/types/tools/bash.d.ts +1 -1
  58. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  59. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  60. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  61. package/dist/types/tools/browser/registry.d.ts +16 -3
  62. package/dist/types/tools/browser/render.d.ts +2 -0
  63. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  65. package/dist/types/tools/browser.d.ts +3 -1
  66. package/dist/types/tools/checkpoint.d.ts +1 -1
  67. package/dist/types/tools/debug.d.ts +1 -1
  68. package/dist/types/tools/eval.d.ts +1 -1
  69. package/dist/types/tools/find.d.ts +1 -1
  70. package/dist/types/tools/gh.d.ts +1 -1
  71. package/dist/types/tools/image-gen.d.ts +1 -1
  72. package/dist/types/tools/index.d.ts +3 -1
  73. package/dist/types/tools/inspect-image.d.ts +1 -1
  74. package/dist/types/tools/irc.d.ts +1 -1
  75. package/dist/types/tools/job.d.ts +1 -1
  76. package/dist/types/tools/memory-edit.d.ts +1 -1
  77. package/dist/types/tools/memory-recall.d.ts +1 -1
  78. package/dist/types/tools/memory-reflect.d.ts +1 -1
  79. package/dist/types/tools/memory-retain.d.ts +1 -1
  80. package/dist/types/tools/read.d.ts +1 -1
  81. package/dist/types/tools/render-mermaid.d.ts +1 -1
  82. package/dist/types/tools/resolve.d.ts +1 -1
  83. package/dist/types/tools/review.d.ts +1 -1
  84. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  85. package/dist/types/tools/search.d.ts +1 -1
  86. package/dist/types/tools/ssh.d.ts +1 -1
  87. package/dist/types/tools/todo.d.ts +1 -1
  88. package/dist/types/tools/tts.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +1 -1
  90. package/dist/types/utils/clipboard.d.ts +4 -3
  91. package/dist/types/utils/image-loading.d.ts +18 -1
  92. package/dist/types/utils/thinking-display.d.ts +17 -0
  93. package/dist/types/web/search/index.d.ts +1 -1
  94. package/package.json +14 -14
  95. package/src/autoresearch/storage.ts +2 -1
  96. package/src/autoresearch/tools/init-experiment.ts +1 -1
  97. package/src/autoresearch/tools/log-experiment.ts +1 -1
  98. package/src/autoresearch/tools/run-experiment.ts +1 -1
  99. package/src/autoresearch/tools/update-notes.ts +1 -1
  100. package/src/cli/args.ts +0 -8
  101. package/src/cli/auth-gateway-cli.ts +1 -1
  102. package/src/cli/bench-cli.ts +1 -1
  103. package/src/cli/dry-balance-cli.ts +1 -1
  104. package/src/cli/models-cli.ts +427 -0
  105. package/src/cli-commands.ts +2 -0
  106. package/src/collab/host.ts +9 -12
  107. package/src/commands/launch.ts +0 -3
  108. package/src/commands/models.ts +61 -0
  109. package/src/commands/token.ts +89 -0
  110. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  111. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  112. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  113. package/src/commit/agentic/tools/git-overview.ts +1 -1
  114. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  115. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  116. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  117. package/src/commit/agentic/tools/schemas.ts +1 -1
  118. package/src/commit/agentic/tools/split-commit.ts +1 -1
  119. package/src/commit/analysis/summary.ts +1 -1
  120. package/src/commit/changelog/generate.ts +1 -1
  121. package/src/commit/shared-llm.ts +1 -1
  122. package/src/config/model-registry.ts +15 -12
  123. package/src/config/model-resolver.ts +2 -2
  124. package/src/config/models-config-schema.ts +1 -1
  125. package/src/config/settings-schema.ts +19 -1
  126. package/src/edit/hashline/params.ts +1 -1
  127. package/src/edit/modes/apply-patch.ts +1 -1
  128. package/src/edit/modes/patch.ts +1 -1
  129. package/src/edit/modes/replace.ts +1 -1
  130. package/src/eval/agent-bridge.ts +1 -1
  131. package/src/eval/completion-bridge.ts +1 -1
  132. package/src/export/html/template.js +24 -2
  133. package/src/export/html/tool-views.generated.js +2 -2
  134. package/src/extensibility/custom-commands/loader.ts +1 -1
  135. package/src/extensibility/custom-commands/types.ts +2 -2
  136. package/src/extensibility/custom-tools/loader.ts +1 -1
  137. package/src/extensibility/custom-tools/types.ts +2 -2
  138. package/src/extensibility/extensions/loader.ts +2 -2
  139. package/src/extensibility/extensions/types.ts +2 -2
  140. package/src/extensibility/hooks/loader.ts +1 -1
  141. package/src/extensibility/hooks/types.ts +2 -2
  142. package/src/extensibility/skills.ts +18 -3
  143. package/src/goals/tools/goal-tool.ts +1 -1
  144. package/src/internal-urls/docs-index.generated.ts +6 -3
  145. package/src/lsp/types.ts +1 -1
  146. package/src/main.ts +0 -25
  147. package/src/mcp/config-writer.ts +7 -3
  148. package/src/mcp/manager.ts +11 -0
  149. package/src/memories/index.ts +3 -1
  150. package/src/memories/storage.ts +2 -1
  151. package/src/mnemopi/config.ts +95 -11
  152. package/src/modes/acp/acp-agent.ts +5 -48
  153. package/src/modes/acp/acp-event-mapper.ts +5 -1
  154. package/src/modes/components/agent-hub.ts +2 -1
  155. package/src/modes/components/assistant-message.ts +8 -7
  156. package/src/modes/components/index.ts +1 -0
  157. package/src/modes/components/logout-account-selector.ts +130 -0
  158. package/src/modes/components/mcp-add-wizard.ts +1 -1
  159. package/src/modes/components/model-selector.ts +2 -2
  160. package/src/modes/components/status-line/component.ts +54 -157
  161. package/src/modes/components/status-line/segments.ts +1 -1
  162. package/src/modes/components/status-line/types.ts +2 -1
  163. package/src/modes/controllers/command-controller.ts +0 -12
  164. package/src/modes/controllers/event-controller.ts +23 -62
  165. package/src/modes/controllers/input-controller.ts +60 -31
  166. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  167. package/src/modes/controllers/selector-controller.ts +56 -10
  168. package/src/modes/controllers/streaming-reveal.ts +4 -3
  169. package/src/modes/interactive-mode.ts +2 -8
  170. package/src/modes/theme/theme.ts +1 -1
  171. package/src/modes/types.ts +0 -5
  172. package/src/modes/utils/ui-helpers.ts +2 -1
  173. package/src/prompts/system/empty-stop-retry.md +4 -6
  174. package/src/sdk.ts +15 -19
  175. package/src/session/agent-session.ts +125 -234
  176. package/src/session/agent-storage.ts +18 -9
  177. package/src/session/history-storage.ts +2 -1
  178. package/src/session/indexed-session-storage.ts +7 -0
  179. package/src/session/messages.ts +9 -11
  180. package/src/session/session-dump-format.ts +4 -2
  181. package/src/session/session-manager.ts +116 -0
  182. package/src/session/session-storage.ts +20 -0
  183. package/src/slash-commands/builtin-registry.ts +15 -1
  184. package/src/slash-commands/helpers/logout.ts +88 -0
  185. package/src/task/types.ts +1 -1
  186. package/src/tools/ask.ts +1 -1
  187. package/src/tools/ast-edit.ts +13 -4
  188. package/src/tools/ast-grep.ts +1 -1
  189. package/src/tools/bash.ts +1 -1
  190. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  191. package/src/tools/browser/cmux/rpc.ts +156 -0
  192. package/src/tools/browser/cmux/socket-client.ts +309 -0
  193. package/src/tools/browser/registry.ts +37 -3
  194. package/src/tools/browser/render.ts +6 -1
  195. package/src/tools/browser/tab-protocol.ts +2 -0
  196. package/src/tools/browser/tab-supervisor.ts +189 -18
  197. package/src/tools/browser/tab-worker.ts +1 -1
  198. package/src/tools/browser.ts +16 -1
  199. package/src/tools/checkpoint.ts +1 -1
  200. package/src/tools/debug.ts +1 -1
  201. package/src/tools/eval.ts +11 -6
  202. package/src/tools/fetch.ts +13 -2
  203. package/src/tools/find.ts +1 -1
  204. package/src/tools/gh.ts +1 -1
  205. package/src/tools/github-cache.ts +2 -1
  206. package/src/tools/image-gen.ts +1 -1
  207. package/src/tools/index.ts +3 -1
  208. package/src/tools/inspect-image.ts +3 -1
  209. package/src/tools/irc.ts +1 -1
  210. package/src/tools/job.ts +1 -1
  211. package/src/tools/memory-edit.ts +1 -1
  212. package/src/tools/memory-recall.ts +1 -1
  213. package/src/tools/memory-reflect.ts +1 -1
  214. package/src/tools/memory-retain.ts +1 -1
  215. package/src/tools/read.ts +8 -2
  216. package/src/tools/render-mermaid.ts +1 -1
  217. package/src/tools/report-tool-issue.ts +3 -2
  218. package/src/tools/resolve.ts +1 -1
  219. package/src/tools/review.ts +1 -1
  220. package/src/tools/search-tool-bm25.ts +1 -1
  221. package/src/tools/search.ts +1 -1
  222. package/src/tools/ssh.ts +1 -1
  223. package/src/tools/todo.ts +1 -1
  224. package/src/tools/tts.ts +1 -1
  225. package/src/tools/write.ts +1 -1
  226. package/src/utils/clipboard.ts +35 -18
  227. package/src/utils/image-loading.ts +35 -4
  228. package/src/utils/thinking-display.ts +37 -0
  229. package/src/web/search/index.ts +1 -1
  230. package/dist/types/cli/list-models.d.ts +0 -30
  231. package/src/cli/list-models.ts +0 -194
@@ -253,7 +253,7 @@ import {
253
253
  type CustomMessage,
254
254
  convertToLlm,
255
255
  type PythonExecutionMessage,
256
- readPendingDisplayTag,
256
+ readQueueChipText,
257
257
  SILENT_ABORT_MARKER,
258
258
  SKILL_PROMPT_MESSAGE_TYPE,
259
259
  stripImagesFromMessage,
@@ -862,18 +862,42 @@ function extractPermissionLocations(
862
862
  // AgentSession Class
863
863
  // ============================================================================
864
864
 
865
- /** Internal record stored in the steering/followUp display queues. The optional
866
- * `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
867
- * custom messages queued during streaming) and is matched by the custom-role
868
- * `message_start` dequeue branch; user-message pushes leave it undefined and
869
- * rely on the existing text-equality match. `images` carries the original
870
- * (pre-normalization) image blocks so queue restoration (Esc / Alt+Up) can
871
- * hand them back to the editor instead of dropping them. */
872
- type QueuedDisplayEntry = { text: string; tag?: string; images?: ImageContent[] };
873
-
874
865
  /** Entry returned by {@link AgentSession.clearQueue} / {@link AgentSession.popLastQueuedMessage}. */
875
866
  export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };
876
867
 
868
+ function queuedTextContent(message: AgentMessage): string | undefined {
869
+ if (!("content" in message)) return undefined;
870
+ const content = message.content;
871
+ if (typeof content === "string") return content;
872
+ return content.find((part): part is TextContent => part.type === "text")?.text;
873
+ }
874
+
875
+ function queuedImageContent(message: AgentMessage): ImageContent[] | undefined {
876
+ if (!("content" in message) || typeof message.content === "string") return undefined;
877
+ const images = message.content.filter(
878
+ (part): part is ImageContent =>
879
+ part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string",
880
+ );
881
+ return images.length > 0 ? images : undefined;
882
+ }
883
+
884
+ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
885
+ return !(message.role === "custom" && message.display === false);
886
+ }
887
+
888
+ function queueChipText(message: AgentMessage): string {
889
+ if (message.role === "custom") {
890
+ return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
891
+ }
892
+ const text = queuedTextContent(message) ?? "";
893
+ if (text) return text;
894
+ return queuedImageContent(message) ? "[Image]" : "";
895
+ }
896
+
897
+ function toRestoredQueuedMessage(message: AgentMessage): RestoredQueuedMessage {
898
+ return { text: queueChipText(message), images: queuedImageContent(message) };
899
+ }
900
+
877
901
  export class AgentSession {
878
902
  readonly agent: Agent;
879
903
  readonly sessionManager: SessionManager;
@@ -904,15 +928,6 @@ export class AgentSession {
904
928
  #eventListeners: AgentSessionEventListener[] = [];
905
929
  #commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
906
930
 
907
- /** Tracks pending steering messages for UI display. Removed when delivered.
908
- * Entry shape: `{ text }` for plain-text steers (user-message dequeue
909
- * matches by `.text`); `{ text, tag }` for queued custom messages (skill
910
- * invocations dispatched while streaming) — the custom-role dequeue
911
- * matches by `.tag` so duplicate-args queued skills cannot collide. */
912
- #steeringMessages: QueuedDisplayEntry[] = [];
913
- /** Tracks pending follow-up messages for UI display. Removed when delivered.
914
- * See `#steeringMessages` for entry shape. */
915
- #followUpMessages: QueuedDisplayEntry[] = [];
916
931
  /** Messages queued to be included with the next user prompt as context ("asides"). */
917
932
  #pendingNextTurnMessages: CustomMessage[] = [];
918
933
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
@@ -1058,10 +1073,6 @@ export class AgentSession {
1058
1073
  * without producing an aborted message_end). */
1059
1074
  #planCompactAbortPending = false;
1060
1075
 
1061
- /** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
1062
- * combined with `Date.now()` so tags stay unique even across rapid
1063
- * same-tick enqueues. */
1064
- #customDisplayTagCounter = 0;
1065
1076
  #postPromptTasks = new Set<Promise<unknown>>();
1066
1077
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
1067
1078
  #postPromptTasksResolve: (() => void) | undefined = undefined;
@@ -1471,28 +1482,6 @@ export class AgentSession {
1471
1482
  this.#planCompactAbortPending = false;
1472
1483
  }
1473
1484
 
1474
- /** Register a compact display string for a custom message that the caller is
1475
- * about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
1476
- * Returns a stable tag the caller MUST embed in
1477
- * `CustomMessage.details.__pendingDisplayTag` so the agent-side
1478
- * `message_start` handler can remove the matching display entry when the
1479
- * queued message is consumed.
1480
- *
1481
- * Does NOT push to the agent's steering/followUp queue — that happens
1482
- * separately inside `sendCustomMessage`. */
1483
- enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string {
1484
- const tag = `omp-cmd-${Date.now()}-${++this.#customDisplayTagCounter}`;
1485
- const displayText = text.trim();
1486
- if (!displayText) return tag;
1487
- const entry: QueuedDisplayEntry = { text: displayText, tag };
1488
- if (mode === "steer") {
1489
- this.#steeringMessages.push(entry);
1490
- } else {
1491
- this.#followUpMessages.push(entry);
1492
- }
1493
- return tag;
1494
- }
1495
-
1496
1485
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
1497
1486
  const manager = this.#asyncJobManager;
1498
1487
  if (!manager) return null;
@@ -1614,45 +1603,6 @@ export class AgentSession {
1614
1603
 
1615
1604
  /** Internal handler for agent events - shared by subscribe and reconnect */
1616
1605
  #handleAgentEvent = async (event: AgentEvent): Promise<void> => {
1617
- // When a user message starts, check if it's from either queue and remove it BEFORE emitting
1618
- // This ensures the UI sees the updated queue state
1619
- if (event.type === "message_start" && event.message.role === "user") {
1620
- const messageText = this.#getUserMessageText(event.message);
1621
- if (messageText) {
1622
- // Check steering queue first (match by .text on tagged records)
1623
- const steeringIndex = this.#steeringMessages.findIndex(e => e.text === messageText);
1624
- if (steeringIndex !== -1) {
1625
- this.#steeringMessages.splice(steeringIndex, 1);
1626
- } else {
1627
- // Check follow-up queue
1628
- const followUpIndex = this.#followUpMessages.findIndex(e => e.text === messageText);
1629
- if (followUpIndex !== -1) {
1630
- this.#followUpMessages.splice(followUpIndex, 1);
1631
- }
1632
- }
1633
- }
1634
- }
1635
-
1636
- // Tag-based dequeue for custom messages (skills queued via promptCustomMessage).
1637
- // The InputController attached a stable tag via CustomMessage.details when it
1638
- // registered the display chip; pull it back here to remove the matching entry
1639
- // from the pending bar atomically with the agent's queue consumption. Match by
1640
- // tag (not text) — two queued skills with identical args cannot collide.
1641
- if (event.type === "message_start" && event.message.role === "custom") {
1642
- const tag = readPendingDisplayTag(event.message.details);
1643
- if (tag) {
1644
- const steerIdx = this.#steeringMessages.findIndex(e => e.tag === tag);
1645
- if (steerIdx !== -1) {
1646
- this.#steeringMessages.splice(steerIdx, 1);
1647
- } else {
1648
- const followUpIdx = this.#followUpMessages.findIndex(e => e.tag === tag);
1649
- if (followUpIdx !== -1) {
1650
- this.#followUpMessages.splice(followUpIdx, 1);
1651
- }
1652
- }
1653
- }
1654
- }
1655
-
1656
1606
  // Plan-mode → compaction transition: stamp `SILENT_ABORT_MARKER` on the
1657
1607
  // persisted message BEFORE the obfuscator's display-side copy below.
1658
1608
  // Invariant (must hold across refactors): this branch precedes the
@@ -2620,18 +2570,6 @@ export class AgentSession {
2620
2570
 
2621
2571
  return Array.from(candidates);
2622
2572
  }
2623
- /** Extract text content from a message */
2624
- #getUserMessageText(message: Message): string {
2625
- if (message.role !== "user") return "";
2626
- const content = message.content;
2627
- if (typeof content === "string") return content;
2628
- const textBlocks = content.filter(c => c.type === "text");
2629
- const text = textBlocks.map(c => (c as TextContent).text).join("");
2630
- if (text.length > 0) return text;
2631
- const hasImages = content.some(c => c.type === "image");
2632
- return hasImages ? "[Image]" : "";
2633
- }
2634
-
2635
2573
  /** Find the last assistant message in agent state (including aborted ones) */
2636
2574
  #findLastAssistantMessage(): AssistantMessage | undefined {
2637
2575
  const messages = this.agent.state.messages;
@@ -3340,6 +3278,10 @@ export class AgentSession {
3340
3278
  return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
3341
3279
  }
3342
3280
 
3281
+ get isAborting(): boolean {
3282
+ return this.agent.isAborting;
3283
+ }
3284
+
3343
3285
  /** Wait until streaming and deferred recovery work are fully settled. */
3344
3286
  async waitForIdle(): Promise<void> {
3345
3287
  await this.agent.waitForIdle();
@@ -4527,13 +4469,17 @@ export class AgentSession {
4527
4469
  };
4528
4470
  }
4529
4471
 
4472
+ #normalizeImagesForModel(images: ImageContent[] | undefined): Promise<ImageContent[] | undefined> {
4473
+ return normalizeModelContextImages(images, { model: this.model });
4474
+ }
4475
+
4530
4476
  async #normalizeMessageContentImages(
4531
4477
  content: string | (TextContent | ImageContent)[],
4532
4478
  ): Promise<string | (TextContent | ImageContent)[]> {
4533
4479
  if (typeof content === "string") return content;
4534
4480
  const images = content.filter((part): part is ImageContent => part.type === "image");
4535
4481
  if (images.length === 0) return content;
4536
- const normalizedImages = await normalizeModelContextImages(images);
4482
+ const normalizedImages = await this.#normalizeImagesForModel(images);
4537
4483
  if (!normalizedImages) return content;
4538
4484
  let imageIndex = 0;
4539
4485
  return content.map(part => (part.type === "image" ? normalizedImages[imageIndex++]! : part));
@@ -4646,9 +4592,9 @@ export class AgentSession {
4646
4592
  throw new AgentBusyError();
4647
4593
  }
4648
4594
  if (options.streamingBehavior === "followUp") {
4649
- await this.#queueFollowUp(expandedText, options?.images);
4595
+ await this.#queueUserMessage(expandedText, options?.images, "followUp");
4650
4596
  } else {
4651
- await this.#queueSteer(expandedText, options?.images);
4597
+ await this.#queueUserMessage(expandedText, options?.images, "steer");
4652
4598
  }
4653
4599
  // Steer/follow-up the keyword notices alongside the queued user message.
4654
4600
  for (const notice of keywordNotices) {
@@ -4661,7 +4607,7 @@ export class AgentSession {
4661
4607
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4662
4608
  const eagerTodoPrelude =
4663
4609
  !options?.synthetic && !hasPendingUserDirective ? this.#createEagerTodoPrelude(expandedText) : undefined;
4664
- const normalizedImages = await normalizeModelContextImages(options?.images);
4610
+ const normalizedImages = await this.#normalizeImagesForModel(options?.images);
4665
4611
 
4666
4612
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
4667
4613
  if (normalizedImages) {
@@ -4699,7 +4645,7 @@ export class AgentSession {
4699
4645
 
4700
4646
  async promptCustomMessage<T = unknown>(
4701
4647
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
4702
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
4648
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice"> & { queueChipText?: string },
4703
4649
  ): Promise<void> {
4704
4650
  const textContent =
4705
4651
  typeof message.content === "string"
@@ -4723,7 +4669,10 @@ export class AgentSession {
4723
4669
  if (!options?.streamingBehavior) {
4724
4670
  throw new AgentBusyError();
4725
4671
  }
4726
- await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
4672
+ await this.sendCustomMessage(message, {
4673
+ deliverAs: options.streamingBehavior,
4674
+ queueChipText: options.queueChipText,
4675
+ });
4727
4676
  for (const notice of keywordNotices) {
4728
4677
  await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
4729
4678
  }
@@ -5060,7 +5009,7 @@ export class AgentSession {
5060
5009
  }
5061
5010
 
5062
5011
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5063
- await this.#queueSteer(expandedText, images);
5012
+ await this.#queueUserMessage(expandedText, images, "steer");
5064
5013
  }
5065
5014
 
5066
5015
  /**
@@ -5072,73 +5021,47 @@ export class AgentSession {
5072
5021
  }
5073
5022
 
5074
5023
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5075
- await this.#queueFollowUp(expandedText, images);
5024
+ await this.#queueUserMessage(expandedText, images, "followUp");
5076
5025
  }
5077
5026
 
5078
- /**
5079
- * Internal: Queue a steering message (already expanded, no extension command check).
5080
- */
5081
- async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
5082
- const normalizedImages = await normalizeModelContextImages(images);
5083
- const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5084
- this.#steeringMessages.push({ text: displayText, images });
5027
+ async #queueUserMessage(
5028
+ text: string,
5029
+ images: ImageContent[] | undefined,
5030
+ mode: "steer" | "followUp",
5031
+ ): Promise<void> {
5032
+ const normalizedImages = await this.#normalizeImagesForModel(images);
5085
5033
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5086
- if (normalizedImages && normalizedImages.length > 0) {
5034
+ if (normalizedImages?.length) {
5087
5035
  content.push(...normalizedImages);
5088
5036
  }
5089
- this.agent.steer({
5090
- role: "user",
5091
- content,
5092
- steering: true,
5093
- attribution: "user",
5094
- timestamp: Date.now(),
5095
- });
5096
- // A steer can land on an idle session: the caller checked isStreaming
5097
- // before the (potentially slow) image normalization above, so the turn
5098
- // may have ended in between. Without a drain the message would strand in
5099
- // the queue until the next manual prompt — schedule an immediate continue,
5100
- // mirroring #queueFollowUp's idle-path delivery.
5101
- if (this.#canAutoContinueForFollowUp()) {
5102
- this.#scheduleAgentContinue({
5103
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5037
+ if (mode === "followUp") {
5038
+ this.agent.followUp({
5039
+ role: "user",
5040
+ content,
5041
+ attribution: "user",
5042
+ timestamp: Date.now(),
5043
+ });
5044
+ } else {
5045
+ this.agent.steer({
5046
+ role: "user",
5047
+ content,
5048
+ steering: true,
5049
+ attribution: "user",
5050
+ timestamp: Date.now(),
5104
5051
  });
5105
5052
  }
5053
+ this.#scheduleIdleQueueDrain();
5106
5054
  }
5107
5055
 
5108
- /**
5109
- * Internal: Queue a follow-up message (already expanded, no extension command check).
5110
- */
5111
- async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5112
- const normalizedImages = await normalizeModelContextImages(images);
5113
- const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5114
- this.#followUpMessages.push({ text: displayText, images });
5115
- const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5116
- if (normalizedImages && normalizedImages.length > 0) {
5117
- content.push(...normalizedImages);
5118
- }
5119
- this.agent.followUp({
5120
- role: "user",
5121
- content,
5122
- attribution: "user",
5123
- timestamp: Date.now(),
5056
+ #scheduleIdleQueueDrain(): void {
5057
+ if (!this.#canAutoContinueForFollowUp()) return;
5058
+ this.#scheduleAgentContinue({
5059
+ shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5124
5060
  });
5125
- // When fully idle AND the session is in a resumable assistant-ended state,
5126
- // schedule an immediate continue so the queued follow-up is delivered
5127
- // without waiting for the next user turn. We gate on isStreaming (model
5128
- // actively producing), isRetrying (auto-retry backoff is sleeping between
5129
- // attempts, #retryPromise set), and the last message being assistant —
5130
- // agent.continue() only dequeues follow-ups from an assistant-ended state;
5131
- // resuming from user/toolResult state runs an extra model call on the
5132
- // stale prompt before draining the queue.
5133
- if (this.#canAutoContinueForFollowUp()) {
5134
- this.#scheduleAgentContinue({
5135
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5136
- });
5137
- }
5138
5061
  }
5139
5062
 
5140
5063
  /**
5141
- * Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
5064
+ * Gate for idle-path queued-message auto-continue. See `#scheduleIdleQueueDrain` for rationale.
5142
5065
  */
5143
5066
  #canAutoContinueForFollowUp(): boolean {
5144
5067
  if (this.isStreaming) return false;
@@ -5247,14 +5170,24 @@ export class AgentSession {
5247
5170
  */
5248
5171
  async sendCustomMessage<T = unknown>(
5249
5172
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5250
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
5173
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; queueChipText?: string },
5251
5174
  ): Promise<void> {
5175
+ const details =
5176
+ options?.queueChipText && options.deliverAs !== "nextTurn"
5177
+ ? ({
5178
+ ...((message.details && typeof message.details === "object" ? message.details : {}) as Record<
5179
+ string,
5180
+ unknown
5181
+ >),
5182
+ __queueChipText: options.queueChipText,
5183
+ } as T)
5184
+ : message.details;
5252
5185
  const appMessage: CustomMessage<T> = {
5253
5186
  role: "custom",
5254
5187
  customType: message.customType,
5255
5188
  content: message.content,
5256
5189
  display: message.display,
5257
- details: message.details,
5190
+ details,
5258
5191
  attribution: message.attribution ?? "agent",
5259
5192
  timestamp: Date.now(),
5260
5193
  };
@@ -5270,14 +5203,7 @@ export class AgentSession {
5270
5203
  } else {
5271
5204
  this.agent.steer(normalizedAppMessage);
5272
5205
  }
5273
- // The isStreaming check above can be stale: image normalization is
5274
- // awaited, so the turn may have ended in between, leaving the message
5275
- // queued on an idle agent. Mirror #queueSteer's idle-path delivery.
5276
- if (this.#canAutoContinueForFollowUp()) {
5277
- this.#scheduleAgentContinue({
5278
- shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
5279
- });
5280
- }
5206
+ this.#scheduleIdleQueueDrain();
5281
5207
  return;
5282
5208
  }
5283
5209
 
@@ -5352,11 +5278,11 @@ export class AgentSession {
5352
5278
  }
5353
5279
 
5354
5280
  if (options?.deliverAs === "followUp") {
5355
- await this.#queueFollowUp(text, images);
5281
+ await this.#queueUserMessage(text, images, "followUp");
5356
5282
  return;
5357
5283
  }
5358
5284
  if (options?.deliverAs === "steer") {
5359
- await this.#queueSteer(text, images);
5285
+ await this.#queueUserMessage(text, images, "steer");
5360
5286
  return;
5361
5287
  }
5362
5288
 
@@ -5367,57 +5293,37 @@ export class AgentSession {
5367
5293
  });
5368
5294
  }
5369
5295
 
5370
- /**
5371
- * Clear queued messages and return them (text plus any attached images).
5372
- * Useful for restoring to editor when user aborts. The internal entry
5373
- * arrays are handed out as-is — a `tag` (if any) is inert once the record
5374
- * leaves the queue.
5375
- */
5296
+ /** Clear queued messages and return them (text plus any attached images). */
5376
5297
  clearQueue(): { steering: RestoredQueuedMessage[]; followUp: RestoredQueuedMessage[] } {
5377
- const steering = this.#steeringMessages;
5378
- const followUp = this.#followUpMessages;
5379
- this.#steeringMessages = [];
5380
- this.#followUpMessages = [];
5298
+ const steering = this.agent.peekSteeringQueue().map(toRestoredQueuedMessage);
5299
+ const followUp = this.agent.peekFollowUpQueue().map(toRestoredQueuedMessage);
5381
5300
  this.agent.clearAllQueues();
5382
5301
  return { steering, followUp };
5383
5302
  }
5384
5303
 
5385
- /** Number of pending messages (includes steering, follow-up, and next-turn messages) */
5304
+ /** Number of pending displayable messages (includes steering, follow-up, and next-turn messages) */
5386
5305
  get queuedMessageCount(): number {
5387
- return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
5306
+ return (
5307
+ this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).length +
5308
+ this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).length +
5309
+ this.#pendingNextTurnMessages.length
5310
+ );
5388
5311
  }
5389
5312
 
5390
- /** Get pending messages (read-only). Returns the public text-only view;
5391
- * internal `{text, tag?}` records are mapped to `.text` so callers
5392
- * (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
5393
- * the unchanged historical shape. */
5394
5313
  getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
5395
5314
  return {
5396
- steering: this.#steeringMessages.map(e => e.text),
5397
- followUp: this.#followUpMessages.map(e => e.text),
5315
+ steering: this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
5316
+ followUp: this.agent.peekFollowUpQueue().filter(isDisplayableQueuedMessage).map(queueChipText),
5398
5317
  };
5399
5318
  }
5400
5319
 
5401
5320
  /**
5402
5321
  * Pop the last queued message (steering first, then follow-up).
5403
5322
  * Used by dequeue keybinding to restore messages to editor one at a time.
5404
- * Returns the popped entry's text and images; the tag (if any) dies with
5405
- * the record — no orphan state can outlive the queue entry.
5406
5323
  */
5407
5324
  popLastQueuedMessage(): RestoredQueuedMessage | undefined {
5408
- // Pop from steering first (LIFO)
5409
- if (this.#steeringMessages.length > 0) {
5410
- const entry = this.#steeringMessages.pop();
5411
- this.agent.popLastSteer();
5412
- return entry;
5413
- }
5414
- // Then from follow-up
5415
- if (this.#followUpMessages.length > 0) {
5416
- const entry = this.#followUpMessages.pop();
5417
- this.agent.popLastFollowUp();
5418
- return entry;
5419
- }
5420
- return undefined;
5325
+ const message = this.agent.popLastSteer() ?? this.agent.popLastFollowUp();
5326
+ return message ? toRestoredQueuedMessage(message) : undefined;
5421
5327
  }
5422
5328
 
5423
5329
  get skillsSettings(): SkillsSettings | undefined {
@@ -5499,19 +5405,6 @@ export class AgentSession {
5499
5405
  }
5500
5406
  }
5501
5407
 
5502
- /**
5503
- * Abort active work, then immediately resume the agent so queued steer/follow-up
5504
- * messages drain instead of waiting for another natural turn boundary.
5505
- */
5506
- async interruptAndFlushQueuedMessages(options?: { reason?: string }): Promise<void> {
5507
- if (!this.agent.hasQueuedMessages()) return;
5508
- await this.abort({ reason: options?.reason });
5509
- if (!this.agent.hasQueuedMessages()) return;
5510
- if (this.isCompacting || this.isGeneratingHandoff) return;
5511
- await this.#maybeRestoreRetryFallbackPrimary();
5512
- await this.agent.continue();
5513
- }
5514
-
5515
5408
  /**
5516
5409
  * Start a new session, optionally with initial messages and parent tracking.
5517
5410
  * Clears all messages and starts a new session.
@@ -5562,8 +5455,6 @@ export class AgentSession {
5562
5455
  this.#rekeyMnemopiMemoryForCurrentSessionId();
5563
5456
  this.#resetHindsightConversationTrackingIfHindsight();
5564
5457
  this.#resetMnemopiConversationTrackingIfMnemopi();
5565
- this.#steeringMessages = [];
5566
- this.#followUpMessages = [];
5567
5458
  this.#pendingNextTurnMessages = [];
5568
5459
  this.#scheduledHiddenNextTurnGeneration = undefined;
5569
5460
 
@@ -5678,7 +5569,10 @@ export class AgentSession {
5678
5569
 
5679
5570
  /**
5680
5571
  * Set model directly.
5681
- * Validates API key and saves to the active session. Persists settings only when requested.
5572
+ * Validates that a credential source is configured (synchronously, without
5573
+ * refreshing OAuth or running command-backed key programs) and saves to the
5574
+ * active session. Persists settings only when requested. The concrete key is
5575
+ * resolved lazily per request, so switching never blocks the event loop.
5682
5576
  * @throws Error if no API key available for the model
5683
5577
  */
5684
5578
  async setModel(
@@ -5687,8 +5581,7 @@ export class AgentSession {
5687
5581
  options?: { selector?: string; thinkingLevel?: ThinkingLevel; persist?: boolean },
5688
5582
  ): Promise<void> {
5689
5583
  const previousEditMode = this.#resolveActiveEditMode();
5690
- const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5691
- if (!apiKey) {
5584
+ if (!this.#modelRegistry.hasConfiguredAuth(model)) {
5692
5585
  throw new Error(`No API key for ${model.provider}/${model.id}`);
5693
5586
  }
5694
5587
 
@@ -5711,7 +5604,9 @@ export class AgentSession {
5711
5604
 
5712
5605
  /**
5713
5606
  * Set model temporarily (for this session only).
5714
- * Validates API key, saves to session log but NOT to settings.
5607
+ * Validates that a credential source is configured (synchronously, without
5608
+ * refreshing OAuth or running command-backed key programs), saves to session
5609
+ * log but NOT to settings.
5715
5610
  * @throws Error if no API key available for the model
5716
5611
  */
5717
5612
  async setModelTemporary(
@@ -5720,8 +5615,7 @@ export class AgentSession {
5720
5615
  options?: { ephemeral?: boolean },
5721
5616
  ): Promise<void> {
5722
5617
  const previousEditMode = this.#resolveActiveEditMode();
5723
- const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
5724
- if (!apiKey) {
5618
+ if (!this.#modelRegistry.hasConfiguredAuth(model)) {
5725
5619
  throw new Error(`No API key for ${model.provider}/${model.id}`);
5726
5620
  }
5727
5621
 
@@ -6705,8 +6599,6 @@ export class AgentSession {
6705
6599
  this.#rekeyMnemopiMemoryForCurrentSessionId();
6706
6600
  this.#resetHindsightConversationTrackingIfHindsight();
6707
6601
  this.#resetMnemopiConversationTrackingIfMnemopi();
6708
- this.#steeringMessages = [];
6709
- this.#followUpMessages = [];
6710
6602
  this.#pendingNextTurnMessages = [];
6711
6603
  this.#scheduledHiddenNextTurnGeneration = undefined;
6712
6604
  this.#todoReminderCount = 0;
@@ -6978,10 +6870,11 @@ export class AgentSession {
6978
6870
  #isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
6979
6871
  switch (assistantMessage.stopReason) {
6980
6872
  case "stop":
6873
+ // Reasoning/thinking-only turns are not actionable: they do not
6874
+ // answer the user and do not give the agent loop a tool call to run.
6981
6875
  for (const content of assistantMessage.content) {
6982
6876
  if (content.type === "toolCall") return false;
6983
6877
  if (content.type === "text" && hasNonWhitespace(content.text)) return false;
6984
- if (content.type === "thinking" && hasNonWhitespace(content.thinking)) return false;
6985
6878
  }
6986
6879
  return true;
6987
6880
  case "toolUse":
@@ -7315,7 +7208,7 @@ export class AgentSession {
7315
7208
  const candidate = this.#resolveContextPromotionConfiguredTarget(currentModel, availableModels);
7316
7209
  if (!candidate) return undefined;
7317
7210
  if (modelsAreEqual(candidate, currentModel)) return undefined;
7318
- if (candidate.contextWindow <= contextWindow) return undefined;
7211
+ if (candidate.contextWindow == null || candidate.contextWindow <= contextWindow) return undefined;
7319
7212
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
7320
7213
  if (!apiKey) return undefined;
7321
7214
  return candidate;
@@ -7626,7 +7519,7 @@ export class AgentSession {
7626
7519
  addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
7627
7520
  }
7628
7521
 
7629
- const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
7522
+ const sortedByContext = [...availableModels].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
7630
7523
  for (const model of sortedByContext) {
7631
7524
  if (!seen.has(this.#getModelKey(model))) {
7632
7525
  addCandidate(model);
@@ -9603,8 +9496,8 @@ export class AgentSession {
9603
9496
  // the existing message objects is sufficient and avoids structured-clone failures for
9604
9497
  // extension/custom metadata that is valid to persist but not cloneable.
9605
9498
  const previousAgentMessages = [...this.agent.state.messages];
9606
- const previousSteeringMessages = [...this.#steeringMessages];
9607
- const previousFollowUpMessages = [...this.#followUpMessages];
9499
+ const previousSteeringMessages = [...this.agent.peekSteeringQueue()];
9500
+ const previousFollowUpMessages = [...this.agent.peekFollowUpQueue()];
9608
9501
  const previousPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
9609
9502
  const previousScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
9610
9503
  const previousModel = this.model;
@@ -9621,8 +9514,7 @@ export class AgentSession {
9621
9514
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
9622
9515
  : undefined;
9623
9516
 
9624
- this.#steeringMessages = [];
9625
- this.#followUpMessages = [];
9517
+ this.agent.clearAllQueues();
9626
9518
  this.#pendingNextTurnMessages = [];
9627
9519
  this.#scheduledHiddenNextTurnGeneration = undefined;
9628
9520
 
@@ -9763,8 +9655,7 @@ export class AgentSession {
9763
9655
  this.#baseSystemPrompt = previousBaseSystemPrompt;
9764
9656
  this.agent.setSystemPrompt(previousSystemPrompt);
9765
9657
  this.agent.replaceMessages(previousAgentMessages);
9766
- this.#steeringMessages = previousSteeringMessages;
9767
- this.#followUpMessages = previousFollowUpMessages;
9658
+ this.agent.replaceQueues(previousSteeringMessages, previousFollowUpMessages);
9768
9659
  this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
9769
9660
  this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
9770
9661
  if (previousModel) {