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
@@ -1,9 +1,29 @@
1
1
  // agent-tree/agent-cancel.mjs
2
2
  // Cancelling an agent subtree: descendants first, then a durable canonical
3
3
  // close of the session itself. Concurrent cancels of one id share a run.
4
- import { cancelBackgroundTasks } from '../../../runtime/shared/background-tasks.mjs';
4
+ import {
5
+ cancelBackgroundTask,
6
+ cancelBackgroundTasks,
7
+ listBackgroundTasks,
8
+ } from '../../../runtime/shared/background-tasks.mjs';
5
9
 
6
- export function createAgentCancellation({ registry, rehydrateAgentSessions, sessionOwner, entryForSession, log }) {
10
+ // Cancel the running Agent task(s) bound to this session itself (owned by its
11
+ // parent) BEFORE the session closes, so the owner hears `cancelled` with the
12
+ // turn's partial output instead of the closed turn failing the task.
13
+ function cancelOwnAgentTasks(sessionId, reason) {
14
+ for (const row of listBackgroundTasks({ surface: 'agent' })) {
15
+ if (row.status === 'running' && row.sessionId === sessionId) cancelBackgroundTask(row.task_id, reason);
16
+ }
17
+ }
18
+
19
+ export function createAgentCancellation({
20
+ registry,
21
+ rehydrateAgentSessions,
22
+ sessionOwner,
23
+ entryForSession,
24
+ log,
25
+ rememberCancelTranscript,
26
+ }) {
7
27
  const agentCancelRuns = new Map();
8
28
 
9
29
  /** The runtime entry that can close this descriptor: the live owner, else a
@@ -39,6 +59,7 @@ export function createAgentCancellation({ registry, rehydrateAgentSessions, sess
39
59
  });
40
60
  const children = registry.childrenOf(id);
41
61
  await Promise.all(children.map((childId) => cancelAgentTree(childId, reason)));
62
+ cancelOwnAgentTasks(id, reason);
42
63
  const descriptor = registry.get(id);
43
64
  if (!descriptor) return children.length > 0;
44
65
  if (descriptor.closed) return true;
@@ -47,6 +68,16 @@ export function createAgentCancellation({ registry, rehydrateAgentSessions, sess
47
68
  if (typeof closeCanonical !== 'function') {
48
69
  throw new TypeError('session runtime must implement closeCanonicalSession');
49
70
  }
71
+ // The close clears the transcript: capture a running turn's transcript
72
+ // first so the cancelled turn still hands off its partial output.
73
+ if (descriptor.status === 'running') {
74
+ try {
75
+ const { messages } = await entry.runtime.readModelMessages(0);
76
+ rememberCancelTranscript(id, messages);
77
+ } catch (error) {
78
+ log(`agent cancel transcript read failed session=${id}: ${error?.message || error}`);
79
+ }
80
+ }
50
81
  const closed = await closeCanonical.call(entry.runtime, reason);
51
82
  if (closed !== true) throw new Error(`agent session ${id} could not be durably closed`);
52
83
  descriptor.closed = true;
@@ -12,16 +12,23 @@ export function createAgentTurns({
12
12
  retainUnwatched,
13
13
  createSession,
14
14
  takeWatchdogStop,
15
+ takeCancelTranscript,
15
16
  }) {
16
17
  const createAgentChild = createAgentChildFactory({ registry, rehydrateAgentSessions, createSession });
17
18
 
18
- // The progress watchdog stopped the turn: rethrow its stall error carrying
19
- // the assistant text the turn produced, so the owner still gets a partial
20
- // result.
21
- async function watchdogStopError(runtime, error, messageStart) {
22
- const { messages } = await runtime.readModelMessages(messageStart);
23
- const partial = partialHandoffTextFromSession({ messages });
24
- if (partial) error.partialHandoff = partial;
19
+ // The turn was stopped (progress watchdog or cancellation): rethrow its error
20
+ // carrying the assistant text the turn produced, so the owner still gets a
21
+ // partial result. A cancel that closed the session captured the transcript
22
+ // before the close cleared it.
23
+ async function withPartialHandoff(runtime, sessionId, error, messageStart) {
24
+ try {
25
+ const captured = takeCancelTranscript(sessionId);
26
+ const messages = captured
27
+ ? captured.slice(messageStart)
28
+ : (await runtime.readModelMessages(messageStart)).messages;
29
+ const partial = partialHandoffTextFromSession({ messages });
30
+ if (partial) error.partialHandoff = partial;
31
+ } catch {}
25
32
  return error;
26
33
  }
27
34
 
@@ -44,6 +51,7 @@ export function createAgentTurns({
44
51
  }
45
52
  // A stop left over from an earlier turn must not relabel this one.
46
53
  takeWatchdogStop(sessionId);
54
+ takeCancelTranscript(sessionId);
47
55
  // Transcript length before this turn: where a watchdog stop's partial
48
56
  // output starts.
49
57
  const { messageCount: messageStart } = await entry.runtime.readModelMessages(Number.MAX_SAFE_INTEGER);
@@ -64,9 +72,16 @@ export function createAgentTurns({
64
72
  throw new Error(String(detail.error || 'agent session turn failed'));
65
73
  }
66
74
  if (detail?.status === 'cancelled') {
67
- const watchdogStop = takeWatchdogStop(sessionId);
68
- if (watchdogStop) throw await watchdogStopError(entry.runtime, watchdogStop, messageStart);
69
- throw new Error('agent session turn cancelled');
75
+ const stop = takeWatchdogStop(sessionId) || new Error('agent session turn cancelled');
76
+ throw await withPartialHandoff(entry.runtime, sessionId, stop, messageStart);
77
+ }
78
+ // Only a completed turn has a result; a rejected submission (session
79
+ // closing/terminal, steering persist failure) or any unknown status is a
80
+ // failure, never an empty completion.
81
+ if (detail?.status !== 'done' && detail?.status !== 'completed') {
82
+ const status = String(detail?.status || 'unknown');
83
+ const reason = String(detail?.reason || detail?.error || '').trim();
84
+ throw new Error(`agent turn ${status}${reason ? `: ${reason}` : ''}`);
70
85
  }
71
86
  const result = detail?.result || { content: '' };
72
87
  descriptor.status = 'idle';
@@ -81,6 +96,7 @@ export function createAgentTurns({
81
96
  descriptor.stage = descriptor.status;
82
97
  throw error;
83
98
  } finally {
99
+ takeCancelTranscript(sessionId);
84
100
  descriptor.updatedAt = Date.now();
85
101
  retainUnwatched(entry, 'agent child idle');
86
102
  }
@@ -37,6 +37,16 @@ export function createAgentTree({
37
37
  watchdogStops.delete(sessionId);
38
38
  return error;
39
39
  };
40
+ // Transcripts of running turns captured by a cancel just before it closes
41
+ // (and clears) the session, so the cancelled turn still reports the output
42
+ // it produced.
43
+ const cancelTranscripts = new Map();
44
+ const rememberCancelTranscript = (sessionId, messages) => cancelTranscripts.set(sessionId, messages);
45
+ const takeCancelTranscript = (sessionId) => {
46
+ const messages = cancelTranscripts.get(sessionId) || null;
47
+ cancelTranscripts.delete(sessionId);
48
+ return messages;
49
+ };
40
50
  function getSessionProgressSnapshot(sessionId) {
41
51
  const runtime = sessionOwner(sessionId)?.runtime;
42
52
  if (typeof runtime?.getTurnLiveness !== 'function') return null;
@@ -58,6 +68,7 @@ export function createAgentTree({
58
68
  retainUnwatched,
59
69
  createSession,
60
70
  takeWatchdogStop,
71
+ takeCancelTranscript,
61
72
  });
62
73
  const { cancelAgentTree, cancelAgentDescendants } = createAgentCancellation({
63
74
  registry,
@@ -65,6 +76,7 @@ export function createAgentTree({
65
76
  sessionOwner,
66
77
  entryForSession,
67
78
  log,
79
+ rememberCancelTranscript,
68
80
  });
69
81
 
70
82
  /** The descriptor as callers see it: status from the live runtime when one
@@ -8,6 +8,8 @@
8
8
  // again, and the session's own lane runs the retry under the entry's
9
9
  // generation. Overdue entries run as soon as the scheduler starts.
10
10
 
11
+ import { isAgentOnlySession } from '../../runtime/shared/quota-resume-store.mjs';
12
+
11
13
  /** A still-exhausted reading is re-taken once, this much later. */
12
14
  export const QUOTA_RESUME_RETRY_MS = 5 * 60_000;
13
15
  /** A busy session is re-checked this much later, without spending an attempt. */
@@ -30,6 +32,12 @@ async function resumeDueEntry(entry, { store, materializeSession, readQuotaUsabl
30
32
  return 'unavailable';
31
33
  }
32
34
  if (!isCurrent(store, entry)) return 'cancelled';
35
+ // A delegated agent child never owns a resume (the Lead decides retries), so
36
+ // an entry persisted for one never fires, whatever wrote it.
37
+ if (isAgentOnlySession({ id: sessionId, ...runtime?.session })) {
38
+ store.cancelQuotaResumeEntry(sessionId, { generation });
39
+ return 'agent-session';
40
+ }
33
41
  let usable = false;
34
42
  try {
35
43
  usable = await readQuotaUsable(entry.provider, entry.model);
@@ -28,6 +28,12 @@ export function createBusyTracker({ sessions, currentSessionId }) {
28
28
  // reaches a terminal state and its completion can be delivered back into
29
29
  // this session.
30
30
  if (sessionId && hasActiveBackgroundTasks({ callerSessionId: sessionId })) return true;
31
+ return sessionTurnBusy(entry);
32
+ }
33
+
34
+ // A turn is running (provider stream / tool call / queued input). Background
35
+ // tasks are deliberately not counted: they run outside the heap-churning turn.
36
+ function sessionTurnBusy(entry) {
31
37
  if (typeof entry?.busy === 'boolean') return entry.busy;
32
38
  try {
33
39
  return updateEntryBusy(entry, entry.runtime.getState?.() || {});
@@ -46,5 +52,13 @@ export function createBusyTracker({ sessions, currentSessionId }) {
46
52
  return count;
47
53
  }
48
54
 
49
- return { stateBusy, updateEntryBusy, sessionBusy, liveBusyCount };
55
+ function liveTurnBusyCount() {
56
+ let count = 0;
57
+ for (const entry of sessions) {
58
+ if (sessionTurnBusy(entry)) count += 1;
59
+ }
60
+ return count;
61
+ }
62
+
63
+ return { stateBusy, updateEntryBusy, sessionBusy, liveBusyCount, liveTurnBusyCount };
50
64
  }
@@ -29,6 +29,7 @@ export function createSessionServiceApi({
29
29
  methods,
30
30
  getSize,
31
31
  getBusyCount,
32
+ getTurnBusyCount,
32
33
  getStatus,
33
34
  getExternalClientCount,
34
35
  }) {
@@ -46,6 +47,10 @@ export function createSessionServiceApi({
46
47
  get busyCount() {
47
48
  return getBusyCount();
48
49
  },
50
+ /** Sessions with a running turn; background-task-only sessions excluded. */
51
+ get turnBusyCount() {
52
+ return getTurnBusyCount();
53
+ },
49
54
  get status() {
50
55
  return getStatus();
51
56
  },
@@ -296,6 +296,7 @@ export function createSessionService({
296
296
  },
297
297
  getSize: () => sessions.size,
298
298
  getBusyCount: retention.liveBusyCount,
299
+ getTurnBusyCount: retention.liveTurnBusyCount,
299
300
  getStatus,
300
301
  getExternalClientCount: () => desktopServices.externalClientCount,
301
302
  });
@@ -235,6 +235,7 @@ export function renderAppView(ctx) {
235
235
  model={state.model}
236
236
  effort={state.effort}
237
237
  fast={state.fast}
238
+ ultra={state.fast === true && state.modelParameters?.serviceTier === 'ultrafast'}
238
239
  cwd={state.cwd}
239
240
  stats={statuslineStats}
240
241
  contextWindow={state.contextWindow}
@@ -88,6 +88,8 @@ export function createAppPickers({
88
88
  const {
89
89
  openMcpServersPicker,
90
90
  openMcpPicker,
91
+ loginMcpServer,
92
+ logoutMcpServer,
91
93
  openProjectSkillsPicker,
92
94
  openSkillsPicker,
93
95
  openSkillDetailPicker,
@@ -219,6 +221,8 @@ export function createAppPickers({
219
221
  enterProject: (...a) => enterProject(...a),
220
222
  openProjectPicker: (...a) => openProjectPicker(...a),
221
223
  openMcpPicker: (...a) => openMcpPicker(...a),
224
+ loginMcpServer,
225
+ logoutMcpServer,
222
226
  openSkillsPicker: (...a) => openSkillsPicker(...a),
223
227
  openPluginsPicker: (...a) => openPluginsPicker(...a),
224
228
  openProviderSetupPicker: (...a) => openProviderSetupPicker(...a),
@@ -0,0 +1,107 @@
1
+ // extension-pickers/mcp-login.mjs
2
+ // `/mcp login <name>` and `/mcp logout <name>`. The sign-in URL is always
3
+ // printed (and kept in the live prompt); the runtime may open the browser. The
4
+ // flow is polled while a paste prompt (the shared 'oauth-code' provider prompt)
5
+ // accepts the final redirect address. Pasted text is never echoed.
6
+ const POLL_MS = 1500;
7
+
8
+ export function createMcpLogin({ store, setProviderPrompt, pollMs = POLL_MS }) {
9
+ let active = null;
10
+
11
+ const stop = (flow) => {
12
+ flow.finished = true;
13
+ clearTimeout(flow.timer);
14
+ if (active === flow) active = null;
15
+ };
16
+
17
+ const closePrompt = (flow) => setProviderPrompt((prompt) => (prompt === flow.prompt ? null : prompt));
18
+
19
+ const showPrompt = (flow) => {
20
+ if (flow.finished) return;
21
+ const prompt = {
22
+ kind: 'oauth-code',
23
+ providerName: `MCP ${flow.name}`,
24
+ // The shared prompt pushes the one completion notice; no provider picker follows.
25
+ successReturn: () => {},
26
+ label: `MCP sign-in · ${flow.name}`,
27
+ hint: 'If the browser page fails to load, paste its address here.',
28
+ detail: flow.url,
29
+ login: {
30
+ completeCode: async (text) => {
31
+ const result = await store.completeMcpOAuthLogin(flow.id, text);
32
+ if (result?.state !== 'complete') throw new Error(result?.error || 'sign-in did not complete');
33
+ stop(flow);
34
+ },
35
+ cancel: () => {
36
+ if (flow.finished) return;
37
+ stop(flow);
38
+ void Promise.resolve(store.cancelMcpOAuthLogin(flow.id)).catch(() => {});
39
+ store.pushNotice(`MCP sign-in cancelled for ${flow.name}`, 'info');
40
+ },
41
+ },
42
+ failureReturn: () => showPrompt(flow),
43
+ };
44
+ flow.prompt = prompt;
45
+ setProviderPrompt(prompt);
46
+ };
47
+
48
+ const poll = (flow) => {
49
+ flow.timer = setTimeout(async () => {
50
+ if (flow.finished) return;
51
+ let status;
52
+ try {
53
+ status = await store.getMcpOAuthLoginStatus(flow.id);
54
+ } catch (e) {
55
+ if (flow.finished) return;
56
+ stop(flow);
57
+ closePrompt(flow);
58
+ store.pushNotice(`MCP sign-in failed for ${flow.name}: ${e?.message || e}`, 'error');
59
+ return;
60
+ }
61
+ if (flow.finished) return;
62
+ if (status?.state === 'pending') {
63
+ poll(flow);
64
+ return;
65
+ }
66
+ stop(flow);
67
+ closePrompt(flow);
68
+ if (status?.state === 'complete') store.pushNotice(`MCP sign-in complete for ${flow.name}`, 'info');
69
+ else if (status?.state === 'cancelled') store.pushNotice(`MCP sign-in cancelled for ${flow.name}`, 'info');
70
+ else store.pushNotice(`MCP sign-in failed for ${flow.name}${status?.error ? `: ${status.error}` : ''}`, 'error');
71
+ }, pollMs);
72
+ };
73
+
74
+ const loginMcpServer = async (name) => {
75
+ if (active) {
76
+ const previous = active;
77
+ stop(previous);
78
+ closePrompt(previous);
79
+ void Promise.resolve(store.cancelMcpOAuthLogin(previous.id)).catch(() => {});
80
+ }
81
+ try {
82
+ const started = await store.beginMcpOAuthLogin(name, { openBrowser: true });
83
+ if (started?.state === 'complete') {
84
+ store.pushNotice(`MCP sign-in complete for ${name}`, 'info');
85
+ return;
86
+ }
87
+ const flow = { id: started.flowId, name, url: started.url || '', finished: false, timer: null, prompt: null };
88
+ active = flow;
89
+ if (flow.url) store.pushNotice(`Sign in to ${name}: ${flow.url}`, 'info');
90
+ showPrompt(flow);
91
+ poll(flow);
92
+ } catch (e) {
93
+ store.pushNotice(`MCP sign-in failed for ${name}: ${e?.message || e}`, 'error');
94
+ }
95
+ };
96
+
97
+ const logoutMcpServer = async (name) => {
98
+ try {
99
+ await store.signOutMcpServer(name);
100
+ store.pushNotice(`MCP signed out of ${name}`, 'info');
101
+ } catch (e) {
102
+ store.pushNotice(`MCP sign-out failed for ${name}: ${e?.message || e}`, 'error');
103
+ }
104
+ };
105
+
106
+ return { loginMcpServer, logoutMcpServer };
107
+ }
@@ -8,8 +8,9 @@ const serverDescription = (server, pending, optimistic) => {
8
8
  const transport = server.transport || 'unknown';
9
9
  if (pending) return `${optimistic.enabled ? 'enabling' : 'disabling'}… · ${transport}`;
10
10
  const error = server.error ? ` · ${server.error}` : '';
11
+ const signIn = server.authRequired ? ` · sign-in required: /mcp login ${server.name}` : '';
11
12
  return withScope(
12
- `${server.source || 'config'} · ${server.status || 'unknown'} · ${transport} · ${server.toolCount || 0} tools${error}`,
13
+ `${server.source || 'config'} · ${server.status || 'unknown'} · ${transport} · ${server.toolCount || 0} tools${error}${signIn}`,
13
14
  server
14
15
  );
15
16
  };
@@ -7,6 +7,7 @@
7
7
  * store state, so they can't be pure. openMcpPicker is exposed for the
8
8
  * plugin-detail enable-mcp path (also aliased on the App body).
9
9
  */
10
+ import { createMcpLogin } from './extension-pickers/mcp-login.mjs';
10
11
  import { createMcpServersPicker } from './extension-pickers/mcp-servers-picker.mjs';
11
12
  import { createPluginsPickers } from './extension-pickers/plugins-pickers.mjs';
12
13
  import { createSkillsPickers } from './extension-pickers/skills-pickers.mjs';
@@ -43,5 +44,5 @@ export function createExtensionPickers({
43
44
  setSettingsPrompt,
44
45
  openMcpPicker: mcp.openMcpPicker,
45
46
  });
46
- return { ...mcp, ...skills, ...plugins };
47
+ return { ...mcp, ...createMcpLogin({ store, setProviderPrompt }), ...skills, ...plugins };
47
48
  }
@@ -6,7 +6,7 @@
6
6
  * displayModelName formatter.
7
7
  */
8
8
  import { theme } from '../theme.mjs';
9
- import { displayModelName } from '../../runtime/shared/model-display.mjs';
9
+ import { displayModelName, effortDisplayLabel } from '../../runtime/shared/model-display.mjs';
10
10
  import { isRollingModelAlias } from '../../runtime/shared/model-alias.mjs';
11
11
  import { DAYBREAK_PROVIDER, daybreakBlueOn } from '../../runtime/shared/llm/openai-daybreak.mjs';
12
12
 
@@ -248,18 +248,7 @@ const PROVIDER_DISPLAY_RANKS = {
248
248
 
249
249
  export const providerDisplayRank = (provider) => PROVIDER_DISPLAY_RANKS[String(provider || '').toLowerCase()] ?? 900;
250
250
 
251
- const titleCaseOption = (value) =>
252
- String(value || '')
253
- .split(/([\s_-]+)/)
254
- .map((part) => (/^[\s_-]+$/.test(part) ? part : `${part.charAt(0).toUpperCase()}${part.slice(1).toLowerCase()}`))
255
- .join('');
256
-
257
- export const effortDisplayLabel = (value) => {
258
- const text = String(value || '').trim();
259
- if (!text) return '';
260
- if (text.toLowerCase() === 'xhigh') return 'XHigh';
261
- return titleCaseOption(text);
262
- };
251
+ export { effortDisplayLabel };
263
252
 
264
253
  export const fastDisplayLabel = (enabled = true) => `Fast ${enabled ? 'On' : 'Off'}`;
265
254
 
@@ -330,9 +319,15 @@ export const buildProviderModelItems = (models, provider, currentRoute = null) =
330
319
  }));
331
320
  };
332
321
 
333
- // Model · Effort · Fast, dropping the empty parts.
322
+ // The configured speed tier: Ultrafast when the fast route asks for it.
323
+ const routeSpeedLabel = (route) => {
324
+ if (!route?.fast) return '';
325
+ return route.modelParameters?.serviceTier === 'ultrafast' ? 'Ultrafast' : 'Fast';
326
+ };
327
+
328
+ // Model · Effort · Fast/Ultrafast, dropping the empty parts.
334
329
  const routeModelDetails = (route) =>
335
- [routeModelDisplayName(route), route.effort ? effortDisplayLabel(route.effort) : '', route.fast ? 'Fast' : '']
330
+ [routeModelDisplayName(route), route.effort ? effortDisplayLabel(route.effort) : '', routeSpeedLabel(route)]
336
331
  .filter(Boolean)
337
332
  .join(' · ');
338
333
 
@@ -348,7 +343,7 @@ export const agentModelProfile = (route) => (route?.model ? routeModelDetails(ro
348
343
  export const agentModelParts = (route) => [
349
344
  { text: route?.model ? routeModelDisplayName(route) : '', width: 17 },
350
345
  { text: route?.effort ? effortDisplayLabel(route.effort) : '', width: 6 },
351
- { text: route?.fast ? 'Fast' : '', width: 4 },
346
+ { text: routeSpeedLabel(route), width: 9 },
352
347
  ];
353
348
 
354
349
  export const routeFromModel = (model, effort = null) => ({
@@ -50,8 +50,13 @@ export const SLASH_COMMANDS = [
50
50
  { name: 'theme', usage: '/theme', params: '[id]', description: 'Change the TUI color theme' },
51
51
  { name: 'agents', usage: '/agents', params: '[refresh]', description: 'Show available workflow agents' },
52
52
  { name: 'effort', usage: '/effort', params: '[level]', description: 'Set reasoning effort for the current model' },
53
- { name: 'fast', usage: '/fast', params: '[on|off|ultra]', description: 'Set Fast or Ultrafast mode for the current model' },
54
- { name: 'mcp', usage: '/mcp', description: 'Manage MCP servers and tools' },
53
+ {
54
+ name: 'fast',
55
+ usage: '/fast',
56
+ params: '[on|off|ultra]',
57
+ description: 'Set Fast or Ultrafast mode for the current model',
58
+ },
59
+ { name: 'mcp', usage: '/mcp', params: '[login|logout <name>]', description: 'Manage MCP servers and tools' },
55
60
  { name: 'skills', usage: '/skills', description: 'Choose a skill for the next request' },
56
61
  { name: 'memory', usage: '/memory', params: '[status|core ...]', description: 'List and edit core memories' },
57
62
  { name: 'plugins', usage: '/plugins', description: 'Manage local plugin integrations' },
@@ -14,6 +14,8 @@ const desktopOnlyNotice = (ctx) => {
14
14
  return true;
15
15
  };
16
16
 
17
+ const openMcpPanel = panelCommand('mcp', 'MCP Servers', (deps) => deps.openMcpPicker());
18
+
17
19
  const openSettings = panelCommand('settings', 'Settings', (deps) => deps.openSettingsPicker());
18
20
 
19
21
  export const panelCommands = {
@@ -26,7 +28,20 @@ export const panelCommands = {
26
28
  ctx.openSlashPanel('project', 'Projects', () => ctx.deps.openProjectPicker());
27
29
  return true;
28
30
  },
29
- mcp: panelCommand('mcp', 'MCP Servers', (deps) => deps.openMcpPicker()),
31
+ mcp(ctx, arg) {
32
+ const [sub, ...rest] = arg.trim().split(/\s+/);
33
+ const action = sub?.toLowerCase();
34
+ if (action === 'login' || action === 'logout') {
35
+ const name = rest.join(' ');
36
+ if (!name) {
37
+ ctx.store.pushNotice(`usage: /mcp ${action} <name>`, 'warn');
38
+ return true;
39
+ }
40
+ void (action === 'login' ? ctx.deps.loginMcpServer(name) : ctx.deps.logoutMcpServer(name));
41
+ return true;
42
+ }
43
+ return openMcpPanel(ctx);
44
+ },
30
45
  skills: panelCommand('skills', 'Skills', (deps) => deps.openSkillsPicker()),
31
46
  plugins: panelCommand('plugins', 'Plugins', (deps) => deps.openPluginsPicker()),
32
47
  providers: panelCommand('providers', 'Providers', (deps) => deps.openProviderSetupPicker()),
@@ -19,6 +19,7 @@ function StatusLineView({
19
19
  model,
20
20
  effort,
21
21
  fast,
22
+ ultra = false,
22
23
  cwd,
23
24
  stats,
24
25
  contextWindow,
@@ -42,6 +43,7 @@ function StatusLineView({
42
43
  model,
43
44
  effort,
44
45
  fast,
46
+ ultra,
45
47
  cwd,
46
48
  stats,
47
49
  contextWindow,
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { rgbSgr } from '../../../runtime/shared/statusline/ansi.mjs';
6
6
  import { measuredContextUsage, contextMeasurementLabel } from '../../../runtime/shared/context-measurement.mjs';
7
- import { displayModelName, shortenModelName } from '../../../runtime/shared/model-display.mjs';
7
+ import { displayModelName, effortDisplayLabel, shortenModelName } from '../../../runtime/shared/model-display.mjs';
8
8
  import { theme } from '../../theme.mjs';
9
9
  import { normalizeStatuslineAnsi } from '../../statusline-ansi-bridge.mjs';
10
10
  // Cache-only synchronous read (never touches fs on the render path), so the
@@ -160,6 +160,7 @@ export function localBootStatusLine(args = {}) {
160
160
  model = '',
161
161
  effort = '',
162
162
  fast = false,
163
+ ultra = false,
163
164
  stats = null,
164
165
  contextWindow = 0,
165
166
  displayContextWindow = 0,
@@ -168,7 +169,9 @@ export function localBootStatusLine(args = {}) {
168
169
  const raw = String(model || '').trim();
169
170
  const { STATUS, SUBTLE } = statusColors();
170
171
  const display = shortenModelName(displayModelName(raw, provider, ''), terminalColumns());
171
- const flags = [effort ? String(effort).toUpperCase() : '', fast === true ? 'FAST' : ''].filter(Boolean);
172
+ let speedLabel = '';
173
+ if (fast === true) speedLabel = ultra === true ? 'Ultrafast' : 'Fast';
174
+ const flags = [effort ? effortDisplayLabel(effort) : '', speedLabel].filter(Boolean);
172
175
  const modelBits = [display, ...flags].join(` ${SUBTLE}·${RESET} `);
173
176
  const ctxPct = measuredContextUsage({
174
177
  stats,
@@ -29,6 +29,7 @@ export function useStatusLineText({
29
29
  model,
30
30
  effort,
31
31
  fast,
32
+ ultra,
32
33
  cwd,
33
34
  stats,
34
35
  contextWindow,
@@ -52,6 +53,7 @@ export function useStatusLineText({
52
53
  model,
53
54
  effort,
54
55
  fast,
56
+ ultra,
55
57
  stats,
56
58
  contextWindow,
57
59
  displayContextWindow,
@@ -83,6 +85,7 @@ export function useStatusLineText({
83
85
  model,
84
86
  effort,
85
87
  fast,
88
+ ultra,
86
89
  cwd,
87
90
  stats,
88
91
  contextWindow,
@@ -180,7 +183,8 @@ export function useStatusLineText({
180
183
  prevImmediate.provider !== args.provider ||
181
184
  prevImmediate.model !== args.model ||
182
185
  prevImmediate.effort !== args.effort ||
183
- prevImmediate.fast !== args.fast;
186
+ prevImmediate.fast !== args.fast ||
187
+ prevImmediate.ultra !== args.ultra;
184
188
  // A theme switch must re-tone the footer immediately: the stored `line`
185
189
  // holds already-normalized ANSI with the OLD palette, so re-running
186
190
  // normalizeStatusLine on it is a no-op. Force a fresh local rebuild (new
@@ -218,6 +222,7 @@ export function useStatusLineText({
218
222
  model: args.model,
219
223
  effort: args.effort,
220
224
  fast: args.fast,
225
+ ultra: args.ultra,
221
226
  contextWindow: args.contextWindow,
222
227
  displayContextWindow: args.displayContextWindow,
223
228
  rawContextWindow: args.rawContextWindow,
@@ -299,6 +304,7 @@ export function useStatusLineText({
299
304
  model,
300
305
  effort,
301
306
  fast,
307
+ ultra,
302
308
  cwd,
303
309
  statsSignature,
304
310
  contextWindow,