@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925

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 (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-D5eN1TID.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
@@ -19,7 +19,8 @@ import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-sta
19
19
  * 011 (Claude Code), 012 (Grok).
20
20
  */
21
21
  import { join } from "node:path";
22
- import { loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
22
+ import { existsSync } from "node:fs";
23
+ import { getConfigPath, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../../config";
23
24
  import { readRuntimePort } from "../../config/process-state";
24
25
  import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
25
26
  import { getCodexHome } from "../../codex/paths";
@@ -30,11 +31,15 @@ import {
30
31
  applyDesktopFirstParty,
31
32
  captureDesktopFirstPartyRollback,
32
33
  inspectDesktopFirstParty,
34
+ observeClaudeDesktopMode,
33
35
  recordClaudeDesktopMode,
34
36
  removeDesktopFirstParty,
35
37
  resolveClaudeDesktopApplyMode,
36
38
  type ClaudeDesktopMode,
37
39
  } from "../../claude/desktop-first-party";
40
+ import { FIRST_PARTY_ACCOUNT_RISK } from "../../claude/desktop-risk";
41
+ import type { DesktopPickerStatus } from "../../claude/desktop-picker";
42
+ import { pickerPreferenceOn, runPickerTransition } from "./claude-desktop-picker-routes";
38
43
  import { projectGrokCatalog } from "../../grok/catalog";
39
44
  import { injectGrokConfig, stripGrokConfig } from "../../grok/inject";
40
45
  import { inspectGrokConfig } from "../../grok/inspect";
@@ -149,7 +154,7 @@ function desktopStatus(config: ManagementContext["config"]): NativeStatus {
149
154
  : null;
150
155
  // First-party mode lives in Claude Code's settings.json, not in Desktop's library. A leftover
151
156
  // gateway profile still counts as current (it is what Desktop is actually running).
152
- const firstParty = resolveClaudeDesktopApplyMode(config) === "first-party" && gatewayState === "absent"
157
+ const firstParty = resolveClaudeDesktopApplyMode(config, observeClaudeDesktopMode(config)) === "first-party" && gatewayState === "absent"
153
158
  ? inspectDesktopFirstParty(config)
154
159
  : null;
155
160
  const state: NativeStatus["state"] = firstParty
@@ -183,11 +188,26 @@ function claudeStatus(config: ManagementContext["config"], configPath: string):
183
188
  };
184
189
  }
185
190
 
191
+ /**
192
+ * The state the latest Codex toggle in this process reported, keyed by the intent it
193
+ * applied. Intent alone cannot describe an apply or restore that did not complete: the
194
+ * PUT reports `absent` for a skipped or failed enable and `unsafe` for an incomplete
195
+ * restore, and the next GET must not turn those into `current` or `absent`. It applies
196
+ * only while the persisted intent still matches; a restart re-runs startup convergence.
197
+ */
198
+ let codexLastToggle: { desiredEnabled: boolean; state: NativeStatus["state"] } | null = null;
199
+
200
+ function rememberCodexToggle(desiredEnabled: boolean, state: NativeStatus["state"]): NativeStatus["state"] {
201
+ codexLastToggle = { desiredEnabled, state };
202
+ return state;
203
+ }
204
+
186
205
  function codexStatus(config: ManagementContext["config"], configPath: string): NativeStatus {
187
206
  const desiredEnabled = config.clientIntegrations?.codex !== false;
207
+ const reported = codexLastToggle?.desiredEnabled === desiredEnabled ? codexLastToggle.state : null;
188
208
  return {
189
209
  clientId: "codex",
190
- state: desiredEnabled ? "current" : "absent",
210
+ state: reported ?? (desiredEnabled ? "current" : "absent"),
191
211
  installed: true,
192
212
  configPath,
193
213
  desiredEnabled,
@@ -356,7 +376,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
356
376
  if (applied.status === "skipped") {
357
377
  return jsonResponse({
358
378
  ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
359
- state: "absent",
379
+ state: rememberCodexToggle(true, "absent"),
360
380
  desiredEnabled: enabled,
361
381
  message: "Codex integration is OFF; enable did not change Codex.",
362
382
  reason: "apply_incomplete",
@@ -364,7 +384,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
364
384
  }
365
385
  return jsonResponse({
366
386
  ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
367
- state: applied.ok ? "current" : "absent",
387
+ state: rememberCodexToggle(true, applied.ok ? "current" : "absent"),
368
388
  desiredEnabled: enabled,
369
389
  message: applied.ok
370
390
  ? "Codex now routes through opencodex"
@@ -379,6 +399,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
379
399
  if (durable && persisted.status === "unchanged") {
380
400
  const { classifyNativeRoutedResidue } = await import("../../codex/native-residue");
381
401
  if (classifyNativeRoutedResidue().kind === "clean") {
402
+ rememberCodexToggle(false, "absent");
382
403
  return jsonResponse({
383
404
  ok: true, clientId: "codex", changed: false, state: "absent", desiredEnabled: false,
384
405
  message: "Codex integration is already OFF and native; no Codex files changed.",
@@ -390,7 +411,7 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
390
411
  const restored = await restoreNativeCodexAsync({ revalidateDesiredState: true });
391
412
  return jsonResponse({
392
413
  ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
393
- state: restored.success ? "absent" : "unsafe",
414
+ state: rememberCodexToggle(false, restored.success ? "absent" : "unsafe"),
394
415
  desiredEnabled: enabled,
395
416
  message: restored.success
396
417
  ? `Codex restored to its native path; the proxy is still serving other clients. ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`
@@ -635,6 +656,24 @@ export function firstPartyRefusalMessage(
635
656
  }
636
657
  }
637
658
 
659
+ /** One line for a toggle message about picker mode after a first-party enable. */
660
+ function pickerStateNote(picker: DesktopPickerStatus): string {
661
+ if (picker.reason === "active" || picker.reason === "restart_required") {
662
+ return "Picker mode is on: fully quit and reopen Claude Desktop to see OpenCodex models in the Code tab.";
663
+ }
664
+ if (picker.reason === "trust_pending" || picker.reason === "trust_declined") {
665
+ return `Picker mode is waiting for the keychain step: run ${picker.hint ?? "ocx claude desktop picker trust"}.`;
666
+ }
667
+ return `Picker mode is off (${picker.reason}).`;
668
+ }
669
+
670
+ /** Empty unless turning picker mode off left something behind. */
671
+ function pickerCleanupNote(picker: DesktopPickerStatus): string {
672
+ return picker.residual?.length
673
+ ? `Picker mode cleanup is incomplete (${picker.residual.join(", ")}); run ocx claude desktop picker off.`
674
+ : "";
675
+ }
676
+
638
677
  /** Record which Desktop mode is applied; `false` when the config file could not be updated. */
639
678
  function persistDesktopModeMarker(desktopMode: ClaudeDesktopMode): boolean {
640
679
  const outcome = mutatePersistedConfig(persisted => recordClaudeDesktopMode(persisted, desktopMode));
@@ -668,104 +707,125 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
668
707
  const fingerprint = current.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
669
708
 
670
709
  if (!body.enabled) {
671
- const firstPartyRemoved = removeDesktopFirstParty();
672
- if (!firstPartyRemoved.ok) {
673
- return postCommitRefusal(409, "claude-desktop", "write_failed",
674
- `Claude Code settings could not be read (${firstPartyRemoved.path}); the first-party proxy env was left in place.`, { desiredEnabled });
675
- }
676
- const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint });
677
- if (removed.kind === "cleanup_incomplete") {
678
- return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
679
- "Claude Desktop now points at standard mode, but credential cleanup is incomplete.",
680
- { desiredEnabled, residualPaths: removed.residualPaths ?? [] });
681
- }
682
- if (!removed.ok) {
683
- return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
684
- "Claude Desktop configuration could not be changed safely.", { desiredEnabled });
685
- }
686
- const changed = removed.changed || firstPartyRemoved.changed;
687
- return jsonResponse({
688
- ok: true, clientId: "claude-desktop", changed, state: "absent", desiredEnabled,
689
- message: changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
690
- } satisfies NativeToggleEnvelope);
691
- }
692
-
693
- if (resolveClaudeDesktopApplyMode(current) === "first-party") {
694
- const rollback = captureDesktopFirstPartyRollback(current);
695
- const applied = applyDesktopFirstParty(current);
696
- if (!applied.ok) {
697
- const reason = applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? applied.reason : "write_failed";
698
- return postCommitRefusal(applied.reason === "unreadable" || applied.reason === "ca_unavailable" ? 500 : 409, "claude-desktop", reason,
699
- firstPartyRefusalMessage(applied.reason, applied.path), { desiredEnabled });
700
- }
701
- const library = inspectDesktop3pConfigLibrary({ appliedFingerprint: fingerprint });
702
- let gatewayRemoved = false;
703
- if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
704
- const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint, replaceWhileEnabled: true });
705
- if (!removed.ok && !removed.changed && applied.changed && !rollback()) {
706
- return postCommitRefusal(500, "claude-desktop", "write_failed",
707
- "Gateway cleanup and first-party settings rollback did not complete.", { desiredEnabled });
710
+ // Picker mode goes first: stop terminating claude.ai, drop its profile and trust.
711
+ return await runPickerTransition(current, async ops => {
712
+ const pickerOff = await ops.disableLocked({ persist: false });
713
+ const firstPartyRemoved = removeDesktopFirstParty();
714
+ if (!firstPartyRemoved.ok) {
715
+ return postCommitRefusal(409, "claude-desktop", "write_failed",
716
+ `Claude Code settings could not be read (${firstPartyRemoved.path}); the first-party proxy env was left in place.`, { desiredEnabled });
708
717
  }
709
- const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker("first-party")
710
- ? " First-party is active but its mode marker was not saved." : "";
718
+ const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint });
711
719
  if (removed.kind === "cleanup_incomplete") {
712
720
  return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
713
- "Claude Desktop now points at standard mode, but gateway credential cleanup is incomplete; the first-party connection remains active." + partialModeWarning,
721
+ "Claude Desktop now points at standard mode, but credential cleanup is incomplete.",
714
722
  { desiredEnabled, residualPaths: removed.residualPaths ?? [] });
715
723
  }
716
724
  if (!removed.ok) {
717
725
  return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
718
- "The gateway profile could not be removed safely; the mode switch is incomplete." + partialModeWarning, { desiredEnabled });
726
+ "Claude Desktop configuration could not be changed safely.", { desiredEnabled });
719
727
  }
720
- gatewayRemoved = removed.changed;
721
- }
722
- const modeSaved = persistDesktopModeMarker("first-party");
723
- const changed = applied.changed || gatewayRemoved;
724
- return jsonResponse({
725
- ok: true, clientId: "claude-desktop", changed, state: "current", desiredEnabled,
726
- message: [
727
- changed
728
- ? "Claude Desktop integration enabled (first-party). Fully quit and reopen Claude Desktop."
729
- : "Claude Desktop integration is already on.",
730
- modeSaved ? "" : "The first-party mode marker could not be saved to config; status may report the mode as unsaved.",
731
- ].filter(Boolean).join(" "),
732
- } satisfies NativeToggleEnvelope);
728
+ const changed = removed.changed || firstPartyRemoved.changed;
729
+ return jsonResponse({
730
+ ok: true, clientId: "claude-desktop", changed, state: "absent", desiredEnabled,
731
+ message: [
732
+ changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
733
+ pickerCleanupNote(pickerOff),
734
+ ].filter(Boolean).join(" "),
735
+ } satisfies NativeToggleEnvelope);
736
+ });
737
+ }
738
+
739
+ if (resolveClaudeDesktopApplyMode(current, observeClaudeDesktopMode(current)) === "first-party") {
740
+ // The whole switch runs under the picker lock, in today's order: env first, then gateway cleanup.
741
+ return await runPickerTransition(current, async ops => {
742
+ const rollback = captureDesktopFirstPartyRollback(current);
743
+ const applied = applyDesktopFirstParty(current);
744
+ if (!applied.ok) {
745
+ const reason = applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? applied.reason : "write_failed";
746
+ return postCommitRefusal(applied.reason === "unreadable" || applied.reason === "ca_unavailable" ? 500 : 409, "claude-desktop", reason,
747
+ firstPartyRefusalMessage(applied.reason, applied.path), { desiredEnabled });
748
+ }
749
+ const library = inspectDesktop3pConfigLibrary({ appliedFingerprint: fingerprint });
750
+ let gatewayRemoved = false;
751
+ if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
752
+ const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint, replaceWhileEnabled: true });
753
+ if (!removed.ok && !removed.changed && applied.changed && !rollback()) {
754
+ return postCommitRefusal(500, "claude-desktop", "write_failed",
755
+ "Gateway cleanup and first-party settings rollback did not complete.", { desiredEnabled });
756
+ }
757
+ const partialModeWarning = !removed.ok && removed.changed && !persistDesktopModeMarker("first-party")
758
+ ? " First-party is active but its mode marker was not saved." : "";
759
+ if (removed.kind === "cleanup_incomplete") {
760
+ return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
761
+ "Claude Desktop now points at standard mode, but gateway credential cleanup is incomplete; the first-party connection remains active." + partialModeWarning,
762
+ { desiredEnabled, residualPaths: removed.residualPaths ?? [] });
763
+ }
764
+ if (!removed.ok) {
765
+ return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
766
+ "The gateway profile could not be removed safely; the mode switch is incomplete." + partialModeWarning, { desiredEnabled });
767
+ }
768
+ gatewayRemoved = removed.changed;
769
+ }
770
+ const modeSaved = persistDesktopModeMarker("first-party");
771
+ const changed = applied.changed || gatewayRemoved;
772
+ // Picker mode is on by default in first-party; only a committed mode turns it on.
773
+ const picker = modeSaved && pickerPreferenceOn(loadConfig())
774
+ ? await ops.enableLocked({ persist: false, context: "server" })
775
+ : null;
776
+ return jsonResponse({
777
+ ok: true, clientId: "claude-desktop", changed, state: "current", desiredEnabled,
778
+ message: [
779
+ changed
780
+ ? "Claude Desktop integration enabled (first-party). Fully quit and reopen Claude Desktop."
781
+ : "Claude Desktop integration is already on.",
782
+ FIRST_PARTY_ACCOUNT_RISK.message,
783
+ picker ? pickerStateNote(picker) : "",
784
+ modeSaved ? "" : "The first-party mode marker could not be saved to config; status may report the mode as unsaved.",
785
+ ].filter(Boolean).join(" "),
786
+ } satisfies NativeToggleEnvelope);
787
+ });
733
788
  }
