sneakoscope 6.7.0 → 7.0.2
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 +3 -3
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +71 -0
- package/dist/cli/install-helpers-codex-lb-selftest.js +11 -14
- package/dist/cli/install-helpers.js +305 -53
- package/dist/config/skills-manifest.json +52 -52
- package/dist/core/agents/agent-orchestrator.js +56 -5
- package/dist/core/agents/agent-roster.js +4 -2
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +18 -1
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +16 -0
- package/dist/core/codex-app.js +52 -6
- package/dist/core/commands/naruto-command.js +23 -5
- package/dist/core/hooks-runtime.js +77 -3
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
- package/dist/core/mission.js +74 -4
- package/dist/core/release/package-size-budget.js +1 -1
- package/dist/core/subagents/naruto-proof-projection.js +2 -2
- package/dist/core/subagents/official-subagent-config.js +8 -5
- package/dist/core/subagents/official-subagent-preparation.js +24 -1
- package/dist/core/subagents/official-subagent-prompt.js +3 -2
- package/dist/core/subagents/official-subagent-runner.js +8 -1
- package/dist/core/subagents/wave-lifecycle.js +20 -0
- package/dist/core/subagents/wave-parent-guidance.js +42 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AlertFactory.swift +21 -2
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +3 -0
- package/dist/native/sks-menubar/Sources/AppIdentity.swift +28 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +14 -8
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +71 -7
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +50 -9
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +61 -8
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +169 -31
- package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +58 -23
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +41 -7
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +34 -3
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +38 -8
- package/dist/scripts/docs-truthfulness-check.js +2 -2
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +7 -3
- package/package.json +1 -1
|
@@ -1,51 +1,121 @@
|
|
|
1
1
|
import Cocoa
|
|
2
2
|
|
|
3
|
-
final class ProvidersViewController: NSViewController {
|
|
3
|
+
final class ProvidersViewController: NSViewController, ControlCenterPage {
|
|
4
4
|
private let processClient: ProcessClient
|
|
5
|
+
private let operations: OperationCoordinator
|
|
5
6
|
private let providerStatus = NativeView.detail("Provider status unchecked.")
|
|
6
7
|
private let fastStatus = NativeView.detail("Fast Mode: checking current desktop setting…")
|
|
7
|
-
|
|
8
|
+
private var actionButtons: [NSButton] = []
|
|
9
|
+
private var busy = false
|
|
10
|
+
|
|
11
|
+
init(processClient: ProcessClient, operations: OperationCoordinator) {
|
|
12
|
+
self.processClient = processClient
|
|
13
|
+
self.operations = operations
|
|
14
|
+
super.init(nibName: nil, bundle: nil)
|
|
15
|
+
}
|
|
8
16
|
required init?(coder: NSCoder) { nil }
|
|
9
17
|
|
|
10
18
|
override func loadView() {
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
let setDomain = NativeView.button("Set Domain and Key…", target: self, action: #selector(setDomainAndKey))
|
|
20
|
+
let replace = NativeView.button("Replace Key…", target: self, action: #selector(replaceKey))
|
|
21
|
+
let testConnection = NativeView.button("Test Connection", target: self, action: #selector(testConnection))
|
|
22
|
+
let useOAuth = NativeView.button("Use ChatGPT OAuth", target: self, action: #selector(useOAuth))
|
|
23
|
+
let useLb = NativeView.button("Use codex-lb", target: self, action: #selector(useCodexLb))
|
|
24
|
+
let fastOn = NativeView.button("Fast Mode On", target: self, action: #selector(fastOn))
|
|
25
|
+
let fastOff = NativeView.button("Fast Mode Off", target: self, action: #selector(fastOff))
|
|
26
|
+
actionButtons = [setDomain, replace, testConnection, useOAuth, useLb, fastOn, fastOff]
|
|
27
|
+
let credentials = NSStackView(views: [setDomain, replace, testConnection])
|
|
15
28
|
credentials.orientation = .horizontal; credentials.spacing = 8
|
|
16
|
-
let buttons = NSStackView(views: [
|
|
17
|
-
NativeView.button("Use ChatGPT OAuth", target: self, action: #selector(useOAuth)),
|
|
18
|
-
NativeView.button("Use codex-lb", target: self, action: #selector(useCodexLb)),
|
|
19
|
-
NativeView.button("Fast Mode On", target: self, action: #selector(fastOn)),
|
|
20
|
-
NativeView.button("Fast Mode Off", target: self, action: #selector(fastOff))
|
|
21
|
-
])
|
|
29
|
+
let buttons = NSStackView(views: [useOAuth, useLb, fastOn, fastOff])
|
|
22
30
|
buttons.orientation = .horizontal; buttons.spacing = 8
|
|
23
31
|
view = NativeView.stack([
|
|
24
32
|
NativeView.title("Providers"),
|
|
25
|
-
NativeView.detail("
|
|
33
|
+
NativeView.detail("Set Domain and Key only stores credentials. Domain may be a hostname or full https URL — SKS adds https:// and /backend-api/codex when needed. Use codex-lb activates them with a shared OpenAI routing guard so sk-clb keys never hit api.openai.com. Keys are typed/pasted in clear text here, then sent through stdin and kept out of logs."),
|
|
26
34
|
credentials, providerStatus, fastStatus, buttons
|
|
27
35
|
])
|
|
28
36
|
refresh()
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
func refreshOnAppear() { refresh() }
|
|
40
|
+
|
|
41
|
+
private func setBusy(_ value: Bool) {
|
|
42
|
+
busy = value
|
|
43
|
+
for button in actionButtons { button.isEnabled = !value }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private func run(_ args: [String], title: String, kind: String, group: String?, timeout: TimeInterval = NativeView.mutationTimeout, completion: (() -> Void)? = nil) {
|
|
47
|
+
guard !busy else {
|
|
48
|
+
providerStatus.stringValue = "Another provider action is already running."
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
guard let snapshot = operations.begin(kind: kind, mutationGroup: group, summary: title) else {
|
|
52
|
+
providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
setBusy(true)
|
|
32
56
|
providerStatus.stringValue = "\(title)…"
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
_ = operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
|
|
58
|
+
processClient.run(args, timeout: timeout) { [weak self] result in
|
|
59
|
+
guard let self = self else { return }
|
|
60
|
+
self.setBusy(false)
|
|
61
|
+
_ = self.operations.update(
|
|
62
|
+
snapshot,
|
|
63
|
+
state: result.code == 0 ? .succeeded : .failed,
|
|
64
|
+
stage: "complete",
|
|
65
|
+
progress: 1,
|
|
66
|
+
summary: result.code == 0 ? "\(title) completed" : "\(title) failed"
|
|
67
|
+
)
|
|
68
|
+
if result.code != 0 {
|
|
69
|
+
self.providerStatus.stringValue = "\(title) failed · \(NativeView.redactPreview(result.output))"
|
|
70
|
+
}
|
|
71
|
+
self.refresh()
|
|
35
72
|
completion?()
|
|
36
73
|
}
|
|
37
74
|
}
|
|
38
75
|
|
|
39
76
|
private func refresh() {
|
|
40
|
-
processClient.run(["codex-lb", "status", "--json"]) { [weak self] result in
|
|
41
|
-
self
|
|
77
|
+
processClient.run(["codex-lb", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
78
|
+
guard let self = self else { return }
|
|
79
|
+
guard result.code == 0, let json = self.json(result.output) else {
|
|
80
|
+
if !self.busy { self.providerStatus.stringValue = "Provider status unavailable. Retry or open Diagnostics." }
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if !self.busy { self.providerStatus.stringValue = self.describeProviderStatus(json) }
|
|
42
84
|
}
|
|
43
85
|
refreshFastStatus()
|
|
44
86
|
}
|
|
45
87
|
|
|
88
|
+
private func describeProviderStatus(_ json: [String: Any]) -> String {
|
|
89
|
+
let configured = json["provider_configured"] as? Bool == true
|
|
90
|
+
let selected = json["selected"] as? Bool == true
|
|
91
|
+
let ok = json["ok"] as? Bool == true
|
|
92
|
+
let authMode = json["auth_mode"] as? String ?? "unknown"
|
|
93
|
+
let coherent = json["auth_routing_coherent"] as? Bool
|
|
94
|
+
let routing = json["shared_openai_routing"] as? [String: Any]
|
|
95
|
+
let routingSafe = routing?["safe"] as? Bool
|
|
96
|
+
let keyInShared = json["codex_lb_key_in_shared_auth"] as? Bool == true
|
|
97
|
+
|
|
98
|
+
if !configured {
|
|
99
|
+
return "Codex LB: not configured. Use Set Domain and Key, then Use codex-lb."
|
|
100
|
+
}
|
|
101
|
+
if routingSafe == false || (keyInShared && coherent == false) {
|
|
102
|
+
return "Codex LB: routing unsafe — shared key can hit api.openai.com. Click Use codex-lb to repair."
|
|
103
|
+
}
|
|
104
|
+
if selected && ok {
|
|
105
|
+
return "Codex LB: active (auth=\(authMode), routing guarded)."
|
|
106
|
+
}
|
|
107
|
+
if selected && !ok {
|
|
108
|
+
return "Codex LB: selected but not ready. Click Use codex-lb or Test Connection."
|
|
109
|
+
}
|
|
110
|
+
if keyInShared {
|
|
111
|
+
return "Codex LB: credentials in shared auth but provider not selected. Click Use ChatGPT OAuth or Use codex-lb."
|
|
112
|
+
}
|
|
113
|
+
return "Codex LB: credentials stored, not selected (auth=\(authMode)). Click Use codex-lb to activate."
|
|
114
|
+
}
|
|
115
|
+
|
|
46
116
|
private func refreshFastStatus() {
|
|
47
|
-
fastStatus.stringValue = "Fast Mode: checking current desktop setting…"
|
|
48
|
-
processClient.run(["fast-mode", "status", "--json"]) { [weak self] result in
|
|
117
|
+
if !busy { fastStatus.stringValue = "Fast Mode: checking current desktop setting…" }
|
|
118
|
+
processClient.run(["fast-mode", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
49
119
|
guard let self = self else { return }
|
|
50
120
|
guard result.code == 0, let json = self.json(result.output),
|
|
51
121
|
let global = json["global"] as? [String: Any], let on = global["on"] as? Bool else {
|
|
@@ -64,29 +134,97 @@ final class ProvidersViewController: NSViewController {
|
|
|
64
134
|
|
|
65
135
|
@objc private func setDomainAndKey() {
|
|
66
136
|
guard let window = view.window else { return }
|
|
67
|
-
AlertFactory.textSheet(
|
|
137
|
+
AlertFactory.textSheet(
|
|
138
|
+
window: window,
|
|
139
|
+
title: "Codex LB Domain",
|
|
140
|
+
message: "Enter a hostname or full base URL. https:// is optional — SKS adds https:// and /backend-api/codex when missing.\nExamples: lb.example.com or https://lb.example.com",
|
|
141
|
+
secure: false,
|
|
142
|
+
placeholder: "https://lb.example.com"
|
|
143
|
+
) { [weak self] host in
|
|
68
144
|
guard let self = self, let host = host else { return }
|
|
69
|
-
self.promptForKey(window: window, args: ["codex-lb", "setup", "--host", host, "--api-key-stdin", "--yes", "--no-default-provider", "--preserve-auth", "--no-keychain", "--no-restart-app", "--json"])
|
|
145
|
+
self.promptForKey(window: window, args: ["codex-lb", "setup", "--host", host, "--api-key-stdin", "--yes", "--no-default-provider", "--preserve-auth", "--no-keychain", "--no-restart-app", "--json"], kind: "codex-lb-setup", title: "Save Codex LB credentials")
|
|
70
146
|
}
|
|
71
147
|
}
|
|
72
148
|
|
|
73
149
|
@objc private func replaceKey() {
|
|
74
150
|
guard let window = view.window else { return }
|
|
75
|
-
promptForKey(window: window, args: ["codex-lb", "set-key", "--api-key-stdin", "--preserve-auth", "--restart-app", "--json"])
|
|
151
|
+
promptForKey(window: window, args: ["codex-lb", "set-key", "--api-key-stdin", "--preserve-auth", "--restart-app", "--json"], kind: "codex-lb-set-key", title: "Replace Codex LB API key")
|
|
76
152
|
}
|
|
77
153
|
|
|
78
|
-
private func promptForKey(window: NSWindow, args: [String]) {
|
|
79
|
-
AlertFactory.textSheet(
|
|
154
|
+
private func promptForKey(window: NSWindow, args: [String], kind: String, title: String) {
|
|
155
|
+
AlertFactory.textSheet(
|
|
156
|
+
window: window,
|
|
157
|
+
title: "Codex LB API Key",
|
|
158
|
+
message: "Paste your Codex LB API key (usually starts with sk-clb-). It is shown here so you can verify the paste, then sent through stdin and never logged.",
|
|
159
|
+
secure: false,
|
|
160
|
+
placeholder: "sk-clb-…"
|
|
161
|
+
) { [weak self] key in
|
|
80
162
|
guard let self = self, let key = key else { return }
|
|
163
|
+
guard !self.busy else {
|
|
164
|
+
self.providerStatus.stringValue = "Another provider action is already running."
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
guard let snapshot = self.operations.begin(kind: kind, mutationGroup: "codex-config", summary: title) else {
|
|
168
|
+
self.providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
self.setBusy(true)
|
|
81
172
|
self.providerStatus.stringValue = "Saving Codex LB API key…"
|
|
82
|
-
self.
|
|
83
|
-
|
|
173
|
+
_ = self.operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
|
|
174
|
+
self.processClient.run(args, stdin: key + "\n", timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
175
|
+
guard let self = self else { return }
|
|
176
|
+
self.setBusy(false)
|
|
177
|
+
_ = self.operations.update(
|
|
178
|
+
snapshot,
|
|
179
|
+
state: result.code == 0 ? .succeeded : .failed,
|
|
180
|
+
stage: "complete",
|
|
181
|
+
progress: 1,
|
|
182
|
+
summary: result.code == 0 ? "Codex LB API key saved" : "Codex LB API key save failed"
|
|
183
|
+
)
|
|
184
|
+
if result.code != 0 {
|
|
185
|
+
self.providerStatus.stringValue = "Codex LB API key save failed · \(NativeView.redactPreview(result.output))"
|
|
186
|
+
}
|
|
187
|
+
self.refresh()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@objc private func testConnection() {
|
|
193
|
+
guard !busy else {
|
|
194
|
+
providerStatus.stringValue = "Another provider action is already running."
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
guard let snapshot = operations.begin(kind: "codex-lb-health", mutationGroup: nil, summary: "Test Codex LB connection") else {
|
|
198
|
+
providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
setBusy(true)
|
|
202
|
+
providerStatus.stringValue = "Testing Codex LB connection…"
|
|
203
|
+
_ = operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: "Test Codex LB connection")
|
|
204
|
+
processClient.run(["codex-lb", "health", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
205
|
+
guard let self = self else { return }
|
|
206
|
+
self.setBusy(false)
|
|
207
|
+
let json = self.json(result.output)
|
|
208
|
+
let ok = result.code == 0 && (json?["ok"] as? Bool == true)
|
|
209
|
+
let status = json?["status"] as? String ?? (result.code == 0 ? "ok" : "failed")
|
|
210
|
+
_ = self.operations.update(
|
|
211
|
+
snapshot,
|
|
212
|
+
state: ok ? .succeeded : .failed,
|
|
213
|
+
stage: "complete",
|
|
214
|
+
progress: 1,
|
|
215
|
+
summary: ok ? "Codex LB connection ok" : "Codex LB connection failed"
|
|
216
|
+
)
|
|
217
|
+
if ok {
|
|
218
|
+
self.providerStatus.stringValue = "Connection test passed (\(status))."
|
|
219
|
+
} else {
|
|
220
|
+
self.providerStatus.stringValue = "Connection test failed (\(status)) · \(NativeView.redactPreview(result.output))"
|
|
84
221
|
}
|
|
222
|
+
self.refresh()
|
|
85
223
|
}
|
|
86
224
|
}
|
|
87
225
|
|
|
88
|
-
@objc private func useOAuth() { run(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth") }
|
|
89
|
-
@objc private func useCodexLb() { run(["codex-lb", "use-codex-lb", "--restart-app", "--json"], title: "Use codex-lb") }
|
|
90
|
-
@objc private func fastOn() { run(["fast-mode", "on", "--json"], title: "Fast Mode On") { [weak self] in self?.refreshFastStatus() } }
|
|
91
|
-
@objc private func fastOff() { run(["fast-mode", "off", "--json"], title: "Fast Mode Off") { [weak self] in self?.refreshFastStatus() } }
|
|
226
|
+
@objc private func useOAuth() { run(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth", kind: "codex-lb-use-oauth", group: "codex-config") }
|
|
227
|
+
@objc private func useCodexLb() { run(["codex-lb", "use-codex-lb", "--restart-app", "--json"], title: "Use codex-lb", kind: "codex-lb-use-lb", group: "codex-config") }
|
|
228
|
+
@objc private func fastOn() { run(["fast-mode", "on", "--json"], title: "Fast Mode On", kind: "fast-mode-on", group: "codex-config", timeout: NativeView.statusTimeout) { [weak self] in self?.refreshFastStatus() } }
|
|
229
|
+
@objc private func fastOff() { run(["fast-mode", "off", "--json"], title: "Fast Mode Off", kind: "fast-mode-off", group: "codex-config", timeout: NativeView.statusTimeout) { [weak self] in self?.refreshFastStatus() } }
|
|
92
230
|
}
|
|
@@ -1,61 +1,96 @@
|
|
|
1
1
|
import Cocoa
|
|
2
2
|
|
|
3
|
-
final class RemoteTelegramViewController: NSViewController {
|
|
3
|
+
final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
|
|
4
4
|
private let processClient: ProcessClient
|
|
5
5
|
private let status = NativeView.detail("Remote readiness has not been checked.")
|
|
6
6
|
private var generation = 0
|
|
7
|
+
private var checkButton: NSButton!
|
|
8
|
+
|
|
7
9
|
init(processClient: ProcessClient) { self.processClient = processClient; super.init(nibName: nil, bundle: nil) }
|
|
8
10
|
required init?(coder: NSCoder) { nil }
|
|
9
11
|
|
|
10
12
|
override func loadView() {
|
|
11
|
-
|
|
13
|
+
checkButton = NativeView.button("Check Readiness", target: self, action: #selector(check))
|
|
12
14
|
view = NativeView.stack([
|
|
13
15
|
NativeView.title("Remote & Telegram"),
|
|
14
16
|
NativeView.detail("OpenAI Remote remains the high-fidelity coding surface. Telegram is limited to proof-aware fleet status and typed approvals; arbitrary remote shell is never enabled."),
|
|
15
|
-
status,
|
|
17
|
+
status, checkButton
|
|
16
18
|
])
|
|
17
19
|
check()
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
func refreshOnAppear() { check() }
|
|
23
|
+
|
|
20
24
|
@objc private func check() {
|
|
21
25
|
generation += 1
|
|
22
26
|
let requestGeneration = generation
|
|
27
|
+
checkButton?.isEnabled = false
|
|
23
28
|
status.stringValue = "Checking official Remote compatibility and Telegram Hub state…"
|
|
24
29
|
var remote: [String: Any]?
|
|
25
30
|
var telegram: [String: Any]?
|
|
31
|
+
var remoteFailed = false
|
|
32
|
+
var telegramFailed = false
|
|
26
33
|
let group = DispatchGroup()
|
|
27
34
|
group.enter()
|
|
28
|
-
processClient.run(["remote", "readiness", "--project-root", AppRuntime.projectRoot, "--json"]) { [weak self] result in
|
|
35
|
+
processClient.run(["remote", "readiness", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
29
36
|
remote = self?.json(result.output)
|
|
37
|
+
remoteFailed = result.code != 0 || remote == nil
|
|
30
38
|
group.leave()
|
|
31
39
|
}
|
|
32
40
|
group.enter()
|
|
33
|
-
processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"]) { [weak self] result in
|
|
34
|
-
telegram = self?.json(result.output)
|
|
41
|
+
processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
|
|
42
|
+
telegram = result.code == 0 ? self?.json(result.output) : nil
|
|
43
|
+
telegramFailed = result.code != 0 || telegram == nil
|
|
35
44
|
group.leave()
|
|
36
45
|
}
|
|
37
46
|
group.notify(queue: .main) { [weak self] in
|
|
38
47
|
guard let self = self, self.generation == requestGeneration else { return }
|
|
39
|
-
self.
|
|
48
|
+
self.checkButton?.isEnabled = true
|
|
49
|
+
self.status.stringValue = self.summary(remote: remote, telegram: telegram, remoteFailed: remoteFailed, telegramFailed: telegramFailed)
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
private func summary(remote: [String: Any]?, telegram: [String: Any]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"Remote
|
|
58
|
-
|
|
53
|
+
private func summary(remote: [String: Any]?, telegram: [String: Any]?, remoteFailed: Bool, telegramFailed: Bool) -> String {
|
|
54
|
+
if remoteFailed && telegramFailed {
|
|
55
|
+
return "Remote readiness and Telegram status are unavailable. Retry Check Readiness, or open Diagnostics for the redacted log."
|
|
56
|
+
}
|
|
57
|
+
var lines: [String] = []
|
|
58
|
+
if remoteFailed || remote == nil {
|
|
59
|
+
lines.append("Official Remote compatibility: unavailable — probe failed or returned no readable JSON.")
|
|
60
|
+
} else if let remote = remote {
|
|
61
|
+
let host = remote["host"] as? [String: Any]
|
|
62
|
+
let project = remote["project"] as? [String: Any]
|
|
63
|
+
let mcp = remote["mcp"] as? [String: Any]
|
|
64
|
+
let sks = remote["sks"] as? [String: Any]
|
|
65
|
+
let remoteBlockers = remote["blockers"] as? [String] ?? []
|
|
66
|
+
let remoteWarnings = remote["warnings"] as? [String] ?? []
|
|
67
|
+
lines.append("Official Remote compatibility: \(remote["ok"] as? Bool == true ? "Ready" : "Needs attention") · Codex app \(host?["codex_app_found"] as? Bool == true ? "found" : "missing") · CLI \(host?["codex_cli_found"] as? Bool == true ? "found" : "missing")")
|
|
68
|
+
lines.append("Project: Git \(project?["git_repo"] as? Bool == true ? "ready" : "missing") · worktree \(project?["worktree"] as? Bool == true ? "yes" : "no") · dirty \(project?["dirty"] as? Bool == true ? "yes" : "no") · allowlist \(project?["allowed"] as? Bool == true ? "ready" : "blocked")")
|
|
69
|
+
lines.append("MCP: \(mcp?["effective_count"] as? Int ?? 0) effective · \(mcp?["failed_count"] as? Int ?? 0) failed · SKS proof \(sks?["proof_surfaces_ready"] as? Bool == true ? "ready" : "missing")")
|
|
70
|
+
if !remoteBlockers.isEmpty {
|
|
71
|
+
lines.append("Remote blockers: \(remoteBlockers.prefix(3).joined(separator: "; "))\(remoteBlockers.count > 3 ? "…" : "")")
|
|
72
|
+
} else if !remoteWarnings.isEmpty {
|
|
73
|
+
lines.append("Remote warnings: \(remoteWarnings.prefix(3).joined(separator: "; "))\(remoteWarnings.count > 3 ? "…" : "")")
|
|
74
|
+
} else {
|
|
75
|
+
lines.append("Remote blockers 0 · warnings 0")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if telegramFailed || telegram == nil {
|
|
80
|
+
lines.append("Telegram Hub: unavailable — status probe failed.")
|
|
81
|
+
lines.append("Remote fleet: unavailable until Telegram status succeeds.")
|
|
82
|
+
} else if let telegram = telegram {
|
|
83
|
+
let owner = telegram["owner"] as? [String: Any]
|
|
84
|
+
let configured = telegram["configured"] as? Bool
|
|
85
|
+
let hubState = owner != nil ? "Running" : configured == true ? "Stopped" : configured == false ? "Not configured" : "Unknown"
|
|
86
|
+
let telegramIssues = (telegram["config_issues"] as? [String] ?? []) + (telegram["remote_config_issues"] as? [String] ?? [])
|
|
87
|
+
lines.append("Telegram Hub: \(hubState) · \(telegram["topic_count"] as? Int ?? 0) topics · \(telegram["machine_count"] as? Int ?? 0) machines · \(telegram["target_count"] as? Int ?? 0) targets")
|
|
88
|
+
if telegramIssues.isEmpty {
|
|
89
|
+
lines.append("Telegram issues 0")
|
|
90
|
+
} else {
|
|
91
|
+
lines.append("Telegram issues \(telegramIssues.count): \(telegramIssues.prefix(2).joined(separator: "; "))\(telegramIssues.count > 2 ? "…" : "")")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
59
94
|
return lines.joined(separator: "\n")
|
|
60
95
|
}
|
|
61
96
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Cocoa
|
|
2
2
|
|
|
3
|
-
final class SettingsViewController: NSViewController {
|
|
3
|
+
final class SettingsViewController: NSViewController, ControlCenterPage {
|
|
4
4
|
private let notifications: NotificationCoordinator
|
|
5
5
|
private let quitWithCodex = NSButton(checkboxWithTitle: "Quit SKS Menu when Codex quits (otherwise hide)", target: nil, action: nil)
|
|
6
6
|
private let status = NativeView.detail("Settings use the native app configuration file.")
|
|
@@ -10,26 +10,60 @@ final class SettingsViewController: NSViewController {
|
|
|
10
10
|
override func loadView() {
|
|
11
11
|
quitWithCodex.target = self; quitWithCodex.action = #selector(save)
|
|
12
12
|
quitWithCodex.state = readConfig()["quit_with_codex"] as? Bool == true ? .on : .off
|
|
13
|
+
quitWithCodex.setAccessibilityLabel("Quit SKS Menu when Codex quits")
|
|
13
14
|
let authorize = NativeView.button("Enable Notifications", target: self, action: #selector(enableNotifications))
|
|
14
15
|
view = NativeView.stack([
|
|
15
16
|
NativeView.title("Settings"),
|
|
16
|
-
NativeView.detail("
|
|
17
|
+
NativeView.detail("These options stay on this Mac. Notification permission and Codex lifecycle behavior never leave the machine."),
|
|
17
18
|
quitWithCodex, authorize, status
|
|
18
19
|
])
|
|
20
|
+
refreshOnAppear()
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
func refreshOnAppear() {
|
|
24
|
+
quitWithCodex.state = readConfig()["quit_with_codex"] as? Bool == true ? .on : .off
|
|
25
|
+
if notifications.authorizationDenied {
|
|
26
|
+
status.stringValue = "Notifications are denied in macOS Settings. Operation results still appear in Control Center."
|
|
27
|
+
} else if FileManager.default.fileExists(atPath: AppRuntime.configPath) {
|
|
28
|
+
status.stringValue = "Settings loaded from the native app configuration file."
|
|
29
|
+
} else {
|
|
30
|
+
status.stringValue = "No settings file yet. Changing an option creates one with owner-only permissions."
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@objc private func enableNotifications() {
|
|
35
|
+
notifications.requestAuthorizationFromSettings()
|
|
36
|
+
status.stringValue = "Notification authorization was requested from macOS."
|
|
37
|
+
}
|
|
22
38
|
|
|
23
39
|
@objc private func save() {
|
|
24
40
|
var config = readConfig()
|
|
25
41
|
config["schema"] = "sks.sks-menubar-config.v1"
|
|
26
42
|
config["codex_bundle_id"] = AppRuntime.codexBundleId as Any
|
|
27
43
|
config["quit_with_codex"] = quitWithCodex.state == .on
|
|
28
|
-
guard JSONSerialization.isValidJSONObject(config), let data = try? JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted]) else {
|
|
44
|
+
guard JSONSerialization.isValidJSONObject(config), let data = try? JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted]) else {
|
|
45
|
+
status.stringValue = "Settings could not be encoded."
|
|
46
|
+
return
|
|
47
|
+
}
|
|
29
48
|
let target = URL(fileURLWithPath: AppRuntime.configPath)
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
let directory = target.deletingLastPathComponent()
|
|
50
|
+
let temporary = directory.appendingPathComponent(".config.\(UUID().uuidString).tmp")
|
|
51
|
+
do {
|
|
52
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
53
|
+
try data.write(to: temporary, options: .atomic)
|
|
54
|
+
if FileManager.default.fileExists(atPath: target.path) {
|
|
55
|
+
_ = try FileManager.default.replaceItemAt(target, withItemAt: temporary)
|
|
56
|
+
} else {
|
|
57
|
+
try FileManager.default.moveItem(at: temporary, to: target)
|
|
58
|
+
}
|
|
59
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: target.path)
|
|
60
|
+
status.stringValue = quitWithCodex.state == .on
|
|
61
|
+
? "Saved. SKS Menu will quit when Codex quits."
|
|
62
|
+
: "Saved. SKS Menu will hide when Codex quits."
|
|
63
|
+
} catch {
|
|
64
|
+
try? FileManager.default.removeItem(at: temporary)
|
|
65
|
+
status.stringValue = "Settings could not be saved. Confirm \(directory.path) is writable."
|
|
66
|
+
}
|
|
33
67
|
}
|
|
34
68
|
|
|
35
69
|
private func readConfig() -> [String: Any] {
|
|
@@ -93,6 +93,7 @@ final class StatusItemController: NSObject, NSMenuDelegate {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
private func refreshLocalState() {
|
|
96
|
+
hydrateFromLatestOperation()
|
|
96
97
|
let update = readJson(path: FileManager.default.homeDirectoryForCurrentUser
|
|
97
98
|
.appendingPathComponent(".sneakoscope-global/cache/update-status.json").path)
|
|
98
99
|
let sksUpdate = ((update?["sks"] as? [String: Any])?["update_available"] as? Bool) == true
|
|
@@ -101,18 +102,45 @@ final class StatusItemController: NSObject, NSMenuDelegate {
|
|
|
101
102
|
let signatureBroken = menuBar?["signature_ok"] as? Bool == false
|
|
102
103
|
let resourcesBroken = menuBar?["resources_ok"] as? Bool == false
|
|
103
104
|
let integrityBroken = signatureBroken || resourcesBroken || !FileManager.default.fileExists(atPath: AppRuntime.buildStampPath)
|
|
105
|
+
let pendingCount = operations.latestSnapshot()?.state == .waitingForConfirmation ? 1 : 0
|
|
106
|
+
pendingLine.title = "Pending approvals (\(pendingCount))"
|
|
104
107
|
let icon: SKSStatusIcon
|
|
105
108
|
let summary: String
|
|
106
109
|
if integrityBroken { icon = .warning; summary = "Needs attention — Menu Bar integrity" }
|
|
107
110
|
else if operationFailed { icon = .warning; summary = "Needs attention — last operation failed" }
|
|
108
|
-
else if actionRequired || notificationAuthorizationDenied
|
|
111
|
+
else if actionRequired || notificationAuthorizationDenied || pendingCount > 0 {
|
|
112
|
+
icon = .attention
|
|
113
|
+
summary = notificationAuthorizationDenied ? "Notifications require attention" : pendingCount > 0 ? "Input or approval required" : "Input or approval required"
|
|
114
|
+
}
|
|
109
115
|
else if sksUpdate || codexUpdate { icon = .updateAvailable; summary = "Update available" }
|
|
110
116
|
else if operationRunning { icon = .working; summary = "Working" }
|
|
111
117
|
else { icon = .healthy; summary = "Healthy" }
|
|
112
|
-
apply(icon)
|
|
118
|
+
apply(icon, summary: summary)
|
|
113
119
|
statusLine.title = "Status: \(summary)"
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
private func hydrateFromLatestOperation() {
|
|
123
|
+
guard let latest = operations.latestSnapshot() else { return }
|
|
124
|
+
switch latest.state {
|
|
125
|
+
case .queued, .running:
|
|
126
|
+
operationRunning = true
|
|
127
|
+
operationFailed = false
|
|
128
|
+
actionRequired = false
|
|
129
|
+
case .waitingForConfirmation:
|
|
130
|
+
operationRunning = false
|
|
131
|
+
operationFailed = false
|
|
132
|
+
actionRequired = true
|
|
133
|
+
case .failed, .terminalUncertain:
|
|
134
|
+
operationRunning = false
|
|
135
|
+
operationFailed = true
|
|
136
|
+
actionRequired = false
|
|
137
|
+
case .succeeded, .cancelled:
|
|
138
|
+
operationRunning = false
|
|
139
|
+
operationFailed = false
|
|
140
|
+
actionRequired = false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
private func refreshExpiredUpdateStatusIfNeeded() {
|
|
117
145
|
let update = readJson(path: FileManager.default.homeDirectoryForCurrentUser
|
|
118
146
|
.appendingPathComponent(".sneakoscope-global/cache/update-status.json").path)
|
|
@@ -190,7 +218,7 @@ final class StatusItemController: NSObject, NSMenuDelegate {
|
|
|
190
218
|
return expiry <= now
|
|
191
219
|
}
|
|
192
220
|
|
|
193
|
-
private func apply(_ state: SKSStatusIcon) {
|
|
221
|
+
private func apply(_ state: SKSStatusIcon, summary: String) {
|
|
194
222
|
let pair: (String, String)
|
|
195
223
|
switch state {
|
|
196
224
|
case .healthy: pair = ("SKSStatusTemplate", "checkmark.circle")
|
|
@@ -201,6 +229,9 @@ final class StatusItemController: NSObject, NSMenuDelegate {
|
|
|
201
229
|
}
|
|
202
230
|
statusItem.button?.image = AppIdentity.statusImage(resource: pair.0, symbol: pair.1)
|
|
203
231
|
statusItem.button?.imagePosition = .imageOnly
|
|
232
|
+
statusItem.button?.setAccessibilityLabel("SKS status")
|
|
233
|
+
statusItem.button?.setAccessibilityValue(summary)
|
|
234
|
+
statusItem.button?.toolTip = "SKS Control Center — \(summary)"
|
|
204
235
|
}
|
|
205
236
|
|
|
206
237
|
private func run(_ arguments: [String], kind: String, mutationGroup: String?, summary: String, completion: (() -> Void)? = nil) {
|