mixdog 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -1
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/test-direct.mjs +4 -2
  6. package/src/defaults/shipped-history.json +6 -0
  7. package/src/defaults/skills/setup/references/actions.md +5 -2
  8. package/src/defaults/skills/visualize/SKILL.md +157 -0
  9. package/src/defaults/skills/visualize/references/craft.md +88 -0
  10. package/src/headless-command.mjs +56 -3
  11. package/src/headless-exec.mjs +17 -1
  12. package/src/help.mjs +2 -0
  13. package/src/rules/shared/70-delivery.md +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  16. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  17. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  18. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  19. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/config.mjs +9 -1
  21. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
  22. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
  23. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
  24. package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
  25. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
  29. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  30. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  32. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
  33. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  34. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  37. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  38. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  39. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  40. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
  41. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
  42. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  43. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  44. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  45. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  50. package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  53. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
  54. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  57. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  58. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  59. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  60. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  61. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
  62. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  63. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  65. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  66. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  69. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
  70. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
  71. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
  72. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
  76. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  77. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  78. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  79. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  80. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  81. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  82. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  85. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  86. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  87. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  88. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
  89. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  90. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  91. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  92. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
  93. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
  94. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
  95. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  96. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
  97. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  100. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  101. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
  103. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  113. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  114. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  115. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  116. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  117. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  119. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  120. package/src/runtime/attachments/store.mjs +38 -0
  121. package/src/runtime/shared/atomic-file.mjs +8 -2
  122. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  123. package/src/runtime/shared/code-graph-state.mjs +12 -0
  124. package/src/runtime/shared/defaults-separation.mjs +2 -0
  125. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  126. package/src/runtime/shared/git-runner.mjs +4 -3
  127. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  128. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  129. package/src/runtime/shared/llm/model-catalog.mjs +240 -118
  130. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  131. package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
  132. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  133. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  134. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  135. package/src/runtime/shared/provider-accounts.mjs +2 -2
  136. package/src/runtime/shared/provider-api-key.mjs +21 -1
  137. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  138. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  139. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  140. package/src/runtime/shared/singleton-owner.mjs +36 -6
  141. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  142. package/src/runtime/shared/tool-surface.mjs +7 -0
  143. package/src/runtime/shared/tool-work-units.mjs +14 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  145. package/src/runtime/shared/turn-changes.mjs +791 -0
  146. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  147. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  148. package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
  149. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  150. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  151. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  152. package/src/runtime/shared/worktrees/git.mjs +247 -0
  153. package/src/runtime/shared/worktrees/include.mjs +480 -0
  154. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  155. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  156. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  157. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  158. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  159. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  160. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  161. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  162. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  163. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  164. package/src/runtime/visualize-bridge/client.mjs +181 -0
  165. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  166. package/src/session-runtime/boot/apis.mjs +35 -0
  167. package/src/session-runtime/boot/config.mjs +2 -0
  168. package/src/session-runtime/boot/facade.mjs +20 -0
  169. package/src/session-runtime/boot/infrastructure.mjs +1 -0
  170. package/src/session-runtime/boot/session.mjs +1 -0
  171. package/src/session-runtime/boot/tools.mjs +4 -0
  172. package/src/session-runtime/config-lifecycle.mjs +10 -1
  173. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
  174. package/src/session-runtime/global-extensions.mjs +2 -2
  175. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  176. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  177. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  178. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  179. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  180. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  181. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  182. package/src/session-runtime/mcp-glue/connect.mjs +11 -6
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
  184. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  185. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  186. package/src/session-runtime/model-route/set-route.mjs +15 -2
  187. package/src/session-runtime/notification-bus.mjs +9 -1
  188. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  189. package/src/session-runtime/resource-extension-sync.mjs +30 -7
  190. package/src/session-runtime/resource-mcp-api.mjs +20 -7
  191. package/src/session-runtime/route-resolution.mjs +10 -1
  192. package/src/session-runtime/route-state.mjs +16 -2
  193. package/src/session-runtime/runtime-core.mjs +5 -0
  194. package/src/session-runtime/runtime-facade.mjs +8 -0
  195. package/src/session-runtime/runtime-review-api.mjs +9 -0
  196. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  197. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  198. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  199. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  200. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  201. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  202. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  203. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  204. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  205. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  206. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  207. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  208. package/src/session-runtime/services/agent-tool.mjs +1 -0
  209. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  210. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  211. package/src/session-runtime/session-create/session-options.mjs +10 -1
  212. package/src/session-runtime/session-turn-api.mjs +1 -0
  213. package/src/session-runtime/settings-api.mjs +4 -2
  214. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  215. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  216. package/src/session-runtime/setup-tool/executor.mjs +11 -4
  217. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
  218. package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
  219. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  220. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
  221. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  222. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  223. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  224. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  225. package/src/session-runtime/turn/turn-run.mjs +6 -7
  226. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  227. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  228. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  229. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  230. package/src/session-runtime/workspace/transitions.mjs +651 -0
  231. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  232. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  233. package/src/standalone/channel-transport.mjs +13 -1
  234. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  235. package/src/standalone/daemon-owner.mjs +372 -0
  236. package/src/standalone/daemon-stored-session-views.mjs +4 -1
  237. package/src/standalone/daemon.mjs +139 -5
  238. package/src/standalone/desktop-service-registry.mjs +20 -0
  239. package/src/standalone/local-session-runtime.mjs +11 -0
  240. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  241. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  242. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  243. package/src/standalone/session-client.mjs +104 -13
  244. package/src/standalone/session-protocol.mjs +16 -0
  245. package/src/standalone/session-proxy.mjs +3 -0
  246. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  247. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  248. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  249. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  250. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  251. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  252. package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
  253. package/src/standalone/session-transport/call-route.mjs +13 -0
  254. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  255. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  256. package/src/standalone/session-transport/client-registry.mjs +20 -3
  257. package/src/standalone/session-transport/routes.mjs +79 -2
  258. package/src/standalone/session-transport/stack.mjs +2 -0
  259. package/src/standalone/session-transport.mjs +24 -1
  260. package/src/tui/app/message-selector.mjs +2 -1
  261. package/src/tui/app/model-options.mjs +35 -5
  262. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  263. package/src/tui/app/model-picker.mjs +4 -2
  264. package/src/tui/dist/index.mjs +332 -36
  265. package/src/tui/index.jsx +10 -2
  266. package/src/tui/session/compaction-view.mjs +147 -0
  267. package/src/tui/session/context-state.mjs +6 -0
  268. package/src/tui/session/initial-state.mjs +3 -0
  269. package/src/tui/session/quota-resume.mjs +2 -1
  270. package/src/tui/session/session-api/commands.mjs +10 -2
  271. package/src/tui/session/session-api/controls.mjs +4 -0
  272. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  273. package/src/tui/session/session-api/integrations.mjs +17 -0
  274. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  275. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  276. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  277. package/src/tui/session/session-api/routes.mjs +10 -1
  278. package/src/tui/session/session-api/settings.mjs +2 -0
  279. package/src/tui/session/session-api/worktree.mjs +60 -0
  280. package/src/tui/session/session-api.mjs +2 -0
  281. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  282. package/src/tui/session/session-flow/drain.mjs +15 -1
  283. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  284. package/src/tui/session/session-flow.mjs +2 -0
  285. package/src/tui/session/transcript-intake.mjs +14 -5
  286. package/src/tui/session/turn-stream.mjs +32 -4
  287. package/src/tui/session/turn-tool-cards.mjs +10 -2
  288. package/src/tui/session/turn.mjs +39 -10