734
789
 
735
790
  const fetchModels = ctx.deps.fetchAllModels ?? defaultFetchAllModels;
736
791
  try {
737
792
  const fetched = await fetchModels(current);
738
- const latest = loadConfig();
739
- const latestDesiredEnabled = latest.clientIntegrations?.["claude-desktop"] !== false;
740
- if (!latestDesiredEnabled) {
741
- return postCommitRefusal(409, "claude-desktop", "desired_state_changed",
742
- "Claude Desktop enable was cancelled because the desired state changed to off.", { desiredEnabled: latestDesiredEnabled });
743
- }
744
- const routed = filterCatalogVisibleModels(fetched, latest).map(model => ({
745
- provider: model.provider, id: model.id, contextWindow: model.contextWindow,
746
- }));
747
- const runtime = (ctx.deps.readRuntimePort ?? readRuntimePort)(process.pid);
748
- const result = (ctx.deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
749
- runtime?.port ?? latest.port,
750
- [...desktopVisibleNativeSlugs(latest)],
751
- routed,
752
- latest.apiKeys?.[0]?.key,
753
- "static",
754
- latest.claudeCode?.desktopProfile,
755
- nativeContextLimits(latest),
756
- );
757
- if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
758
- const committed = persistCommittedDesktopGateway(ctx.config, latest.claudeCode?.desktopProfile, result.fingerprint);
759
- const stateWarning = committed.ok ? "" : " The committed gateway mode/profile state was not saved.";
760
- const removed = removeDesktopFirstParty();
761
- if (!removed.ok) return postCommitRefusal(500, "claude-desktop", "write_failed", "Gateway applied, but first-party settings cleanup did not complete." + stateWarning, { desiredEnabled: latestDesiredEnabled });
762
- return jsonResponse({
763
- ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
764
- message: [
765
- "Claude Desktop integration enabled.",
766
- stateWarning,
767
- ].filter(Boolean).join(" "),
768
- } satisfies NativeToggleEnvelope);
793
+ return await runPickerTransition(current, async ops => {
794
+ const latest = loadConfig();
795
+ const latestDesiredEnabled = latest.clientIntegrations?.["claude-desktop"] !== false;
796
+ if (!latestDesiredEnabled) {
797
+ return postCommitRefusal(409, "claude-desktop", "desired_state_changed",
798
+ "Claude Desktop enable was cancelled because the desired state changed to off.", { desiredEnabled: latestDesiredEnabled });
799
+ }
800
+ // Picker mode belongs to first-party: stop terminating claude.ai before the gateway lands.
801
+ const pickerOff = await ops.disableLocked({ persist: false });
802
+ const routed = filterCatalogVisibleModels(fetched, latest).map(model => ({
803
+ provider: model.provider, id: model.id, contextWindow: model.contextWindow,
804
+ }));
805
+ const runtime = (ctx.deps.readRuntimePort ?? readRuntimePort)(process.pid);
806
+ const result = (ctx.deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
807
+ runtime?.port ?? latest.port,
808
+ [...desktopVisibleNativeSlugs(latest)],
809
+ routed,
810
+ latest.apiKeys?.[0]?.key,
811
+ "static",
812
+ latest.claudeCode?.desktopProfile,
813
+ nativeContextLimits(latest),
814
+ );
815
+ if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
816
+ const committed = persistCommittedDesktopGateway(ctx.config, latest.claudeCode?.desktopProfile, result.fingerprint);
817
+ const stateWarning = committed.ok ? "" : " The committed gateway mode/profile state was not saved.";
818
+ const removed = removeDesktopFirstParty();
819
+ if (!removed.ok) return postCommitRefusal(500, "claude-desktop", "write_failed", "Gateway applied, but first-party settings cleanup did not complete." + stateWarning, { desiredEnabled: latestDesiredEnabled });
820
+ return jsonResponse({
821
+ ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
822
+ message: [
823
+ "Claude Desktop integration enabled.",
824
+ stateWarning,
825
+ pickerCleanupNote(pickerOff),
826
+ ].filter(Boolean).join(" "),
827
+ } satisfies NativeToggleEnvelope);
828
+ });
769
829
  } catch {
770
830
  return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled });
