sneakoscope 7.3.0 → 7.6.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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -1,74 +1,276 @@
1
1
  import Cocoa
2
2
 
3
3
  final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
4
- private let processClient: ProcessClient
4
+ let processClient: ProcessClient
5
+ private let hubSupervisor: TelegramHubSupervisor
5
6
  private let operations: OperationCoordinator
6
- private let readinessStatus = NativeView.detail("Remote readiness has not been checked.")
7
- private let setupStatus = NativeView.detail("Telegram setup has not been checked.")
8
- private let hubStatus = NativeView.detail("Telegram Hub status has not been checked.")
9
- private var generation = 0
10
- private var busy = false
11
- private var setupReady = false
12
- private var hubRunning = false
13
- private var checkButton: NSButton!
14
- private var connectButton: NSButton!
7
+
8
+ let readinessStatus = NativeView.detail("Project readiness has not been checked.")
9
+ let setupBadge = ControlKit.badge("Telegram setup not checked", tone: .neutral)
10
+ let setupStatus = NativeView.detail("Create the bot, send /start in its private chat, then connect the token.")
11
+ let connectionBadge = ControlKit.badge("Telegram connection not checked", tone: .neutral)
12
+ let connectionStatus = NativeView.detail("Connection details are loading…")
13
+ let hubBadge = ControlKit.badge("Telegram Hub not checked", tone: .neutral)
14
+ let hubStatus = NativeView.detail("Complete setup to use the local Hub.")
15
+ let liveSourceBadge = ControlKit.badge("Codex thread not loaded", tone: .neutral)
16
+ let snapshotStatus = NativeView.detail("Conversation and public work activity have not been loaded.")
17
+ let sessionPicker = NSPopUpButton()
18
+ let conversationView = RemoteTelegramViewController.readOnlyTextView(
19
+ label: "Telegram Codex conversation history"
20
+ )
21
+ let activityView = RemoteTelegramViewController.readOnlyTextView(
22
+ label: "Telegram Codex live public activity"
23
+ )
24
+
25
+ var setupCard: NSView!
26
+ var connectedCard: NSView!
27
+ var workspaceCard: NSView!
28
+ var generation = 0
29
+ var snapshotGeneration = 0
30
+ var liveGeneration = 0
31
+ var busy = false
32
+ var statusInFlight = false
33
+ var snapshotInFlight = false
34
+ var liveInFlight = false
35
+ var setupReady = false
36
+ var hubRunning = false
37
+ var deadLetterIngressCount = 0
38
+ var selectedSessionId: String?
39
+ var registeredSessionCount = 0
40
+ var conversationCursor: String?
41
+ var conversationSortDirection = "desc"
42
+ var isViewingLatestConversationPage = true
43
+ var olderConversationCursor: String?
44
+ var olderConversationSortDirection = "desc"
45
+ var newerConversationCursor: String?
46
+ var newerConversationSortDirection = "asc"
47
+ var loadedSessionId: String?
48
+ var loadedConversationRevision: String?
49
+ var loadedLiveActivityDigest: String?
50
+ var liveTriggeredConversationRevision: String?
51
+ var loadedTranscriptRows: [[String: Any]] = []
52
+ var loadedHistoryActivityRows: [[String: Any]] = []
53
+ var loadedLiveActivityRows: [[String: Any]] = []
54
+ var loadedThreadId: String?
55
+ var loadedThreadStatus = "unknown"
56
+ var loadedConversationPageTurnCount = 0
57
+ var loadedSnapshotTruncated = false
58
+ var loadedLiveActivityState = "pending"
59
+ var loadedLiveActivityError: String?
60
+ var renderedTranscriptSignature: String?
61
+ var renderedActivitySignature: String?
62
+ var liveSourceFileSignature: String?
63
+ var liveCatchUpInProgress = false
64
+ var liveTimer: Timer?
65
+
66
+ var checkButton: NSButton!
67
+ private var snapshotRefreshButton: NSButton!
68
+ private var olderConversationButton: NSButton!
69
+ private var newerConversationButton: NSButton!
70
+ private var latestConversationButton: NSButton!
71
+ private var setupBotFatherButton: NSButton!
72
+ private var setupConnectButton: NSButton!
73
+ private var connectedBotFatherButton: NSButton!
74
+ private var reconnectButton: NSButton!
15
75
  private var startButton: NSButton!
