sneakoscope 8.1.2 → 8.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 (261) hide show
  1. package/README.md +96 -58
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/bin/fast-inline.js +5 -51
  5. package/dist/cli/command-manifest-lite.js +2 -1
  6. package/dist/cli/command-registry.js +2 -1
  7. package/dist/cli/global-mode-router.js +1 -1
  8. package/dist/cli/install-helpers-codex-lb-config.js +112 -914
  9. package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
  10. package/dist/cli/install-helpers-install-support.js +4 -20
  11. package/dist/cli/install-helpers.js +36 -2716
  12. package/dist/cli/router.js +9 -9
  13. package/dist/commands/bridge-command-output.js +111 -0
  14. package/dist/commands/bridge.js +320 -0
  15. package/dist/commands/codex-app.js +39 -123
  16. package/dist/commands/doctor-helpers.js +380 -0
  17. package/dist/commands/doctor-profile.js +3 -0
  18. package/dist/commands/doctor.js +114 -873
  19. package/dist/commands/mad-sks.js +0 -4
  20. package/dist/config/skills-manifest.json +1 -1
  21. package/dist/core/agents/agent-effort-policy.js +11 -21
  22. package/dist/core/agents/agent-recursion-guard.js +26 -0
  23. package/dist/core/agents/agent-runner-codex-exec.js +7 -9
  24. package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
  25. package/dist/core/architecture-hardening/state/state-service.js +49 -13
  26. package/dist/core/auto-review.js +15 -9
  27. package/dist/core/codex/codex-config-guard.js +8 -0
  28. package/dist/core/codex-adapter.js +8 -10
  29. package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
  30. package/dist/core/codex-app/codex-app-launcher.js +10 -12
  31. package/dist/core/codex-app/codex-app-restart.js +10 -12
  32. package/dist/core/codex-app/menubar/constants.js +2 -5
  33. package/dist/core/codex-app/menubar/global-install.js +3 -2
  34. package/dist/core/codex-app/menubar/installer/result.js +23 -0
  35. package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
  36. package/dist/core/codex-app/menubar/installer.js +5 -96
  37. package/dist/core/codex-app/menubar/migration.js +1 -42
  38. package/dist/core/codex-app/openrouter-provider.js +0 -78
  39. package/dist/core/codex-app.js +97 -360
  40. package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
  41. package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
  42. package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
  43. package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
  44. package/dist/core/codex-control/codex-task-runner.js +16 -60
  45. package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
  46. package/dist/core/codex-exec-output-schema.js +11 -13
  47. package/dist/core/codex-lb/bridge-contracts.js +2 -0
  48. package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
  49. package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
  50. package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
  51. package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
  52. package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
  53. package/dist/core/codex-lb/capability-runner.js +505 -201
  54. package/dist/core/codex-lb/codex-lb-env.js +179 -12
  55. package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
  56. package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
  57. package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
  58. package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
  59. package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
  60. package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
  61. package/dist/core/codex-lb/combined-catalog.js +12 -0
  62. package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
  63. package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
  64. package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
  65. package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
  66. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
  67. package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
  68. package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
  69. package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
  70. package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
  71. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
  72. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
  73. package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
  74. package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
  75. package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
  76. package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
  77. package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
  78. package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
  79. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
  80. package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
  81. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
  82. package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
  83. package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
  84. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
  85. package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
  86. package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
  87. package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
  88. package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
  89. package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
  90. package/dist/core/codex-lb/desktop-controller.js +2 -1730
  91. package/dist/core/codex-lb/desktop-service.js +462 -627
  92. package/dist/core/codex-lb/migration-receipt.js +560 -118
  93. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
  94. package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
  95. package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
  96. package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
  97. package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
  98. package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
  99. package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
  100. package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
  101. package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
  102. package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
  103. package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
  104. package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
  105. package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
  106. package/dist/core/codex-lb/provider-credentials/types.js +1 -0
  107. package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
  108. package/dist/core/codex-lb/provider-credentials.js +5 -0
  109. package/dist/core/codex-lb/provider-registry.js +323 -0
  110. package/dist/core/codex-lb/provider-route-policy.js +118 -0
  111. package/dist/core/codex-lb/request-route-resolver.js +172 -0
  112. package/dist/core/codex-lb/route-index.js +148 -0
  113. package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
  114. package/dist/core/commands/basic-cli.js +1 -1
  115. package/dist/core/commands/check-command.js +3 -10
  116. package/dist/core/commands/gate-result-contract.js +10 -14
  117. package/dist/core/commands/intent-normalization/normalizer.js +1 -1
  118. package/dist/core/commands/mad-sks-command.js +2 -6
  119. package/dist/core/commands/release-command.js +11 -1
  120. package/dist/core/commands/telegram-command.js +46 -6
  121. package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
  122. package/dist/core/doctor/skill-legacy-surface.js +5 -5
  123. package/dist/core/evidence/v2/evidence-key.js +30 -3
  124. package/dist/core/feature-fixtures.js +8 -1
  125. package/dist/core/feature-registry.js +3 -2
  126. package/dist/core/hooks-runtime/stop-finalization.js +11 -1
  127. package/dist/core/hooks-runtime/subagent-context.js +15 -1
  128. package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
  129. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  130. package/dist/core/hooks-runtime.js +73 -87
  131. package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
  132. package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
  133. package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
  134. package/dist/core/imagegen/imagegen-capability.js +43 -151
  135. package/dist/core/init/skills/filesystem.js +89 -0
  136. package/dist/core/init/skills/inventory.js +97 -0
  137. package/dist/core/init/skills/manifest.js +151 -0
  138. package/dist/core/init/skills.js +5 -325
  139. package/dist/core/init.js +0 -63
  140. package/dist/core/install/installed-package-smoke.js +1 -1
  141. package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
  142. package/dist/core/policy/latest-version-guidance.js +15 -2
  143. package/dist/core/preflight/parallel-preflight-engine.js +27 -51
  144. package/dist/core/provider/provider-badge.js +9 -4
  145. package/dist/core/provider/provider-context.js +57 -107
  146. package/dist/core/providers/openrouter/openrouter-account.js +3 -1
  147. package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
  148. package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
  149. package/dist/core/release/gate-manifest.js +0 -1
  150. package/dist/core/release/main-push-guard.js +13 -0
  151. package/dist/core/release/main-push-receipt.js +11 -0
  152. package/dist/core/release/physical-release-gates.js +664 -61
  153. package/dist/core/release/publish-preflight.js +3 -3
  154. package/dist/core/release/release-gate-batch-runner.js +19 -6
  155. package/dist/core/release/release-gate-dag.js +33 -6
  156. package/dist/core/release/release-gate-node.js +1 -3
  157. package/dist/core/release/release-gate-output-contract.js +55 -0
  158. package/dist/core/release/release-pack-contract.js +1 -1
  159. package/dist/core/release/release-pack-receipt.js +209 -8
  160. package/dist/core/release/release-real-contract.js +1 -1
  161. package/dist/core/release/stage-publish.js +106 -28
  162. package/dist/core/retention/temp-lease.js +204 -0
  163. package/dist/core/retention.js +18 -195
  164. package/dist/core/routes.js +1 -2
  165. package/dist/core/safety/intent-contract/intent-contract.js +3 -3
  166. package/dist/core/subagents/naruto-host-credentials.js +6 -1
  167. package/dist/core/subagents/official-subagent-config.js +27 -9
  168. package/dist/core/subagents/official-subagent-runner.js +14 -31
  169. package/dist/core/subagents/wave-lifecycle.js +36 -8
  170. package/dist/core/subagents/wave-parent-guidance.js +2 -1
  171. package/dist/core/telegram/doctor.js +2 -2
  172. package/dist/core/telegram/liveness.js +10 -4
  173. package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
  174. package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
  175. package/dist/core/update/update-migration-state/session-state-split.js +247 -0
  176. package/dist/core/update/update-migration-state/simple-stages.js +46 -0
  177. package/dist/core/update/update-migration-state.js +4 -481
  178. package/dist/core/version.js +1 -1
  179. package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
  180. package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
  181. package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
  182. package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
  183. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
  184. package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
  185. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
  186. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
  187. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
  188. package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
  189. package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
  190. package/dist/scripts/canonical-test-runner.js +7 -1
  191. package/dist/scripts/check-runtime-schemas.js +7 -2
  192. package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
  193. package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
  194. package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
  195. package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
  196. package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
  197. package/dist/scripts/desktop-bridge-transport-client.js +116 -0
  198. package/dist/scripts/desktop-bridge-unification-check.js +276 -0
  199. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
  200. package/dist/scripts/gate-policy-audit-check.js +52 -2
  201. package/dist/scripts/provider-badge-context-check.js +55 -12
  202. package/dist/scripts/publish-preflight.js +1 -2
  203. package/dist/scripts/release-affected-selector-check.js +1 -2
  204. package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
  205. package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
  206. package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
  207. package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
  208. package/dist/scripts/release-gate-batch-runner-check.js +9 -0
  209. package/dist/scripts/release-gate-dag-runner-check.js +6 -0
  210. package/dist/scripts/release-gate-dag-runner.js +4 -2
  211. package/dist/scripts/release-metadata-check.js +16 -3
  212. package/dist/scripts/release-pack-receipt.js +29 -6
  213. package/dist/scripts/release-physical-gates-check.js +39 -0
  214. package/dist/scripts/release-real-check.js +7 -5
  215. package/package.json +6 -8
  216. package/release-gates.v2.json +156 -157
  217. package/runtime-required-scripts.json +11 -11
  218. package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
  219. package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
  220. package/schemas/release/release-gate-node.schema.json +2 -3
  221. package/dist/cli/codex-lb-setup-warning-output.js +0 -17
  222. package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
  223. package/dist/commands/codex-lb.js +0 -1232
  224. package/dist/core/architecture-hardening/migration/migration.js +0 -111
  225. package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
  226. package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
  227. package/dist/core/codex-app/child-policy/child-policy.js +0 -35
  228. package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
  229. package/dist/core/codex-app/glm-model-profile.js +0 -1
  230. package/dist/core/codex-app/glm-profile-installer.js +0 -191
  231. package/dist/core/codex-app/glm-profile-schema.js +0 -42
  232. package/dist/core/codex-app/multi-provider-router.js +0 -391
  233. package/dist/core/codex-app/openrouter-activate.js +0 -405
  234. package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
  235. package/dist/core/codex-app/provider-child-policy.js +0 -64
  236. package/dist/core/codex-app/provider-mode.js +0 -91
  237. package/dist/core/codex-app/provider-session-policy.js +0 -85
  238. package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
  239. package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
  240. package/dist/core/codex-lb/codex-lb-setup.js +0 -293
  241. package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
  242. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
  243. package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
  244. package/dist/core/codex-lb/desktop-mode.js +0 -32
  245. package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
  246. package/dist/core/codex-lb/legacy-migration.js +0 -576
  247. package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
  248. package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
  249. package/dist/core/codex-lb/routing-truth.js +0 -327
  250. package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
  251. package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
  252. package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
  253. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
  254. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
  255. package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
  256. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
  257. package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
  258. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
  259. package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
  260. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
  261. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +0 -231