771
831
  }
@@ -777,14 +837,30 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
777
837
  }
778
838
  }
779
839
 
840
+ function persistedIntentConfig(snapshot: ManagementContext["config"]): ManagementContext["config"] {
841
+ // Only the per-client intent is refreshed; every other field keeps the snapshot the
842
+ // rest of this request already reasons about.
843
+ try {
844
+ // No config file means no persisted intent: loadConfig would return defaults, which
845
+ // must not override the in-memory intent this request carries.
846
+ if (!existsSync(getConfigPath())) return snapshot;
847
+ return { ...snapshot, clientIntegrations: loadConfig().clientIntegrations };
848
+ } catch {
849
+ return snapshot;
850
+ }
851
+ }
852
+
780
853
  export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
781
854
  const { req, url, config, deps } = ctx;
782
855
 
783
856
  if (url.pathname === "/api/native-integrations" && req.method === "GET") {
784
- const { getConfigPath } = await import("../../config");
785
857
  const codexConfigPath = join(getCodexHome(), "config.toml");
858
+ // The Codex, Grok and Claude Desktop toggles persist intent independently of the
859
+ // server's startup config snapshot. Read that intent once so the next dashboard
860
+ // refresh reflects a completed PUT; fall back to the snapshot if the file is unreadable.
861
+ const persisted = persistedIntentConfig(config);
786
862
  return jsonResponse({
787
- clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config, codexConfigPath), desktopStatus(config)],
863
+ clients: [claudeStatus(config, getConfigPath()), grokStatus(persisted), codexStatus(persisted, codexConfigPath), desktopStatus(persisted)],
788
864
  } satisfies NativeStatusListEnvelope);