@@ -91,14 +91,27 @@ export function createCallRoute({ handleCall, clients, lanes, callCache, nowMs }
91
91
  // tell a failed CALL from a dead TRANSPORT (which must re-attach). The
92
92
  // machine-readable `code` (ECALLIDCONFLICT / ECALLRESULTDROPPED) travels
93
93
  // with it: a client must be able to branch on it, not parse prose.
94
+ // Typed (worktree) failures also carry their plain JSON `details`.
94
95
  sendJson(
95
96
  res,
96
97
  {
97
98
  error: err?.message || String(err),
98
99
  ...(err?.code ? { code: String(err.code) } : {}),
100
+ ...jsonDetails(err),
99
101
  },
100
102
  200
101
103
  );
102
104
  }
103
105
  };
104
106
  }
107
+
108
+ /** `{ details }` when the error's details are a JSON-safe object, else {}. */
109
+ function jsonDetails(err) {
110
+ const details = err?.details;
111
+ if (!details || typeof details !== 'object') return {};
112
+ try {
113
+ return { details: JSON.parse(JSON.stringify(details)) };
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
@@ -5,11 +5,44 @@
5
5
  */
6
6
  import { isPidAlive } from '../../../runtime/shared/pid-liveness.mjs';
7
7
 
8
- export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClientsEmpty, isClosed, dropClient }) {
8
+ const LEASE_MIN_MS = 3_000;
9
+ const LEASE_MAX_MS = 600_000;
10
+ const LEASE_DEFAULT_MS = 30_000;
11
+ const REMOTE_CLIENT_GRACE_DEFAULT_MS = 1_800_000;
12
+
13
+ /** A remote client's lease: the requested `leaseMs`, clamped to 3s..10min. */
14
+ export function clampLeaseMs(value) {
15
+ const leaseMs = Number(value);
16
+ if (!Number.isFinite(leaseMs) || leaseMs <= 0) return LEASE_DEFAULT_MS;
17
+ return Math.min(LEASE_MAX_MS, Math.max(LEASE_MIN_MS, leaseMs));
18
+ }
19
+
20
+ // A remote desktop behind a flaky tunnel needs far longer to come back than a
21
+ // local terminal restarting. Read at arm time so the env can change per run.
22
+ function remoteClientGraceMs() {
23
+ const configured = Number(process.env.MIXDOG_REMOTE_CLIENT_GRACE_MS);
24
+ return Number.isFinite(configured) && configured >= 0 && process.env.MIXDOG_REMOTE_CLIENT_GRACE_MS
25
+ ? configured
26
+ : REMOTE_CLIENT_GRACE_DEFAULT_MS;
27
+ }
28
+
29
+ export function createClientGrace({
30
+ clients,
31
+ log,
32
+ clientGraceMs,
33
+ sweepMs,
34
+ nowMs = () => Date.now(),
35
+ onClientsEmpty,
36
+ isClosed,
37
+ dropClient,
38
+ }) {
9
39
  let graceTimer = null;
10
40
  let sweepTimer = null;
11
41
  let everHadLifecycleClient = false;
12
42
  let signalled = false;
43
+ // Whether the most recently departed lifecycle client was remote; it picks
44
+ // the empty-clients grace.
45
+ let lastLifecycleWasRemote = false;
13
46
 
14
47
  function lifecycleClientCount() {
15
48
  let count = 0;
@@ -45,10 +78,14 @@ export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClie
45
78
  try {
46
79
  onClientsEmpty();
47
80
  } catch {}
48
- }, clientGraceMs);
81
+ }, lastLifecycleWasRemote ? remoteClientGraceMs() : clientGraceMs);
49
82
  graceTimer.unref?.();
