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.
- package/README.md +96 -58
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +5 -51
- package/dist/cli/command-manifest-lite.js +2 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/cli/global-mode-router.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +112 -914
- package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
- package/dist/cli/install-helpers-install-support.js +4 -20
- package/dist/cli/install-helpers.js +36 -2716
- package/dist/cli/router.js +9 -9
- package/dist/commands/bridge-command-output.js +111 -0
- package/dist/commands/bridge.js +320 -0
- package/dist/commands/codex-app.js +39 -123
- package/dist/commands/doctor-helpers.js +380 -0
- package/dist/commands/doctor-profile.js +3 -0
- package/dist/commands/doctor.js +114 -873
- package/dist/commands/mad-sks.js +0 -4
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-effort-policy.js +11 -21
- package/dist/core/agents/agent-recursion-guard.js +26 -0
- package/dist/core/agents/agent-runner-codex-exec.js +7 -9
- package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
- package/dist/core/architecture-hardening/state/state-service.js +49 -13
- package/dist/core/auto-review.js +15 -9
- package/dist/core/codex/codex-config-guard.js +8 -0
- package/dist/core/codex-adapter.js +8 -10
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
- package/dist/core/codex-app/codex-app-launcher.js +10 -12
- package/dist/core/codex-app/codex-app-restart.js +10 -12
- package/dist/core/codex-app/menubar/constants.js +2 -5
- package/dist/core/codex-app/menubar/global-install.js +3 -2
- package/dist/core/codex-app/menubar/installer/result.js +23 -0
- package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
- package/dist/core/codex-app/menubar/installer.js +5 -96
- package/dist/core/codex-app/menubar/migration.js +1 -42
- package/dist/core/codex-app/openrouter-provider.js +0 -78
- package/dist/core/codex-app.js +97 -360
- package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
- package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
- package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
- package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
- package/dist/core/codex-control/codex-task-runner.js +16 -60
- package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
- package/dist/core/codex-exec-output-schema.js +11 -13
- package/dist/core/codex-lb/bridge-contracts.js +2 -0
- package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
- package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
- package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
- package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
- package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
- package/dist/core/codex-lb/capability-runner.js +505 -201
- package/dist/core/codex-lb/codex-lb-env.js +179 -12
- package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
- package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
- package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
- package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
- package/dist/core/codex-lb/combined-catalog.js +12 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
- package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
- package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
- package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
- package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
- package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
- package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
- package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
- package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
- package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
- package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
- package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
- package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
- package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
- package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
- package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
- package/dist/core/codex-lb/desktop-controller.js +2 -1730
- package/dist/core/codex-lb/desktop-service.js +462 -627
- package/dist/core/codex-lb/migration-receipt.js +560 -118
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
- package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
- package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
- package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
- package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
- package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
- package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
- package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
- package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
- package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
- package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
- package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
- package/dist/core/codex-lb/provider-credentials/types.js +1 -0
- package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
- package/dist/core/codex-lb/provider-credentials.js +5 -0
- package/dist/core/codex-lb/provider-registry.js +323 -0
- package/dist/core/codex-lb/provider-route-policy.js +118 -0
- package/dist/core/codex-lb/request-route-resolver.js +172 -0
- package/dist/core/codex-lb/route-index.js +148 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
- package/dist/core/commands/basic-cli.js +1 -1
- package/dist/core/commands/check-command.js +3 -10
- package/dist/core/commands/gate-result-contract.js +10 -14
- package/dist/core/commands/intent-normalization/normalizer.js +1 -1
- package/dist/core/commands/mad-sks-command.js +2 -6
- package/dist/core/commands/release-command.js +11 -1
- package/dist/core/commands/telegram-command.js +46 -6
- package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
- package/dist/core/doctor/skill-legacy-surface.js +5 -5
- package/dist/core/evidence/v2/evidence-key.js +30 -3
- package/dist/core/feature-fixtures.js +8 -1
- package/dist/core/feature-registry.js +3 -2
- package/dist/core/hooks-runtime/stop-finalization.js +11 -1
- package/dist/core/hooks-runtime/subagent-context.js +15 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +73 -87
- package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
- package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
- package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
- package/dist/core/imagegen/imagegen-capability.js +43 -151
- package/dist/core/init/skills/filesystem.js +89 -0
- package/dist/core/init/skills/inventory.js +97 -0
- package/dist/core/init/skills/manifest.js +151 -0
- package/dist/core/init/skills.js +5 -325
- package/dist/core/init.js +0 -63
- package/dist/core/install/installed-package-smoke.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
- package/dist/core/policy/latest-version-guidance.js +15 -2
- package/dist/core/preflight/parallel-preflight-engine.js +27 -51
- package/dist/core/provider/provider-badge.js +9 -4
- package/dist/core/provider/provider-context.js +57 -107
- package/dist/core/providers/openrouter/openrouter-account.js +3 -1
- package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
- package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
- package/dist/core/release/gate-manifest.js +0 -1
- package/dist/core/release/main-push-guard.js +13 -0
- package/dist/core/release/main-push-receipt.js +11 -0
- package/dist/core/release/physical-release-gates.js +664 -61
- package/dist/core/release/publish-preflight.js +3 -3
- package/dist/core/release/release-gate-batch-runner.js +19 -6
- package/dist/core/release/release-gate-dag.js +33 -6
- package/dist/core/release/release-gate-node.js +1 -3
- package/dist/core/release/release-gate-output-contract.js +55 -0
- package/dist/core/release/release-pack-contract.js +1 -1
- package/dist/core/release/release-pack-receipt.js +209 -8
- package/dist/core/release/release-real-contract.js +1 -1
- package/dist/core/release/stage-publish.js +106 -28
- package/dist/core/retention/temp-lease.js +204 -0
- package/dist/core/retention.js +18 -195
- package/dist/core/routes.js +1 -2
- package/dist/core/safety/intent-contract/intent-contract.js +3 -3
- package/dist/core/subagents/naruto-host-credentials.js +6 -1
- package/dist/core/subagents/official-subagent-config.js +27 -9
- package/dist/core/subagents/official-subagent-runner.js +14 -31
- package/dist/core/subagents/wave-lifecycle.js +36 -8
- package/dist/core/subagents/wave-parent-guidance.js +2 -1
- package/dist/core/telegram/doctor.js +2 -2
- package/dist/core/telegram/liveness.js +10 -4
- package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
- package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
- package/dist/core/update/update-migration-state/session-state-split.js +247 -0
- package/dist/core/update/update-migration-state/simple-stages.js +46 -0
- package/dist/core/update/update-migration-state.js +4 -481
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
- package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
- package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
- package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
- package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
- package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
- package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/check-runtime-schemas.js +7 -2
- package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
- package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
- package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
- package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
- package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
- package/dist/scripts/desktop-bridge-transport-client.js +116 -0
- package/dist/scripts/desktop-bridge-unification-check.js +276 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
- package/dist/scripts/gate-policy-audit-check.js +52 -2
- package/dist/scripts/provider-badge-context-check.js +55 -12
- package/dist/scripts/publish-preflight.js +1 -2
- package/dist/scripts/release-affected-selector-check.js +1 -2
- package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
- package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
- package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
- package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
- package/dist/scripts/release-gate-batch-runner-check.js +9 -0
- package/dist/scripts/release-gate-dag-runner-check.js +6 -0
- package/dist/scripts/release-gate-dag-runner.js +4 -2
- package/dist/scripts/release-metadata-check.js +16 -3
- package/dist/scripts/release-pack-receipt.js +29 -6
- package/dist/scripts/release-physical-gates-check.js +39 -0
- package/dist/scripts/release-real-check.js +7 -5
- package/package.json +6 -8
- package/release-gates.v2.json +156 -157
- package/runtime-required-scripts.json +11 -11
- package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
- package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
- package/schemas/release/release-gate-node.schema.json +2 -3
- package/dist/cli/codex-lb-setup-warning-output.js +0 -17
- package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
- package/dist/commands/codex-lb.js +0 -1232
- package/dist/core/architecture-hardening/migration/migration.js +0 -111
- package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
- package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
- package/dist/core/codex-app/child-policy/child-policy.js +0 -35
- package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
- package/dist/core/codex-app/glm-model-profile.js +0 -1
- package/dist/core/codex-app/glm-profile-installer.js +0 -191
- package/dist/core/codex-app/glm-profile-schema.js +0 -42
- package/dist/core/codex-app/multi-provider-router.js +0 -391
- package/dist/core/codex-app/openrouter-activate.js +0 -405
- package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
- package/dist/core/codex-app/provider-child-policy.js +0 -64
- package/dist/core/codex-app/provider-mode.js +0 -91
- package/dist/core/codex-app/provider-session-policy.js +0 -85
- package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
- package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
- package/dist/core/codex-lb/codex-lb-setup.js +0 -293
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
- package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
- package/dist/core/codex-lb/desktop-mode.js +0 -32
- package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
- package/dist/core/codex-lb/legacy-migration.js +0 -576
- package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
- package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
- package/dist/core/codex-lb/routing-truth.js +0 -327
- package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
- package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
- package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
- 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
|
-
}
|