789
865
  }
790
866
 
@@ -43,6 +43,8 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
43
43
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
44
44
  import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
45
45
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
46
+ import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
47
+ import { resolveDevinApiBaseUrl } from "../../oauth/devin/api-base";
46
48
  import { fetchQoderModels } from "../../adapters/qoder/live-models";
47
49
  import { resolveQoderProfile } from "../../adapters/qoder/profiles";
48
50
  import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
@@ -448,6 +450,13 @@ function applyProviderPatchFields(
448
450
  }
449
451
  touched = true;
450
452
  }
453
+ if (Object.hasOwn(rawBody, "fastEnabled")) {
454
+ const value = rawBody.fastEnabled;
455
+ if (value === null) delete next.fastEnabled;
456
+ else if (typeof value === "boolean") next.fastEnabled = value;
457
+ else return { error: "fastEnabled must be a boolean or null" };
458
+ touched = true;
459
+ }
451
460
  if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
452
461
  if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
453
462
  if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
@@ -934,12 +943,15 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
934
943
  retainModels: p.retainModels,
935
944
  omitReasoningEffortWithToolsModels: p.omitReasoningEffortWithToolsModels,
936
945
  upstreamHttpVersion: p.upstreamHttpVersion,
937
- upstreamWebsocket: p.upstreamWebsocket === true,
946
+ // As configured: unset on canonical `openai` means upstream WebSocket, so never coerce to false.
947
+ upstreamWebsocket: p.upstreamWebsocket,
938
948
  authMode: p.authMode,