16
76
  private var stopButton: NSButton!
17
77
  private var restartButton: NSButton!
78
+ private var acknowledgeDeadLettersButton: NSButton!
18
79
 
19
- init(processClient: ProcessClient, operations: OperationCoordinator) {
80
+ init(
81
+ processClient: ProcessClient,
82
+ hubSupervisor: TelegramHubSupervisor,
83
+ operations: OperationCoordinator
84
+ ) {
20
85
  self.processClient = processClient
86
+ self.hubSupervisor = hubSupervisor
21
87
  self.operations = operations
22
88
  super.init(nibName: nil, bundle: nil)
23
89
  }
24
90
  required init?(coder: NSCoder) { nil }
25
91
 
92
+ deinit {
93
+ liveTimer?.invalidate()
94
+ }
95
+
26
96
  override func loadView() {
27
- connectButton = NativeView.button("Connect Bot & Register Coding Session…", target: self, action: #selector(connectBot))
97
+ for label in [setupStatus, connectionStatus, hubStatus, snapshotStatus, readinessStatus] {
98
+ label.textColor = .labelColor
99
+ label.alignment = .left
100
+ }
101
+
102
+ checkButton = NativeView.button("Refresh All", target: self, action: #selector(check))
103
+ snapshotRefreshButton = NativeView.button("Refresh Conversation", target: self, action: #selector(refreshSnapshotManually))
104
+ olderConversationButton = NativeView.button("Older", target: self, action: #selector(showOlderConversationPage))
105
+ newerConversationButton = NativeView.button("Newer", target: self, action: #selector(showNewerConversationPage))
106
+ latestConversationButton = NativeView.button("Latest", target: self, action: #selector(showLatestConversationPage))
107
+ setupBotFatherButton = ControlKit.primaryButton("Open @BotFather", target: self, action: #selector(openBotFather))
108
+ setupConnectButton = NativeView.button("Connect This Project…", target: self, action: #selector(connectBot))
109
+ connectedBotFatherButton = NativeView.button("Open @BotFather", target: self, action: #selector(openBotFather))
110
+ reconnectButton = NativeView.button("Reconnect / Rotate Token…", target: self, action: #selector(connectBot))
28
111
  startButton = NativeView.button("Start Hub", target: self, action: #selector(startHub))
29
112
  stopButton = NativeView.button("Stop Hub", target: self, action: #selector(stopHub))
30
113
  restartButton = NativeView.button("Restart Hub", target: self, action: #selector(restartHub))
31
- checkButton = NativeView.button("Refresh Status", target: self, action: #selector(check))
114
+ acknowledgeDeadLettersButton = NativeView.button(
115
+ "Acknowledge Failed Ingress…",
116
+ target: self,
117
+ action: #selector(acknowledgeDeadLetters)
118
+ )
32
119
 
33
- let setupCard = NativeView.card(
34
- title: "1. Connect your private Telegram bot",
35
- subtitle: "Create a bot with BotFather, open that bot in Telegram, send /start, then paste the bot token here. SKS verifies the bot, detects only that private chat/user, saves the token in macOS Keychain, and registers one dedicated coding session for this project.",
36
- views: [setupStatus, NativeView.row([connectButton])]
120
+ setupBotFatherButton.setAccessibilityHelp("Open Telegram BotFather to create or manage the private bot.")
121
+ setupConnectButton.setAccessibilityHelp("Paste the BotFather token securely and register this project.")
122
+ reconnectButton.setAccessibilityHelp("Stop the Hub first, then reconnect the bot or rotate its token.")
123
+ acknowledgeDeadLettersButton.setAccessibilityHelp(
124
+ "After the Hub is stopped, retire failed ingress metadata so the request can be resent from Telegram."
37
125
  )
38
- let hubCard = NativeView.card(
39
- title: "2. Keep remote coding available",
40
- subtitle: "The LaunchAgent keeps the Hub running and prevents idle system sleep while this Mac user is logged in. Closing the lid or logging out can still stop access. It uses a local in-process worker on this Mac; SSH remains available only for explicitly configured remote machines.",
41
- views: [hubStatus, NativeView.row([startButton, stopButton, restartButton])]
126
+ olderConversationButton.setAccessibilityHelp("Load the next older page from the complete stored Codex conversation.")
127
+ newerConversationButton.setAccessibilityHelp("Load the next newer page from the complete stored Codex conversation.")
128
+ latestConversationButton.setAccessibilityHelp("Return to the live latest page and resume automatic refresh.")
129
+ sessionPicker.target = self
130
+ sessionPicker.action = #selector(sessionChanged)
131
+ sessionPicker.setAccessibilityLabel("Registered Telegram coding session")
132
+ sessionPicker.widthAnchor.constraint(greaterThanOrEqualToConstant: 260).isActive = true
133
+
134
+ let setupSteps = NSStackView(views: [
135
+ guideStep("1", title: "Create the bot", detail: "Open @BotFather, send /newbot, and copy the HTTP API token."),
136
+ guideStep("2", title: "Pair your account", detail: "Open the new bot in a private chat and send /start."),
137
+ guideStep("3", title: "Connect this project", detail: "Paste the token here. SKS verifies one private chat/user and stores the token only in macOS Keychain.")
138
+ ])
139
+ setupSteps.orientation = .vertical; setupSteps.alignment = .leading; setupSteps.spacing = 9
140
+ for step in setupSteps.arrangedSubviews { step.widthAnchor.constraint(equalTo: setupSteps.widthAnchor).isActive = true }
141
+ setupCard = NativeView.card(
142
+ title: "Connect Telegram in 3 steps",
143
+ subtitle: "This creates one private, project-scoped Telegram coding session. SKS never puts the bot token in arguments, project config, or logs.",
144
+ views: [
145
+ setupBadge,
146
+ setupStatus,
147
+ setupSteps,
148
+ ControlKit.actionRow([setupBotFatherButton, setupConnectButton])
149
+ ],
150
+ fullWidthLeadingContent: true
151
+ )
152
+
153
+ let sessionLabel = NSTextField(labelWithString: "Coding session")
154
+ sessionLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
155
+ sessionLabel.textColor = .secondaryLabelColor
156
+ sessionLabel.setContentHuggingPriority(.required, for: .horizontal)
157
+ let sessionRow = ControlKit.actionRow([sessionLabel, sessionPicker])
158
+ connectedCard = NativeView.card(
159
+ title: "Telegram connected",
160
+ subtitle: "Plain text from the paired private chat continues this project's dedicated Codex thread.",
161
+ views: [
162
+ connectionBadge,
163
+ connectionStatus,
164
+ sessionRow,
165
+ hubBadge,
166
+ hubStatus,
167
+ ControlKit.actionRow([startButton, stopButton, restartButton]),
168
+ ControlKit.actionRow([acknowledgeDeadLettersButton]),
169
+ ControlKit.actionRow([connectedBotFatherButton, reconnectButton])
170
+ ],
171
+ fullWidthLeadingContent: true
172
+ )
173
+
174
+ let conversationScroll = textArea(conversationView, height: 210)
175
+ let activityScroll = textArea(activityView, height: 150)
176
+ let conversationTitle = NativeView.sectionTitle("Conversation history")
177
+ let conversationPageRow = ControlKit.actionRow(
178
+ [conversationTitle],
179
+ trailing: [olderConversationButton, newerConversationButton, latestConversationButton]
180
+ )
181
+ let activityTitle = NativeView.sectionTitle("Live public activity")
182
+ workspaceCard = NativeView.card(
183
+ title: "Connected Codex thread",
184
+ subtitle: "Browse the real Telegram-bound Codex conversation and bounded public activity. Latest checks local journal files every 2.5 seconds and refreshes only after a change; reasoning, raw tool output, and credentials stay private.",
185
+ views: [
186
+ ControlKit.actionRow([liveSourceBadge], trailing: [snapshotRefreshButton]),
187
+ snapshotStatus,
188
+ conversationPageRow,
189
+ conversationScroll,
190
+ activityTitle,
191
+ activityScroll
192
+ ],
193
+ fullWidthLeadingContent: true
42
194
  )
195
+
196
+ let usageSteps = NSStackView(views: [
197
+ guideStep(
198
+ "1",
199
+ title: "Send a coding request",
200
+ detail: "Type ordinary text in the paired private chat. It starts or resumes only this project's dedicated Codex thread."
201
+ ),
202
+ guideStep(
203
+ "2",
204
+ title: "Follow the real Codex work",
205
+ detail: "Conversation history shows stored user and assistant messages. Latest also follows bounded public tool, file, search, subagent, and turn activity."
206
+ ),
207
+ guideStep(
208
+ "3",
209
+ title: "Receive and control the result",
210
+ detail: "SKS waits for the turn and sends Codex's final response back to Telegram. Use the commands below for status and guarded actions."
211
+ )
212
+ ])
213
+ usageSteps.orientation = .vertical; usageSteps.alignment = .leading; usageSteps.spacing = 9
214
+ for step in usageSteps.arrangedSubviews { step.widthAnchor.constraint(equalTo: usageSteps.widthAnchor).isActive = true }
215
+ let readCommandGuide = NativeView.detail("/status /tail /diff /gates /trust /proof /artifacts")
216
+ readCommandGuide.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular); readCommandGuide.setAccessibilityLabel("Read-only Telegram commands")
217
+ let actionCommandGuide = NativeView.detail("/input <text> /verify /cancel /refresh /open")
218
+ actionCommandGuide.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
219
+ actionCommandGuide.setAccessibilityLabel("Telegram action commands")
43
220
  let usageCard = NativeView.card(
44
- title: "3. Code from Telegram",
45
- subtitle: "Send ordinary text in the paired private chat. The first message creates and persists the Codex thread with its first turn; later messages resume that exact thread, wait for completion, and return Codex’s final answer. /status, /diff, /proof, and /verify remain available. Arbitrary remote shell and unpaired chats stay blocked.",
46
- views: []
221
+ title: "How to use it",
222
+ subtitle: "Send ordinary text to continue the linked Codex conversation; no special command is required for everyday coding requests.",
223
+ views: [
224
+ usageSteps,
225
+ NativeView.sectionTitle("Useful bot commands"),
226
+ readCommandGuide,
227
+ actionCommandGuide,
228
+ NativeView.detail("Read commands inspect the linked project without starting arbitrary shell access. /cancel remains owner-proof guarded; unpaired chats stay blocked.")
229
+ ],
230
+ fullWidthLeadingContent: true
47
231
  )
48
232
  let readinessCard = NativeView.card(
49
233
  title: "Project readiness",
50
- subtitle: "Codex app, CLI, and Git state for this project — checked together with every status refresh.",
51
- views: [readinessStatus]
234
+ subtitle: "Codex App, CLI, and Git state for this project.",
235
+ views: [readinessStatus],
236
+ fullWidthLeadingContent: true
52
237
  )
53
- view = NativeView.page([
54
- ControlKit.header("Remote & Telegram", "Telegram remote coding is isolated to this project, uses workspace-write with network disabled, and never exposes the bot token in config, arguments, or logs."),
55
- NativeView.row([checkButton]),
56
- setupCard, hubCard, usageCard, readinessCard
238
+
239
+ let page = NativeView.page([
240
+ ControlKit.header("Remote & Telegram", "Code through a private BotFather bot and inspect the exact linked Codex conversation in SKS Center."),
241
+ ControlKit.actionRow([], trailing: [checkButton]),
242
+ connectedCard,
243
+ setupCard,
244
+ workspaceCard,
245
+ usageCard,
246
+ readinessCard
57
247
  ])
248
+ view = page
249
+ renderVisibility()
250
+ renderConversationPlaceholder("Connect Telegram to load a Codex conversation.")
251
+ renderActivityPlaceholder("Public work activity will appear after a linked Codex turn starts.")
58
252
  updateButtons()
59
- check()
60
253
  }
61
254
 
62
- func refreshOnAppear() { check() }
255
+ func refreshOnAppear() {
256
+ startLivePolling()
257
+ checkStatus(refreshConversation: loadedSessionId == nil)
258
+ }
259
+
260
+ @objc private func openBotFather() {
261
+ guard let url = URL(string: "https://t.me/BotFather") else { return }
262
+ NSWorkspace.shared.open(url)
263
+ }
63
264
 
64
265
  @objc private func connectBot() {
65
266
  guard let window = view.window else { return }
66
267
  AlertFactory.textSheet(
67
268
  window: window,
68
269
  title: "Telegram Bot Token",
69
- message: "First send /start to your bot from the private Telegram account you want to pair. Paste the BotFather token below. It is sent through stdin, stored in macOS Keychain, and never logged.",
270
+ message: "First send /start to your bot from the private Telegram account you want to pair. Paste the current BotFather token below. It is sent through stdin, verified, stored in macOS Keychain, and never logged.",
70
271
  secure: true,
71
- placeholder: "123456789:ABC…"
272
+ placeholder: "123456789:ABC…",
273
+ actionTitle: "Connect"
72
274
  ) { [weak self] token in
73
275
  guard let self = self, let token = token else { return }
74
276
  self.runMutation(
@@ -81,58 +283,115 @@ final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
81
283
  }
82
284
 
83
285
  @objc private func startHub() {
84
- runMutation(
85
- args: ["telegram", "hub", "start", "--project-root", AppRuntime.projectRoot, "--json"],
286
+ runHubMutation(
86
287
  kind: "telegram-hub-start",
87
- summary: "Start Telegram Hub"
288
+ summary: "Start Telegram Hub",
289
+ action: hubSupervisor.start
88
290
  )
89
291
  }
90
292
 
91
293
  @objc private func stopHub() {
92
- runMutation(
93
- args: ["telegram", "hub", "stop", "--project-root", AppRuntime.projectRoot, "--json"],
294
+ runHubMutation(
94
295
  kind: "telegram-hub-stop",
95
- summary: "Stop Telegram Hub"
296
+ summary: "Stop Telegram Hub",
297
+ action: hubSupervisor.stop
96
298
  )
97
299
  }
98
300
 
99
301
  @objc private func restartHub() {
100
- runMutation(
101
- args: ["telegram", "hub", "restart", "--project-root", AppRuntime.projectRoot, "--json"],
302
+ runHubMutation(
102
303
  kind: "telegram-hub-restart",
103
- summary: "Restart Telegram Hub"
304
+ summary: "Restart Telegram Hub",
305
+ action: hubSupervisor.restart
104
306
  )
105
307
  }
106
308
 
107
- @objc private func check() {
108
- guard !busy else { return }
109
- generation += 1
110
- let requestGeneration = generation
111
- checkButton?.isEnabled = false
112
- readinessStatus.stringValue = "Checking project readiness…"
113
- setupStatus.stringValue = "Checking Keychain, pairing, and dedicated Codex session…"
114
- hubStatus.stringValue = "Checking LaunchAgent state…"
115
- var remote: [String: Any]?
116
- var telegram: [String: Any]?
117
- let group = DispatchGroup()
118
- group.enter()
119
- processClient.run(["remote", "readiness", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
120
- remote = self?.json(result.output)
121
- group.leave()
309
+ private func runHubMutation(
310
+ kind: String,
311
+ summary: String,
312
+ action: (@escaping (Bool) -> Void) -> Void
313
+ ) {
314
+ guard !busy else {
315
+ hubStatus.stringValue = "Another Telegram operation is already running."
316
+ return
122
317
  }
123
- group.enter()
124
- processClient.run(["telegram", "status", "--project-root", AppRuntime.projectRoot, "--json"], timeout: NativeView.statusTimeout) { [weak self] result in
125
- telegram = self?.json(result.output)
126
- group.leave()
318
+ guard let operation = operations.begin(kind: kind, mutationGroup: "telegram", summary: summary) else {
319
+ hubStatus.stringValue = "Another guarded mutation is already running. Wait or open Diagnostics."
320
+ return
127
321
  }
128
- group.notify(queue: .main) { [weak self] in
129
- guard let self = self, self.generation == requestGeneration else { return }
130
- self.render(remote: remote, telegram: telegram)
131
- self.updateButtons()
322
+ setBusy(true)
323
+ _ = operations.update(operation, state: .running, stage: "running", progress: nil, summary: summary)
324
+ ControlKit.setBadge(hubBadge, text: "\(summary)…", tone: .busy)
325
+ hubStatus.stringValue = "\(summary)…"
326
+ action { [weak self] ok in
327
+ guard let self = self else { return }
328
+ _ = self.operations.update(
329
+ operation,
330
+ state: ok ? .succeeded : .failed,
331
+ stage: "complete",
332
+ progress: 1,
333
+ summary: ok ? "\(summary) completed" : "\(summary) failed"
334
+ )
335
+ self.setBusy(false)
336
+ if !ok {
337
+ ControlKit.setBadge(self.hubBadge, text: "Telegram Hub action failed", tone: .error)
338
+ self.hubStatus.stringValue = "\(summary) failed. Open Diagnostics and retry."
339
+ }
340
+ self.checkStatus(refreshConversation: false)
341
+ if ok {
342
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
343
+ self?.checkStatus(refreshConversation: false)
344
+ }
345
+ }
132
346
  }
133
347
  }
134
348
 
135
- private func runMutation(args: [String], stdin: String? = nil, kind: String, summary: String) {
349
+ @objc private func check() {
350
+ checkStatus(refreshConversation: true)
351
+ }
352
+
353
+ @objc private func refreshSnapshotManually() {
354
+ refreshSnapshot(force: true)
355
+ }
356
+
357
+ @objc private func sessionChanged() {
358
+ guard let item = sessionPicker.selectedItem,
359
+ let sessionId = item.representedObject as? String,
360
+ !sessionId.isEmpty else { return }
361
+ guard sessionId != selectedSessionId || loadedSessionId != sessionId else { return }
362
+ selectedSessionId = sessionId
363
+ invalidateCenterRequests()
364
+ clearLoadedCenterState()
365
+ resetConversationPagination()
366
+ ControlKit.setBadge(liveSourceBadge, text: "Loading selected Codex thread…", tone: .busy)
367
+ snapshotStatus.stringValue = "Reading the selected session’s conversation and bounded public activity…"
368
+ renderConversationPlaceholder("Loading the selected Codex conversation…")
369
+ renderActivityPlaceholder("Loading the selected session’s public activity…")
370
+ refreshSnapshot(force: true)
371
+ }
372
+
373
+ @objc private func showOlderConversationPage() {
374
+ guard let cursor = olderConversationCursor else { return }
375
+ conversationCursor = cursor
376
+ conversationSortDirection = olderConversationSortDirection
377
+ isViewingLatestConversationPage = false
378
+ refreshSnapshot(force: true)
379
+ }
380
+
381
+ @objc private func showNewerConversationPage() {
382
+ guard let cursor = newerConversationCursor else { return }
383
+ conversationCursor = cursor
384
+ conversationSortDirection = newerConversationSortDirection
385
+ isViewingLatestConversationPage = false
386
+ refreshSnapshot(force: true)
387
+ }
388
+
389
+ @objc private func showLatestConversationPage() {
390
+ resetConversationPagination()
391
+ refreshSnapshot(force: true)
392
+ }
393
+
394
+ func runMutation(args: [String], stdin: String? = nil, kind: String, summary: String) {
136
395
  guard !busy else {
137
396
  hubStatus.stringValue = "Another Telegram operation is already running."
138
397
  return
@@ -143,7 +402,14 @@ final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
143
402
  }
144
403
  setBusy(true)
145
404
  _ = operations.update(operation, state: .running, stage: "running", progress: nil, summary: summary)
146
- hubStatus.stringValue = "\(summary)…"
405
+ if kind == "telegram-setup" {
406
+ ControlKit.setBadge(setupReady ? connectionBadge : setupBadge, text: "Connecting Telegram…", tone: .busy)
407
+ if setupReady { connectionStatus.stringValue = "\(summary)…" }
408
+ else { setupStatus.stringValue = "\(summary)…" }
409
+ } else {
410
+ ControlKit.setBadge(hubBadge, text: "\(summary)…", tone: .busy)
411
+ hubStatus.stringValue = "\(summary)…"
412
+ }
147
413
  processClient.run(args, stdin: stdin, timeout: NativeView.mutationTimeout) { [weak self] result in
148
414
  guard let self = self else { return }
149
415
  let ok = result.code == 0
@@ -156,15 +422,17 @@ final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
156
422
  )
157
423
  self.setBusy(false)
158
424
  if !ok {
159
- // Route setup failures to the setup card so the guidance sits
160
- // next to the Connect button instead of the Hub status line.
161
425
  if kind == "telegram-setup" {
162
- self.setupStatus.stringValue = "Setup failed. Confirm you sent /start to this bot, then retry. The token was not written to logs."
426
+ ControlKit.setBadge(self.setupReady ? self.connectionBadge : self.setupBadge, text: "Telegram setup needs attention", tone: .error)
427
+ let message = self.setupFailureMessage(result.output)
428
+ if self.setupReady { self.connectionStatus.stringValue = message }
429
+ else { self.setupStatus.stringValue = message }
163
430
  } else {
431
+ ControlKit.setBadge(self.hubBadge, text: "Telegram Hub action failed", tone: .error)
164
432
  self.hubStatus.stringValue = "\(summary) failed · \(NativeView.redactPreview(result.output))"
165
433
  }
166
434
  }
167
- self.check()
435
+ if ok { self.checkStatus(refreshConversation: kind == "telegram-setup") }
168
436
  }
169
437
  }
170
438
 
@@ -173,49 +441,30 @@ final class RemoteTelegramViewController: NSViewController, ControlCenterPage {
173
441
  updateButtons()
174
442
  }
175
443
 
176
- private func updateButtons() {
177
- connectButton?.isEnabled = !busy
178
- startButton?.isEnabled = !busy && setupReady && !hubRunning
179
- stopButton?.isEnabled = !busy && hubRunning
180
- restartButton?.isEnabled = !busy && setupReady && hubRunning
181
- checkButton?.isEnabled = !busy
444
+ func updateButtons() {
445
+ let controlsBusy = busy || statusInFlight
446
+ setupBotFatherButton?.isEnabled = !busy
447
+ connectedBotFatherButton?.isEnabled = !busy
448
+ setupConnectButton?.isEnabled = !busy && !hubRunning
449
+ reconnectButton?.isEnabled = !busy && !hubRunning
450
+ startButton?.isEnabled = !controlsBusy && setupReady && !hubRunning
451
+ stopButton?.isEnabled = !controlsBusy && hubRunning
452
+ restartButton?.isEnabled = !controlsBusy && setupReady && hubRunning
453
+ acknowledgeDeadLettersButton?.isEnabled = !controlsBusy
454
+ && setupReady
455
+ && !hubRunning
456
+ && deadLetterIngressCount > 0
457
+ checkButton?.isEnabled = !controlsBusy
458
+ snapshotRefreshButton?.isEnabled = !busy && setupReady && !snapshotInFlight
459
+ olderConversationButton?.isEnabled = !busy && setupReady && !snapshotInFlight && olderConversationCursor != nil
460
+ newerConversationButton?.isEnabled = !busy && setupReady && !snapshotInFlight && newerConversationCursor != nil
461
+ latestConversationButton?.isEnabled = !busy && setupReady && !snapshotInFlight && !isViewingLatestConversationPage
462
+ sessionPicker.isEnabled = !busy && setupReady && !snapshotInFlight && registeredSessionCount > 0
463
+ startButton?.isHidden = !setupReady || hubRunning
464
+ stopButton?.isHidden = !hubRunning
465
+ restartButton?.isHidden = !hubRunning
466
+ acknowledgeDeadLettersButton?.isHidden = deadLetterIngressCount == 0
467
+ reconnectButton?.isHidden = hubRunning
182
468
  }
183
469
 
184
- private func render(remote: [String: Any]?, telegram: [String: Any]?) {
185
- if let remote = remote {
186
- let host = remote["host"] as? [String: Any]
187
- let project = remote["project"] as? [String: Any]
188
- let blockers = remote["blockers"] as? [String] ?? []
189
- readinessStatus.stringValue = "Project readiness: \(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") · Git \(project?["git_repo"] as? Bool == true ? "ready" : "missing")" + (blockers.isEmpty ? "" : " · \(blockers.prefix(2).joined(separator: "; "))")
190
- } else {
191
- readinessStatus.stringValue = "Project readiness is unavailable. Open Diagnostics for the redacted log."
192
- }
193
-
194
- guard let telegram = telegram else {
195
- setupReady = false
196
- hubRunning = false
197
- setupStatus.stringValue = "Telegram setup status is unavailable."
198
- hubStatus.stringValue = "Telegram Hub status is unavailable."
199
- return
200
- }
201
- let token = telegram["token_configured"] as? Bool == true
202
- let pairing = telegram["pairing_valid"] as? Bool == true
203
- let sessions = telegram["registered_session_count"] as? Int ?? 0
204
- setupReady = token && pairing && sessions > 0
205
- hubRunning = telegram["hub_running"] as? Bool == true
206
- setupStatus.stringValue = "Keychain token \(token ? "ready" : "missing") · private pairing \(pairing ? "ready" : "missing") · dedicated Codex sessions \(sessions)" + (setupReady ? " · ready to start" : " · connect the bot to continue")
207
- let service = telegram["service"] as? [String: Any]
208
- let installed = service?["installed"] as? Bool == true
209
- let state = service?["state"] as? String
210
- hubStatus.stringValue = "Hub \(hubRunning ? "Running" : installed ? "Stopped" : "Not installed")" + (state.map { " · launchd \($0)" } ?? "") + (hubRunning ? " · send a Telegram message to code" : setupReady ? " · click Start Hub" : "")
211
- }
212
-
213
- private func json(_ text: String) -> [String: Any]? {
214
- guard let data = text.data(using: .utf8) else { return nil }
215
- if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { return object }
216
- guard let start = text.range(of: "{", options: [.backwards])?.lowerBound else { return nil }
217
- let slice = String(text[start...])
218
- guard let sliced = slice.data(using: .utf8) else { return nil }
219
- return try? JSONSerialization.jsonObject(with: sliced) as? [String: Any]
220
- }
221
470
  }