@@ -1,331 +0,0 @@
1
- import Cocoa
2
-
3
- final class MultiProviderRouterControls {
4
- let baseURL: NSTextField = {
5
- let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
6
- field.placeholderString = "http://127.0.0.1:10100/v1"
7
- field.stringValue = "http://127.0.0.1:10100/v1"
8
- field.setAccessibilityLabel("Multi-provider router base URL")
9
- field.setAccessibilityHelp("Only loopback HTTP or HTTPS endpoints ending in /v1 are accepted.")
10
- field.toolTip = "Loopback only: localhost, 127.0.0.1, or ::1, ending in /v1."
11
- field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
12
- return field
13
- }()
14
- let catalogPath: NSTextField = {
15
- let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 340, height: 24))
16
- field.placeholderString = "~/.codex/opencodex-catalog.json"
17
- field.stringValue = "~/.codex/opencodex-catalog.json"
18
- field.setAccessibilityLabel("Codex multi-provider model catalog path")
19
- field.setAccessibilityHelp("OpenCodex writes this owner-only Codex catalog. Other routers must export a complete Codex ModelInfo catalog.")
20
- field.toolTip = "OpenCodex default: $CODEX_HOME/opencodex-catalog.json"
21
- field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
22
- return field
23
- }()
24
- let model: NSTextField = {
25
- let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
26
- field.placeholderString = "anthropic/claude-sonnet"
27
- field.setAccessibilityLabel("Multi-provider router model slug")
28
- field.setAccessibilityPlaceholderValue("provider/model")
29
- field.setAccessibilityHelp("Enter an exact model slug from the configured Codex catalog.")
30
- field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
31
- return field
32
- }()
33
- let modelPopup = NSPopUpButton()
34
- let status: NSTextField = {
35
- let field = NativeView.detail("Multi-provider router status has not loaded yet.")
36
- field.setAccessibilityLabel("Multi-provider router status")
37
- return field
38
- }()
39
- weak var refreshButton: NSButton?
40
- var models: [String] = []
41
- var refreshInFlight = false
42
- var modelSelectionPending = false
43
- }
44
-
45
- extension ProvidersViewController {
46
- func makeMultiProviderRouterCard() -> NSBox {
47
- multiProvider.model.delegate = self
48
- multiProvider.modelPopup.addItem(withTitle: "Choose from catalog…")
49
- multiProvider.modelPopup.target = self
50
- multiProvider.modelPopup.action = #selector(selectMultiProviderModel(_:))
51
- multiProvider.modelPopup.setAccessibilityLabel("Multi-provider routed model catalog")
52
- multiProvider.modelPopup.setAccessibilityHelp("Choose an exact provider/model slug from the active Codex catalog.")
53
- multiProvider.modelPopup.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
54
- multiProvider.modelPopup.widthAnchor.constraint(equalToConstant: 230).isActive = true
55
- multiProvider.modelPopup.isEnabled = false
56
-
57
- let refresh = NativeView.button("Refresh Router", target: self, action: #selector(refreshMultiProviderRouterAction(_:)))
58
- let test = NativeView.button("Check Router", target: self, action: #selector(testMultiProviderRouter))
59
- let activate = NativeView.button("Configure Router Model", target: self, action: #selector(useMultiProviderRouter))
60
- refresh.setAccessibilityLabel("Refresh multi-provider router and catalog status")
61
- test.setAccessibilityLabel("Check the loopback multi-provider router")
62
- activate.setAccessibilityLabel("Configure the selected routed model and restart Codex App")
63
- test.setAccessibilityHelp("Checks that the selected model exists in both the local catalog and the live loopback router. No configuration is changed.")
64
- activate.setAccessibilityHelp("Validates the loopback router, writes its provider and catalog settings to Codex config, and restarts Codex App. Runtime adoption still requires a live Codex check.")
65
- multiProvider.refreshButton = refresh
66
- registerProviderAction(refresh, id: "sks-provider-refresh-router")
67
- registerProviderAction(test, id: "sks-provider-test-router")
68
- registerProviderAction(activate, id: "sks-provider-configure-router")
69
- actionButtons += [refresh, test, activate]
70
-
71
- let routerLabel = NSTextField(labelWithString: "Router URL")
72
- let catalogLabel = NSTextField(labelWithString: "Catalog")
73
- let modelsLabel = NSTextField(labelWithString: "Models")
74
- let modelLabel = NSTextField(labelWithString: "Model slug")
75
- for label in [routerLabel, catalogLabel, modelsLabel, modelLabel] {
76
- label.setContentHuggingPriority(.required, for: .horizontal)
77
- }
78
- let security = NativeView.detail(
79
- "Security: only loopback HTTP(S) /v1 endpoints are accepted. SKS Control Center does not accept or store router credentials; keep upstream credentials inside the local router."
80
- )
81
- security.setAccessibilityLabel("Multi-provider router security")
82
- let setup = NativeView.detail(
83
- "OpenCodex setup: run ocx start, then ensure the catalog stamps multi_agent_version = \"v2\" on routed models. If OpenCodex chose a fallback port, replace 10100 with the live port reported by ocx status."
84
- )
85
- setup.setAccessibilityLabel("OpenCodex multi-provider setup guidance")
86
-
87
- return NativeView.card(
88
- title: "Multi-Provider Router",
89
- subtitle: "Connects Codex to one local Responses router, such as OpenCodex. Multi-agent v2 catalog slugs (multi_agent_version = \"v2\") can then be assigned to different agent roles.",
90
- views: [
91
- security,
92
- setup,
93
- multiProvider.status,
94
- NativeView.row([routerLabel, multiProvider.baseURL]),
95
- NativeView.row([catalogLabel, multiProvider.catalogPath]),
96
- NativeView.row([modelsLabel, multiProvider.modelPopup, refresh]),
97
- NativeView.row([modelLabel, multiProvider.model]),
98
- NativeView.row([test, activate])
99
- ]
100
- )
101
- }
102
-
103
- func setMultiProviderRouterBusy(_ value: Bool) {
104
- multiProvider.baseURL.isEnabled = !value
105
- multiProvider.catalogPath.isEnabled = !value
106
- multiProvider.model.isEnabled = !value
107
- multiProvider.modelPopup.isEnabled = !value && !multiProvider.models.isEmpty
108
- multiProvider.refreshButton?.isEnabled = !value && !multiProvider.refreshInFlight
109
- }
110
-
111
- @objc func refreshMultiProviderRouterAction(_ sender: NSButton) {
112
- refreshMultiProviderRouterStatus()
113
- }
114
-
115
- func refreshMultiProviderRouterStatus() {
116
- guard !multiProvider.refreshInFlight else { return }
117
- multiProvider.refreshInFlight = true
118
- multiProvider.refreshButton?.isEnabled = false
119
- if !busy { multiProvider.status.stringValue = "Checking multi-provider router and catalog…" }
120
- processClient.run(["codex-app", "router-status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
121
- guard let self = self else { return }
122
- self.multiProvider.refreshInFlight = false
123
- self.multiProvider.refreshButton?.isEnabled = !self.busy
124
- guard let json = self.json(result.output) else {
125
- if !self.busy {
126
- self.multiProvider.status.stringValue = "Router status unavailable · no configuration was changed."
127
- self.multiProvider.status.textColor = .systemOrange
128
- }
129
- self.setMultiProviderRouterBusy(self.busy)
130
- return
131
- }
132
- if let baseURL = json["base_url"] as? String, !baseURL.isEmpty {
133
- self.multiProvider.baseURL.stringValue = baseURL
134
- }
135
- if let catalogPath = json["catalog_path"] as? String, !catalogPath.isEmpty {
136
- self.multiProvider.catalogPath.stringValue = catalogPath
137
- }
138
- let models = (json["models"] as? [String]) ?? []
139
- self.multiProvider.models = models
140
- self.multiProvider.modelPopup.removeAllItems()
141
- self.multiProvider.modelPopup.addItem(withTitle: models.isEmpty ? "Catalog has no models" : "Choose from \(models.count) models…")
142
- if !models.isEmpty { self.multiProvider.modelPopup.addItems(withTitles: models) }
143
- if let activeModel = json["active_model"] as? String,
144
- !activeModel.isEmpty,
145
- !self.multiProvider.modelSelectionPending,
146
- self.multiProvider.model.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
147
- self.multiProvider.model.stringValue = activeModel
148
- }
149
- self.synchronizeMultiProviderPopupSelection()
150
- let selected = json["selected"] as? Bool == true
151
- self.routerSelectedNow = selected
152
- self.routerActiveModel = (json["active_model"] as? String) ?? ""
153
- self.renderActiveProviderSummary()
154
- let providerReady = json["provider_contract_ok"] as? Bool == true
155
- let catalog = json["catalog"] as? [String: Any]
156
- let catalogReady = catalog?["ok"] as? Bool == true
157
- let activeModel = json["active_model"] as? String ?? "unset"
158
- let modelCount = json["model_count"] as? Int ?? models.count
159
- let truncated = json["models_truncated"] as? Bool == true
160
- let modelSummary = truncated
161
- ? "\(models.count) of \(modelCount) catalog models shown"
162
- : "\(modelCount) catalog models available"
163
- let state = json["status"] as? String ?? "unknown"
164
- let ownership = json["routing_ownership"] as? [String: Any]
165
- let ownershipClass = ownership?["classification"] as? String ?? ""
166
- let desktopPicker = json["desktop_picker"] as? [String: Any]
167
- let cacheInvalidated = desktopPicker?["models_cache_invalidated"] as? Bool == true
168
- let restartRecommended = desktopPicker?["restart_recommended"] as? Bool == true
169
- if !self.busy {
170
- var suffix = ""
171
- if ownershipClass == "opencodex_design_b" {
172
- suffix += " · OpenCodex Design B owns openai_base_url (do not overwrite without --force-routing-override)"
173
- } else if !ownershipClass.isEmpty && ownershipClass != "sks_router" && ownershipClass != "unconfigured" {
174
- suffix += " · routing owner: \(ownershipClass)"
175
- }
176
- if restartRecommended {
177
- suffix += cacheInvalidated
178
- ? " · Desktop models cache marked stale; restart Codex App if the picker is still old"
179
- : " · restart Codex App after catalog changes so the Desktop picker refreshes"
180
- }
181
- if selected && providerReady && catalogReady {
182
- self.multiProvider.status.stringValue = "Router configured · \(activeModel) · \(modelSummary). Runtime adoption is not verified here.\(suffix)"
183
- self.multiProvider.status.textColor = .systemGreen
184
- } else if providerReady && catalogReady {
185
- self.multiProvider.status.stringValue = "Router definition ready, not selected · \(modelSummary). Choose a model and configure when ready.\(suffix)"
186
- self.multiProvider.status.textColor = .secondaryLabelColor
187
- } else {
188
- self.multiProvider.status.stringValue = "Router \(state.replacingOccurrences(of: "_", with: " ")) · \(self.multiProviderPublicDetail(json, fallback: result.output))\(suffix)"
189
- self.multiProvider.status.textColor = .systemOrange
190
- }
191
- }
192
- self.setMultiProviderRouterBusy(self.busy)
193
- }
194
- }
195
-
196
- @objc private func selectMultiProviderModel(_ sender: NSPopUpButton) {
197
- guard sender.indexOfSelectedItem > 0, let model = sender.titleOfSelectedItem else { return }
198
- multiProvider.modelSelectionPending = true
199
- multiProvider.model.stringValue = model
200
- multiProvider.status.stringValue = "Selected \(model). Check the router before activation."
201
- multiProvider.status.textColor = .secondaryLabelColor
202
- }
203
-
204
- @objc func testMultiProviderRouter() {
205
- let values = multiProviderRouterInput()
206
- guard !values.model.isEmpty else {
207
- multiProvider.status.stringValue = "Router check blocked · choose or enter a provider/model catalog slug."
208
- multiProvider.status.textColor = .systemOrange
209
- return
210
- }
211
- guard !busy else { multiProvider.status.stringValue = "Another provider action is already running."; return }
212
- guard let snapshot = operations.begin(kind: "multi-provider-router-test", mutationGroup: nil, summary: "Check multi-provider router") else {
213
- multiProvider.status.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
214
- return
215
- }
216
- setBusy(true)
217
- multiProvider.status.stringValue = "Checking \(values.model) through the loopback router…"
218
- _ = operations.update(snapshot, state: .running, stage: "testing", progress: nil, summary: "Check multi-provider router")
219
- processClient.run([
220
- "codex-app", "router-test",
221
- "--base-url", values.baseURL,
222
- "--catalog", values.catalogPath,
223
- "--model", values.model,
224
- "--json"
225
- ], timeout: NativeView.mutationTimeout) { [weak self] result in
226
- guard let self = self else { return }
227
- self.setBusy(false)
228
- let json = self.json(result.output)
229
- let ok = result.code == 0 && json?["ok"] as? Bool == true
230
- _ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "Multi-provider router ready" : "Multi-provider router needs action")
231
- self.multiProvider.status.stringValue = ok
232
- ? "Router check passed · \(values.model) is present in both the Codex catalog and live router."
233
- : "Router check failed · \(self.multiProviderPublicDetail(json, fallback: result.output))"
234
- self.multiProvider.status.textColor = ok ? .systemGreen : .systemOrange
235
- }
236
- }
237
-
238
- @objc func useMultiProviderRouter() {
239
- let values = multiProviderRouterInput()
240
- guard !values.model.isEmpty else {
241
- multiProvider.status.stringValue = "Configuration blocked · choose or enter a provider/model catalog slug."
242
- multiProvider.status.textColor = .systemOrange
243
- return
244
- }
245
- guard !busy else { multiProvider.status.stringValue = "Another provider action is already running."; return }
246
- guard let window = view.window else { return }
247
- AlertFactory.confirmSheet(
248
- window: window,
249
- title: "Configure Router Model?",
250
- message: "\(values.model) becomes the routed Codex main model and Codex App restarts.",
251
- destructive: false
252
- ) { [weak self] approved in
253
- guard let self = self, approved else { return }
254
- self.performUseMultiProviderRouter(values: values)
255
- }
256
- }
257
-
258
- private func performUseMultiProviderRouter(values: (baseURL: String, catalogPath: String, model: String)) {
259
- guard let snapshot = operations.begin(kind: "multi-provider-router-use", mutationGroup: "codex-config", summary: "Configure multi-provider router") else {
260
- multiProvider.status.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
261
- return
262
- }
263
- setBusy(true)
264
- multiProvider.status.stringValue = "Validating and configuring \(values.model), then restarting Codex App…"
265
- _ = operations.update(snapshot, state: .running, stage: "configuring", progress: nil, summary: "Configure multi-provider router")
266
- processClient.run([
267
- "codex-app", "use-router",
268
- "--base-url", values.baseURL,
269
- "--catalog", values.catalogPath,
270
- "--model", values.model,
271
- "--restart-app",
272
- "--json"
273
- ], timeout: NativeView.mutationTimeout) { [weak self] result in
274
- guard let self = self else { return }
275
- self.setBusy(false)
276
- let json = self.json(result.output)
277
- let applied = json?["config_applied"] as? Bool == true
278
- let restarted = json?["restart_completed"] as? Bool == true
279
- let ok = result.code == 0 && json?["ok"] as? Bool == true && applied && restarted
280
- _ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "Multi-provider router configured" : applied ? "Router configured; restart needs action" : "Router configuration needs action")
281
- if ok {
282
- let desktopPicker = json?["desktop_picker"] as? [String: Any]
283
- let cacheNote = desktopPicker?["models_cache_invalidated"] as? Bool == true
284
- ? " Desktop models cache was marked stale."
285
- : ""
286
- self.multiProvider.status.stringValue = "Configuration saved and Codex App restarted · \(values.model).\(cacheNote) Run a live Codex turn before treating runtime adoption as verified."
287
- self.multiProvider.status.textColor = .systemGreen
288
- } else if applied {
289
- let desktopPicker = json?["desktop_picker"] as? [String: Any]
290
- let cacheNote = desktopPicker?["models_cache_invalidated"] as? Bool == true
291
- ? " Desktop models cache was marked stale."
292
- : ""
293
- self.multiProvider.status.stringValue = "Router configuration saved, but Codex App did not restart.\(cacheNote) Reopen the app, then refresh status."
294
- self.multiProvider.status.textColor = .systemOrange
295
- } else {
296
- self.multiProvider.status.stringValue = "Router configuration failed · \(self.multiProviderPublicDetail(json, fallback: result.output))"
297
- self.multiProvider.status.textColor = .systemOrange
298
- }
299
- if applied {
300
- self.multiProvider.modelSelectionPending = false
301
- }
302
- self.refreshMultiProviderRouterStatus()
303
- self.refreshRoleModels()
304
- }
305
- }
306
-
307
- func synchronizeMultiProviderPopupSelection() {
308
- let current = multiProvider.model.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
309
- if let index = multiProvider.models.firstIndex(of: current) {
310
- multiProvider.modelPopup.selectItem(at: index + 1)
311
- } else {
312
- multiProvider.modelPopup.selectItem(at: 0)
313
- }
314
- }
315
-
316
- private func multiProviderRouterInput() -> (baseURL: String, catalogPath: String, model: String) {
317
- (
318
- multiProvider.baseURL.stringValue.trimmingCharacters(in: .whitespacesAndNewlines),
319
- multiProvider.catalogPath.stringValue.trimmingCharacters(in: .whitespacesAndNewlines),
320
- multiProvider.model.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
321
- )
322
- }
323
-
324
- private func multiProviderPublicDetail(_ json: [String: Any]?, fallback: String) -> String {
325
- if let blockers = json?["blockers"] as? [String], !blockers.isEmpty {
326
- return "Reason: \(blockers.joined(separator: ", ").replacingOccurrences(of: "_", with: " "))."
327
- }
328
- if let hint = json?["hint"] as? String, !hint.isEmpty { return "Next: \(hint)." }
329
- return "Next: \(NativeView.redactPreview(fallback))"
330
- }
331
- }
@@ -1,260 +0,0 @@
1
- import Cocoa
2
-
3
- final class RoleModelControls {
4
- let role: String
5
- let model = NSPopUpButton()
6
- let reasoning = NSPopUpButton()
7
- let current = NativeView.detail("Current: loading…")
8
- let save: NSButton
9
- let reset: NSButton
10
-
11
- init(role: String, target: AnyObject) {
12
- self.role = role
13
- save = NativeView.button("Save", target: target, action: #selector(ProvidersViewController.saveRoleModel(_:)))
14
- reset = NativeView.button("Reset", target: target, action: #selector(ProvidersViewController.resetRoleModel(_:)))
15
- save.identifier = NSUserInterfaceItemIdentifier(role)
16
- reset.identifier = NSUserInterfaceItemIdentifier(role)
17
- }
18
- }
19
-
20
- // Role-scoped model overrides (Models by Work Type). Split from
21
- // ProvidersOpenRouter.swift to keep both files inside the release line budget.
22
- extension ProvidersViewController {
23
- func makeRoleModelsCard() -> NSBox {
24
- let definitions = [
25
- ("ui_implementer", "UI implementation"),
26
- ("native_app_specialist", "Native app"),
27
- ("implementation_specialist", "Implementation"),
28
- ("test_engineer", "Testing")
29
- ]
30
- var views: [NSView] = [roleStatus]
31
- for (role, title) in definitions {
32
- let controls = RoleModelControls(role: role, target: self)
33
- controls.model.addItem(withTitle: "Loading profiles…")
34
- controls.model.setAccessibilityLabel("\(title) model")
35
- controls.model.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
36
- controls.model.widthAnchor.constraint(equalToConstant: 210).isActive = true
37
- controls.model.identifier = NSUserInterfaceItemIdentifier(role)
38
- controls.model.target = self
39
- controls.model.action = #selector(roleModelSelectionChanged(_:))
40
- controls.reasoning.addItem(withTitle: "Loading…")
41
- controls.reasoning.setAccessibilityLabel("\(title) reasoning effort")
42
- controls.reasoning.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
43
- controls.reasoning.widthAnchor.constraint(equalToConstant: 110).isActive = true
44
- controls.current.setAccessibilityLabel("\(title) current and effective model")
45
- controls.save.setAccessibilityLabel("Save \(title) model override")
46
- controls.reset.setAccessibilityLabel("Reset \(title) model override")
47
- registerProviderAction(controls.save, id: "sks-provider-save-role-model")
48
- registerProviderAction(controls.reset, id: "sks-provider-reset-role-model")
49
- controls.save.setAccessibilityIdentifier("sks-provider-save-role-model-\(role)")
50
- controls.reset.setAccessibilityIdentifier("sks-provider-reset-role-model-\(role)")
51
- controls.model.isEnabled = false
52
- controls.reasoning.isEnabled = false
53
- controls.save.isEnabled = false
54
- controls.reset.isEnabled = false
55
- roleRows[role] = controls
56
- actionButtons += [controls.save, controls.reset]
57
-
58
- let heading = NativeView.sectionTitle(title)
59
- let roleId = NativeView.detail(role)
60
- let modelLabel = NSTextField(labelWithString: "Model")
61
- let reasoningLabel = NSTextField(labelWithString: "Reasoning")
62
- views.append(NativeView.row([heading, roleId]))
63
- views.append(controls.current)
64
- views.append(NativeView.row([modelLabel, controls.model, reasoningLabel, controls.reasoning]))
65
- views.append(NativeView.row([controls.save, controls.reset]))
66
- if role != definitions.last?.0 { let separator = NSBox(); separator.boxType = .separator; views.append(separator) }
67
- }
68
- let refresh = NativeView.button("Refresh Role Settings", target: self, action: #selector(refreshRoleModelsAction(_:)))
69
- registerProviderAction(refresh, id: "sks-provider-refresh-role-models")
70
- roleRefreshButton = refresh
71
- actionButtons.append(refresh)
72
- views.append(refresh)
73
- return NativeView.card(
74
- title: "Models by Work Type",
75
- subtitle: "Overrides apply only to the selected agent role. Without an override, new work inherits the active main model; if no main model is selected, the managed GPT role default is used. Multi-Provider Router catalog slugs let different roles use different upstream providers.",
76
- views: views
77
- )
78
- }
79
-
80
- @objc func refreshRoleModelsAction(_ sender: NSButton) {
81
- refreshRoleModels()
82
- }
83
-
84
- func refreshRoleModels() {
85
- guard !roleRefreshInFlight else { return }
86
- roleRefreshInFlight = true
87
- roleRefreshButton?.isEnabled = false
88
- roleStatus.stringValue = "Loading role model settings…"
89
- processClient.run(["codex-app", "role-models", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
90
- guard let self = self else { return }
91
- self.roleRefreshInFlight = false
92
- self.roleRefreshButton?.isEnabled = !self.busy
93
- // role-models exits 1 on recoverable preference blockers (an override
94
- // outside the active catalog, no router selected) while still returning
95
- // the full roles/supported_profiles payload. Gating on the exit code
96
- // would disable Reset — the only control that clears such an override.
97
- // A store-level blocker is different: the preference file could not be
98
- // read, so every save/reset fails closed and the controls must lock.
99
- let storeBlockers = (self.json(result.output)?["store_blockers"] as? [String] ?? [])
100
- guard let json = self.json(result.output), storeBlockers.isEmpty else {
101
- self.roleProfilesLoaded = false
102
- self.updateRoleControlAvailability()
103
- self.roleStatus.stringValue = storeBlockers.isEmpty
104
- ? "Role settings unavailable · existing configuration was not changed."
105
- : "Role settings unavailable · \(storeBlockers.joined(separator: ", ")) · existing configuration was not changed."
106
- return
107
- }
108
- let roleBlockers = (json["preference_blockers"] as? [String] ?? json["blockers"] as? [String] ?? []).joined(separator: ", ")
109
- self.configureSupportedRoleProfiles(json)
110
- var loaded = 0
111
- for (role, controls) in self.roleRows {
112
- guard let payload = self.rolePayload(json, role: role) else {
113
- controls.current.stringValue = "Current: unavailable · Effective: unavailable"
114
- continue
115
- }
116
- let configured = (payload["configured"] as? [String: Any]) ?? (payload["override"] as? [String: Any])
117
- let effective = (payload["effective"] as? [String: Any]) ?? payload
118
- let configuredModel = (configured?["model"] as? String) ?? (payload["configured_model"] as? String)
119
- let configuredReasoning = self.reasoningValue(configured) ?? (payload["configured_reasoning"] as? String)
120
- let configuredProvider = (configured?["provider"] as? String) ?? (payload["configured_provider"] as? String)
121
- let defaultModel = payload["default_model"] as? String
122
- let defaultReasoning = payload["default_reasoning_effort"] as? String
123
- let effectiveModel = (effective["model"] as? String) ?? (payload["effective_model"] as? String) ?? "unavailable"
124
- let effectiveProvider = (effective["provider"] as? String)
125
- ?? (payload["effective_provider"] as? String)
126
- ?? "unknown"
127
- let effectiveReasoning = self.reasoningValue(effective)
128
- ?? (payload["effective_reasoning_effort"] as? String)
129
- ?? (payload["effective_reasoning"] as? String)
130
- ?? "unavailable"
131
- let effectiveSource = payload["effective_source"] as? String
132
- let editorModel = configuredModel ?? (effectiveSource == "selected-main-model" ? effectiveModel : (defaultModel ?? effectiveModel))
133
- let editorReasoning = configuredReasoning ?? (effectiveSource == "selected-main-model" ? effectiveReasoning : (defaultReasoning ?? effectiveReasoning))
134
- self.selectRoleProfile(model: editorModel, reasoning: editorReasoning, controls: controls)
135
- let current = configuredModel.map {
136
- "\(self.roleModelDisplay(provider: configuredProvider ?? effectiveProvider, model: $0)) / \(configuredReasoning ?? "default")"
137
- } ?? "default (no override)"
138
- let effectiveDisplay = self.roleModelDisplay(provider: effectiveProvider, model: effectiveModel)
139
- let inherited = effectiveSource == "selected-main-model" ? " · inherits active main model" : ""
140
- controls.current.stringValue = "Current: \(current) · Effective: \(effectiveDisplay) / \(effectiveReasoning)\(inherited)"
141
- loaded += 1
142
- }
143
- self.roleProfilesLoaded = !self.supportedRoleProfiles.isEmpty
144
- self.updateRoleControlAvailability()
145
- self.roleStatus.stringValue = roleBlockers.isEmpty
146
- ? "Loaded \(loaded) of \(self.roleRows.count) role settings. Save creates an override; Reset removes it."
147
- : "Loaded \(loaded) of \(self.roleRows.count) role settings · needs attention: \(roleBlockers). Reset removes an override."
148
- }
149
- }
150
-
151
- func updateRoleControlAvailability() {
152
- let enabled = !busy && roleProfilesLoaded
153
- for controls in roleRows.values {
154
- controls.model.isEnabled = enabled
155
- controls.reasoning.isEnabled = enabled
156
- controls.save.isEnabled = enabled
157
- controls.reset.isEnabled = enabled
158
- }
159
- }
160
-
161
- private func configureSupportedRoleProfiles(_ json: [String: Any]) {
162
- let rows = json["supported_profiles"] as? [[String: Any]] ?? []
163
- supportedRoleProfiles = rows.compactMap { row in
164
- guard let model = row["model"] as? String,
165
- let reasoning = row["reasoning_effort"] as? String,
166
- !model.isEmpty, !reasoning.isEmpty else { return nil }
167
- return (model, reasoning)
168
- }
169
- let models = Array(Set(supportedRoleProfiles.map(\.model))).sorted()
170
- for controls in roleRows.values {
171
- controls.model.removeAllItems()
172
- controls.model.addItems(withTitles: models)
173
- controls.reasoning.removeAllItems()
174
- }
175
- }
176
-
177
- private func rolePayload(_ json: [String: Any], role: String) -> [String: Any]? {
178
- if let roles = json["roles"] as? [String: Any], let payload = roles[role] as? [String: Any] { return payload }
179
- if let payload = json[role] as? [String: Any] { return payload }
180
- if let roles = json["roles"] as? [[String: Any]] {
181
- return roles.first { ($0["role"] as? String) == role || ($0["id"] as? String) == role }
182
- }
183
- return nil
184
- }
185
-
186
- private func reasoningValue(_ json: [String: Any]?) -> String? {
187
- (json?["reasoning"] as? String) ?? (json?["reasoning_effort"] as? String) ?? (json?["model_reasoning_effort"] as? String)
188
- }
189
-
190
- private func roleModelDisplay(provider: String, model: String) -> String { model.contains("/") ? model : "\(provider):\(model)" }
191
-
192
- private func selectRoleProfile(model: String, reasoning: String, controls: RoleModelControls) {
193
- if !controls.model.itemTitles.contains(model) {
194
- controls.model.addItem(withTitle: model)
195
- }
196
- controls.model.selectItem(withTitle: model)
197
- updateReasoningChoices(controls, preferred: reasoning)
198
- }
199
-
200
- @objc private func roleModelSelectionChanged(_ sender: NSPopUpButton) {
201
- guard let role = sender.identifier?.rawValue, let controls = roleRows[role] else { return }
202
- updateReasoningChoices(controls, preferred: nil)
203
- }
204
-
205
- private func updateReasoningChoices(_ controls: RoleModelControls, preferred: String?) {
206
- let model = controls.model.titleOfSelectedItem ?? ""
207
- var efforts = supportedRoleProfiles.filter { $0.model == model }.map(\.reasoning)
208
- if let preferred = preferred, !preferred.isEmpty, !efforts.contains(preferred) {
209
- efforts.append(preferred)
210
- }
211
- controls.reasoning.removeAllItems()
212
- controls.reasoning.addItems(withTitles: efforts)
213
- if let preferred = preferred, efforts.contains(preferred) { controls.reasoning.selectItem(withTitle: preferred) }
214
- }
215
-
216
- @objc func saveRoleModel(_ sender: NSButton) {
217
- guard let role = sender.identifier?.rawValue, let controls = roleRows[role] else { return }
218
- let model = controls.model.titleOfSelectedItem ?? ""
219
- let reasoning = controls.reasoning.titleOfSelectedItem ?? "medium"
220
- guard !model.isEmpty else { roleStatus.stringValue = "Save blocked for \(role) · enter a model id."; return }
221
- runRoleMutation(
222
- ["codex-app", "set-role-model", "--role", role, "--model", model, "--reasoning", reasoning, "--json"],
223
- role: role,
224
- kind: "role-model-set",
225
- running: "Saving \(role) override…",
226
- success: "Saved \(role) · \(model) / \(reasoning). Next: new tasks for this role use the effective override."
227
- )
228
- }
229
-
230
- @objc func resetRoleModel(_ sender: NSButton) {
231
- guard let role = sender.identifier?.rawValue else { return }
232
- runRoleMutation(
233
- ["codex-app", "reset-role-model", "--role", role, "--json"],
234
- role: role,
235
- kind: "role-model-reset",
236
- running: "Resetting \(role) override…",
237
- success: "Reset \(role). Next: new tasks use the effective default shown below."
238
- )
239
- }
240
-
241
- private func runRoleMutation(_ args: [String], role: String, kind: String, running: String, success: String) {
242
- guard !busy else { roleStatus.stringValue = "Another provider action is already running."; return }
243
- guard let snapshot = operations.begin(kind: kind, mutationGroup: "codex-config", summary: running) else {
244
- roleStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
245
- return
246
- }
247
- setBusy(true)
248
- roleStatus.stringValue = running
249
- _ = operations.update(snapshot, state: .running, stage: "saving", progress: nil, summary: running)
250
- processClient.run(args, timeout: NativeView.mutationTimeout) { [weak self] result in
251
- guard let self = self else { return }
252
- self.setBusy(false)
253
- let json = self.json(result.output)
254
- let ok = result.code == 0 && json?["ok"] as? Bool == true
255
- _ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "Role model setting saved" : "Role model setting needs action")
256
- self.roleStatus.stringValue = ok ? success : "\(role) change failed · \(self.structuredPublicDetail(json, fallback: result.output))"
257
- self.refreshRoleModels()
258
- }
259
- }
260
- }