sneakoscope 6.7.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +3 -3
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/install-helpers-codex-lb-config.js +71 -0
  5. package/dist/cli/install-helpers-codex-lb-selftest.js +11 -14
  6. package/dist/cli/install-helpers.js +305 -53
  7. package/dist/config/skills-manifest.json +52 -52
  8. package/dist/core/agents/agent-orchestrator.js +56 -5
  9. package/dist/core/agents/agent-roster.js +4 -2
  10. package/dist/core/codex-app/codex-app-fast-ui-repair.js +18 -1
  11. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +16 -0
  12. package/dist/core/codex-app.js +52 -6
  13. package/dist/core/hooks-runtime.js +77 -3
  14. package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
  15. package/dist/core/release/package-size-budget.js +1 -1
  16. package/dist/core/subagents/official-subagent-config.js +8 -5
  17. package/dist/core/subagents/official-subagent-preparation.js +24 -1
  18. package/dist/core/subagents/official-subagent-prompt.js +3 -2
  19. package/dist/core/subagents/wave-lifecycle.js +20 -0
  20. package/dist/core/subagents/wave-parent-guidance.js +42 -0
  21. package/dist/core/version.js +1 -1
  22. package/dist/native/sks-menubar/Sources/AppIdentity.swift +3 -1
  23. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +14 -8
  24. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +71 -7
  25. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +50 -9
  26. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +61 -8
  27. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +120 -29
  28. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +58 -23
  29. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +41 -7
  30. package/dist/native/sks-menubar/Sources/StatusItemController.swift +34 -3
  31. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +38 -8
  32. package/dist/scripts/docs-truthfulness-check.js +2 -2
  33. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +7 -3
  34. package/package.json +1 -1
@@ -65,7 +65,7 @@ private struct McpRow {
65
65
 
66
66
  private struct McpDraft { let scope: String; let payload: [String: Any] }
67
67
 
68
- final class MCPServersViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
68
+ final class MCPServersViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, ControlCenterPage {
69
69
  private let processClient: ProcessClient
70
70
  private let operations: OperationCoordinator
71
71
  private let notifications: NotificationCoordinator
@@ -197,11 +197,18 @@ final class MCPServersViewController: NSViewController, NSTableViewDataSource, N
197
197
  }
198
198
  }
199
199
 