939
949
  apiKeyTransport: p.apiKeyTransport,
940
950
  disabled: p.disabled === true,
941
951
  codexAccountMode: providerCodexAccountMode(name, p),
942
952
  ...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
953
+ // Only opt-in Fast lanes (Anthropic fast mode bills usage credits) get a dashboard switch.
954
+ ...(getProviderRegistryEntry(name)?.fastOptIn === true ? { fastOptIn: { enabled: p.fastEnabled === true } } : {}),
943
955
  discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
944
956
  ...(name === "openai" && isCanonicalOpenAiForwardProvider(p)
945
957
  ? { entitlement: getCodexModelEntitlementStatus(config) }
@@ -1203,6 +1215,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1203
1215
  const submittedModelDisplayNames = Object.hasOwn(prov, "modelDisplayNames");
1204
1216
  const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
1205
1217
  const submittedUpstreamWebsocket = Object.hasOwn(prov, "upstreamWebsocket");
1218
+ const submittedFastEnabled = Object.hasOwn(prov, "fastEnabled");
1206
1219
  // Same trap, one more field: DeepSeek carries a registry default of `true` for
1207
1220
  // annotateEmptyToolOutputs, so enrichment cannot distinguish "the client omitted it"
1208
1221
  // from "the registry supplied it" either. Without this sample, an unrelated edit that
@@ -1277,6 +1290,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1277
1290
  if (!submittedUpstreamWebsocket && existing?.upstreamWebsocket !== undefined) {
1278
1291
  prov.upstreamWebsocket = existing.upstreamWebsocket;
1279
1292
  }
1293
+ // The Models-page Fast switch is PATCH-owned and the provider form never sends it, so an
1294
+ // unrelated full save must not silently turn an opted-in Anthropic Fast lane back off.
1295
+ const liveFastEnabled = config.providers[name]?.fastEnabled;
1296
+ if (!submittedFastEnabled && liveFastEnabled !== undefined) prov.fastEnabled = liveFastEnabled;
1280
1297
  // The form sends none of the compatibility settings either (#5563). Read the live row rather
1281
1298
  // than `existing`, like the alias overlays below: a PATCH that saved one of them while DNS
1282
1299
  // validation awaited must not be undone. Nothing is carried to a new destination.
@@ -1602,10 +1619,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1602
1619
  }
