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
@@ -0,0 +1,484 @@
1
+ import Cocoa
2
+
3
+ extension RemoteTelegramViewController {
4
+ func render(remote: [String: Any]?, telegram: [String: Any]?) {
5
+ if let remote = remote {
6
+ let host = remote["host"] as? [String: Any]
7
+ let project = remote["project"] as? [String: Any]
8
+ let blockers = remote["blockers"] as? [String] ?? []
9
+ readinessStatus.stringValue = "Project \(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: "; "))")
10
+ } else {
11
+ readinessStatus.stringValue = "Project readiness is unavailable. Open Diagnostics for the redacted log."
12
+ }
13
+
14
+ guard let telegram = telegram else {
15
+ ControlKit.setBadge(
16
+ setupReady ? connectionBadge : setupBadge,
17
+ text: "Telegram live status unavailable",
18
+ tone: .warning
19
+ )
20
+ if setupReady {
21
+ connectionStatus.stringValue = "The linked session and locally readable Codex history are retained. Retry status to restore live bot diagnostics."
22
+ } else {
23
+ setupStatus.stringValue = "Telegram setup status is unavailable."
24
+ }
25
+ renderVisibility()
26
+ return
27
+ }
28
+ let token = telegram["token_configured"] as? Bool == true
29
+ let botVerified = telegram["bot_verified"] as? Bool == true
30
+ let bot = telegram["bot"] as? [String: Any]
31
+ let username = (bot?["username"] as? String).map { "@\($0)" }
32
+ let webhookConfigured = telegram["webhook_configured"] as? Bool == true
33
+ let longPollReady = telegram["long_poll_ready"] as? Bool == true
34
+ let pairing = telegram["pairing_valid"] as? Bool == true
35
+ let sessions = telegram["registered_session_count"] as? Int ?? 0
36
+ let blockers = telegram["blockers"] as? [String] ?? []
37
+ let identityMigrationRequired = blockers.contains("telegram_identity_migration_required")
38
+ let transientBlockers: Set<String> = [
39
+ "telegram_hub_not_running",
40
+ "telegram_receive_not_ready",
41
+ "telegram_bot_probe_failed",
42
+ "telegram_ingress_dead_letter_present"
43
+ ]
44
+ let setupBlockers = blockers.filter { !transientBlockers.contains($0) }
45
+ let nextAction = (telegram["next_actions"] as? [String])?.first
46
+ setupReady = token && pairing && sessions > 0 && setupBlockers.isEmpty
47
+ hubRunning = telegram["hub_running"] as? Bool == true
48
+ let hubReady = telegram["hub_ready"] as? Bool == true
49
+ deadLetterIngressCount = telegram["dead_letter_ingress"] as? Int ?? 0
50
+ registeredSessionCount = sessions
51
+ updateSessions(telegram["registered_sessions"] as? [[String: Any]] ?? [])
52
+
53
+ if setupReady && botVerified && longPollReady {
54
+ ControlKit.setBadge(connectionBadge, text: "\(username ?? "Telegram bot") connected", tone: .ok)
55
+ connectionStatus.stringValue = "Private account paired · \(sessions) coding session\(sessions == 1 ? "" : "s") · the selector chooses the thread shown below."
56
+ } else if setupReady {
57
+ ControlKit.setBadge(connectionBadge, text: "Telegram linked · live probe degraded", tone: .warning)
58
+ connectionStatus.stringValue = "The saved pairing, session selector, and Codex history remain available. Refresh status to restore live Bot API readiness."
59
+ } else if identityMigrationRequired {
60
+ ControlKit.setBadge(setupBadge, text: "Reconnect once for Telegram v2", tone: .warning)
61
+ setupStatus.stringValue = nextAction
62
+ ?? "Stop the Telegram Hub, then reconnect this project once to write the v2 bot and private-pair identity."
63
+ } else if webhookConfigured {
64
+ ControlKit.setBadge(setupBadge, text: "Existing webhook blocks long polling", tone: .error)
65
+ setupStatus.stringValue = "Remove the bot’s existing webhook in the service that created it, send /start again, then reconnect."
66
+ } else {
67
+ ControlKit.setBadge(setupBadge, text: "Complete the 3 setup steps", tone: .warning)
68
+ setupStatus.stringValue = setupGuidance(
69
+ token: token,
70
+ botVerified: botVerified,
71
+ longPollReady: longPollReady,
72
+ pairing: pairing,
73
+ sessions: sessions,
74
+ fallback: nextAction
75
+ )
76
+ }
77
+
78
+ let service = telegram["service"] as? [String: Any]
79
+ let installed = service?["installed"] as? Bool == true
80
+ let state = service?["state"] as? String
81
+ let mode = service?["mode"] as? String
82
+ let runtimeLabel = mode == "direct"
83
+ ? "Center supervised"
84
+ : state.map { "launchd \($0)" }
85
+ ControlKit.setBadge(
86
+ hubBadge,
87
+ text: deadLetterIngressCount > 0
88
+ ? "Telegram ingress needs recovery"
89
+ : hubReady
90
+ ? "Telegram Hub ready"
91
+ : hubRunning
92
+ ? "Telegram Hub receiving degraded"
93
+ : setupReady
94
+ ? "Telegram Hub stopped"
95
+ : "Telegram Hub waits for setup",
96
+ tone: deadLetterIngressCount > 0 ? .error : hubReady ? .ok : hubRunning || setupReady ? .warning : .neutral
97
+ )
98
+ hubStatus.stringValue = deadLetterIngressCount > 0
99
+ ? hubRunning
100
+ ? "\(deadLetterIngressCount) failed ingress record\(deadLetterIngressCount == 1 ? "" : "s") · Stop Hub, acknowledge the failure, restart, then resend the request."
101
+ : "\(deadLetterIngressCount) failed ingress record\(deadLetterIngressCount == 1 ? "" : "s") · Acknowledge below, restart Hub, then resend the request."
102
+ : (runtimeLabel ?? (installed ? "Local Hub" : "Hub not installed"))
103
+ + (hubReady ? " · ordinary Telegram text reaches Codex" : hubRunning ? " · receive path not ready" : setupReady ? " · stopped; click Start Hub" : "")
104
+ renderVisibility()
105
+ }
106
+
107
+ private func updateSessions(_ sessions: [[String: Any]]) {
108
+ let previous = selectedSessionId
109
+ let projectName = URL(fileURLWithPath: AppRuntime.projectRoot).lastPathComponent
110
+ sessionPicker.removeAllItems()
111
+ for session in sessions {
112
+ guard let sessionId = session["session_id"] as? String, !sessionId.isEmpty else { continue }
113
+ let threadReady = (session["codex_thread_id"] as? String)?.isEmpty == false
114
+ let suffix = threadReady ? "thread ready" : "awaiting first message"
115
+ sessionPicker.addItem(withTitle: "\(projectName) · session …\(sessionId.suffix(4)) · \(suffix)")
116
+ sessionPicker.lastItem?.representedObject = sessionId
117
+ }
118
+ if sessionPicker.numberOfItems == 0 {
119
+ sessionPicker.addItem(withTitle: "No registered coding session")
120
+ selectedSessionId = nil
121
+ if previous != nil {
122
+ invalidateCenterRequests()
123
+ clearLoadedCenterState()
124
+ resetConversationPagination()
125
+ }
126
+ return
127
+ }
128
+ if let previous = previous,
129
+ let index = sessionPicker.itemArray.firstIndex(where: { ($0.representedObject as? String) == previous }) {
130
+ sessionPicker.selectItem(at: index)
131
+ selectedSessionId = previous
132
+ } else {
133
+ sessionPicker.selectItem(at: 0)
134
+ selectedSessionId = sessionPicker.selectedItem?.representedObject as? String
135
+ }
136
+ if selectedSessionId != previous {
137
+ invalidateCenterRequests()
138
+ clearLoadedCenterState()
139
+ resetConversationPagination()
140
+ }
141
+ }
142
+
143
+ func renderVisibility() {
144
+ setupCard?.isHidden = setupReady
145
+ connectedCard?.isHidden = !setupReady
146
+ workspaceCard?.isHidden = !setupReady
147
+ }
148
+
149
+ func renderSnapshot(_ snapshot: [String: Any]) {
150
+ guard snapshot["schema"] as? String == "sks.telegram-center-snapshot.v1" else {
151
+ ControlKit.setBadge(liveSourceBadge, text: "Unexpected snapshot format", tone: .error)
152
+ snapshotStatus.stringValue = "Center refused an unrecognized Telegram snapshot."
153
+ return
154
+ }
155
+ if let selected = snapshot["selected_session_id"] as? String, !selected.isEmpty {
156
+ selectedSessionId = selected
157
+ }
158
+ loadedSessionId = selectedSessionId
159
+ loadedConversationRevision = snapshot["conversation_revision"] as? String
160
+ loadedLiveActivityDigest = snapshot["live_activity_digest"] as? String
161
+ liveTriggeredConversationRevision = nil
162
+ let source = snapshot["source"] as? [String: Any]
163
+ loadedLiveActivityState = source?["live_activity_state"] as? String ?? "pending"
164
+ loadedLiveActivityError = source?["live_activity_error"] as? String
165
+ let state = snapshot["state"] as? String ?? "unavailable"
166
+ let guidance = snapshot["guidance"] as? String
167
+ if state == "awaiting_first_message" {
168
+ loadedTranscriptRows = []
169
+ loadedHistoryActivityRows = []
170
+ loadedLiveActivityRows = []
171
+ loadedThreadId = nil
172
+ loadedThreadStatus = "awaiting_first_message"
173
+ loadedConversationPageTurnCount = 0
174
+ loadedSnapshotTruncated = false
175
+ applyConversationPage(nil)
176
+ ControlKit.setBadge(liveSourceBadge, text: "Waiting for the first ordinary Telegram message", tone: .warning)
177
+ snapshotStatus.stringValue = (guidance ?? "Send ordinary text to create the dedicated Codex thread.") + " Local journal files are checked every 2.5 seconds and refreshed only after a change."
178
+ renderConversationPlaceholder("No Codex conversation exists yet.\n\nSend an ordinary text message to the paired bot; the first completed turn creates and persists this thread.")
179
+ renderActivityPlaceholder("Live public activity starts with that first real Codex turn.")
180
+ return
181
+ }
182
+ if state != "ready" {
183
+ loadedTranscriptRows = []
184
+ loadedHistoryActivityRows = []
185
+ loadedLiveActivityRows = []
186
+ loadedThreadId = nil
187
+ loadedThreadStatus = state
188
+ loadedConversationPageTurnCount = 0
189
+ loadedSnapshotTruncated = false
190
+ applyConversationPage(nil)
191
+ ControlKit.setBadge(liveSourceBadge, text: "Codex thread not ready", tone: .neutral)
192
+ snapshotStatus.stringValue = guidance ?? "Connect Telegram and select a registered coding session."
193
+ renderConversationPlaceholder("No linked conversation is available.")
194
+ renderActivityPlaceholder("No linked activity is available.")
195
+ return
196
+ }
197
+
198
+ let thread = snapshot["thread"] as? [String: Any]
199
+ loadedThreadId = thread?["id"] as? String
200
+ loadedThreadStatus = thread?["status"] as? String ?? "unknown"
201
+ loadedTranscriptRows = boundedRows(snapshot["transcript"] as? [[String: Any]] ?? [])
202
+ let activity = boundedRows(snapshot["activity"] as? [[String: Any]] ?? [])
203
+ loadedHistoryActivityRows = activity.filter { ($0["source"] as? String) != "remote_event_journal" }
204
+ loadedLiveActivityRows = activity.filter { ($0["source"] as? String) == "remote_event_journal" }
205
+ let conversationPage = snapshot["conversation_page"] as? [String: Any]
206
+ applyConversationPage(conversationPage)
207
+ let truncation = snapshot["truncation"] as? [String: Any]
208
+ loadedSnapshotTruncated = truncation?["truncated"] as? Bool == true
209
+ loadedConversationPageTurnCount = conversationPage?["page_turn_count"] as? Int ?? 0
210
+ renderLoadedStatus()
211
+ renderTranscript(loadedTranscriptRows)
212
+ renderActivity(mergedActivityRows())
213
+ }
214
+
215
+ func renderLiveSnapshot(_ live: [String: Any]) {
216
+ guard live["schema"] as? String == "sks.telegram-center-live.v1" else {
217
+ renderLiveFailure()
218
+ return
219
+ }
220
+ let source = live["source"] as? [String: Any]
221
+ loadedLiveActivityState = source?["live_activity_state"] as? String ?? "pending"
222
+ loadedLiveActivityError = source?["live_activity_error"] as? String
223
+ let digest = live["live_activity_digest"] as? String
224
+ if digest != loadedLiveActivityDigest {
225
+ loadedLiveActivityRows = boundedRows(live["activity"] as? [[String: Any]] ?? [])
226
+ loadedLiveActivityDigest = digest
227
+ renderActivity(mergedActivityRows())
228
+ }
229
+ renderLoadedStatus()
230
+ }
231
+
232
+ func renderLiveFailure() {
233
+ liveSourceFileSignature = nil
234
+ ControlKit.setBadge(liveSourceBadge, text: "Journal refresh temporarily unavailable", tone: .warning)
235
+ snapshotStatus.stringValue = loadedSessionId == nil
236
+ ? "The lightweight public journal could not be read. The next 2.5-second check will retry."
237
+ : "The last loaded conversation and public activity remain visible. The next lightweight journal check will retry."
238
+ }
239
+
240
+ private func renderTranscript(_ rows: [[String: Any]]) {
241
+ guard !rows.isEmpty else {
242
+ renderConversationPlaceholder("The linked thread has no retained user or assistant messages.")
243
+ return
244
+ }
245
+ let signature = rowSignature(
246
+ rows,
247
+ fields: ["id", "role", "phase", "occurred_at", "text"]
248
+ )
249
+ guard signature != renderedTranscriptSignature else { return }
250
+ renderedTranscriptSignature = signature
251
+ let output = NSMutableAttributedString()
252
+ for row in rows {
253
+ let role = row["role"] as? String == "assistant" ? "Codex" : "You"
254
+ let phase = row["phase"] as? String
255
+ let time = shortTime(row["occurred_at"] as? String)
256
+ let header = [time, role, phase].compactMap { value -> String? in
257
+ guard let value = value, !value.isEmpty else { return nil }
258
+ return value
259
+ }.joined(separator: " · ")
260
+ output.append(NSAttributedString(
261
+ string: "\(header)\n",
262
+ attributes: [
263
+ .font: NSFont.systemFont(ofSize: 11, weight: .semibold),
264
+ .foregroundColor: role == "Codex" ? NSColor.systemBlue : NSColor.labelColor
265
+ ]
266
+ ))
267
+ output.append(NSAttributedString(
268
+ string: "\(row["text"] as? String ?? "")\n\n",
269
+ attributes: [
270
+ .font: NSFont.systemFont(ofSize: 12),
271
+ .foregroundColor: NSColor.labelColor
272
+ ]
273
+ ))
274
+ }
275
+ conversationView.textStorage?.setAttributedString(output)
276
+ }
277
+
278
+ private func renderActivity(_ rows: [[String: Any]]) {
279
+ guard !rows.isEmpty else {
280
+ renderActivityPlaceholder("No public tool, file, search, subagent, or turn activity is retained yet.")
281
+ return
282
+ }
283
+ let signature = rowSignature(
284
+ rows,
285
+ fields: ["id", "occurred_at", "title", "status", "detail"]
286
+ )
287
+ guard signature != renderedActivitySignature else { return }
288
+ renderedActivitySignature = signature
289
+ let output = NSMutableAttributedString()
290
+ for row in rows {
291
+ let time = shortTime(row["occurred_at"] as? String) ?? "—"
292
+ let title = row["title"] as? String ?? "Codex activity"
293
+ let status = row["status"] as? String ?? "unknown"
294
+ output.append(NSAttributedString(
295
+ string: "\(time) \(title) · \(status)\n",
296
+ attributes: [
297
+ .font: NSFont.systemFont(ofSize: 11, weight: .medium),
298
+ .foregroundColor: NSColor.labelColor
299
+ ]
300
+ ))
301
+ if let detail = row["detail"] as? String, !detail.isEmpty {
302
+ output.append(NSAttributedString(
303
+ string: " \(detail)\n",
304
+ attributes: [
305
+ .font: NSFont.monospacedSystemFont(ofSize: 10, weight: .regular),
306
+ .foregroundColor: NSColor.secondaryLabelColor
307
+ ]
308
+ ))
309
+ }
310
+ }
311
+ activityView.textStorage?.setAttributedString(output)
312
+ }
313
+
314
+ func renderConversationPlaceholder(_ text: String) {
315
+ renderedTranscriptSignature = nil
316
+ conversationView.string = text
317
+ conversationView.font = NSFont.systemFont(ofSize: 12)
318
+ conversationView.textColor = .secondaryLabelColor
319
+ }
320
+
321
+ func renderActivityPlaceholder(_ text: String) {
322
+ renderedActivitySignature = nil
323
+ activityView.string = text
324
+ activityView.font = NSFont.systemFont(ofSize: 11)
325
+ activityView.textColor = .secondaryLabelColor
326
+ }
327
+
328
+ func guideStep(_ number: String, title: String, detail: String) -> NSStackView {
329
+ let numberLabel = NSTextField(labelWithString: number)
330
+ numberLabel.font = NSFont.systemFont(ofSize: 11, weight: .bold)
331
+ numberLabel.textColor = .white
332
+ numberLabel.alignment = .center
333
+ numberLabel.backgroundColor = .controlAccentColor
334
+ numberLabel.drawsBackground = true
335
+ numberLabel.isBordered = false
336
+ numberLabel.widthAnchor.constraint(equalToConstant: 22).isActive = true
337
+ numberLabel.heightAnchor.constraint(equalToConstant: 22).isActive = true
338
+ numberLabel.setContentHuggingPriority(.required, for: .horizontal)
339
+
340
+ let titleLabel = NSTextField(labelWithString: title)
341
+ titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
342
+ let detailLabel = NativeView.detail(detail)
343
+ detailLabel.textColor = .labelColor
344
+ let copy = NSStackView(views: [titleLabel, detailLabel])
345
+ copy.orientation = .vertical; copy.alignment = .leading; copy.spacing = 2
346
+ copy.setContentHuggingPriority(.defaultLow, for: .horizontal); for view in copy.arrangedSubviews { view.widthAnchor.constraint(equalTo: copy.widthAnchor).isActive = true }
347
+ let row = NSStackView(views: [numberLabel, copy])
348
+ row.orientation = .horizontal
349
+ row.alignment = .top
350
+ row.spacing = 9
351
+ copy.widthAnchor.constraint(equalTo: row.widthAnchor, constant: -31).isActive = true
352
+ return row
353
+ }
354
+
355
+ func textArea(_ textView: NSTextView, height: CGFloat) -> NSScrollView {
356
+ let scroll = NSScrollView()
357
+ scroll.documentView = textView
358
+ scroll.hasVerticalScroller = true
359
+ scroll.hasHorizontalScroller = false
360
+ scroll.autohidesScrollers = true
361
+ scroll.borderType = .bezelBorder
362
+ scroll.drawsBackground = true
363
+ scroll.backgroundColor = .textBackgroundColor
364
+ scroll.heightAnchor.constraint(equalToConstant: height).isActive = true
365
+ return scroll
366
+ }
367
+
368
+ static func readOnlyTextView(label: String) -> NSTextView {
369
+ let text = NSTextView()
370
+ text.isEditable = false
371
+ text.isSelectable = true
372
+ text.isRichText = true
373
+ text.drawsBackground = false
374
+ text.textContainerInset = NSSize(width: 10, height: 9)
375
+ text.isHorizontallyResizable = false
376
+ text.autoresizingMask = [.width]
377
+ text.textContainer?.widthTracksTextView = true
378
+ text.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)
379
+ text.setAccessibilityLabel(label)
380
+ return text
381
+ }
382
+
383
+ private func shortId(_ value: String) -> String {
384
+ value.count > 14 ? "\(value.prefix(8))…\(value.suffix(4))" : value
385
+ }
386
+
387
+ private func shortTime(_ value: String?) -> String? {
388
+ guard let value = value,
389
+ let date = Self.iso8601FractionalFormatter.date(from: value)
390
+ ?? Self.iso8601Formatter.date(from: value) else { return nil }
391
+ return Self.shortTimeFormatter.string(from: date)
392
+ }
393
+
394
+ func json(_ text: String) -> [String: Any]? {
395
+ guard let data = text.data(using: .utf8) else { return nil }
396
+ if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { return object }
397
+ guard let start = text.firstIndex(of: "{"), let end = text.lastIndex(of: "}") else { return nil }
398
+ let slice = String(text[start...end])
399
+ guard let sliced = slice.data(using: .utf8) else { return nil }
400
+ return try? JSONSerialization.jsonObject(with: sliced) as? [String: Any]
401
+ }
402
+
403
+ private func renderLoadedStatus() {
404
+ guard loadedSessionId != nil, loadedThreadId != nil else { return }
405
+ let liveActivityDegraded = loadedLiveActivityState == "degraded"
406
+ let pageName = isViewingLatestConversationPage ? "Latest page" : "Stored history page"
407
+ let refreshState = isViewingLatestConversationPage
408
+ ? "journal files checked every 2.5 seconds; lightweight refresh runs only after a change, and conversation refreshes after completed turns or manual navigation"
409
+ : "journal polling paused; select Latest to resume"
410
+ let activityCount = mergedActivityRows().count
411
+ let threadStatus = userFacingThreadStatus(loadedThreadStatus)
412
+ ControlKit.setBadge(
413
+ liveSourceBadge,
414
+ text: liveActivityDegraded
415
+ ? "History ready · live activity degraded"
416
+ : isViewingLatestConversationPage
417
+ ? "Live · lightweight journal polling"
418
+ : "History · live polling paused",
419
+ tone: liveActivityDegraded ? .warning : isViewingLatestConversationPage ? .ok : .neutral
420
+ )
421
+ snapshotStatus.stringValue = "Thread \(shortId(loadedThreadId ?? "unknown")) · \(threadStatus). \(pageName): \(loadedConversationPageTurnCount) turns, \(loadedTranscriptRows.count) messages, \(activityCount) public activity items. \(refreshState)." + (loadedSnapshotTruncated ? " This page was display-bounded." : "") + (liveActivityDegraded ? " Live journal warning: \(loadedLiveActivityError ?? "remote_event_journal_degraded")." : "")
422
+ }
423
+
424
+ private func mergedActivityRows() -> [[String: Any]] {
425
+ var rowsById: [String: [String: Any]] = [:]
426
+ let historyDedupeKeys = Set(loadedHistoryActivityRows.compactMap { row -> String? in
427
+ guard let key = row["dedupe_key"] as? String, !key.isEmpty else { return nil }
428
+ return key
429
+ })
430
+ let uniqueLiveRows = loadedLiveActivityRows.filter { row in
431
+ guard let key = row["dedupe_key"] as? String, !key.isEmpty else { return true }
432
+ return !historyDedupeKeys.contains(key)
433
+ }
434
+ for row in loadedHistoryActivityRows + uniqueLiveRows {
435
+ guard let id = row["id"] as? String, !id.isEmpty else { continue }
436
+ rowsById[id] = row
437
+ }
438
+ let sorted = rowsById.values.sorted { left, right in
439
+ let leftTime = activitySortDate(left["occurred_at"] as? String)
440
+ let rightTime = activitySortDate(right["occurred_at"] as? String)
441
+ if leftTime != rightTime { return leftTime < rightTime }
442
+ return (left["id"] as? String ?? "") < (right["id"] as? String ?? "")
443
+ }
444
+ return boundedRows(sorted)
445
+ }
446
+
447
+ private func boundedRows(_ rows: [[String: Any]], limit: Int = 768) -> [[String: Any]] {
448
+ guard rows.count > limit else { return rows }
449
+ return Array(rows.suffix(limit))
450
+ }
451
+
452
+ private func activitySortDate(_ value: String?) -> Date {
453
+ guard let value = value else { return .distantPast }
454
+ return Self.iso8601FractionalFormatter.date(from: value)
455
+ ?? Self.iso8601Formatter.date(from: value)
456
+ ?? .distantPast
457
+ }
458
+
459
+ private func rowSignature(_ rows: [[String: Any]], fields: [String]) -> String {
460
+ rows.map { row in
461
+ fields.map { field in
462
+ row[field].map { String(describing: $0) } ?? ""
463
+ }.joined(separator: "\u{1f}")
464
+ }.joined(separator: "\u{1e}")
465
+ }
466
+
467
+ private static let iso8601FractionalFormatter: ISO8601DateFormatter = {
468
+ let formatter = ISO8601DateFormatter()
469
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
470
+ return formatter
471
+ }()
472
+
473
+ private static let iso8601Formatter: ISO8601DateFormatter = {
474
+ let formatter = ISO8601DateFormatter()
475
+ formatter.formatOptions = [.withInternetDateTime]
476
+ return formatter
477
+ }()
478
+
479
+ private static let shortTimeFormatter: DateFormatter = {
480
+ let formatter = DateFormatter()
481
+ formatter.dateFormat = "HH:mm:ss"
482
+ return formatter
483
+ }()
484
+ }