mixdog 1.2.1 → 1.2.2

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 (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -0,0 +1,174 @@
1
+ // Explicit MCP OAuth sign-in flows: the only interactive MCP connect. A flow
2
+ // reconnects one configured server with browser login allowed, hands the
3
+ // authorization URL back to the caller (opening the host browser only when
4
+ // asked), and is then polled until the reconnect settles.
5
+ import { randomUUID } from 'node:crypto';
6
+ import { openInBrowser } from '../runtime/shared/open-url.mjs';
7
+
8
+ // Settled flows stay readable long enough for a poller to observe the result.
9
+ const SETTLED_FLOW_TTL_MS = 10 * 60_000;
10
+ const NOT_COMPLETED = 'MCP OAuth sign-in was not completed';
11
+
12
+ const NOT_PENDING = 'MCP OAuth sign-in is no longer pending';
13
+
14
+ // flowId -> flow, shared by every session runtime of this process. A caller
15
+ // is not bound to the runtime that began a flow (the desktop's control
16
+ // session is replaced after a failed call, and an idle one is evicted), so
17
+ // status / complete / cancel reach the flow from any runtime; each flow keeps
18
+ // the handlers of the runtime that began it (its MCP scope and connect).
19
+ const flows = new Map();
20
+
21
+ // A pending flow keeps its authorization URL readable: a client polling the
22
+ // status (remote sign-in) still shows the page to open.
23
+ const view = (flow) => ({
24
+ flowId: flow.flowId,
25
+ state: flow.state,
26
+ ...(flow.state === 'pending' && flow.url ? { url: flow.url } : {}),
27
+ ...(flow.error ? { error: flow.error } : {}),
28
+ });
29
+ function settle(flow, state, error = null) {
30
+ if (flow.state !== 'pending') return;
31
+ flow.state = state;
32
+ if (state === 'failed') flow.error = error || NOT_COMPLETED;
33
+ setTimeout(() => flows.delete(flow.flowId), SETTLED_FLOW_TTL_MS).unref?.();
34
+ }
35
+ function flowOf(flowId) {
36
+ const flow = flows.get(String(flowId || ''));
37
+ if (!flow) throw new Error('MCP OAuth login not found');
38
+ return flow;
39
+ }
40
+ async function cancelFlow(flow) {
41
+ if (flow.state === 'pending') {
42
+ flow.cancelled = true;
43
+ flow.owner.cancel(flow.name);
44
+ await flow.done;
45
+ }
46
+ return view(flow);
47
+ }
48
+
49
+ /**
50
+ * `connect(name, openBrowser)` reconnects the server interactively, calling
51
+ * `openBrowser(url)` when a login starts, and resolves `{ connected, error }`.
52
+ * `cancel(name)` aborts that server's pending login. `completeCallback(name,
53
+ * callbackUrl)` answers it with a pasted redirect URL (throws a fixed message
54
+ * for an invalid one). `onComplete(name)` runs after a successful sign-in,
55
+ * before the flow reports complete.
56
+ */
57
+ export function createMcpOAuthLogins({
58
+ connect,
59
+ cancel,
60
+ completeCallback = () => {
61
+ throw new Error(NOT_PENDING);
62
+ },
63
+ onComplete = async () => {},
64
+ openBrowser = openInBrowser,
65
+ }) {
66
+ // name -> tail of that server's serialized begin/cancel/sign-out steps, so
67
+ // replacing a pending flow is atomic and exactly one can be pending.
68
+ const lanes = new Map();
69
+ function exclusive(name, step) {
70
+ const run = (lanes.get(name) || Promise.resolve()).then(step);
71
+ const tail = run.then(
72
+ () => {},
73
+ () => {}
74
+ );
75
+ lanes.set(name, tail);
76
+ tail.then(() => {
77
+ if (lanes.get(name) === tail) lanes.delete(name);
78
+ });
79
+ return run;
80
+ }
81
+ // This runtime's handlers, kept on each flow it begins.
82
+ const owner = {
83
+ exclusive,
84
+ cancel,
85
+ completeCallback,
86
+ };
87
+ // One sign-in per server of this runtime (its MCP scope).
88
+ const cancelServerFlows = (name) =>
89
+ Promise.all([...flows.values()].filter((flow) => flow.owner === owner && flow.name === name).map(cancelFlow));
90
+
91
+ async function begin(name, options = {}) {
92
+ const { openBrowser: hostBrowser } = options || {};
93
+ if (hostBrowser !== undefined && typeof hostBrowser !== 'boolean') {
94
+ throw new TypeError('openBrowser must be a boolean');
95
+ }
96
+ let announceUrl;
97
+ const urlReady = new Promise((resolve) => {
98
+ announceUrl = resolve;
99
+ });
100
+ // One sign-in per server: a new one replaces the pending one, and the
101
+ // replacement is registered before the next queued step of this server.
102
+ const flow = await exclusive(name, async () => {
103
+ await cancelServerFlows(name);
104
+ const flow = {
105
+ flowId: `mcp_oauth_${randomUUID()}`,
106
+ name,
107
+ owner,
108
+ state: 'pending',
109
+ error: null,
110
+ cancelled: false,
111
+ url: null,
112
+ };
113
+ flows.set(flow.flowId, flow);
114
+ const launch = async (url) => {
115
+ // A login reached after its flow was cancelled (queued behind another
116
+ // connect of this server) never opens anything.
117
+ if (flow.cancelled) throw new Error('MCP OAuth login cancelled');
118
+ flow.awaitingCallback = true;
119
+ flow.url = url;
120
+ announceUrl(url);
121
+ if (hostBrowser !== false) openBrowser(url);
122
+ };
123
+ flow.done = (async () => {
124
+ try {
125
+ const outcome = await connect(name, launch);
126
+ if (flow.cancelled) settle(flow, 'cancelled');
127
+ else if (outcome?.connected) {
128
+ // The sign-in itself succeeded; a failed follow-up never undoes it.
129
+ await Promise.resolve()
130
+ .then(() => onComplete(name))
131
+ .catch(() => {});
132
+ settle(flow, 'complete');
133
+ } else settle(flow, 'failed', outcome?.error);
134
+ } catch (error) {
135
+ settle(flow, flow.cancelled ? 'cancelled' : 'failed', error?.message || String(error));
136
+ }
137
+ })();
138
+ return flow;
139
+ });
140
+ const url = await Promise.race([urlReady, flow.done.then(() => null)]);
141
+ if (url) return { flowId: flow.flowId, state: 'pending', url };
142
+ // Settled without a login: stored credentials (or no challenge) sufficed.
143
+ if (flow.state === 'complete') return view(flow);
144
+ // A superseded or failed begin stays readable (cancelled / failed).
145
+ throw new Error(flow.error || NOT_COMPLETED);
146
+ }
147
+
148
+ return {
149
+ begin,
150
+ status: (flowId) => view(flowOf(flowId)),
151
+ cancel(flowId) {
152
+ const flow = flowOf(flowId);
153
+ return flow.owner.exclusive(flow.name, () => cancelFlow(flow));
154
+ },
155
+ /** Answer the pending flow with the redirect URL pasted from the browser
156
+ * and resolve once it settles: `{ flowId, state: 'complete' | 'failed',
157
+ * error? }`. An invalid paste throws and leaves the flow pending. */
158
+ async complete(flowId, callbackUrl) {
159
+ const flow = flowOf(flowId);
160
+ if (flow.state !== 'pending' || flow.cancelled || !flow.awaitingCallback) throw new Error(NOT_PENDING);
161
+ flow.owner.completeCallback(flow.name, callbackUrl);
162
+ await flow.done;
163
+ return view(flow);
164
+ },
165
+ /** Cancel every pending sign-in of one server, then run `after` (sign-out)
166
+ * before any later begin of that server starts; resolves its result. */
167
+ cancelServer(name, after = async () => {}) {
168
+ return exclusive(name, async () => {
169
+ await cancelServerFlows(name);
170
+ return after();
171
+ });
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * model-switch-target.mjs — what a user's model selection names, and whether
3
+ * it switches an established conversation to another model. Shared by every
4
+ * surface that offers the choice (session API routes, plain REPL /model).
5
+ */
6
+
7
+ /** The provider/model a selection names; a bare preset id or name names the
8
+ * preset's model. */
9
+ export function selectedModel(runtime, request, sessionProvider, sessionModel) {
10
+ const model = String(request?.model ?? '').trim();
11
+ if (!request?.provider && model) {
12
+ const wanted = model.toLowerCase();
13
+ const presets = runtime?.listPresets?.();
14
+ const preset = (Array.isArray(presets) ? presets : []).find((entry) =>
15
+ [entry?.id, entry?.name].some(
16
+ (key) =>
17
+ String(key ?? '')
18
+ .trim()
19
+ .toLowerCase() === wanted
20
+ )
21
+ );
22
+ if (preset?.provider && preset?.model) return { provider: String(preset.provider), model: String(preset.model) };
23
+ }
24
+ return { provider: String(request?.provider ?? sessionProvider), model: model || sessionModel };
25
+ }
26
+
27
+ /** A selection of another model than the established conversation's own. */
28
+ export function switchesSessionModel(runtime, request) {
29
+ const sessionProvider = String(runtime?.sessionProvider ?? '');
30
+ const sessionModel = String(runtime?.sessionModel ?? '');
31
+ if (!sessionProvider || !sessionModel) return false;
32
+ const { provider, model } = selectedModel(runtime, request, sessionProvider, sessionModel);
33
+ return provider !== sessionProvider || model !== sessionModel;
34
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * set-route.mjs — select the main route: resolve and validate the request,
3
3
  * adopt it into config, then decide how the current session follows it
4
- * (untouched, tuned, recreated, or updated in place when empty).
4
+ * (untouched, tuned, recreated, updated in place when empty, or switched in
5
+ * place when the selection is addressed to an established conversation).
5
6
  */
6
7
  import { clean, hasOwn } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
7
8
  import {
@@ -117,6 +118,51 @@ function updateSessionInPlace(deps, session, updateOptions) {
117
118
  writeStatuslineRoute(statusRoutes, getSession(), route);
118
119
  }
119
120
 
121
+ /** Index of the message a model switch boundary is recorded on: the last one
122
+ * before a trailing unanswered prompt, which a retry may rewind. */
123
+ function modelSwitchAnchorIndex(messages) {
124
+ let index = messages.length - 1;
125
+ while (index >= 0 && (messages[index]?.role === 'user' || messages[index]?.role === 'system')) index -= 1;
126
+ if (index >= 0) return index;
127
+ return messages.findLastIndex((message) => message?.role !== 'system');
128
+ }
129
+
130
+ /** Display-only boundary of an in-place model switch, kept in message meta
131
+ * (like an inheritance boundary): the transcript shows it live and after a
132
+ * restore, and the model conversation gains no synthetic message. */
133
+ export function recordModelSwitchBoundary(session, boundary) {
134
+ const messages = Array.isArray(session?.messages) ? session.messages : [];
135
+ const index = modelSwitchAnchorIndex(messages);
136
+ if (index < 0) return false;
137
+ const anchor = messages[index];
138
+ const switches = Array.isArray(anchor.meta?.modelSwitches) ? anchor.meta.modelSwitches : [];
139
+ if (switches.some((entry) => entry?.at === boundary.at && entry?.modelId === boundary.modelId)) return true;
140
+ // Replace the message object: the session save worker detects changed
141
+ // messages by reference identity, so an in-place meta edit is never saved.
142
+ messages[index] = { ...anchor, meta: { ...anchor.meta, modelSwitches: [...switches, boundary] } };
143
+ return true;
144
+ }
145
+
146
+ /** Move the addressed, established session onto the adopted route in place.
147
+ * Its provider replay state and context readings are dropped, so the next
148
+ * turn re-measures and auto-compacts against the new model's window. Returns
149
+ * the recorded boundary, or null when the session has nothing to mark. */
150
+ function switchSessionInPlace(deps, session) {
151
+ const { getRoute, getSession } = deps;
152
+ const route = getRoute();
153
+ if (route.provider === session.provider && route.model === session.model) {
154
+ updateSessionInPlace(deps, session);
155
+ return null;
156
+ }
157
+ const boundary = { provider: route.provider, modelId: route.model, at: Date.now() };
158
+ const recorded = recordModelSwitchBoundary(session, boundary);
159
+ updateSessionInPlace(deps, session, { crossProvider: true });
160
+ // A manager that answered with another copy of the session carries the
161
+ // boundary too; the next save persists it.
162
+ if (recorded && getSession() && getSession() !== session) recordModelSwitchBoundary(getSession(), boundary);
163
+ return recorded ? boundary : null;
164
+ }
165
+
120
166
  /** Validate, persist and adopt the requested route as the runtime's route. */
121
167
  async function adoptRoute(deps, persist, next, options) {
122
168
  const {
@@ -144,7 +190,9 @@ async function adoptRoute(deps, persist, next, options) {
144
190
  * Quota-exhaustion escape hatch: apply `next` to the addressed, ESTABLISHED
145
191
  * session in place. The caller (session API) has already proven the latest
146
192
  * settled turn failed on quota and that nothing is running. Starts no turn;
147
- * the next turn's auto-compaction fits history to the new window.
193
+ * the next turn's auto-compaction fits history to the new window. Answers
194
+ * `{ modelSwitch }` with the recorded boundary (as setRoute does) so the
195
+ * session surface shows the same live divider, or true when nothing switched.
148
196
  */
149
197
  export function createContinueSessionInModel(deps, persist) {
150
198
  const { getSession, invalidateContextStatusCache } = deps;
@@ -154,33 +202,53 @@ export function createContinueSessionInModel(deps, persist) {
154
202
  throw new Error(`session ${sessionId || ''} is not the active session`);
155
203
  }
156
204
  await adoptRoute(deps, persist, next, {});
157
- updateSessionInPlace(deps, session, { crossProvider: true });
205
+ const modelSwitch = switchSessionInPlace(deps, session);
158
206
  invalidateContextStatusCache();
159
- return true;
207
+ return modelSwitch ? { modelSwitch } : true;
160
208
  };
161
209
  }
162
210
 
211
+ /** Rejects an unacceptable selection without applying it. */
212
+ async function validateSelectedRoute(deps, next) {
213
+ await resolveSelectedRoute(deps, next);
214
+ return deps.getRoute();
215
+ }
216
+
163
217
  export function createSetRoute(deps, persist) {
164
218
  const { getRoute, getSession, invalidateContextStatusCache, createCurrentSession } = deps;
165
219
 
166
220
  return async function setRoute(next, options = {}) {
167
- // Selection is for a new/inherited session. An explicit durable address
168
- // permits initializing an EMPTY session, never rewriting an existing
169
- // conversation's model, context boundary or provider cache.
221
+ // Without an explicit durable address a selection is for a new/inherited
222
+ // session and never rewrites an existing conversation's model, unless it
223
+ // is the user's model choice for this conversation (`switchEstablished`,
224
+ // the session surface): that one switches an established conversation in
225
+ // place while an empty one follows the unaddressed rules below.
170
226
  const applyToCurrentSession = options?.applyToCurrentSession === true;
227
+ const switchEstablished = applyToCurrentSession || options?.switchEstablished === true;
228
+ // Lets a caller prove the selection is acceptable before it does
229
+ // irreversible work (compaction) ahead of the switch; changes nothing.
230
+ if (options?.validateOnly === true) return validateSelectedRoute(deps, next);
171
231
  await adoptRoute(deps, persist, next, options);
172
232
  const session = getSession();
173
- // Model/provider changes are next-session-only for a session the user
174
- // has already talked in or compacted (provider-keyed prompt cache). But
175
- // a current session with no model reply, no compaction summary and no
176
- // in-flight first turn — empty, or holding only refused prompts — has no
177
- // cache to protect, so /model takes effect live: route + statusline
233
+ // A session the user has already talked in or compacted keeps its route
234
+ // unless the selection was addressed to it. A current session with no
235
+ // model reply, no compaction summary and no in-flight first turn — empty,
236
+ // or holding only refused prompts — takes effect live: route + statusline
178
237
  // update immediately. Only a truly empty session is recreated below.
179
238
  const currentSessionEmpty = !!session && !sessionRouteCommitted(session);
180
239
  if (!currentSessionEmpty) {
181
- // Some desktop effort changes travel as a same-model route update.
182
- // Only effort/Fast may affect the source, starting with its next turn.
183
- if (applyToCurrentSession) persist.applySessionTuning();
240
+ if (!switchEstablished) return getRoute();
241
+ const route = getRoute();
242
+ // An addressed selection of another model switches the conversation
243
+ // itself in place; the caller (session surface) refuses it while a
244
+ // turn runs. A same-model update (effort/Fast) only tunes the session
245
+ // from its next turn.
246
+ if (session && (route.provider !== session.provider || route.model !== session.model)) {
247
+ const modelSwitch = switchSessionInPlace(deps, session);
248
+ invalidateContextStatusCache();
249
+ return modelSwitch ? { ...getRoute(), modelSwitch } : getRoute();
250
+ }
251
+ persist.applySessionTuning();
184
252
  return getRoute();
185
253
  }
186
254
  // An addressed runtime keeps its durable id: the empty session is updated
@@ -62,7 +62,7 @@ export function createWebSearchRouteApi(deps) {
62
62
  await awaitKeychainPrewarm();
63
63
  ensureFullConfig();
64
64
  await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
65
- const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
65
+ const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model, { allowFetch: true });
66
66
  if (!webSearchCapableFor(selectedRoute.provider, modelMeta)) {
67
67
  throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
68
68
  }
@@ -7,19 +7,37 @@ import { webSearchRouteOrDefault } from '../../runtime/agent/orchestrator/runtim
7
7
 
8
8
  export function createWebSearchRouteCandidates({
9
9
  getRoute,
10
+ getReg,
10
11
  getWebSearchRoute,
11
12
  setWebSearchRoute,
12
13
  ensureFullConfig,
13
14
  awaitKeychainPrewarm,
15
+ ensureProvidersReady,
16
+ ensureProviderEnabled,
14
17
  normalizeWebSearchProviderId,
15
18
  isDefaultWebSearchRouteConfig,
16
19
  isWebSearchCapableProvider,
17
20
  webSearchCapableFor,
18
21
  }) {
19
- function currentMainWebSearchModelMeta() {
22
+ function currentMainWebSearchModelMeta(loadedRow = null) {
20
23
  const route = getRoute();
21
24
  if (!route?.provider || !route?.model) return null;
22
- return { ...route, id: route.model, display: route.model, name: route.model };
25
+ // Capability comes from the provider's catalog row (supportedTools etc.),
26
+ // not from the bare model id.
27
+ const catalogRow = getReg()?.getProvider(route.provider)?.getCachedModelInfo?.(route.model) || loadedRow;
28
+ return { ...catalogRow, ...route, id: route.model, display: route.model, name: route.model };
29
+ }
30
+
31
+ /** A cold catalog (fresh or model-proxy provider instance) is loaded once
32
+ * before the capability check, so it never falls back to the bare id.
33
+ * Returns the listed row, which carries the catalog fields even when the
34
+ * provider keeps no per-model cache. */
35
+ async function loadMainCatalogRow(cfg, route) {
36
+ await ensureProvidersReady(ensureProviderEnabled(cfg, route.provider));
37
+ const provider = getReg().getProvider(route.provider);
38
+ if (provider?.getCachedModelInfo?.(route.model) || typeof provider?.listModels !== 'function') return null;
39
+ const models = await provider.listModels();
40
+ return (Array.isArray(models) ? models.find((m) => m?.id === route.model) : null) || null;
23
41
  }
24
42
 
25
43
  async function nativeWebSearchRoutes() {
@@ -33,7 +51,8 @@ export function createWebSearchRouteCandidates({
33
51
  const webSearchRoute = webSearchRouteOrDefault(cfg.webSearchRoute, getWebSearchRoute());
34
52
  setWebSearchRoute(webSearchRoute);
35
53
  if (isDefaultWebSearchRouteConfig(webSearchRoute)) {
36
- const mainModel = currentMainWebSearchModelMeta();
54
+ const loadedRow = route?.provider && route?.model ? await loadMainCatalogRow(cfg, route) : null;
55
+ const mainModel = currentMainWebSearchModelMeta(loadedRow);
37
56
  if (!mainModel || !webSearchCapableFor(route.provider, mainModel)) return [];
38
57
  return [
39
58
  {
@@ -10,6 +10,8 @@ export function modelMetaKey(providerId, modelId) {
10
10
 
11
11
  export function createModelMetaIndex({ caches, modelMetaByRoute, reg, scheduleProviderModelWarmup }) {
12
12
  let observedCatalogRevision = catalogRevision(reg());
13
+ // Keys holding only the bare-id placeholder: a fetching lookup replaces them.
14
+ const placeholderKeys = new Set();
13
15
 
14
16
  /** Invalidates the provider caches when the registry's catalog moved on;
15
17
  * returns the current revision either way. */
@@ -22,6 +24,7 @@ export function createModelMetaIndex({ caches, modelMetaByRoute, reg, schedulePr
22
24
  caches.providerModelsPromise = null;
23
25
  caches.webSearchProviderModelsCache = { models: null, at: 0, revision };
24
26
  modelMetaByRoute.clear();
27
+ placeholderKeys.clear();
25
28
  return revision;
26
29
  }
27
30
 
@@ -32,14 +35,16 @@ export function createModelMetaIndex({ caches, modelMetaByRoute, reg, schedulePr
32
35
  }
33
36
 
34
37
  function remember(providerId, modelId, meta) {
35
- modelMetaByRoute.set(modelMetaKey(providerId, modelId), meta);
38
+ const key = modelMetaKey(providerId, modelId);
39
+ modelMetaByRoute.set(key, meta);
40
+ placeholderKeys.delete(key);
36
41
  }
37
42
 
38
43
  async function lookupModelMeta(providerId, modelId, { allowFetch = false } = {}) {
39
44
  const revision = syncCatalogRevision();
40
45
  const seq = caches.providerModelsLoadSeq;
41
46
  const key = modelMetaKey(providerId, modelId);
42
- if (modelMetaByRoute.has(key)) return modelMetaByRoute.get(key);
47
+ if (modelMetaByRoute.has(key) && !(allowFetch && placeholderKeys.has(key))) return modelMetaByRoute.get(key);
43
48
  const fallback = () => ({ id: modelId, provider: providerId });
44
49
  const providerImpl = reg().getProvider(providerId);
45
50
  if (!providerImpl || typeof providerImpl.listModels !== 'function') {
@@ -58,6 +63,7 @@ export function createModelMetaIndex({ caches, modelMetaByRoute, reg, schedulePr
58
63
  if (!allowFetch) {
59
64
  const meta = fallback();
60
65
  modelMetaByRoute.set(key, meta);
66
+ placeholderKeys.add(key);
61
67
  scheduleProviderModelWarmup();
62
68
  return meta;
63
69
  }
@@ -65,7 +71,10 @@ export function createModelMetaIndex({ caches, modelMetaByRoute, reg, schedulePr
65
71
  const models = await providerImpl.listModels();
66
72
  const found = Array.isArray(models) ? models.find((m) => m?.id === modelId) : null;
67
73
  const meta = found || fallback();
68
- if (snapshotIsCurrent(revision, seq)) modelMetaByRoute.set(key, meta);
74
+ if (snapshotIsCurrent(revision, seq)) {
75
+ modelMetaByRoute.set(key, meta);
76
+ placeholderKeys.delete(key);
77
+ }
69
78
  return meta;
70
79
  } catch {
71
80
  // A failed fetch is not authoritative: nothing is pinned.
@@ -2,8 +2,11 @@
2
2
  // main model, cached or built-in quick models per capable provider, and the
3
3
  // configured/current routes, deduplicated per provider:model.
4
4
  import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
5
- import { QUICK_WEB_SEARCH_MODELS } from '../quick-web-search-models.mjs';
6
- import { WEB_SEARCH_DEFAULT_PROVIDER, WEB_SEARCH_DEFAULT_MODEL } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
5
+ import { quickWebSearchModels } from '../quick-web-search-models.mjs';
6
+ import {
7
+ WEB_SEARCH_DEFAULT_PROVIDER,
8
+ WEB_SEARCH_DEFAULT_MODEL,
9
+ } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
7
10
  import { providerCachedModelsSync } from '../../runtime/agent/orchestrator/providers/provider-catalog-cache.mjs';
8
11
  import { hydratedModel } from './model-meta.mjs';
9
12
 
@@ -33,7 +36,12 @@ export function createQuickWebSearchRows({
33
36
  contextWindow: resolved?.contextWindow || null,
34
37
  outputTokens: resolved?.outputTokens || null,
35
38
  latest: resolved?.latest === true,
36
- supportsWebSearch: true,
39
+ // Only what the source row itself declares; webSearchCapableFor adds the
40
+ // provider catalog's row and the id fallback. Never forced on.
41
+ ...(typeof model?.supportsWebSearch === 'boolean' ? { supportsWebSearch: model.supportsWebSearch } : {}),
42
+ ...(Array.isArray(model?.supportedTools) ? { supportedTools: model.supportedTools } : {}),
43
+ ...(Array.isArray(model?.tools) ? { tools: model.tools } : {}),
44
+ ...(model?.capabilities && typeof model.capabilities === 'object' ? { capabilities: model.capabilities } : {}),
37
45
  supportsFunctionCalling: resolved?.supportsFunctionCalling === true,
38
46
  supportsPromptCaching: resolved?.supportsPromptCaching === true,
39
47
  supportsReasoning: resolved?.supportsReasoning === true,
@@ -95,7 +103,7 @@ export function createQuickWebSearchRows({
95
103
  const providerName = normalizeWebSearchProviderId(name);
96
104
  if (!providerConfig?.enabled || !isWebSearchCapableProvider(providerName)) continue;
97
105
  const cachedModels = providerCachedModelsSync(providerName);
98
- const quickModels = cachedModels.length ? cachedModels : QUICK_WEB_SEARCH_MODELS[providerName] || [];
106
+ const quickModels = cachedModels.length ? cachedModels : quickWebSearchModels(providerName);
99
107
  for (const model of quickModels) {
100
108
  addQuickWebSearchModel(rows, seen, providerName, model);
101
109
  }
@@ -110,10 +118,7 @@ export function createQuickWebSearchRows({
110
118
  }
111
119
  const mainModel = currentMainWebSearchModelMeta();
112
120
  if (mainModel && webSearchCapableFor(route.provider, mainModel)) {
113
- addQuickWebSearchModel(rows, seen, route.provider, {
114
- id: route.model,
115
- display: route.model,
116
- });
121
+ addQuickWebSearchModel(rows, seen, route.provider, mainModel);
117
122
  }
118
123
  return webSearchModelsFromRows(rows);
119
124
  }
@@ -1,41 +1,16 @@
1
- // Static quick-pick model ids surfaced during onboarding / web-search setup.
2
- // Limits are intentionally not hardcoded here: rows are hydrated from live
3
- // provider caches and provider-scoped metadata during catalog construction.
1
+ // Offline quick-pick model rows for onboarding / web-search setup. No model id
2
+ // is listed here: when a provider has no live roster yet, its newest rows come
3
+ // from the provider-scoped public catalog, so a new release appears without a
4
+ // code change. Rows are hydrated further during catalog construction.
5
+ import { getModelsDevProviderModelsSync } from '../runtime/shared/llm/model-catalog.mjs';
4
6
 
5
7
  export const ONBOARDING_VERSION = 1;
6
8
 
7
- export const QUICK_WEB_SEARCH_MODELS = Object.freeze({
8
- 'openai-oauth': [
9
- { id: 'gpt-6-sol', display: 'GPT-6 Sol', latest: true },
10
- { id: 'gpt-6-luna', display: 'GPT-6 Luna', latest: true },
11
- { id: 'gpt-5.6-sol', display: 'GPT-5.6 Sol' },
12
- { id: 'gpt-5.6-terra', display: 'GPT-5.6 Terra' },
13
- { id: 'gpt-5.6-luna', display: 'GPT-5.6 Luna' },
14
- ],
15
- openai: [
16
- { id: 'gpt-5.5', display: 'GPT-5.5', latest: true },
17
- { id: 'gpt-5.4', display: 'GPT-5.4', latest: true },
18
- { id: 'gpt-5', display: 'GPT-5' },
19
- { id: 'gpt-4.1', display: 'GPT-4.1' },
20
- { id: 'gpt-4o', display: 'GPT-4o' },
21
- ],
22
- 'grok-oauth': [{ id: 'grok-4.5', display: 'Grok 4.5', latest: true }],
23
- xai: [{ id: 'grok-4.5', display: 'Grok 4.5', latest: true }],
24
- gemini: [
25
- { id: 'gemini-3.6-flash', display: 'Gemini 3.6 Flash', latest: true },
26
- { id: 'gemini-3.5-flash-lite', display: 'Gemini 3.5 Flash Lite', latest: true },
27
- { id: 'gemini-3.1-pro-preview', display: 'Gemini 3.1 Pro Preview' },
28
- ],
29
- 'anthropic-oauth': [
30
- { id: 'claude-opus-5-5', display: 'Claude Opus 5.5', latest: true },
31
- { id: 'claude-sonnet-5', display: 'Claude Sonnet 5', latest: true },
32
- { id: 'claude-fable-5', display: 'Claude Fable 5', latest: true },
33
- { id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5' },
34
- ],
35
- anthropic: [
36
- { id: 'claude-opus-5-5', display: 'Claude Opus 5.5', latest: true },
37
- { id: 'claude-sonnet-5', display: 'Claude Sonnet 5', latest: true },
38
- { id: 'claude-fable-5', display: 'Claude Fable 5', latest: true },
39
- { id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5' },
40
- ],
41
- });
9
+ const QUICK_ROWS_PER_PROVIDER = 5;
10
+
11
+ export function quickWebSearchModels(provider, models = getModelsDevProviderModelsSync(provider)) {
12
+ return Object.entries(models || {})
13
+ .map(([id, row]) => ({ id, display: row?.name || id, releaseDate: row?.release_date || null }))
14
+ .sort((a, b) => (Date.parse(b.releaseDate) || 0) - (Date.parse(a.releaseDate) || 0))
15
+ .slice(0, QUICK_ROWS_PER_PROVIDER);
16
+ }
@@ -5,6 +5,7 @@ import { publishGlobalExtensionChange, subscribeGlobalExtensionChanges } from '.
5
5
 
6
6
  export function createExtensionSync({
7
7
  connectConfiguredMcp,
8
+ cancelMcpLogin,
8
9
  invalidatePreSessionToolSurface,
9
10
  refreshEmptySessionToolPolicy,
10
11
  reloadFullConfig,
@@ -60,6 +61,9 @@ export function createExtensionSync({
60
61
  const chain = mcpToggleChains.get(serverName) || { desired: enabled, running: null };
61
62
  chain.desired = enabled;
62
63
  mcpToggleChains.set(serverName, chain);
64
+ // A disable riding the chain would otherwise wait out an explicit sign-in
65
+ // (beginMcpOAuthLogin) of this server still in flight.
66
+ if (enabled === false) cancelMcpLogin?.(serverName);
63
67
  if (!chain.running) {
64
68
  chain.running = (async () => {
65
69
  let status;