1603
1620
  const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
1604
1621
  const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
1605
- const snapshot = antigravity
1622
+ const snapshot = prov.authMode === "oauth"
1606
1623
  ? await getValidAccessTokenSnapshot(name).catch(() => undefined)
1607
1624
  : undefined;
1608
- const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
1625
+ const apiKey = prov.authMode === "oauth" ? snapshot?.accessToken : await resolveModelsAuthToken(name, prov);
1609
1626
  if (prov.authMode === "oauth" && !apiKey) {
1610
1627
  return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
1611
1628
  }
@@ -1630,6 +1647,29 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1630
1647
  message: `Connected. ${live.models.length} models.`,
1631
1648
  });
1632
1649
  }
1650
+ if (prov.adapter === "devin") {
1651
+ const started = Date.now();
1652
+ const configuredBase = name === "devin" ? getProviderRegistryEntry(name)?.baseUrl ?? prov.baseUrl : prov.baseUrl;
1653
+ const destination = resolveDevinApiBaseUrl(snapshot?.apiBaseUrl ?? configuredBase);
1654
+ const liveResult = await fetchDevinUsableModels({
1655
+ apiKey: apiKey ?? "",
1656
+ baseUrl: destination,
1657
+ });
1658
+ const latencyMs = Date.now() - started;
1659
+ if (!liveResult.ok) {
1660
+ return jsonResponse({
1661
+ ok: false,
1662
+ latencyMs,
1663
+ error: `devin discovery ${liveResult.error}${liveResult.detail ? `: ${liveResult.detail}` : ""}`,
1664
+ });
1665
+ }
1666
+ return jsonResponse({
1667
+ ok: true,
1668
+ latencyMs,
1669
+ models: liveResult.models.length,
1670
+ message: `Connected. ${liveResult.models.length} models.`,
1671
+ });
1672
+ }
1633
1673
  if (prov.adapter === "qoder") {
1634
1674
  const started = Date.now();
1635
1675
  const profile = resolveQoderProfile(prov.baseUrl);
@@ -1656,7 +1696,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1656
1696
  if (antigravity && !project) {
1657
1697
  return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
1658
1698
  }
1659
- const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
1699
+ const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name, {
1700
+ oauthApiBaseUrl: snapshot?.apiBaseUrl,
1701
+ });
1660
1702
  const discovery = resolveProviderModelDiscovery(name, prov);