50
83
  }
51
84
 
85
+ function noteLifecycleDropped(client) {
86
+ lastLifecycleWasRemote = client.remote === true;
87
+ }
88
+
52
89
  /** A lifecycle client holds the daemon up: remember one ever attached and
53
90
  * cancel any pending grace. */
54
91
  function noteLifecycleClient() {
@@ -61,6 +98,11 @@ export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClie
61
98
  if (sweepTimer || isClosed()) return;
62
99
  sweepTimer = setInterval(() => {
63
100
  for (const [token, c] of [...clients]) {
101
+ if (c.remote) {
102
+ // Its owning process is on another machine: liveness is the lease.
103
+ if (nowMs() - c.lastSeen > c.leaseMs) dropClient(token, 'lease expired');
104
+ continue;
105
+ }
64
106
  // A client whose owning process died can never read its stream again.
65
107
  if (c.leadPid && !isPidAlive(c.leadPid)) dropClient(token, 'lead pid gone');
66
108
  }
@@ -77,5 +119,5 @@ export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClie
77
119
  }
78
120
  }
79
121
 
80
- return { lifecycleClientCount, cancel, maybeArm, noteLifecycleClient, startSweep, stopTimers };
122
+ return { lifecycleClientCount, cancel, maybeArm, noteLifecycleClient, noteLifecycleDropped, startSweep, stopTimers };
81
123
  }
