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.
- package/README.md +129 -10
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/codex-app-command.js +1 -1
- package/dist/cli/command-manifest-lite.js +4 -1
- package/dist/cli/command-registry.js +2 -0
- package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
- package/dist/cli/install-helpers-codex-lb-config.js +114 -9
- package/dist/cli/install-helpers-install-support.js +6 -16
- package/dist/cli/install-helpers.js +788 -600
- package/dist/cli/router.js +30 -2
- package/dist/commands/codex-app.js +37 -1
- package/dist/commands/codex-lb.js +491 -300
- package/dist/config/skills-manifest.json +57 -71
- package/dist/core/align/align-route.js +127 -0
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
- package/dist/core/codex-app/codex-app-restart.js +83 -0
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
- package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
- package/dist/core/codex-app/menubar/constants.js +5 -0
- package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
- package/dist/core/codex-app/menubar/installer.js +18 -0
- package/dist/core/codex-app/multi-provider-router.js +22 -10
- package/dist/core/codex-app/openrouter-activate.js +26 -1
- package/dist/core/codex-app.js +118 -26
- package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
- package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
- package/dist/core/codex-control/codex-task-runner.js +2 -2
- package/dist/core/codex-lb/capability-runner.js +204 -0
- package/dist/core/codex-lb/capability-types.js +1 -0
- package/dist/core/codex-lb/codex-lb-env.js +9 -13
- package/dist/core/codex-lb/codex-lb-setup.js +45 -8
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
- package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
- package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
- package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
- package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
- package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
- package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
- package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
- package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
- package/dist/core/codex-lb/desktop-controller.js +1410 -0
- package/dist/core/codex-lb/desktop-mode.js +32 -0
- package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
- package/dist/core/codex-lb/desktop-service.js +613 -0
- package/dist/core/codex-lb/legacy-migration.js +575 -0
- package/dist/core/codex-lb/migration-receipt.js +246 -0
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
- package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
- package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
- package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
- package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
- package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
- package/dist/core/codex-native/core-skill-manifest.js +22 -6
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
- package/dist/core/commands/align-command.js +220 -0
- package/dist/core/commands/image-ux-review-command.js +280 -68
- package/dist/core/commands/naruto-command.js +257 -8
- package/dist/core/commands/ppt-command.js +2 -2
- package/dist/core/commands/telegram-command.js +402 -34
- package/dist/core/commands/triwiki-command.js +11 -1
- package/dist/core/commands/triwiki-graph-command.js +160 -0
- package/dist/core/commands/wiki-command.js +100 -20
- package/dist/core/doctor/skill-legacy-surface.js +1 -3
- package/dist/core/errors/message.js +3 -0
- package/dist/core/evidence/evidence-freshness.js +14 -0
- package/dist/core/feature-fixtures.js +9 -1
- package/dist/core/feature-registry.js +5 -3
- package/dist/core/fsx.js +7 -3
- package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
- package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
- package/dist/core/hooks-runtime/hook-context.js +15 -2
- package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
- package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +33 -89
- package/dist/core/image-ux-review/callout-extraction.js +6 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
- package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
- package/dist/core/image-ux-review.js +230 -12
- package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
- package/dist/core/imagegen/imagegen-capability.js +10 -4
- package/dist/core/imagegen/require-imagegen.js +29 -5
- package/dist/core/init/skills.js +25 -13
- package/dist/core/init.js +1 -1
- package/dist/core/install/installed-package-smoke.js +157 -4
- package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
- package/dist/core/managed-path-safety.js +8 -10
- package/dist/core/mcp-config/backup.js +2 -4
- package/dist/core/mcp-config/config-reader.js +2 -4
- package/dist/core/mcp-config/scope.js +3 -5
- package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
- package/dist/core/naruto/context-graph-advisor.js +0 -0
- package/dist/core/perf/release-latency-slo.js +12 -6
- package/dist/core/pipeline-internals/runtime-core.js +78 -0
- package/dist/core/pipeline-internals/runtime-gates.js +5 -2
- package/dist/core/policy/latest-version-guidance.js +113 -0
- package/dist/core/ppt.js +8 -8
- package/dist/core/preflight/parallel-preflight-engine.js +1 -1
- package/dist/core/proof/auto-finalize.js +98 -6
- package/dist/core/proof/route-proof-gate.js +12 -1
- package/dist/core/proof/route-proof-policy.js +8 -8
- package/dist/core/release/gate-manifest.js +32 -1
- package/dist/core/release/npm-stage-contract.js +21 -0
- package/dist/core/release/package-size-budget.js +2 -2
- package/dist/core/release/release-gate-contract.js +5 -0
- package/dist/core/release/release-proof-io.js +1 -3
- package/dist/core/release/release-real-contract.js +5 -4
- package/dist/core/release/release-stamp-proof.js +1 -3
- package/dist/core/release/stage-publish.js +27 -0
- package/dist/core/remote/audit-idempotency.js +50 -18
- package/dist/core/remote/codex-public-activity.js +283 -0
- package/dist/core/remote/index.js +1 -0
- package/dist/core/remote/ssh-worker-client.js +4 -1
- package/dist/core/remote/worker.js +206 -27
- package/dist/core/research/research-adversarial-review.js +1 -3
- package/dist/core/research/research-claim-synthesizer.js +1 -3
- package/dist/core/research/research-falsification-runner.js +1 -3
- package/dist/core/research/research-source-evidence.js +1 -3
- package/dist/core/responses-stream.js +65 -19
- package/dist/core/routes/constants.js +1 -1
- package/dist/core/routes/design-policy.js +7 -2
- package/dist/core/routes/dollar-manifest-lite.js +2 -4
- package/dist/core/routes/evidence.js +1 -1
- package/dist/core/routes.js +30 -10
- package/dist/core/safety/command-contract/contracts.js +2 -0
- package/dist/core/safety/command-contract/types.js +1 -1
- package/dist/core/search/context-graph-seeds.js +205 -0
- package/dist/core/search/context.js +0 -0
- package/dist/core/search/provider.js +2 -2
- package/dist/core/secret-redaction.js +6 -0
- package/dist/core/subagents/naruto-help-contract.js +2 -1
- package/dist/core/subagents/naruto-host-credentials.js +161 -0
- package/dist/core/subagents/official-subagent-preparation.js +4 -1
- package/dist/core/subagents/official-subagent-prompt.js +44 -2
- package/dist/core/subagents/official-subagent-runner.js +26 -8
- package/dist/core/subagents/triwiki-attention.js +135 -123
- package/dist/core/telegram/bot-api.js +85 -10
- package/dist/core/telegram/center-snapshot.js +718 -0
- package/dist/core/telegram/config.js +158 -31
- package/dist/core/telegram/hub.js +288 -33
- package/dist/core/telegram/index.js +2 -0
- package/dist/core/telegram/ingress-spool.js +419 -0
- package/dist/core/telegram/ledgers.js +188 -29
- package/dist/core/telegram/messages.js +26 -6
- package/dist/core/telegram/owner-lock.js +10 -7
- package/dist/core/telegram/runtime-projection.js +38 -3
- package/dist/core/telegram/runtime.js +298 -63
- package/dist/core/telegram/service.js +137 -8
- package/dist/core/telegram/setup.js +291 -73
- package/dist/core/text/strings.js +3 -0
- package/dist/core/triwiki/code-pack.js +12 -120
- package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
- package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
- package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
- package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
- package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
- package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
- package/dist/core/triwiki/context-graph/contracts.js +236 -0
- package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
- package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
- package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
- package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
- package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
- package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
- package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
- package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
- package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
- package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
- package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
- package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
- package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
- package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
- package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
- package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
- package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
- package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
- package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
- package/dist/core/triwiki/context-graph/graph-index.js +143 -0
- package/dist/core/triwiki/context-graph/ids.js +0 -0
- package/dist/core/triwiki/context-graph/lint/index.js +0 -0
- package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
- package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
- package/dist/core/triwiki/context-graph/paths.js +116 -0
- package/dist/core/triwiki/context-graph/profiles.js +78 -0
- package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
- package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
- package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
- package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
- package/dist/core/triwiki/context-graph/projections/index.js +7 -0
- package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
- package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
- package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
- package/dist/core/triwiki/context-graph/query/explain.js +0 -0
- package/dist/core/triwiki/context-graph/query/index.js +178 -0
- package/dist/core/triwiki/context-graph/query/load.js +116 -0
- package/dist/core/triwiki/context-graph/query/pack.js +117 -0
- package/dist/core/triwiki/context-graph/query/rank.js +142 -0
- package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
- package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
- package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
- package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
- package/dist/core/triwiki/context-graph/query-types.js +29 -0
- package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
- package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
- package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
- package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
- package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
- package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
- package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
- package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
- package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
- package/dist/core/verification/context-graph-affected.js +271 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
- package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
- package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
- package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
- package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
- package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
- package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
- package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
- package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
- package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
- package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
- package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
- package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
- package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
- package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
- package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
- package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
- package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
- package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
- package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
- package/dist/scripts/context-graph-check.js +191 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
- package/dist/scripts/gate-policy-audit-check.js +1 -1
- package/dist/scripts/helper-dedup-check.js +0 -2
- package/dist/scripts/installed-package-smoke-check.js +4 -1
- package/dist/scripts/latest-version-guidance-check.js +74 -0
- package/dist/scripts/release-metadata-check.js +21 -4
- package/dist/scripts/release-real-check.js +7 -0
- package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
- package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
- package/package.json +8 -3
- package/release-gates.v2.json +176 -1
- package/runtime-required-scripts.json +30 -0
- package/schemas/triwiki/context-graph.schema.json +119 -0
- package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
- package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
- 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
|
+
}
|