1661
1703
  const started = Date.now();
1662
1704
  try {
@@ -51,6 +51,8 @@ export type ExemptionReason =
51
51
  | "interactive-preview"
52
52
  /** Unreachable in the live dispatch order; delete rather than expose. */
53
53
  | "dead"
54
+ /** Desktop shell internal display-state POST; the CLI has no app updater state to publish. */
55
+ | "desktop-internal"
54
56
  /**
55
57
  * A verb is owed but belongs to a later work-phase. BOUNDED: requires `owner` and
56
58
  * `ownerDoc`, and the parity test asserts that tracked doc exists and names the route.
@@ -152,6 +154,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
152
154
  { method: "POST", path: "/api/anthropic/reset-grants/consume", module: "server/management/anthropic-reset-grant-routes", mutates: true, exempt: { reason: "session-only", why: "Spending a Claude reset grant requires the gui-session principal (anthropic-reset-grant-routes.ts handleConsume); the admin token is refused." } },
153
155
  { method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true },
154
156
  { method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true },
157
+ { method: "PUT", path: "/api/claude-desktop/first-party-bindings", module: "server/management/agent-settings-routes", mutates: true },
158
+ // server/management/claude-desktop-picker-routes
159
+ { method: "GET", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: false },
160
+ { method: "PUT", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: true },
155
161
  { method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
156
162
  { method: "PUT", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: true },
157
163
  { method: "PUT", path: "/api/grok/selection", module: "server/management/agent-settings-routes", mutates: true },
@@ -346,6 +352,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
346
352
  // server/management/sidebar-routes
347
353
  { method: "GET", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: false },
348
354
  { method: "GET", path: "/api/update/badge", module: "server/management/sidebar-routes", mutates: false },
355
+ { method: "POST", path: "/api/update/desktop-snapshot",
356
+ module: "server/management/sidebar-routes", mutates: true,
357
+ exempt: { reason: "desktop-internal",
358
+ why: "Only the Tauri shell has signed-updater state to publish; a CLI verb could only forge that state and would not create an operator action." } },
349
359
  { method: "POST", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: true, exempt: { reason: "session-only", why: "User-consent boundary in AGENTS_INSTALL.md: starring spends the user's identity. Must never gain a CLI verb." } },
350
360
  // server/management/storage-log-guard-routes
351
361
  { method: "GET", path: "/api/storage/codex-logs", module: "server/management/storage-log-guard-routes", mutates: false },
@@ -207,6 +207,15 @@ function migrateProfileModelReferences(
207
207
  ]),
208
208
  );
209
209
  }
210
+ if (claudeCode.intercept?.modelMap) {
211
+ // First-party picker bindings: keys are Anthropic picker ids, only the route follows the rename.
212
+ claudeCode.intercept = {
213
+ ...claudeCode.intercept,
214
+ modelMap: Object.fromEntries(
215
+ Object.entries(claudeCode.intercept.modelMap).map(([pickerId, route]) => [pickerId, migrateAgentReference(route)]),
216
+ ),
217
+ };
218
+ }
210
219
  config.claudeCode = claudeCode;
211
220
  }
212
221
  return shouldSyncClaudeAgentDefs;
@@ -97,6 +97,58 @@ export async function handleSidebarRoutes(ctx: ManagementContext): Promise<Respo
97
97
  });
98
98
  }
99
99
 
100
+ if (url.pathname === "/api/update/desktop-snapshot" && req.method === "POST") {
101
+ // Only the native ProxyClient publishes this state; browsers always send Origin.
102
+ if (req.headers.has("origin")) {
103
+ return Response.json({ error: "desktop snapshot does not accept browser-origin requests" }, { status: 403 });
104
+ }
105
+ if (ctx.principal !== "admin-token") {
106
+ return jsonResponse({ error: "desktop snapshot requires admin token" }, 403, req, ctx.config);
107
+ }
108
+ if (req.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
109
+ return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
110
+ }
111
+ const declared = Number(req.headers.get("content-length") ?? "0");
112
+ if (Number.isFinite(declared) && declared > 1024) {
113
+ return jsonResponse({ error: "desktop snapshot too large" }, 413, req, ctx.config);
114
+ }
115
+ const reader = req.body?.getReader();
116
+ if (!reader) return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
117
+ const bytes = new Uint8Array(1024);
118
+ let used = 0;
119
+ try {
120
+ while (true) {
121
+ const part = await reader.read();
122
+ if (part.done) break;
123
+ if (used + part.value.length > bytes.length) {
124
+ await reader.cancel();
125
+ return jsonResponse({ error: "desktop snapshot too large" }, 413, req, ctx.config);
126
+ }
127
+ bytes.set(part.value, used);
128
+ used += part.value.length;
129
+ }
130
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, used));
131
+ const { desktopBadgeStore } = await import("../../update/desktop-badge");
132
+ if (!desktopBadgeStore.put(JSON.parse(decoded))) {
133
+ return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
134
+ }
135
+ } catch {
136
+ return jsonResponse({ error: "invalid desktop snapshot" }, 400, req, ctx.config);
137
+ }
138
+ return jsonResponse({ ok: true }, 200, req, ctx.config);
139
+ }
140
+
141
+ if (url.pathname === "/api/update/badge" && req.method === "GET"
142
+ && url.searchParams.get("surface") === "desktop") {
143
+ const { desktopBadgeStore } = await import("../../update/desktop-badge");
144
+ return jsonResponse(desktopBadgeStore.read(url.searchParams.get("session")), 200, req, ctx.config);
145
+ }
146
+
147
+ if (url.pathname === "/api/update/badge" && req.method === "GET"
148
+ && url.searchParams.has("surface") && url.searchParams.get("surface") !== "desktop") {
149
+ return jsonResponse({ error: "invalid badge surface" }, 400, req, ctx.config);
150
+ }
151
+
100
152
  if (url.pathname === "/api/update/badge" && req.method === "GET") {
101
153
  const { readUpdateBadge } = await import("../../update/badge");
102
154
  return jsonResponse(readUpdateBadge());
@@ -76,6 +76,7 @@ import { handleCompanionRoutes } from "./management/companion-routes";
76
76
  import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
77
77
  import { handleIntegrationRoutes } from "./management/integration-routes";
78
78
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
79
+ import { handleClaudeDesktopPickerRoutes } from "./management/claude-desktop-picker-routes";
79
80
  import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
80
81
  import type { ManagementContext } from "./management/context";
81
82
  import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
@@ -293,6 +294,7 @@ export async function handleManagementAPI(
293
294
  ?? (await handleIntegrationRoutes(ctx))
294
295
  ?? (await handleNativeIntegrationRoutes(ctx))
295
296
  ?? (await handleCursorIntegrationRoutes(ctx))
297
+ ?? (await handleClaudeDesktopPickerRoutes(ctx))
296
298
  ?? (await handleAgentSettingsRoutes(ctx))
297
299
  ?? (await handleCodexPromptRoutes(ctx))
298
300
  ?? (await handleOauthAccountRoutes(ctx))