sneakoscope 10.3.7 → 10.3.8
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 +12 -181
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/bridge.js +61 -1
- package/dist/commands/codex-app.js +4 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/native-worker-backend-router.js +55 -0
- package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
- package/dist/core/codex-app/codex-context-window.js +61 -24
- package/dist/core/codex-app/menubar/constants.js +4 -0
- package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
- package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
- package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
- package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
- package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
- package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
- package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
- package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
- package/dist/core/codex-lb/desktop-service.js +13 -2
- package/dist/core/commands/naruto-command.js +1 -1
- package/dist/core/decisions/child-model-choice.js +107 -0
- package/dist/core/decisions/cli.js +19 -2
- package/dist/core/decisions/routing.js +5 -4
- package/dist/core/hooks-runtime/hook-context.js +21 -7
- package/dist/core/hooks-runtime/hook-io.js +3 -2
- package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
- package/dist/core/hooks-runtime/subagent-context.js +13 -2
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
- package/dist/core/hooks-runtime.js +12 -5
- package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
- package/dist/core/subagents/child-model-allowlist.js +179 -0
- package/dist/core/subagents/child-model-plan.js +117 -0
- package/dist/core/subagents/naruto-host-credentials.js +82 -13
- package/dist/core/subagents/official-subagent-config.js +66 -15
- package/dist/core/subagents/official-subagent-preparation.js +45 -12
- package/dist/core/subagents/official-subagent-prompt.js +82 -11
- package/dist/core/subagents/official-subagent-runner.js +3 -0
- package/dist/core/subagents/read-only-list-role.js +73 -0
- package/dist/core/subagents/role-model-preferences.js +54 -17
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
- package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
- package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
- package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
- package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
- package/package.json +1 -1
|
@@ -23,6 +23,13 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
23
23
|
var authPriorityEnabled: Bool?
|
|
24
24
|
var authPriorityBusy = false
|
|
25
25
|
var authPriorityGeneration = 0
|
|
26
|
+
// OpenRouter Only shares the Default connection card; its logic lives in ProvidersOpenRouterOnly.swift.
|
|
27
|
+
let openRouterOnlyToggle = NSSwitch()
|
|
28
|
+
let openRouterOnlyStatus = NativeView.detail("Checking OpenRouter Only…")
|
|
29
|
+
let openRouterOnlyDetail = NativeView.detail("")
|
|
30
|
+
var openRouterOnlyEnabled: Bool?
|
|
31
|
+
var openRouterOnlyBusy = false
|
|
32
|
+
var openRouterOnlyGeneration = 0
|
|
26
33
|
let providerStatus = NativeView.detail("Desktop Bridge status has not loaded.")
|
|
27
34
|
let bridgeServiceStatus = NativeView.detail("Service: checking…")
|
|
28
35
|
let bridgeHttpStatus = NativeView.detail("HTTP probe: not attempted")
|
|
@@ -116,64 +123,67 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
116
123
|
return NativeView.card(title: "Default connection", subtitle: "", views: [
|
|
117
124
|
NativeView.row([authPriorityToggle, label]), authPriorityStatus,
|
|
118
125
|
NativeView.detail("Use the saved Codex-LB connection first for eligible models. Explicit provider choices and session pins still apply.")
|
|
119
|
-
])
|
|
126
|
+
] + makeOpenRouterOnlyRows())
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
func refreshAuthPriority(
|
|
123
|
-
guard !
|
|
129
|
+
func refreshAuthPriority(notice: String? = nil) {
|
|
130
|
+
guard !routingModeBusy else { return }
|
|
124
131
|
authPriorityGeneration += 1
|
|
125
132
|
let generation = authPriorityGeneration
|
|
126
133
|
processClient.run(["bridge", "auth-priority", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
127
|
-
guard let self = self, !self.
|
|
134
|
+
guard let self = self, !self.routingModeBusy, generation == self.authPriorityGeneration else { return }
|
|
128
135
|
guard !result.timedOut, !result.truncated,
|
|
129
136
|
let payload = self.json(result.output),
|
|
130
137
|
let priority = AuthPriorityState.decode(payload) else {
|
|
131
138
|
self.authPriorityEnabled = nil
|
|
132
|
-
self.
|
|
139
|
+
self.updateRoutingModeSwitches()
|
|
133
140
|
self.authPriorityToggle.isHidden = true
|
|
134
141
|
self.authPriorityStatus.stringValue = "Saved preference could not be confirmed. Reopen Connections to check again."
|
|
135
142
|
self.authPriorityStatus.textColor = .systemOrange
|
|
136
143
|
return
|
|
137
144
|
}
|
|
138
145
|
self.renderAuthPriority(priority)
|
|
139
|
-
if
|
|
140
|
-
self.authPriorityStatus.stringValue += "\
|
|
146
|
+
if let notice = notice {
|
|
147
|
+
self.authPriorityStatus.stringValue += "\n" + notice
|
|
141
148
|
self.authPriorityStatus.textColor = .systemOrange
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
|
|
153
|
+
func renderAuthPriority(_ state: AuthPriorityState) {
|
|
147
154
|
authPriorityEnabled = state.enabled
|
|
148
155
|
authPriorityToggle.isHidden = false
|
|
149
156
|
authPriorityToggle.state = state.enabled ? .on : .off
|
|
150
|
-
|
|
157
|
+
updateRoutingModeSwitches()
|
|
151
158
|
authPriorityStatus.stringValue = state.message
|
|
152
159
|
authPriorityStatus.textColor = state.state == "active" ? .systemGreen : state.state == "unavailable" ? .systemOrange : .secondaryLabelColor
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
@objc func toggleAuthPriority() {
|
|
156
|
-
guard !
|
|
163
|
+
guard !routingModeBusy, let previous = authPriorityEnabled else { return }
|
|
157
164
|
let desired = authPriorityToggle.state == .on
|
|
158
165
|
guard let operation = operations.begin(kind: "bridge-auth-priority", mutationGroup: "codex-config", summary: "Change Codex-LB preference") else {
|
|
159
166
|
authPriorityToggle.state = previous ? .on : .off
|
|
160
167
|
authPriorityStatus.stringValue = "Another configuration change is running. Try again when it finishes."
|
|
161
168
|
return
|
|
162
169
|
}
|
|
163
|
-
authPriorityGeneration += 1
|
|
164
170
|
authPriorityBusy = true
|
|
165
|
-
|
|
171
|
+
beginRoutingModeMutation()
|
|
166
172
|
authPriorityStatus.stringValue = "Saving preference…"
|
|
167
173
|
_ = operations.update(operation, state: .running, stage: "saving", progress: nil, summary: "Saving Codex-LB preference")
|
|
168
174
|
processClient.run(["bridge", "auth-priority", desired ? "on" : "off", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
169
175
|
guard let self = self else { return }
|
|
170
176
|
self.authPriorityBusy = false
|
|
171
177
|
let payload = self.json(result.output)
|
|
178
|
+
let complete = !result.truncated && !result.timedOut
|
|
172
179
|
let outcome = AuthPriorityMutationOutcome.resolve(
|
|
173
180
|
payload: payload, desired: desired,
|
|
174
|
-
commandSucceeded: result.code == 0 && payload?["ok"] as? Bool == true
|
|
175
|
-
|
|
181
|
+
commandSucceeded: result.code == 0 && payload?["ok"] as? Bool == true
|
|
182
|
+
&& OpenRouterOnlyReceipt.decode(payload).blockers.isEmpty,
|
|
183
|
+
responseComplete: complete
|
|
176
184
|
)
|
|
185
|
+
self.renderOpenRouterOnly(from: payload, responseComplete: complete)
|
|
186
|
+
let issue = OpenRouterOnlyReceipt.decode(payload).primaryIssue.map(ProviderSecretRedactor.redact)
|
|
177
187
|
let operationState: OperationState
|
|
178
188
|
switch outcome {
|
|
179
189
|
case .saved: operationState = .succeeded
|
|
@@ -184,17 +194,19 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
184
194
|
if let state = outcome.observedState {
|
|
185
195
|
self.renderAuthPriority(state)
|
|
186
196
|
if operationState != .succeeded {
|
|
187
|
-
self.authPriorityStatus.stringValue = outcome.operationSummary + "\n" + state.message
|
|
197
|
+
self.authPriorityStatus.stringValue = [outcome.operationSummary, issue].compactMap { $0 }.joined(separator: " · ") + "\n" + state.message
|
|
188
198
|
self.authPriorityStatus.textColor = .systemOrange
|
|
189
199
|
}
|
|
190
200
|
} else {
|
|
191
201
|
self.authPriorityEnabled = nil
|
|
192
|
-
self.
|
|
202
|
+
self.updateRoutingModeSwitches()
|
|
193
203
|
self.authPriorityToggle.isHidden = true
|
|
194
204
|
self.authPriorityStatus.stringValue = "Operation result is uncertain. Checking the saved preference…"
|
|
195
205
|
self.authPriorityStatus.textColor = .systemOrange
|
|
196
|
-
self.refreshAuthPriority(afterUnconfirmedMutation: true)
|
|
197
206
|
}
|
|
207
|
+
// Turning Codex-LB on turns OpenRouter Only off: re-read both modes.
|
|
208
|
+
let notice = outcome.observedState == nil ? "Saved preference rechecked. The earlier operation did not finish normally." : operationState == .succeeded ? nil : outcome.operationSummary
|
|
209
|
+
self.rereadRoutingModes(authPriorityNotice: notice, openRouterOnlyNotice: nil)
|
|
198
210
|
}
|
|
199
211
|
}
|
|
200
212
|
|
|
@@ -221,7 +233,7 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
221
233
|
|
|
222
234
|
func setBusy(_ value: Bool) {
|
|
223
235
|
busy = value
|
|
224
|
-
|
|
236
|
+
updateRoutingModeSwitches()
|
|
225
237
|
for button in actionButtons where !providerButtons.values.flatMap({ $0 }).contains(where: { $0 === button }) { button.isEnabled = !value }
|
|
226
238
|
value ? globalSpinner.startAnimation(nil) : globalSpinner.stopAnimation(nil)
|
|
227
239
|
}
|
|
@@ -235,6 +247,12 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
235
247
|
func refresh() {
|
|
236
248
|
refreshCredentialHealth()
|
|
237
249
|
refreshAuthPriority()
|
|
250
|
+
refreshOpenRouterOnly()
|
|
251
|
+
refreshBridgeStatus()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Runtime, profiles, combined catalog and routes from one v3 status read.
|
|
255
|
+
func refreshBridgeStatus() {
|
|
238
256
|
processClient.run(["bridge", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
239
257
|
guard let self = self, let json = self.json(result.output),
|
|
240
258
|
let status = try? DesktopBridgeStatusV3Truth.decode(from: json),
|
|
@@ -56,7 +56,7 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
|
56
56
|
)
|
|
57
57
|
let contextCard = NativeView.card(
|
|
58
58
|
title: "Codex 1M Context",
|
|
59
|
-
subtitle: "
|
|
59
|
+
subtitle: "Let every Codex model use its largest context window, up to 1M tokens. New sessions use this setting; running Codex restarts. Requests over 272K input tokens use the long-context rate for the entire request.",
|
|
60
60
|
views: [NativeView.row([contextToggleButton]), contextStatus]
|
|
61
61
|
)
|
|
62
62
|
let notificationsCard = NativeView.card(
|
|
@@ -173,12 +173,18 @@ final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
|
173
173
|
self.contextToggleButton.title = enabled ? "Disable 1M Context" : "Enable 1M Context"
|
|
174
174
|
guard !preserveStatusText else { return }
|
|
175
175
|
let model = json["model"] as? String
|
|
176
|
+
let window = json["model_window"] as? [String: Any]
|
|
177
|
+
let effective = window?["effective_window"] as? Int
|
|
176
178
|
var text = enabled
|
|
177
|
-
? "Enabled ·
|
|
179
|
+
? "Enabled · applies to new sessions only."
|
|
178
180
|
: "Disabled · Codex uses its tuned default context window."
|
|
179
181
|
var tone: NSColor = enabled ? .systemGreen : .secondaryLabelColor
|
|
180
|
-
if
|
|
181
|
-
|
|
182
|
+
if let model = model, let effective = effective {
|
|
183
|
+
let size = NumberFormatter.localizedString(from: NSNumber(value: effective), number: .decimal)
|
|
184
|
+
text += enabled ? " \(model) window \(size) tokens." : " \(model) can use up to \(size) tokens."
|
|
185
|
+
}
|
|
186
|
+
if enabled, let model = model, window?["extends"] as? Bool == false {
|
|
187
|
+
text += " \(model) has a fixed window, so this setting does not enlarge it."
|
|
182
188
|
tone = .systemOrange
|
|
183
189
|
}
|
|
184
190
|
self.contextStatus.stringValue = text
|
|
@@ -5,6 +5,7 @@ import Foundation
|
|
|
5
5
|
enum SidebarItem: String, CaseIterable {
|
|
6
6
|
case overview = "Overview"
|
|
7
7
|
case providers = "Providers"
|
|
8
|
+
case subagentModels = "Subagent Models"
|
|
8
9
|
case remoteCoding = "Remote Coding"
|
|
9
10
|
case updates = "Updates"
|
|
10
11
|
case mcpServers = "MCP Servers"
|
|
@@ -19,6 +20,7 @@ enum SidebarItem: String, CaseIterable {
|
|
|
19
20
|
switch self {
|
|
20
21
|
case .overview: return "gauge"
|
|
21
22
|
case .providers: return "network"
|
|
23
|
+
case .subagentModels: return "person.2"
|
|
22
24
|
case .updates: return "arrow.down.circle"
|
|
23
25
|
case .mcpServers: return "server.rack"
|
|
24
26
|
case .localDecision: return "cpu"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// One `available` row of `sks bridge subagent-models list --json`: an
|
|
4
|
+
/// OpenRouter model the bridge catalog knows.
|
|
5
|
+
struct SubagentModelOption: Equatable {
|
|
6
|
+
let publicId: String
|
|
7
|
+
let displayName: String
|
|
8
|
+
/// false for a saved or drafted model the catalog no longer lists.
|
|
9
|
+
var listed = true
|
|
10
|
+
|
|
11
|
+
var menuTitle: String {
|
|
12
|
+
guard listed else { return "\(publicId) · not in the current catalog" }
|
|
13
|
+
return displayName == publicId ? publicId : "\(displayName) · \(publicId)"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// `subagent-models.list` / `subagent-models.set` answers: the mode with its
|
|
18
|
+
/// saved list, and (when reported) the models the row popups can offer.
|
|
19
|
+
struct SubagentModelsSnapshot: Equatable {
|
|
20
|
+
static let operations: Set<String> = ["subagent-models.list", "subagent-models.set"]
|
|
21
|
+
|
|
22
|
+
let mode: OpenRouterOnlyState
|
|
23
|
+
/// nil when the answer did not include `available` (keep the previous list).
|
|
24
|
+
let available: [SubagentModelOption]?
|
|
25
|
+
|
|
26
|
+
static func decode(_ payload: [String: Any]) -> SubagentModelsSnapshot? {
|
|
27
|
+
guard payload["schema"] as? String == OpenRouterOnlyCommand.resultSchema,
|
|
28
|
+
let operation = payload["operation"] as? String, operations.contains(operation),
|
|
29
|
+
let mode = OpenRouterOnlyState.decode(payload) else { return nil }
|
|
30
|
+
let result = payload["result"] as? [String: Any]
|
|
31
|
+
var seen = Set<String>()
|
|
32
|
+
let available = (result?["available"] as? [Any]).map { rows in
|
|
33
|
+
rows.compactMap { value -> SubagentModelOption? in
|
|
34
|
+
guard let row = value as? [String: Any],
|
|
35
|
+
let id = OpenRouterOnlyJSON.text(row["public_id"]),
|
|
36
|
+
SubagentModelRules.isModelId(id),
|
|
37
|
+
seen.insert(id.lowercased()).inserted else { return nil }
|
|
38
|
+
return SubagentModelOption(publicId: id, displayName: OpenRouterOnlyJSON.text(row["display_name"]) ?? id)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return SubagentModelsSnapshot(mode: mode, available: available)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Pure edits of the unapplied list. The page keeps one draft array and
|
|
46
|
+
/// always sends the complete list, never a patch.
|
|
47
|
+
enum SubagentModelDraft {
|
|
48
|
+
static let effortTitles = ["Default", "low", "medium", "high", "xhigh"]
|
|
49
|
+
|
|
50
|
+
/// Popup rows: every available model, then saved or drafted models the CLI
|
|
51
|
+
/// no longer lists, so a popup never misstates a choice.
|
|
52
|
+
static func popupOptions(available: [SubagentModelOption], entries: [SubagentModelEntry]) -> [SubagentModelOption] {
|
|
53
|
+
var seen = Set(available.map { $0.publicId.lowercased() })
|
|
54
|
+
let missing = entries.filter { seen.insert($0.model.lowercased()).inserted }
|
|
55
|
+
.map { SubagentModelOption(publicId: $0.model, displayName: $0.model, listed: false) }
|
|
56
|
+
return available + missing
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// A new row takes the first available model not already on the list.
|
|
60
|
+
static func adding(_ entries: [SubagentModelEntry], available: [SubagentModelOption]) -> [SubagentModelEntry]? {
|
|
61
|
+
guard entries.count < SubagentModelRules.maxModels else { return nil }
|
|
62
|
+
let used = Set(entries.map { $0.model.lowercased() })
|
|
63
|
+
guard let next = available.first(where: { !used.contains($0.publicId.lowercased()) }) else { return nil }
|
|
64
|
+
return entries + [SubagentModelEntry(model: next.publicId, criteria: "", reasoningEffort: nil, isDefault: entries.isEmpty, routable: nil)]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static func removing(_ entries: [SubagentModelEntry], at index: Int) -> [SubagentModelEntry] {
|
|
68
|
+
guard entries.indices.contains(index) else { return entries }
|
|
69
|
+
var next = entries
|
|
70
|
+
next.remove(at: index)
|
|
71
|
+
return withOneDefault(next)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static func settingDefault(_ entries: [SubagentModelEntry], at index: Int) -> [SubagentModelEntry] {
|
|
75
|
+
guard entries.indices.contains(index) else { return entries }
|
|
76
|
+
return entries.enumerated().map { offset, entry in
|
|
77
|
+
var copy = entry
|
|
78
|
+
copy.isDefault = offset == index
|
|
79
|
+
return copy
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Same rule as the CLI: the first default wins, else the first row.
|
|
84
|
+
static func withOneDefault(_ entries: [SubagentModelEntry]) -> [SubagentModelEntry] {
|
|
85
|
+
let first = entries.firstIndex(where: \.isDefault) ?? 0
|
|
86
|
+
return entries.enumerated().map { offset, entry in
|
|
87
|
+
var copy = entry
|
|
88
|
+
copy.isDefault = offset == first
|
|
89
|
+
return copy
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static func effort(title: String?) -> String? {
|
|
94
|
+
guard let title, SubagentModelRules.efforts.contains(title) else { return nil }
|
|
95
|
+
return title
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The CLI caps criteria at 240 UTF-16 units (JavaScript `slice`). Capping
|
|
99
|
+
/// whole characters to the same budget keeps its slice a no-op, so the
|
|
100
|
+
/// saved text reads back exactly. While typing, spaces are kept.
|
|
101
|
+
static func limitCriteria(_ text: String) -> String {
|
|
102
|
+
let limit = SubagentModelRules.maxCriteriaCharacters
|
|
103
|
+
guard text.utf16.count > limit else { return text }
|
|
104
|
+
var kept = ""
|
|
105
|
+
for character in text {
|
|
106
|
+
guard kept.utf16.count + String(character).utf16.count <= limit else { break }
|
|
107
|
+
kept.append(character)
|
|
108
|
+
}
|
|
109
|
+
return kept
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// JavaScript `\s` plus the C0 controls and DEL the CLI folds into spaces.
|
|
113
|
+
private static let criteriaSeparators: CharacterSet = {
|
|
114
|
+
var set = CharacterSet(charactersIn: "\u{0}"..."\u{20}")
|
|
115
|
+
set.insert(charactersIn: "\u{7F}\u{A0}\u{1680}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{FEFF}")
|
|
116
|
+
set.insert(charactersIn: "\u{2000}"..."\u{200A}")
|
|
117
|
+
return set
|
|
118
|
+
}()
|
|
119
|
+
|
|
120
|
+
/// Same cleanup as the CLI's cleanCriteria: controls and whitespace runs
|
|
121
|
+
/// become one space, then trim and cap.
|
|
122
|
+
static func cleanCriteria(_ text: String) -> String {
|
|
123
|
+
let collapsed = text.components(separatedBy: criteriaSeparators)
|
|
124
|
+
.filter { !$0.isEmpty }
|
|
125
|
+
.joined(separator: " ")
|
|
126
|
+
return limitCriteria(collapsed)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static func normalizedForSubmit(_ entries: [SubagentModelEntry]) -> [SubagentModelEntry] {
|
|
130
|
+
withOneDefault(entries.map { entry in
|
|
131
|
+
var copy = entry.submitted
|
|
132
|
+
copy.model = entry.model.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
133
|
+
copy.criteria = cleanCriteria(entry.criteria)
|
|
134
|
+
return copy
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Local mirror of normalizeSubagentModelList; codes carry the row index.
|
|
139
|
+
static func issues(_ entries: [SubagentModelEntry]) -> [String] {
|
|
140
|
+
var seen = Set<String>()
|
|
141
|
+
var issues: [String] = []
|
|
142
|
+
for (index, entry) in entries.enumerated() {
|
|
143
|
+
let model = entry.model.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
144
|
+
if !SubagentModelRules.isModelId(model) {
|
|
145
|
+
issues.append("subagent_model_id_invalid:\(index)")
|
|
146
|
+
} else if !seen.insert(model.lowercased()).inserted {
|
|
147
|
+
issues.append("subagent_model_duplicate:\(index)")
|
|
148
|
+
} else if let effort = entry.reasoningEffort, !SubagentModelRules.efforts.contains(effort) {
|
|
149
|
+
issues.append("subagent_model_effort_invalid:\(index)")
|
|
150
|
+
} else if entry.criteriaUnreadable {
|
|
151
|
+
issues.append("subagent_model_criteria_redacted:\(index)")
|
|
152
|
+
} else if index >= SubagentModelRules.maxModels {
|
|
153
|
+
issues.append("subagent_model_list_too_long:\(index)")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return issues
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// A redacted answer row takes the text this page already holds when that
|
|
160
|
+
/// text redacts to exactly the answer; any other redacted row stays
|
|
161
|
+
/// unreadable, and `issues` keeps it from being applied.
|
|
162
|
+
static func restoringCriteria(_ incoming: [SubagentModelEntry], known: [SubagentModelEntry], redact: (String) -> String) -> [SubagentModelEntry] {
|
|
163
|
+
incoming.map { entry in
|
|
164
|
+
guard entry.criteriaUnreadable, let match = known.first(where: {
|
|
165
|
+
$0.model.lowercased() == entry.model.lowercased() && !$0.criteriaUnreadable
|
|
166
|
+
&& (redact(cleanCriteria($0.criteria)) == entry.criteria || fitsRedacted(cleanCriteria($0.criteria), answer: entry.criteria))
|
|
167
|
+
}) else { return entry }
|
|
168
|
+
var copy = entry
|
|
169
|
+
copy.criteria = cleanCriteria(match.criteria)
|
|
170
|
+
return copy
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// The CLI and Control Center redact with different patterns; either way
|
|
175
|
+
/// each "[redacted]" stands for some text the known criteria must supply.
|
|
176
|
+
static func fitsRedacted(_ text: String, answer: String) -> Bool {
|
|
177
|
+
let marker = NSRegularExpression.escapedPattern(for: "[redacted]")
|
|
178
|
+
let pattern = "^" + NSRegularExpression.escapedPattern(for: answer).replacingOccurrences(of: marker, with: ".+?") + "$"
|
|
179
|
+
guard answer.contains("[redacted]"), let regex = try? NSRegularExpression(pattern: pattern, options: [.dotMatchesLineSeparators]) else { return false }
|
|
180
|
+
return regex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)) != nil
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static func isDirty(draft: [SubagentModelEntry], saved: [SubagentModelEntry]) -> Bool {
|
|
184
|
+
normalizedForSubmit(draft) != normalizedForSubmit(saved)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// stdin for `bridge subagent-models set --stdin --json`.
|
|
188
|
+
static func stdinPayload(_ entries: [SubagentModelEntry]) -> String? {
|
|
189
|
+
let rows: [[String: Any]] = normalizedForSubmit(entries).map { entry in
|
|
190
|
+
[
|
|
191
|
+
"model": entry.model,
|
|
192
|
+
"criteria": entry.criteria,
|
|
193
|
+
"reasoning_effort": entry.reasoningEffort.map { $0 as Any } ?? NSNull(),
|
|
194
|
+
"default": entry.isDefault
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
guard let data = try? JSONSerialization.data(withJSONObject: ["subagent_models": rows], options: [.sortedKeys, .withoutEscapingSlashes]),
|
|
198
|
+
let text = String(data: data, encoding: .utf8) else { return nil }
|
|
199
|
+
return text + "\n"
|
|
200
|
+
}
|
|
201
|
+
}
|