@@ -31,6 +31,7 @@ export function createRegistrationReplays({ clients, log }) {
31
31
  replay.leadPid === identity.leadPid &&
32
32
  replay.cwd === identity.cwd &&
33
33
  replay.lifecycle === identity.lifecycle &&
34
+ replay.remote === identity.remote &&
34
35
  replay.clientKind === identity.clientKind &&
35
36
  replay.revision === identity.revision;
36
37
  if (sameIdentity && clients.has(replay.token)) {
@@ -7,7 +7,7 @@
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { parsePid } from '../../runtime/shared/pid-liveness.mjs';
9
9
  import { createRegistrationReplays } from './client-registry/registration-replays.mjs';
10
- import { createClientGrace } from './client-registry/client-grace.mjs';
10
+ import { createClientGrace, clampLeaseMs } from './client-registry/client-grace.mjs';
11
11
 
12
12
  export function createClientRegistry({
13
13
  log,
@@ -18,7 +18,7 @@ export function createClientRegistry({
18
18
  onClientRegistered,
19
19
  onClientDropped,
20
20
  }) {
21
- // token -> { token, leadPid, cwd, lifecycle, sse, pending, lastSeen }
21
+ // token -> { token, leadPid, cwd, lifecycle, remote, leaseMs, sse, pending, lastSeen }
22
22
  const clients = new Map();
23
23
  let closed = false;
24
24
  const replays = createRegistrationReplays({ clients, log });
@@ -27,6 +27,7 @@ export function createClientRegistry({
27
27
  log,
28
28
  clientGraceMs,
29
29
  sweepMs,
30
+ nowMs,
30
31
  onClientsEmpty,
31
32
  isClosed: () => closed,
32
33
  dropClient: (token, reason) => dropClient(token, reason),
@@ -35,6 +36,7 @@ export function createClientRegistry({
35
36
  function removeClientRecord(token) {
36
37
  const c = clients.get(token);
37
38
  if (!c) return;
39
+ if (c.lifecycle) grace.noteLifecycleDropped(c);
38
40
  try {
39
41
  c.sse?.end?.();
40
42
  } catch {}
@@ -57,6 +59,14 @@ export function createClientRegistry({
57
59
  grace.maybeArm(reason);
58
60
  }
59
61
 
62
+ /** Refresh a client's lease clock (heartbeat). False for an unknown token. */
63
+ function touch(token) {
64
+ const c = clients.get(token);
65
+ if (!c) return false;
66
+ c.lastSeen = nowMs();
67
+ return true;
68
+ }
69
+
60
70
  function register({
61
71
  leadPid,
62
72
  cwd,
@@ -64,11 +74,14 @@ export function createClientRegistry({
64
74
  clientKind = 'session',
65
75
  registrationId = null,
66
76
  revision = 0,
77
+ remote = false,
78
+ leaseMs,
67
79
  } = {}) {
68
80
  const identity = {
69
81
  leadPid: parsePid(leadPid) || 0,
70
82
  cwd: cwd || null,
71
83
  lifecycle: lifecycle !== false,
84
+ remote: remote === true,
72
85
  clientKind: clientKind === 'desktop' ? 'desktop' : 'session',
73
86
  revision: Math.max(0, Number(revision) || 0),
74
87
  };
@@ -79,6 +92,7 @@ export function createClientRegistry({
79
92
  clients.set(token, {
80
93
  token,
81
94
  ...identity,
95
+ leaseMs: clampLeaseMs(leaseMs),
82
96
  sse: null,
83
97
  // True while the socket asked us to stop writing (see writeFrame).
84
98
  paused: false,
@@ -92,7 +106,8 @@ export function createClientRegistry({
92
106
  grace.startSweep();
93
107
  log(
94
108
  `client registered token=${token} lead=${identity.leadPid} cwd=${identity.cwd || '-'}` +
95
- ` lifecycle=${identity.lifecycle} kind=${identity.clientKind}`
109
+ ` lifecycle=${identity.lifecycle} kind=${identity.clientKind}` +
110
+ (identity.remote ? ` remote leaseMs=${clients.get(token).leaseMs}` : '')
96
111
  );
97
112
  if (typeof onClientRegistered === 'function') {
98
113
  try {
@@ -102,6 +117,7 @@ export function createClientRegistry({
102
117
  cwd: identity.cwd,
103
118
  lifecycle: identity.lifecycle,
104
119
  clientKind: identity.clientKind,
120
+ ...(identity.remote ? { remote: true } : {}),
105
121
  });
106
122
  } catch {}
107
123
  }
@@ -124,6 +140,7 @@ export function createClientRegistry({
124
140
  forgetReplaysFor: replays.forgetFor,
125
141
  dropClient,
126
142
  register,
143
+ touch,
127
144
  close,
128
145
  };
129
146
  }
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * session-transport/routes.mjs — the HTTP front door: /health (identity
3
3
  * travels with every probe), the daemon-token gate, client register /
4
- * deregister, the /events SSE attach, /call, /shutdown and the /upgrade
4
+ * deregister / heartbeat, the /events SSE attach, /call, /shutdown, /stop and the /upgrade
5
5
  * handshake a newer build uses to replace this daemon.
6
6
  */
7
+ import { readFileSync } from 'node:fs';
7
8
  import { sendJson, sendError } from '../../runtime/memory/lib/http-wire.mjs';
9
+ import { modelProxyClientMode } from '../../runtime/shared/model-proxy-mode.mjs';
8
10
  import {
9
11
  compareRuntimeVersions,
10
12
  SESSION_CAPABILITY_FINGERPRINT,
@@ -13,6 +15,32 @@ import {
13
15
  runtimeVersion,
14
16
  } from '../session-wire.mjs';
15
17
 
18
+ let installBuildHash;
19
+ /** Build hash of a desktop-installed server runtime: the `.install-complete`
20
+ * marker the installer writes at the package root. Null for any other
21
+ * install (npm, source checkout, packaged desktop). */
22
+ function remoteInstallBuildHash() {
23
+ if (installBuildHash !== undefined) return installBuildHash;
24
+ try {
25
+ const marker = JSON.parse(readFileSync(new URL('../../../.install-complete', import.meta.url), 'utf8'));
26
+ installBuildHash = typeof marker?.buildHash === 'string' && marker.buildHash ? marker.buildHash : null;
27
+ } catch {
28
+ installBuildHash = null;
29
+ }
30
+ return installBuildHash;
31
+ }
32
+
33
+ /** Turns in flight: what makes a daemon too busy to replace without asking. */
34
+ function busySessionCount(status) {
35
+ return Math.max(0, Number(status?.busy) || 0);
36
+ }
37
+
38
+ /** Leave through the embedder's own graceful SIGTERM path. */
39
+ function requestProcessStop() {
40
+ if (process.listenerCount('SIGTERM') > 0) process.emit('SIGTERM', 'SIGTERM');
41
+ else process.kill(process.pid, 'SIGTERM');
42
+ }
43
+
16
44
  export function createTransportRoutes({
17
45
  serverToken,
18
46
  drain,
@@ -25,11 +53,23 @@ export function createTransportRoutes({
25
53
  transportMemory,
26
54
  onClientsEmpty,
27
55
  onUpgradeRequested,
56
+ admitWork = () => true,
28
57
  }) {
58
+ // Checked on every register/call, last right before the work is handed on
59
+ // (after the body is read): a daemon that lost (or cannot re-verify) its
60
+ // owner lease serves nothing.
61
+ const refuseUnadmitted = (res) => {
62
+ if (admitWork()) return false;
63
+ sendError(res, 'daemon owner lease is not held', 503);
64
+ return true;
65
+ };
66
+
29
67
  const health = (res) => {
30
68
  // Identity travels with EVERY health probe: a view negotiates version
31
69
  // skew before it attaches, so an embedder can never forget to publish
32
70
  // it and leave clients guessing.
71
+ const status = getStatus();
72
+ const busySessions = busySessionCount(status);
33
73
  sendJson(res, {
34
74
  status: 'ok',
35
75
  pid: process.pid,
@@ -38,10 +78,15 @@ export function createTransportRoutes({
38
78
  protocol: SESSION_PROTOCOL,
39
79
  revision: SESSION_REVISION,
40
80
  capabilityFingerprint: SESSION_CAPABILITY_FINGERPRINT,
81
+ modelProxy: modelProxyClientMode() ? 'client' : null,
41
82
  draining: drain.reason || null,
42
83
  drainCommitted: drain.committed,
43
84
  version: runtimeVersion(),
44
- ...getStatus(),
85
+ packageVersion: runtimeVersion(),
86
+ buildHash: remoteInstallBuildHash(),
87
+ ...status,
88
+ busySessions,
89
+ idle: busySessions === 0,
45
90
  activeCalls: lanes.active,
46
91
  queuedCalls: lanes.queued,
47
92
  queuedUrgentCalls: lanes.queuedUrgent,
@@ -54,6 +99,7 @@ export function createTransportRoutes({
54
99
 
55
100
  const register = async (req, res) => {
56
101
  const body = await readBody(req);
102
+ if (refuseUnadmitted(res)) return;
57
103
  if (drain.committed) {
58
104
  sendError(res, `daemon is draining: ${drain.reason}`, 503);
59
105
  return;
@@ -70,6 +116,8 @@ export function createTransportRoutes({
70
116
  clientKind: body.clientKind,
71
117
  registrationId: body.registrationId,
72
118
  revision: body.revision,
119
+ remote: body.remote === true,
120
+ leaseMs: body.leaseMs,
73
121
  });
74
122
  sendJson(res, {
75
123
  token: clientToken,
@@ -85,6 +133,16 @@ export function createTransportRoutes({
85
133
  sendJson(res, { ok: true });
86
134
  };
87
135
 
136
+ // Lease keep-alive for remote clients (the daemon token gate already ran).
137
+ const heartbeat = async (req, res) => {
138
+ const body = await readBody(req);
139
+ if (!body.token || !registry.touch(String(body.token))) {
140
+ sendError(res, 'unknown client token', 404);
141
+ return;
142
+ }
143
+ sendJson(res, { ok: true });
144
+ };
145
+
88
146
  const events = (url, res) => {
89
147
  const clientToken = url.searchParams.get('token');
90
148
  if (!attachSse(clientToken, res)) sendError(res, 'unknown client token', 404);
@@ -92,11 +150,15 @@ export function createTransportRoutes({
92
150
  };
93
151
 
94
152
  const call = async (req, res) => {
153
+ if (refuseUnadmitted(res)) return;
95
154
  if (drain.committed) {
96
155
  sendError(res, `daemon is draining: ${drain.reason}`, 503);
97
156
  return;
98
157
  }
99
158
  const body = await readBody(req);
159
+ // Again right before dispatch: the lease may have been fenced while the
160
+ // body was still arriving.
161
+ if (refuseUnadmitted(res)) return;
100
162
  await callRoute(body, res);
101
163
  };
102
164
 
@@ -109,6 +171,19 @@ export function createTransportRoutes({
109
171
  }
110
172
  };
111
173
 
174
+ // Authenticated stop for a desktop replacing this daemon with another
175
+ // build: refused while turns run unless forced (the user confirmed).
176
+ const stop = async (req, res) => {
177
+ const body = await readBody(req);
178
+ const busySessions = busySessionCount(getStatus());
179
+ if (busySessions > 0 && body.force !== true) {
180
+ sendError(res, `daemon is busy: ${busySessions} running session(s)`, 409);
181
+ return;
182
+ }
183
+ sendJson(res, { ok: true, pid: process.pid, busySessions });
184
+ setImmediate(requestProcessStop);
185
+ };
186
+
112
187
  const upgrade = async (req, res) => {
113
188
  const body = await readBody(req);
114
189
  const requestedProtocol = Number(body.protocol);
@@ -150,8 +225,10 @@ export function createTransportRoutes({
150
225
  const tokenRoutes = {
151
226
  'POST /client/register': register,
152
227
  'POST /client/deregister': deregister,
228
+ 'POST /client/heartbeat': heartbeat,
153
229
  'POST /call': call,
154
230
  'POST /shutdown': (_req, res) => shutdown(res),
231
+ 'POST /stop': stop,
155
232
  'POST /upgrade': upgrade,
156
233
  };
157
234
 
@@ -76,6 +76,7 @@ export function createTransportStack({
76
76
  onClientDropped,
77
77
  onUpgradeRequested,
78
78
  getStatus,
79
+ admitWork,
79
80
  }) {
80
81
  const body = createBodyReader();
81
82
  const callCache = createSessionCallCache({
@@ -125,6 +126,7 @@ export function createTransportStack({
125
126
  }),
126
127
  onClientsEmpty,
127
128
  onUpgradeRequested,
129
+ admitWork,
128
130
  });
129
131
 
130
132
  return { registry, clients, lanes, callCache, broadcast, handleRequest };
@@ -32,6 +32,9 @@ export function createSessionTransport({
32
32
  onClientDropped = null,
33
33
  onUpgradeRequested = null,
34
34
  getStatus = () => ({}),
35
+ // Asked before every register/call; false answers 503 (the daemon's owner
36
+ // lease is not held — daemon-owner.mjs).
37
+ admitWork = () => true,
35
38
  } = {}) {
36
39
  if (typeof handleCall !== 'function') throw new Error('handleCall is required');
37
40
 
@@ -52,6 +55,7 @@ export function createSessionTransport({
52
55
  onClientDropped,
53
56
  onUpgradeRequested,
54
57
  getStatus,
58
+ admitWork,
55
59
  });
56
60
 
57
61
  function beginDrain(reason = 'daemon replacement') {
@@ -69,10 +73,19 @@ export function createSessionTransport({
69
73
  return true;
70
74
  }
71
75
 
76
+ // MIXDOG_SESSION_PORT pins the port (an SSH forward needs a stable one).
77
+ function configuredPort() {
78
+ const port = Number(process.env.MIXDOG_SESSION_PORT);
79
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : 0;
80
+ }
81
+
72
82
  function start() {
73
83
  if (closed) return Promise.reject(new Error('session service transport is closed'));
84
+ const fixedPort = configuredPort();
74
85
  listener ??= createLoopbackListener({
75
86
  server: http.createServer(handleRequest),
87
+ basePort: fixedPort,
88
+ maxPort: fixedPort,
76
89
  onListening(port) {
77
90
  boundPort = port;
78
91
  log(`session service transport listening on 127.0.0.1:${boundPort} pid=${process.pid}`);
@@ -81,7 +94,17 @@ export function createSessionTransport({
81
94
  if (!fatal) log(`server error: ${error?.message || error}`);
82
95
  },
83
96
  });
84
- return listener.start().then((port) => ({ port, token: serverToken }));
97
+ return listener.start().then(async (port) => {
98
+ // The listener falls back to an OS-chosen port when the range is busy;
99
+ // a pinned port must never silently become a different one.
100
+ if (fixedPort && port !== fixedPort) {
101
+ await listener.stop();
102
+ listener = null;
103
+ boundPort = null;
104
+ throw new Error(`session port ${fixedPort} (MIXDOG_SESSION_PORT) is unavailable`);
105
+ }
106
+ return { port, token: serverToken };
107
+ });
85
108
  }
86
109
 
87
110
  async function stop() {
@@ -18,7 +18,8 @@ function selectableUserItems(items, limit = MAX_SELECTABLE_MESSAGES) {
18
18
  for (let index = source.length - 1; index >= 0 && rows.length < limit; index -= 1) {
19
19
  const item = source[index];
20
20
  if (item?.kind !== 'user') continue;
21
- if (item?.id == null) continue;
21
+ // Prompts a compaction archived are display-only.
22
+ if (item?.id == null || item.archived === true) continue;
22
23
  const text = String(item.text || '').trim();
23
24
  if (!text) continue;
24
25
  rows.push({ id: String(item.id), text });
@@ -191,6 +191,34 @@ const PROVIDER_DISPLAY_NAMES = new Map([
191
191
  export const providerDisplayName = (provider) =>
192
192
  PROVIDER_DISPLAY_NAMES.get(String(provider || '').toLowerCase()) ?? (provider || 'Provider');
193
193
 
194
+ export const resolveProviderDisplayName = (provider, providerNames = null) =>
195
+ (String(provider || '')
196
+ .trim()
197
+ .startsWith('custom-') &&
198
+ providerNames?.[String(provider).trim()]) ||
199
+ providerDisplayName(provider);
200
+
201
+ export const loadProviderDisplayNames = async (store) => {
202
+ let setup = null;
203
+ try {
204
+ setup = await store?.getProviderSetup?.();
205
+ } catch {
206
+ return {};
207
+ }
208
+ const names = {};
209
+ for (const group of ['api', 'oauth', 'local']) {
210
+ const entries = setup?.[group];
211
+ if (!Array.isArray(entries)) continue;
212
+ for (const entry of entries) {
213
+ if (!entry || typeof entry !== 'object') continue;
214
+ const id = String(entry.id || entry.provider || '').trim();
215
+ const name = String(entry.name || entry.label || '').trim();
216
+ if (id && name) names[id] = name;
217
+ }
218
+ }
219
+ return names;
220
+ };
221
+
194
222
  const PROVIDER_DISPLAY_RANKS = {
195
223
  default: 0,
196
224
  'openai-oauth': 10,
@@ -247,7 +275,7 @@ const routeModelDisplayName = (route) =>
247
275
  .filter(Boolean)
248
276
  .join(' ');
249
277
 
250
- const groupModelsByProvider = (models) => {
278
+ const groupModelsByProvider = (models, providerNames = null) => {
251
279
  const providers = new Map();
252
280
  for (const model of models) {
253
281
  if (!providers.has(model.provider)) providers.set(model.provider, []);
@@ -257,22 +285,24 @@ const groupModelsByProvider = (models) => {
257
285
  const orderedProviders = [...providers.keys()].sort((a, b) => {
258
286
  const rank = providerDisplayRank(a) - providerDisplayRank(b);
259
287
  if (rank !== 0) return rank;
260
- const label = providerDisplayName(a).localeCompare(providerDisplayName(b));
288
+ const label = resolveProviderDisplayName(a, providerNames).localeCompare(
289
+ resolveProviderDisplayName(b, providerNames)
290
+ );
261
291
  if (label !== 0) return label;
262
292
  return a.localeCompare(b);
263
293
  });
264
294
  return { providers, orderedProviders };
265
295
  };
266
296
 
267
- export const buildModelProviderItems = (models, currentRoute = null) => {
268
- const { providers, orderedProviders } = groupModelsByProvider(models);
297
+ export const buildModelProviderItems = (models, currentRoute = null, providerNames = null) => {
298
+ const { providers, orderedProviders } = groupModelsByProvider(models, providerNames);
269
299
  return orderedProviders.map((provider) => {
270
300
  const providerModels = providers.get(provider) || [];
271
301
  const currentModel =
272
302
  currentRoute?.provider === provider ? providerModels.find((model) => model.id === currentRoute.model) : null;
273
303
  return {
274
304
  value: `provider:${provider}`,
275
- label: providerDisplayName(provider),
305
+ label: resolveProviderDisplayName(provider, providerNames),
276
306
  marker: currentModel ? '✓' : '',
277
307
  markerColor: theme.success,
278
308
  meta: currentModel ? modelRecordDisplayName(currentModel) : '',
@@ -2,7 +2,7 @@
2
2
  // One provider's model list: rows from the catalog, the footer for the
3
3
  // highlighted row, and the keys that change its selections (←/→ effort,
4
4
  // C/Shift+C context, Tab Fast, T thinking). Enter saves through the caller.
5
- import { buildProviderModelItems, providerDisplayName } from '../model-options.mjs';
5
+ import { buildProviderModelItems, resolveProviderDisplayName } from '../model-options.mjs';
6
6
  import { modelFooter } from './model-footer.mjs';
7
7
  import { createRouteSelection } from './route-selection.mjs';
8
8
 
@@ -73,7 +73,7 @@ export function openProviderModelsPicker({ view, provider, onBack, saveRoute })
73
73
  )
74
74
  );
75
75
  view.paint({
76
- title: providerDisplayName(provider),
76
+ title: resolveProviderDisplayName(provider, view.providerNames),
77
77
  description: options.modelDescription || 'Select a model. Adjust Effort with ←/→.',
78
78
  footer: (item) => modelFooter(selection, item?._model),
79
79
  help:
@@ -10,7 +10,7 @@
10
10
  * state (getState) is read through a getter so it always reflects the current
11
11
  * render.
12
12
  */
13
- import { normalizeModelOptions, buildModelProviderItems } from './model-options.mjs';
13
+ import { normalizeModelOptions, buildModelProviderItems, loadProviderDisplayNames } from './model-options.mjs';
14
14
  import { createModelCatalog } from './model-picker/catalog-load.mjs';
15
15
  import { openProviderModelsPicker, saveSelectedRoute } from './model-picker/provider-models-list.mjs';
16
16
 
@@ -77,6 +77,7 @@ export function createModelPicker({
77
77
  }
78
78
 
79
79
  const models = normalizeModelOptions(providerModels);
80
+ const providerNames = await loadProviderDisplayNames(store);
80
81
  const activeRoute = options.currentRoute || {
81
82
  provider: state.provider,
82
83
  model: state.model,
@@ -90,6 +91,7 @@ export function createModelPicker({
90
91
  state,
91
92
  models,
92
93
  activeRoute,
94
+ providerNames,
93
95
  returnTo,
94
96
  returnLabel,
95
97
  returnOnNestedCancel,
@@ -114,7 +116,7 @@ export function createModelPicker({
114
116
  providerListHighlightProvider = renderOptions.highlightProvider;
115
117
  }
116
118
  const highlightProvider = renderOptions.highlightProvider || providerListHighlightProvider || null;
117
- const providerItems = buildModelProviderItems(models, activeRoute);
119
+ const providerItems = buildModelProviderItems(models, activeRoute, providerNames);
118
120
  const providerHighlight = highlightProvider || activeRoute?.provider || null;
119
121
  const providerInitialIndex = Math.max(
120
122
  0,