200
+ func refreshOnAppear() { refresh() }
201
+
200
202
  @objc private func testConnection() {
201
- guard let selection = selected(), isWritable(selection.row) else { return }
203
+ guard let selection = selected(), selection.row.managedBy != "plugin" else { return }
202
204
  status.stringValue = "Testing \(selection.row.name) with bounded initialize and tools/list…"
203
- processClient.run(["mcp", "config", "test", selection.row.name, "--scope", selection.row.scope] + scopeContext(selection.row.scope, mutation: false) + ["--json"]) { [weak self] result in
204
- guard let self = self, let json = self.json(result.output) else { self?.status.stringValue = "Connection test failed without a readable receipt."; return }
205
+ processClient.run(["mcp", "config", "test", selection.row.name, "--scope", selection.row.scope] + scopeContext(selection.row.scope, mutation: false) + ["--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
206
+ guard let self = self, let json = self.json(result.output) else {
207
+ self?.status.stringValue = result.code == 0
208
+ ? "Connection test failed without a readable receipt."
209
+ : "Connection test failed · \(NativeView.redactPreview(result.output))"
210
+ return
211
+ }
205
212
  let state = json["status"] as? String ?? "unknown"
206
213
  let latency = json["latency_ms"] as? Int
207
214
  let tools = json["tool_count"] as? Int
@@ -363,6 +370,8 @@ private final class McpEditorSheet: NSObject {
363
370
 
364
371
  private func build() {
365
372
  panel.title = row == nil ? "Add MCP Server" : "Edit \(row!.name)"
373
+ panel.styleMask = [.titled, .resizable]
374
+ panel.minSize = NSSize(width: 560, height: 480)
366
375
  let grid = NSGridView(views: [
367
376
  pair("Scope", scope), pair("Transport", transport), pair("Name", name), pair("Executable", command), pair("Remote URL", url),
368
377
  pair("Arguments — one per line", area(args, "Command arguments")), pair("Working directory", cwd), pair("Environment names — no values", area(env, "Environment-variable names")),
@@ -399,20 +408,52 @@ private final class McpEditorSheet: NSObject {
399
408
  guard (payload["startup_timeout_sec"] as? Int).map({ 1...30 ~= $0 }) == true, (payload["tool_timeout_sec"] as? Int).map({ 1...3600 ~= $0 }) == true else { return nil }
400
409
  if selectedTransport == "stdio" {
401
410
  let executable = command.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if row == nil || row?.transport != selectedTransport { guard !executable.isEmpty else { return nil } }; if !executable.isEmpty { payload["command"] = executable }
402
- let argv = lines(args.string); if argv.contains(where: { $0.contains("=") || $0.lowercased().contains("token") || $0.lowercased().contains("secret") }) { return nil }; if !argv.isEmpty { payload["args"] = argv }
403
- let names = lines(env.string); guard names.allSatisfy({ $0.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil }) else { return nil }; if !names.isEmpty { payload["env_vars"] = names }
411
+ let argv = orderedLines(args.string); if argv.contains(where: { $0.contains("=") || $0.lowercased().contains("token") || $0.lowercased().contains("secret") }) { return nil }; if !argv.isEmpty { payload["args"] = argv }
412
+ let names = uniqueSortedLines(env.string); guard names.allSatisfy({ $0.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil }) else { return nil }; if !names.isEmpty { payload["env_vars"] = names }
404
413
  let directory = cwd.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if !directory.isEmpty { guard directory.hasPrefix("/") else { return nil }; payload["cwd"] = directory }
405
414
  if remote.state == .on { payload["experimental_environment"] = "remote" }
406
415
  } else {
407
416
  let endpoint = url.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if row == nil || row?.transport != selectedTransport { guard !endpoint.isEmpty else { return nil } }; if !endpoint.isEmpty { guard let parsed = URL(string: endpoint), ["http", "https"].contains(parsed.scheme?.lowercased() ?? ""), parsed.user == nil, parsed.password == nil else { return nil }; payload["url"] = endpoint }
408
417
  let envName = bearer.stringValue.trimmingCharacters(in: .whitespacesAndNewlines); if !envName.isEmpty { guard envName.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil else { return nil }; payload["bearer_token_env_var"] = envName }
409
418
  }
410
- let allow = lines(enabledTools.string); let deny = lines(disabledTools.string); guard Set(allow).isDisjoint(with: Set(deny)) else { return nil }; if !allow.isEmpty { payload["enabled_tools"] = allow }; if !deny.isEmpty { payload["disabled_tools"] = deny }
419
+ let allow = uniqueSortedLines(enabledTools.string); let deny = uniqueSortedLines(disabledTools.string); guard Set(allow).isDisjoint(with: Set(deny)) else { return nil }; if !allow.isEmpty { payload["enabled_tools"] = allow }; if !deny.isEmpty { payload["disabled_tools"] = deny }
411
420
  return McpDraft(scope: scope.titleOfSelectedItem?.lowercased() ?? "global", payload: payload)
412
421
  }
413
422
 
414
- private func validationMessage() -> String { "Check the server name, selected connection field, timeout ranges, environment names, tool lists, and secret-free arguments before review." }
423
+ private func validationMessage() -> String {
424
+ let server = name.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
425
+ if server.range(of: #"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"#, options: .regularExpression) == nil {
426
+ return "Fix the server name: use 1–64 letters, numbers, underscores, or hyphens."
427
+ }
428
+ if transport.indexOfSelectedItem == 0 {
429
+ let executable = command.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
430
+ if (row == nil || row?.transport != "stdio") && executable.isEmpty { return "Executable is required for a local command server." }
431
+ if orderedLines(args.string).contains(where: { $0.contains("=") || $0.lowercased().contains("token") || $0.lowercased().contains("secret") }) {
432
+ return "Arguments must stay secret-free. Remove token/secret values and inline assignments."
433
+ }
434
+ if !uniqueSortedLines(env.string).allSatisfy({ $0.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil }) {
435
+ return "Environment names must be bare variable names with no values."
436
+ }
437
+ } else {
438
+ let endpoint = url.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
439
+ if (row == nil || row?.transport != "streamable-http") && endpoint.isEmpty { return "Remote URL is required for a remote MCP server." }
440
+ }
441
+ let startupSec = Int(startup.stringValue) ?? 0
442
+ let toolSec = Int(tool.stringValue) ?? 0
443
+ if !(1...30 ~= startupSec) { return "Startup timeout must be between 1 and 30 seconds." }
444
+ if !(1...3600 ~= toolSec) { return "Tool timeout must be between 1 and 3600 seconds." }
445
+ let allow = uniqueSortedLines(enabledTools.string); let deny = uniqueSortedLines(disabledTools.string)
446
+ if !Set(allow).isDisjoint(with: Set(deny)) { return "Enabled and disabled tool lists must not overlap." }
447
+ return "Check the selected connection field, timeout ranges, environment names, tool lists, and secret-free arguments before review."
448
+ }
415
449
  private func pair(_ label: String, _ control: NSView) -> [NSView] { [NSTextField(labelWithString: label), control] }
416
450
  private func area(_ text: NSTextView, _ label: String) -> NSScrollView { text.isRichText = false; text.font = NSFont.systemFont(ofSize: 12); text.setAccessibilityLabel(label); let scroll = NSScrollView(); scroll.documentView = text; scroll.hasVerticalScroller = true; scroll.borderType = .bezelBorder; scroll.heightAnchor.constraint(equalToConstant: 52).isActive = true; return scroll }
417
- private func lines(_ value: String) -> [String] { Array(Set(value.split(whereSeparator: \.isNewline).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty })).sorted() }
451
+ private func uniqueSortedLines(_ value: String) -> [String] { Array(Set(rawLines(value))).sorted() }
452
+ private func orderedLines(_ value: String) -> [String] {
453
+ var seen = Set<String>()
454
+ return rawLines(value).filter { seen.insert($0).inserted }
455
+ }
456
+ private func rawLines(_ value: String) -> [String] {
457
+ value.split(whereSeparator: \.isNewline).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
458
+ }
418
459
  }
@@ -1,6 +1,14 @@
1
1
  import Cocoa
2
2
 
3
+ /// Pages that should reload local status whenever the Control Center section becomes visible.
4
+ protocol ControlCenterPage: AnyObject {
5
+ func refreshOnAppear()
6
+ }
7
+
3
8
  enum NativeView {
9
+ static let statusTimeout: TimeInterval = 8
10
+ static let mutationTimeout: TimeInterval = 90
11
+
4
12
  static func title(_ value: String) -> NSTextField {
5
13
  let field = NSTextField(labelWithString: value)
6
14
  field.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
@@ -29,6 +37,35 @@ enum NativeView {
29
37
  stack.edgeInsets = NSEdgeInsets(top: 22, left: 24, bottom: 22, right: 24)
30
38
  return stack
31
39
  }
40
+
41
+ static func scrollable(_ document: NSView) -> NSScrollView {
42
+ let scroll = NSScrollView()
43
+ scroll.drawsBackground = false
44
+ scroll.hasVerticalScroller = true
45
+ scroll.hasHorizontalScroller = false
46
+ scroll.borderType = .noBorder
47
+ scroll.autohidesScrollers = true
48
+ scroll.translatesAutoresizingMaskIntoConstraints = false
49
+ document.translatesAutoresizingMaskIntoConstraints = false
50
+ scroll.documentView = document
51
+ if let content = scroll.contentView.documentView {
52
+ NSLayoutConstraint.activate([
53
+ content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
54
+ content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
55
+ content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor)
56
+ ])
57
+ }
58
+ return scroll
59
+ }
60
+
61
+ static func redactPreview(_ output: String, limit: Int = 160) -> String {
62
+ let compact = output
63
+ .replacingOccurrences(of: "\n", with: " ")
64
+ .trimmingCharacters(in: .whitespacesAndNewlines)
65
+ guard !compact.isEmpty else { return "No public detail was returned." }
66
+ if compact.count <= limit { return compact }
67
+ return String(compact.prefix(limit)) + "…"
68
+ }
32
69
  }
33
70
 
34
71
  enum OverviewSummary {
@@ -182,12 +219,14 @@ enum OverviewSummary {
182
219
  }
183
220
  }
184
221
 
185
- final class OverviewViewController: NSViewController {
222
+ final class OverviewViewController: NSViewController, ControlCenterPage {
186
223
  private let processClient: ProcessClient
187
224
  private let operations: OperationCoordinator
188
225
  private let status = NativeView.detail("Loading local SKS status…")
189
226
  private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
190
227
  private var generation = 0
228
+ private var doctorButton: NSButton!
229
+ private var refreshButton: NSButton!
191
230
 
192
231
  init(processClient: ProcessClient, operations: OperationCoordinator) {
193
232
  self.processClient = processClient
@@ -197,9 +236,9 @@ final class OverviewViewController: NSViewController {
197
236
  required init?(coder: NSCoder) { nil }
198
237
 
199
238
  override func loadView() {
200
- let runDoctor = NativeView.button("Run Doctor", target: self, action: #selector(doctor))
201
- let refresh = NativeView.button("Refresh", target: self, action: #selector(refreshStatus))
202
- let buttons = NSStackView(views: [runDoctor, refresh])
239
+ doctorButton = NativeView.button("Run Doctor", target: self, action: #selector(doctor))
240
+ refreshButton = NativeView.button("Refresh", target: self, action: #selector(refreshStatus))
241
+ let buttons = NSStackView(views: [doctorButton, refreshButton])
203
242
  buttons.orientation = .horizontal
204
243
  buttons.spacing = 8
205
244
  view = NativeView.stack([
@@ -212,6 +251,10 @@ final class OverviewViewController: NSViewController {
212
251
  loadStatus(forceUpdateRefresh: false)
213
252
  }
214
253
 
254
+ func refreshOnAppear() {
255
+ loadStatus(forceUpdateRefresh: false)
256
+ }
257
+
215
258
  func setNotificationAuthorizationDenied(_ denied: Bool) {
216
259
  notificationInbox.stringValue = denied
217
260
  ? "Notifications: permission denied — operation results remain available in this Control Center inbox."
@@ -225,6 +268,7 @@ final class OverviewViewController: NSViewController {
225
268
  private func loadStatus(forceUpdateRefresh: Bool) {
226
269
  generation += 1
227
270
  let requestGeneration = generation
271
+ refreshButton?.isEnabled = false
228
272
  status.stringValue = "Checking versions, MCP servers, Telegram Hub, remote fleet, and operations…"
229
273
  var update: [String: Any]?
230
274
  var mcp: [String: Any]?
@@ -234,7 +278,7 @@ final class OverviewViewController: NSViewController {
234
278
  var updateArguments = ["update", "status"]
235
279
  if forceUpdateRefresh { updateArguments.append("--refresh") }
236
280
  updateArguments.append("--json")
237
- processClient.run(updateArguments) { [weak self] result in
281
+ processClient.run(updateArguments, timeout: NativeView.statusTimeout) { [weak self] result in
238
282
  update = result.code == 0 ? self?.json(result.output) : nil
239
283
  group.leave()
240
284
  }
@@ -247,24 +291,33 @@ final class OverviewViewController: NSViewController {
247
291
  group.leave()
248
292
  }
249
293
  group.enter()
250
- processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"]) { [weak self] result in
294
+ processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
251
295
  telegram = result.code == 0 ? self?.json(result.output) : nil
252
296
  group.leave()
253
297
  }
254
298
  group.notify(queue: .main) { [weak self] in
255
299
  guard let self = self, self.generation == requestGeneration else { return }
300
+ self.refreshButton?.isEnabled = true
256
301
  self.status.stringValue = self.summary(update: update, mcp: mcp, telegram: telegram)
257
302
  }
258
303
  DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
259
304
  guard let self = self, self.generation == requestGeneration else { return }
305
+ self.refreshButton?.isEnabled = true
260
306
  self.status.stringValue = self.summary(update: update, mcp: mcp, telegram: telegram)
261
307
  }
262
308
  }
263
309
 
264
310
  @objc private func doctor() {
311
+ doctorButton?.isEnabled = false
265
312
  status.stringValue = "Doctor is running…"
266
- processClient.run(["doctor", "--json"]) { [weak self] result in
267
- self?.status.stringValue = result.code == 0 ? "Doctor completed. No blocking issue was reported." : "Doctor found an issue. Open Diagnostics or the operation log."
313
+ processClient.run(["doctor", "--json"], timeout: NativeView.mutationTimeout) { [weak self] result in
314
+ guard let self = self else { return }
315
+ self.doctorButton?.isEnabled = true
316
+ if result.code == 0 {
317
+ self.status.stringValue = "Doctor completed. No blocking issue was reported."
318
+ } else {
319
+ self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
320
+ }
268
321
  }
269
322
  }
270
323
 
@@ -1,51 +1,120 @@
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
- init(processClient: ProcessClient) { self.processClient = processClient; super.init(nibName: nil, bundle: nil) }
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 credentials = NSStackView(views: [
12
- NativeView.button("Set Domain and Key…", target: self, action: #selector(setDomainAndKey)),
13
- NativeView.button("Replace Key…", target: self, action: #selector(replaceKey))
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 useOAuth = NativeView.button("Use ChatGPT OAuth", target: self, action: #selector(useOAuth))
22
+ let useLb = NativeView.button("Use codex-lb", target: self, action: #selector(useCodexLb))
23
+ let fastOn = NativeView.button("Fast Mode On", target: self, action: #selector(fastOn))
24
+ let fastOff = NativeView.button("Fast Mode Off", target: self, action: #selector(fastOff))
25
+ actionButtons = [setDomain, replace, useOAuth, useLb, fastOn, fastOff]
26
+ let credentials = NSStackView(views: [setDomain, replace])
15
27
  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
- ])
28
+ let buttons = NSStackView(views: [useOAuth, useLb, fastOn, fastOff])
22
29
  buttons.orientation = .horizontal; buttons.spacing = 8
23
30
  view = NativeView.stack([
24
31
  NativeView.title("Providers"),
25
- NativeView.detail("Provider changes use typed SKS commands and verified Codex restarts. Credentials stay out of logs and notifications."),
32
+ NativeView.detail("Set Domain and Key only stores credentials. Use codex-lb activates them with a shared OpenAI routing guard so sk-clb keys never hit api.openai.com. Secrets stay on stdin and out of logs."),
26
33
  credentials, providerStatus, fastStatus, buttons
27
34
  ])
28
35
  refresh()
29
36
  }
30
37
 
31
- private func run(_ args: [String], title: String, completion: (() -> Void)? = nil) {
38
+ func refreshOnAppear() { refresh() }
39
+
40
+ private func setBusy(_ value: Bool) {
41
+ busy = value
42
+ for button in actionButtons { button.isEnabled = !value }
43
+ }
44
+
45
+ private func run(_ args: [String], title: String, kind: String, group: String?, timeout: TimeInterval = NativeView.mutationTimeout, completion: (() -> Void)? = nil) {
46
+ guard !busy else {
47
+ providerStatus.stringValue = "Another provider action is already running."
48
+ return
49
+ }
50
+ guard let snapshot = operations.begin(kind: kind, mutationGroup: group, summary: title) else {
51
+ providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
52
+ return
53
+ }
54
+ setBusy(true)
32
55
  providerStatus.stringValue = "\(title)…"
33
- processClient.run(args) { [weak self] result in
34
- self?.providerStatus.stringValue = result.code == 0 ? "\(title) complete." : "\(title) failed. See Diagnostics."
56
+ _ = operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
57
+ processClient.run(args, timeout: timeout) { [weak self] result in
58
+ guard let self = self else { return }
59
+ self.setBusy(false)
60
+ _ = self.operations.update(
61
+ snapshot,
62
+ state: result.code == 0 ? .succeeded : .failed,
63
+ stage: "complete",
64
+ progress: 1,
65
+ summary: result.code == 0 ? "\(title) completed" : "\(title) failed"
66
+ )
67
+ if result.code != 0 {
68
+ self.providerStatus.stringValue = "\(title) failed · \(NativeView.redactPreview(result.output))"
69
+ }
70
+ self.refresh()
35
71
  completion?()
36
72
  }
37
73
  }
38
74
 
39
75
  private func refresh() {
40
- processClient.run(["codex-lb", "status", "--json"]) { [weak self] result in
41
- self?.providerStatus.stringValue = result.code == 0 ? "Provider status loaded." : "Provider status unavailable."
76
+ processClient.run(["codex-lb", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
77
+ guard let self = self else { return }
78
+ guard result.code == 0, let json = self.json(result.output) else {
79
+ if !self.busy { self.providerStatus.stringValue = "Provider status unavailable. Retry or open Diagnostics." }
80
+ return
81
+ }
82
+ if !self.busy { self.providerStatus.stringValue = self.describeProviderStatus(json) }
42
83
  }
43
84
  refreshFastStatus()
44
85
  }
45
86
 
87
+ private func describeProviderStatus(_ json: [String: Any]) -> String {
88
+ let configured = json["provider_configured"] as? Bool == true
89
+ let selected = json["selected"] as? Bool == true
90
+ let ok = json["ok"] as? Bool == true
91
+ let authMode = json["auth_mode"] as? String ?? "unknown"
92
+ let coherent = json["auth_routing_coherent"] as? Bool
93
+ let routing = json["shared_openai_routing"] as? [String: Any]
94
+ let routingSafe = routing?["safe"] as? Bool
95
+ let keyInShared = json["codex_lb_key_in_shared_auth"] as? Bool == true
96
+
97
+ if !configured {
98
+ return "Codex LB: not configured. Use Set Domain and Key, then Use codex-lb."
99
+ }
100
+ if routingSafe == false || (keyInShared && coherent == false) {
101
+ return "Codex LB: routing unsafe — shared key can hit api.openai.com. Click Use codex-lb to repair."
102
+ }
103
+ if selected && ok {
104
+ return "Codex LB: active (auth=\(authMode), routing guarded)."
105
+ }
106
+ if selected && !ok {
107
+ return "Codex LB: selected but not ready. Click Use codex-lb or check Diagnostics."
108
+ }
109
+ if keyInShared {
110
+ return "Codex LB: credentials in shared auth but provider not selected. Click Use ChatGPT OAuth or Use codex-lb."
111
+ }
112
+ return "Codex LB: credentials stored, not selected (auth=\(authMode)). Click Use codex-lb to activate."
113
+ }
114
+
46
115
  private func refreshFastStatus() {
47
- fastStatus.stringValue = "Fast Mode: checking current desktop setting…"
48
- processClient.run(["fast-mode", "status", "--json"]) { [weak self] result in
116
+ if !busy { fastStatus.stringValue = "Fast Mode: checking current desktop setting…" }
117
+ processClient.run(["fast-mode", "status", "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
49
118
  guard let self = self else { return }
50
119
  guard result.code == 0, let json = self.json(result.output),
51
120
  let global = json["global"] as? [String: Any], let on = global["on"] as? Bool else {
@@ -66,27 +135,49 @@ final class ProvidersViewController: NSViewController {
66
135
  guard let window = view.window else { return }
67
136
  AlertFactory.textSheet(window: window, title: "Codex LB Domain", message: "Enter a codex-lb domain or base URL.") { [weak self] host in
68
137
  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"])
138
+ 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
139
  }
71
140
  }
72
141
 
73
142
  @objc private func replaceKey() {
74
143
  guard let window = view.window else { return }
75
- promptForKey(window: window, args: ["codex-lb", "set-key", "--api-key-stdin", "--preserve-auth", "--restart-app", "--json"])
144
+ 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
145
  }
77
146
 
78
- private func promptForKey(window: NSWindow, args: [String]) {
147
+ private func promptForKey(window: NSWindow, args: [String], kind: String, title: String) {
79
148
  AlertFactory.textSheet(window: window, title: "Codex LB API Key", message: "Paste a new key. It is sent through stdin and never shown again.", secure: true) { [weak self] key in
80
149
  guard let self = self, let key = key else { return }
150
+ guard !self.busy else {
151
+ self.providerStatus.stringValue = "Another provider action is already running."
152
+ return
153
+ }
154
+ guard let snapshot = self.operations.begin(kind: kind, mutationGroup: "codex-config", summary: title) else {
155
+ self.providerStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
156
+ return
157
+ }
158
+ self.setBusy(true)
81
159
  self.providerStatus.stringValue = "Saving Codex LB API key…"
82
- self.processClient.run(args, stdin: key + "\n") { [weak self] result in
83
- self?.providerStatus.stringValue = result.code == 0 ? "Codex LB API key saved." : "Codex LB API key save failed. See Diagnostics."
160
+ _ = self.operations.update(snapshot, state: .running, stage: "running", progress: nil, summary: title)
161
+ self.processClient.run(args, stdin: key + "\n", timeout: NativeView.mutationTimeout) { [weak self] result in
162
+ guard let self = self else { return }
163
+ self.setBusy(false)
164
+ _ = self.operations.update(
165
+ snapshot,
166
+ state: result.code == 0 ? .succeeded : .failed,
167
+ stage: "complete",
168
+ progress: 1,
169
+ summary: result.code == 0 ? "Codex LB API key saved" : "Codex LB API key save failed"
170
+ )
171
+ if result.code != 0 {
172
+ self.providerStatus.stringValue = "Codex LB API key save failed · \(NativeView.redactPreview(result.output))"
173
+ }
174
+ self.refresh()
84
175
  }
85
176
  }
86
177
  }
87
178
 
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() } }
179
+ @objc private func useOAuth() { run(["codex-lb", "use-oauth", "--restart-app", "--json"], title: "Use ChatGPT OAuth", kind: "codex-lb-use-oauth", group: "codex-config") }
180
+ @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") }
181
+ @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() } }
182
+ @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
183
  }
@@ -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
- let refresh = NativeView.button("Check Readiness", target: self, action: #selector(check))
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, refresh
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.status.stringValue = self.summary(remote: remote, telegram: telegram)
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]?) -> String {
44
- let host = remote?["host"] as? [String: Any]
45
- let project = remote?["project"] as? [String: Any]
46
- let mcp = remote?["mcp"] as? [String: Any]
47
- let sks = remote?["sks"] as? [String: Any]
48
- let remoteBlockers = remote?["blockers"] as? [String] ?? []
49
- let remoteWarnings = remote?["warnings"] as? [String] ?? []
50
- let owner = telegram?["owner"] as? [String: Any]
51
- let telegramIssues = (telegram?["config_issues"] as? [String] ?? []) + (telegram?["remote_config_issues"] as? [String] ?? [])
52
- let lines = [
53
- "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")",
54
- "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")",
55
- "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")",
56
- "Telegram Hub: \(owner == nil ? "Stopped" : "Running") · \(telegram?["topic_count"] as? Int ?? 0) topics · \(telegram?["machine_count"] as? Int ?? 0) machines · \(telegram?["target_count"] as? Int ?? 0) targets",
57
- "Remote blockers \(remoteBlockers.count) · warnings \(remoteWarnings.count) · Telegram issues \(telegramIssues.count)"
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