codex-autorunner 0.1.2__py3-none-any.whl → 1.1.0__py3-none-any.whl

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 (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,826 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { subscribe } from "./bus.js";
3
+ import { downloadArchiveFile, fetchArchiveSnapshot, listArchiveSnapshots, listArchiveTree, readArchiveFile, } from "./archiveApi.js";
4
+ import { escapeHtml, flash, statusPill, setButtonLoading } from "./utils.js";
5
+ let initialized = false;
6
+ let snapshots = [];
7
+ let selected = null;
8
+ let activeSnapshotKey = "";
9
+ let activeSubTab = "snapshot";
10
+ let lastSnapshotsSignature = "";
11
+ /** Compute a signature of the snapshots list for change detection. */
12
+ function snapshotsSignature(items) {
13
+ return items.map((s) => `${s.snapshot_id}:${s.worktree_repo_id}:${s.status || ""}`).join("|");
14
+ }
15
+ const listEl = document.getElementById("archive-snapshot-list");
16
+ const detailEl = document.getElementById("archive-snapshot-detail");
17
+ const emptyEl = document.getElementById("archive-empty");
18
+ const refreshBtn = document.getElementById("archive-refresh");
19
+ const MAX_PREVIEW_BYTES = 200000;
20
+ const MAX_PREVIEW_CHARS = 200000;
21
+ let fileState = null;
22
+ let fileEls = null;
23
+ let treeRequestToken = 0;
24
+ let fileRequestToken = 0;
25
+ let artifactRequestToken = 0;
26
+ const QUICK_LINKS = [
27
+ { label: "Active Context", path: "workspace/active_context.md", kind: "file" },
28
+ { label: "Decisions", path: "workspace/decisions.md", kind: "file" },
29
+ { label: "Spec", path: "workspace/spec.md", kind: "file" },
30
+ { label: "Tickets", path: "tickets", kind: "folder" },
31
+ { label: "Runs", path: "runs", kind: "folder" },
32
+ { label: "Flows", path: "flows", kind: "folder" },
33
+ { label: "Logs", path: "logs", kind: "folder" },
34
+ ];
35
+ function formatTimestamp(ts) {
36
+ if (!ts)
37
+ return "–";
38
+ const date = new Date(ts);
39
+ if (Number.isNaN(date.getTime()))
40
+ return ts;
41
+ return date.toLocaleString();
42
+ }
43
+ function formatBytes(bytes) {
44
+ if (bytes === null || bytes === undefined)
45
+ return "–";
46
+ if (bytes < 1024)
47
+ return `${bytes} B`;
48
+ const kb = bytes / 1024;
49
+ if (kb < 1024)
50
+ return `${kb.toFixed(1)} KB`;
51
+ const mb = kb / 1024;
52
+ return `${mb.toFixed(1)} MB`;
53
+ }
54
+ function snapshotKey(snapshot) {
55
+ return `${snapshot.snapshot_id}::${snapshot.worktree_repo_id}`;
56
+ }
57
+ function parentPath(path) {
58
+ const parts = path.split("/").filter(Boolean);
59
+ if (parts.length <= 1)
60
+ return "";
61
+ parts.pop();
62
+ return parts.join("/");
63
+ }
64
+ function renderEmptyDetail(message) {
65
+ if (!detailEl)
66
+ return;
67
+ detailEl.innerHTML = `
68
+ <div class="archive-empty-state">
69
+ <div class="archive-empty-title">${escapeHtml(message)}</div>
70
+ <div class="archive-empty-hint">Select a snapshot on the left to view metadata.</div>
71
+ </div>
72
+ `;
73
+ }
74
+ function renderList(items) {
75
+ if (!listEl)
76
+ return;
77
+ if (!items.length) {
78
+ listEl.innerHTML = "";
79
+ if (emptyEl)
80
+ emptyEl.classList.remove("hidden");
81
+ renderEmptyDetail("No archived snapshots yet.");
82
+ return;
83
+ }
84
+ if (emptyEl)
85
+ emptyEl.classList.add("hidden");
86
+ const selectedKey = selected ? snapshotKey(selected) : "";
87
+ listEl.innerHTML = items
88
+ .map((item) => {
89
+ const isActive = selectedKey && selectedKey === snapshotKey(item);
90
+ const created = formatTimestamp(item.created_at);
91
+ const branch = item.branch ? `· ${item.branch}` : "";
92
+ const status = item.status ? item.status : "unknown";
93
+ const note = item.note ? ` · ${item.note}` : "";
94
+ return `
95
+ <button class="archive-snapshot${isActive ? " active" : ""}" data-snapshot-id="${escapeHtml(item.snapshot_id)}" data-worktree-id="${escapeHtml(item.worktree_repo_id)}">
96
+ <div class="archive-snapshot-title">${escapeHtml(item.snapshot_id)}</div>
97
+ <div class="archive-snapshot-meta muted small">${escapeHtml(created)} ${escapeHtml(branch)}</div>
98
+ <div class="archive-snapshot-meta muted small">Status: ${escapeHtml(status)}${escapeHtml(note)}</div>
99
+ </button>
100
+ `;
101
+ })
102
+ .join("");
103
+ }
104
+ function renderSummaryGrid(summary, meta) {
105
+ const created = formatTimestamp(summary.created_at);
106
+ const headSha = summary.head_sha ? summary.head_sha : "–";
107
+ const branch = summary.branch ? summary.branch : "–";
108
+ const note = summary.note ? summary.note : "–";
109
+ const summaryValues = [
110
+ ["Snapshot ID", summary.snapshot_id],
111
+ ["Worktree Repo", summary.worktree_repo_id],
112
+ ["Created", created],
113
+ ["Branch", branch],
114
+ ["Head SHA", headSha],
115
+ ["Note", note],
116
+ ];
117
+ const rows = summaryValues
118
+ .map(([label, value]) => `
119
+ <div class="archive-meta-row">
120
+ <div class="archive-meta-label muted small">${escapeHtml(label)}</div>
121
+ <div class="archive-meta-value">${escapeHtml(value)}</div>
122
+ </div>
123
+ `)
124
+ .join("");
125
+ const summaryObj = summary.summary && typeof summary.summary === "object" ? summary.summary : null;
126
+ const summaryBlock = summaryObj
127
+ ? `
128
+ <div class="archive-summary-block">
129
+ <div class="archive-section-title">Summary</div>
130
+ <pre>${escapeHtml(JSON.stringify(summaryObj, null, 2))}</pre>
131
+ </div>
132
+ `
133
+ : "";
134
+ const metaBlock = meta
135
+ ? `
136
+ <details class="archive-summary-block">
137
+ <summary class="archive-section-title">META.json</summary>
138
+ <pre>${escapeHtml(JSON.stringify(meta, null, 2))}</pre>
139
+ </details>
140
+ `
141
+ : `
142
+ <div class="archive-summary-block muted small">META.json not available for this snapshot.</div>
143
+ `;
144
+ return `
145
+ <div class="archive-meta-grid">
146
+ ${rows}
147
+ </div>
148
+ ${summaryBlock}
149
+ ${metaBlock}
150
+ `;
151
+ }
152
+ function getMetaSourceList(meta, key) {
153
+ if (!meta || typeof meta !== "object")
154
+ return [];
155
+ const source = meta.source;
156
+ if (!source || typeof source !== "object")
157
+ return [];
158
+ const list = source[key];
159
+ if (!Array.isArray(list))
160
+ return [];
161
+ return list.filter((item) => typeof item === "string");
162
+ }
163
+ function pathPresence(meta, path) {
164
+ if (!meta)
165
+ return "–";
166
+ const copied = getMetaSourceList(meta, "copied_paths");
167
+ if (copied.includes(path))
168
+ return "Yes";
169
+ const missing = getMetaSourceList(meta, "missing_paths");
170
+ if (missing.includes(path))
171
+ return "No";
172
+ return "–";
173
+ }
174
+ function summaryValue(summary, key) {
175
+ const summaryObj = summary.summary && typeof summary.summary === "object" ? summary.summary : null;
176
+ return summaryObj ? summaryObj[key] : undefined;
177
+ }
178
+ function formatSummaryCount(value) {
179
+ if (typeof value === "number" && Number.isFinite(value))
180
+ return String(value);
181
+ return "–";
182
+ }
183
+ function formatSummaryString(value) {
184
+ if (typeof value === "string" && value)
185
+ return value;
186
+ return "–";
187
+ }
188
+ function renderArtifactSection(summary, meta) {
189
+ const flowCount = formatSummaryCount(summaryValue(summary, "flow_run_count"));
190
+ const latestFlow = formatSummaryString(summaryValue(summary, "latest_flow_run_id"));
191
+ const runsPresent = pathPresence(meta, "runs");
192
+ const flowsPresent = pathPresence(meta, "flows");
193
+ const flowsDbPresent = pathPresence(meta, "flows.db");
194
+ return `
195
+ <div class="archive-summary-block">
196
+ <div class="archive-section-title">Runs &amp; Flows</div>
197
+ <div class="archive-meta-grid">
198
+ <div class="archive-meta-row">
199
+ <div class="archive-meta-label muted small">Runs present</div>
200
+ <div class="archive-meta-value">${escapeHtml(runsPresent)}</div>
201
+ </div>
202
+ <div class="archive-meta-row">
203
+ <div class="archive-meta-label muted small">Flows present</div>
204
+ <div class="archive-meta-value">${escapeHtml(flowsPresent)}</div>
205
+ </div>
206
+ <div class="archive-meta-row">
207
+ <div class="archive-meta-label muted small">Flows DB present</div>
208
+ <div class="archive-meta-value">${escapeHtml(flowsDbPresent)}</div>
209
+ </div>
210
+ <div class="archive-meta-row">
211
+ <div class="archive-meta-label muted small">Flow run count</div>
212
+ <div class="archive-meta-value">${escapeHtml(flowCount)}</div>
213
+ </div>
214
+ <div class="archive-meta-row">
215
+ <div class="archive-meta-label muted small">Latest flow run</div>
216
+ <div class="archive-meta-value">${escapeHtml(latestFlow)}</div>
217
+ </div>
218
+ </div>
219
+ <div class="archive-quick-links archive-artifact-actions" id="archive-artifact-actions">
220
+ <button class="ghost sm" data-archive-path="runs" data-archive-kind="folder">Runs</button>
221
+ <button class="ghost sm" data-archive-path="flows" data-archive-kind="folder">Flows</button>
222
+ </div>
223
+ <div class="archive-meta-grid">
224
+ <div class="archive-meta-row">
225
+ <div class="archive-meta-label muted small">Run IDs</div>
226
+ <div class="archive-meta-value" id="archive-run-list">Loading…</div>
227
+ </div>
228
+ <div class="archive-meta-row">
229
+ <div class="archive-meta-label muted small">Flow IDs</div>
230
+ <div class="archive-meta-value" id="archive-flow-list">Loading…</div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ `;
235
+ }
236
+ function renderSubTabs() {
237
+ return `
238
+ <div class="archive-subtabs">
239
+ <button class="archive-subtab${activeSubTab === "snapshot" ? " active" : ""}" data-subtab="snapshot">Snapshot</button>
240
+ <button class="archive-subtab${activeSubTab === "files" ? " active" : ""}" data-subtab="files">Files</button>
241
+ </div>
242
+ `;
243
+ }
244
+ function switchSubTab(tab) {
245
+ activeSubTab = tab;
246
+ // Update tab button states
247
+ const tabBtns = document.querySelectorAll(".archive-subtab");
248
+ tabBtns.forEach((btn) => {
249
+ const btnTab = btn.dataset.subtab;
250
+ btn.classList.toggle("active", btnTab === tab);
251
+ });
252
+ // Update content visibility
253
+ const snapshotContent = document.getElementById("archive-tab-snapshot");
254
+ const filesContent = document.getElementById("archive-tab-files");
255
+ snapshotContent?.classList.toggle("active", tab === "snapshot");
256
+ filesContent?.classList.toggle("active", tab === "files");
257
+ }
258
+ function wireSubTabs() {
259
+ const container = document.querySelector(".archive-subtabs");
260
+ if (!container)
261
+ return;
262
+ container.addEventListener("click", (e) => {
263
+ const target = e.target;
264
+ if (!target)
265
+ return;
266
+ const btn = target.closest(".archive-subtab");
267
+ if (!btn)
268
+ return;
269
+ const tab = btn.dataset.subtab;
270
+ if (tab && (tab === "snapshot" || tab === "files")) {
271
+ switchSubTab(tab);
272
+ }
273
+ });
274
+ }
275
+ function renderFileSection() {
276
+ const quickLinks = QUICK_LINKS.map((item) => `<button class="ghost sm" data-archive-path="${escapeHtml(item.path)}" data-archive-kind="${item.kind}">${escapeHtml(item.label)}</button>`).join("");
277
+ return `
278
+ <div class="archive-file-section">
279
+ <div class="archive-file-header-row">
280
+ <div>
281
+ <div class="archive-section-title">Archive files</div>
282
+ <div class="muted small">Browse snapshot files (read-only).</div>
283
+ </div>
284
+ <div class="archive-quick-links" id="archive-quick-links">
285
+ ${quickLinks}
286
+ </div>
287
+ </div>
288
+ <div class="archive-file-path-row">
289
+ <nav class="workspace-breadcrumbs" id="archive-breadcrumbs"></nav>
290
+ <button class="ghost sm" id="archive-tree-refresh">Reload</button>
291
+ </div>
292
+ <div class="workspace-grid archive-file-grid">
293
+ <aside class="workspace-file-browser archive-file-browser">
294
+ <div class="workspace-file-list" id="archive-file-list"></div>
295
+ </aside>
296
+ <div class="workspace-main archive-file-main">
297
+ <div class="archive-file-viewer-header">
298
+ <div>
299
+ <div class="archive-file-title" id="archive-file-title">Select a file</div>
300
+ <div class="archive-file-meta muted small" id="archive-file-meta"></div>
301
+ </div>
302
+ <div class="archive-file-actions">
303
+ <button class="ghost sm" id="archive-file-load" hidden>Load anyway</button>
304
+ <button class="ghost sm" id="archive-file-download" disabled>Download</button>
305
+ </div>
306
+ </div>
307
+ <div class="archive-file-empty muted small" id="archive-file-empty">Select a file to preview.</div>
308
+ <pre class="archive-file-content hidden" id="archive-file-content"></pre>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ `;
313
+ }
314
+ function collectFileEls() {
315
+ const list = document.getElementById("archive-file-list");
316
+ const breadcrumbs = document.getElementById("archive-breadcrumbs");
317
+ const fileTitle = document.getElementById("archive-file-title");
318
+ const fileMeta = document.getElementById("archive-file-meta");
319
+ const fileContent = document.getElementById("archive-file-content");
320
+ const fileEmpty = document.getElementById("archive-file-empty");
321
+ const downloadBtn = document.getElementById("archive-file-download");
322
+ const loadBtn = document.getElementById("archive-file-load");
323
+ const quickLinks = document.getElementById("archive-quick-links");
324
+ const refreshButton = document.getElementById("archive-tree-refresh");
325
+ if (!list || !breadcrumbs || !fileTitle || !fileMeta || !fileContent || !fileEmpty || !downloadBtn || !loadBtn) {
326
+ return null;
327
+ }
328
+ return {
329
+ list,
330
+ breadcrumbs,
331
+ fileTitle,
332
+ fileMeta,
333
+ fileContent,
334
+ fileEmpty,
335
+ downloadBtn,
336
+ loadBtn,
337
+ quickLinks,
338
+ refreshBtn: refreshButton,
339
+ };
340
+ }
341
+ function resetFileViewer() {
342
+ if (!fileEls)
343
+ return;
344
+ fileEls.fileTitle.textContent = "Select a file";
345
+ fileEls.fileMeta.textContent = "";
346
+ fileEls.fileContent.textContent = "";
347
+ fileEls.fileContent.classList.add("hidden");
348
+ fileEls.fileEmpty.classList.remove("hidden");
349
+ fileEls.downloadBtn.disabled = true;
350
+ fileEls.loadBtn.hidden = true;
351
+ }
352
+ function renderBreadcrumbs(path) {
353
+ if (!fileEls)
354
+ return;
355
+ const container = fileEls.breadcrumbs;
356
+ container.innerHTML = "";
357
+ const nav = document.createElement("div");
358
+ nav.className = "workspace-breadcrumbs-inner";
359
+ const rootBtn = document.createElement("button");
360
+ rootBtn.type = "button";
361
+ rootBtn.textContent = "Snapshot";
362
+ rootBtn.addEventListener("click", () => {
363
+ void navigateTo("");
364
+ });
365
+ nav.appendChild(rootBtn);
366
+ const parts = path ? path.split("/") : [];
367
+ let accum = "";
368
+ parts.forEach((part) => {
369
+ const sep = document.createElement("span");
370
+ sep.textContent = " / ";
371
+ nav.appendChild(sep);
372
+ accum = accum ? `${accum}/${part}` : part;
373
+ const btn = document.createElement("button");
374
+ btn.type = "button";
375
+ btn.textContent = part;
376
+ const target = accum;
377
+ btn.addEventListener("click", () => {
378
+ void navigateTo(target);
379
+ });
380
+ nav.appendChild(btn);
381
+ });
382
+ container.appendChild(nav);
383
+ }
384
+ function renderFileList() {
385
+ if (!fileState || !fileEls)
386
+ return;
387
+ const list = fileEls.list;
388
+ list.innerHTML = "";
389
+ renderBreadcrumbs(fileState.currentPath);
390
+ if (fileState.currentPath) {
391
+ const upRow = document.createElement("div");
392
+ upRow.className = "workspace-tree-row workspace-folder-row";
393
+ const label = document.createElement("div");
394
+ label.className = "workspace-tree-label";
395
+ const main = document.createElement("div");
396
+ main.className = "workspace-tree-main";
397
+ const caret = document.createElement("span");
398
+ caret.className = "workspace-tree-caret";
399
+ caret.textContent = "◂";
400
+ main.appendChild(caret);
401
+ const name = document.createElement("button");
402
+ name.type = "button";
403
+ name.className = "workspace-tree-name";
404
+ name.textContent = "Up one level";
405
+ const navigateUp = () => {
406
+ void navigateTo(parentPath(fileState.currentPath));
407
+ };
408
+ name.addEventListener("click", navigateUp);
409
+ main.appendChild(name);
410
+ label.appendChild(main);
411
+ upRow.appendChild(label);
412
+ upRow.addEventListener("click", (evt) => {
413
+ const target = evt.target;
414
+ if (target?.closest("button"))
415
+ return;
416
+ navigateUp();
417
+ });
418
+ list.appendChild(upRow);
419
+ }
420
+ if (!fileState.nodes.length) {
421
+ const empty = document.createElement("div");
422
+ empty.className = "muted small";
423
+ empty.textContent = "This folder is empty.";
424
+ list.appendChild(empty);
425
+ return;
426
+ }
427
+ fileState.nodes.forEach((node) => {
428
+ const row = document.createElement("div");
429
+ row.className = `workspace-tree-row ${node.type === "folder" ? "workspace-folder-row" : "workspace-file-row"}`;
430
+ if (fileState.selectedFile?.path === node.path)
431
+ row.classList.add("active");
432
+ row.tabIndex = 0;
433
+ const label = document.createElement("div");
434
+ label.className = "workspace-tree-label";
435
+ const main = document.createElement("div");
436
+ main.className = "workspace-tree-main";
437
+ if (node.type === "folder") {
438
+ const caret = document.createElement("span");
439
+ caret.className = "workspace-tree-caret";
440
+ caret.textContent = "▸";
441
+ main.appendChild(caret);
442
+ }
443
+ const name = document.createElement("button");
444
+ name.type = "button";
445
+ name.className = "workspace-tree-name";
446
+ name.textContent = node.name;
447
+ const activateNode = () => {
448
+ if (node.type === "folder") {
449
+ void navigateTo(node.path);
450
+ }
451
+ else {
452
+ void selectFile(node);
453
+ }
454
+ };
455
+ name.addEventListener("click", activateNode);
456
+ main.appendChild(name);
457
+ label.appendChild(main);
458
+ const meta = document.createElement("span");
459
+ meta.className = "workspace-tree-meta";
460
+ if (node.type === "file" && node.size_bytes != null) {
461
+ meta.textContent = formatBytes(node.size_bytes);
462
+ }
463
+ if (meta.textContent)
464
+ label.appendChild(meta);
465
+ const actions = document.createElement("div");
466
+ actions.className = "workspace-item-actions";
467
+ if (node.type === "file") {
468
+ const dlBtn = document.createElement("button");
469
+ dlBtn.type = "button";
470
+ dlBtn.className = "ghost sm workspace-download-btn";
471
+ dlBtn.textContent = "⬇";
472
+ dlBtn.title = `Download ${node.name}`;
473
+ dlBtn.addEventListener("click", (evt) => {
474
+ evt.stopPropagation();
475
+ if (!fileState)
476
+ return;
477
+ downloadArchiveFile(fileState.snapshotId, fileState.worktreeRepoId, node.path);
478
+ });
479
+ actions.appendChild(dlBtn);
480
+ }
481
+ row.appendChild(label);
482
+ if (actions.childElementCount)
483
+ row.appendChild(actions);
484
+ row.addEventListener("click", (evt) => {
485
+ const target = evt.target;
486
+ if (target?.closest(".workspace-item-actions"))
487
+ return;
488
+ if (target?.closest("button"))
489
+ return;
490
+ activateNode();
491
+ });
492
+ row.addEventListener("keydown", (evt) => {
493
+ if (evt.target !== row)
494
+ return;
495
+ if (evt.key === "Enter" || evt.key === " ") {
496
+ evt.preventDefault();
497
+ activateNode();
498
+ }
499
+ });
500
+ list.appendChild(row);
501
+ });
502
+ }
503
+ async function navigateTo(path) {
504
+ if (!fileState || !fileEls)
505
+ return;
506
+ fileEls.list.innerHTML = "Loading…";
507
+ const requestId = ++treeRequestToken;
508
+ try {
509
+ const res = await listArchiveTree(fileState.snapshotId, fileState.worktreeRepoId, path);
510
+ if (!fileState || fileState.snapshotKey !== activeSnapshotKey)
511
+ return;
512
+ if (requestId !== treeRequestToken)
513
+ return;
514
+ fileState.currentPath = res.path || "";
515
+ fileState.nodes = res.nodes || [];
516
+ renderFileList();
517
+ }
518
+ catch (err) {
519
+ fileEls.list.innerHTML = "";
520
+ const msg = err.message || "Failed to load archive tree";
521
+ const error = document.createElement("div");
522
+ error.className = "muted small";
523
+ error.textContent = msg;
524
+ fileEls.list.appendChild(error);
525
+ flash("Failed to load archive files.", "error");
526
+ }
527
+ }
528
+ async function openFilePath(path) {
529
+ if (!fileState)
530
+ return;
531
+ const folder = parentPath(path);
532
+ await navigateTo(folder);
533
+ if (!fileState || fileState.snapshotKey !== activeSnapshotKey)
534
+ return;
535
+ const node = fileState.nodes.find((item) => item.path === path && item.type === "file");
536
+ if (node) {
537
+ await selectFile(node);
538
+ }
539
+ else {
540
+ flash("File not found in archive snapshot.", "error");
541
+ }
542
+ }
543
+ async function selectFile(node, forceLoad = false) {
544
+ if (!fileState || !fileEls)
545
+ return;
546
+ if (node.type !== "file")
547
+ return;
548
+ fileState.selectedFile = node;
549
+ renderFileList();
550
+ fileEls.fileTitle.textContent = node.name;
551
+ fileEls.fileMeta.textContent = `${formatBytes(node.size_bytes)} · ${node.path}`;
552
+ fileEls.fileContent.textContent = "";
553
+ fileEls.fileContent.classList.add("hidden");
554
+ fileEls.fileEmpty.classList.add("hidden");
555
+ fileEls.downloadBtn.disabled = false;
556
+ fileEls.downloadBtn.onclick = () => {
557
+ if (!fileState)
558
+ return;
559
+ downloadArchiveFile(fileState.snapshotId, fileState.worktreeRepoId, node.path);
560
+ };
561
+ if (node.size_bytes && node.size_bytes > MAX_PREVIEW_BYTES && !forceLoad) {
562
+ fileEls.fileContent.textContent = `Preview disabled for ${formatBytes(node.size_bytes)} file. Use Download or Load anyway.`;
563
+ fileEls.fileContent.classList.remove("hidden");
564
+ fileEls.loadBtn.hidden = false;
565
+ fileEls.loadBtn.onclick = () => {
566
+ void selectFile(node, true);
567
+ };
568
+ return;
569
+ }
570
+ fileEls.loadBtn.hidden = true;
571
+ const requestId = ++fileRequestToken;
572
+ fileEls.fileContent.textContent = "Loading…";
573
+ fileEls.fileContent.classList.remove("hidden");
574
+ try {
575
+ const text = await readArchiveFile(fileState.snapshotId, fileState.worktreeRepoId, node.path);
576
+ if (!fileState || fileState.snapshotKey !== activeSnapshotKey)
577
+ return;
578
+ if (requestId !== fileRequestToken)
579
+ return;
580
+ let display = text;
581
+ if (display.length > MAX_PREVIEW_CHARS) {
582
+ display = `${display.slice(0, MAX_PREVIEW_CHARS)}\n\n… truncated; download for full file.`;
583
+ }
584
+ fileEls.fileContent.textContent = display;
585
+ }
586
+ catch (err) {
587
+ const msg = err.message || "Failed to load file";
588
+ fileEls.fileContent.textContent = msg;
589
+ flash("Failed to load archive file.", "error");
590
+ }
591
+ }
592
+ function initArchiveFileViewer(summary) {
593
+ fileEls = collectFileEls();
594
+ if (!fileEls)
595
+ return;
596
+ const key = snapshotKey(summary);
597
+ activeSnapshotKey = key;
598
+ fileState = {
599
+ snapshotId: summary.snapshot_id,
600
+ worktreeRepoId: summary.worktree_repo_id,
601
+ snapshotKey: key,
602
+ currentPath: "",
603
+ nodes: [],
604
+ selectedFile: null,
605
+ };
606
+ resetFileViewer();
607
+ wireArchivePathButtons(fileEls.quickLinks);
608
+ fileEls.refreshBtn?.addEventListener("click", () => {
609
+ void navigateTo(fileState?.currentPath || "");
610
+ });
611
+ void navigateTo("");
612
+ }
613
+ function wireArchivePathButtons(container) {
614
+ if (!container)
615
+ return;
616
+ container.addEventListener("click", (event) => {
617
+ const target = event.target;
618
+ if (!target)
619
+ return;
620
+ const btn = target.closest("button[data-archive-path]");
621
+ if (!btn)
622
+ return;
623
+ const path = btn.dataset.archivePath;
624
+ const kind = btn.dataset.archiveKind;
625
+ if (!path)
626
+ return;
627
+ if (kind === "folder") {
628
+ void navigateTo(path);
629
+ }
630
+ else {
631
+ void openFilePath(path);
632
+ }
633
+ });
634
+ }
635
+ function renderArtifactList(container, nodes, emptyMessage) {
636
+ if (!container)
637
+ return;
638
+ const folders = nodes.filter((node) => node.type === "folder");
639
+ if (!folders.length) {
640
+ container.textContent = emptyMessage;
641
+ return;
642
+ }
643
+ container.innerHTML = folders
644
+ .map((node) => `<button class="ghost sm" data-archive-path="${escapeHtml(node.path)}" data-archive-kind="folder">${escapeHtml(node.name)}</button>`)
645
+ .join(" ");
646
+ }
647
+ async function loadArtifactListings(summary) {
648
+ const runList = document.getElementById("archive-run-list");
649
+ const flowList = document.getElementById("archive-flow-list");
650
+ const requestId = ++artifactRequestToken;
651
+ if (runList)
652
+ runList.textContent = "Loading…";
653
+ if (flowList)
654
+ flowList.textContent = "Loading…";
655
+ try {
656
+ const runs = await listArchiveTree(summary.snapshot_id, summary.worktree_repo_id, "runs");
657
+ if (!fileState || fileState.snapshotKey !== activeSnapshotKey)
658
+ return;
659
+ if (requestId !== artifactRequestToken)
660
+ return;
661
+ renderArtifactList(runList, runs.nodes || [], "No run IDs found.");
662
+ }
663
+ catch {
664
+ if (requestId !== artifactRequestToken)
665
+ return;
666
+ if (runList)
667
+ runList.textContent = "Runs folder not present.";
668
+ }
669
+ try {
670
+ const flows = await listArchiveTree(summary.snapshot_id, summary.worktree_repo_id, "flows");
671
+ if (!fileState || fileState.snapshotKey !== activeSnapshotKey)
672
+ return;
673
+ if (requestId !== artifactRequestToken)
674
+ return;
675
+ renderArtifactList(flowList, flows.nodes || [], "No flow IDs found.");
676
+ }
677
+ catch {
678
+ if (requestId !== artifactRequestToken)
679
+ return;
680
+ if (flowList)
681
+ flowList.textContent = "Flows folder not present.";
682
+ }
683
+ wireArchivePathButtons(runList);
684
+ wireArchivePathButtons(flowList);
685
+ }
686
+ async function loadSnapshotDetail(target) {
687
+ if (!detailEl)
688
+ return;
689
+ detailEl.innerHTML = `<div class="muted small">Loading snapshot…</div>`;
690
+ try {
691
+ const res = await fetchArchiveSnapshot(target.snapshot_id, target.worktree_repo_id);
692
+ const summary = res.snapshot;
693
+ const meta = res.meta ?? null;
694
+ detailEl.innerHTML = `
695
+ <div class="archive-detail-header">
696
+ <div>
697
+ <div class="archive-detail-title">${escapeHtml(summary.snapshot_id)}</div>
698
+ <div class="archive-detail-subtitle muted small">${escapeHtml(summary.worktree_repo_id)}</div>
699
+ </div>
700
+ <span class="pill pill-idle" id="archive-detail-status">${escapeHtml(summary.status || "unknown")}</span>
701
+ </div>
702
+ ${renderSubTabs()}
703
+ <div id="archive-tab-snapshot" class="archive-tab-content archive-tab-snapshot${activeSubTab === "snapshot" ? " active" : ""}">
704
+ ${renderSummaryGrid(summary, meta)}
705
+ ${renderArtifactSection(summary, meta)}
706
+ </div>
707
+ <div id="archive-tab-files" class="archive-tab-content archive-tab-files${activeSubTab === "files" ? " active" : ""}">
708
+ ${renderFileSection()}
709
+ </div>
710
+ `;
711
+ const statusEl = document.getElementById("archive-detail-status");
712
+ if (statusEl)
713
+ statusPill(statusEl, summary.status || "unknown");
714
+ wireSubTabs();
715
+ initArchiveFileViewer(summary);
716
+ wireArchivePathButtons(document.getElementById("archive-artifact-actions"));
717
+ void loadArtifactListings(summary);
718
+ }
719
+ catch (err) {
720
+ detailEl.innerHTML = `<div class="archive-empty-state">
721
+ <div class="archive-empty-title">Failed to load snapshot.</div>
722
+ <div class="archive-empty-hint muted small">${escapeHtml(err.message || "Unknown error")}</div>
723
+ </div>`;
724
+ flash("Failed to load archive snapshot.", "error");
725
+ }
726
+ }
727
+ function selectSnapshot(target) {
728
+ selected = target;
729
+ renderList(snapshots);
730
+ void loadSnapshotDetail(target);
731
+ }
732
+ async function loadSnapshots(forceReload = false) {
733
+ if (!listEl)
734
+ return;
735
+ const isInitialLoad = snapshots.length === 0;
736
+ const showRefreshIndicator = !isInitialLoad;
737
+ if (showRefreshIndicator) {
738
+ setButtonLoading(refreshBtn, true);
739
+ }
740
+ // Only show loading indicator on initial load to avoid UI flicker
741
+ if (isInitialLoad) {
742
+ listEl.innerHTML = "Loading…";
743
+ }
744
+ if (emptyEl)
745
+ emptyEl.classList.add("hidden");
746
+ try {
747
+ const items = await listArchiveSnapshots();
748
+ const sorted = items.slice().sort((a, b) => {
749
+ const aTime = a.created_at ? new Date(a.created_at).getTime() : 0;
750
+ const bTime = b.created_at ? new Date(b.created_at).getTime() : 0;
751
+ if (aTime !== bTime)
752
+ return bTime - aTime;
753
+ return (b.snapshot_id || "").localeCompare(a.snapshot_id || "");
754
+ });
755
+ // Check if snapshots have changed
756
+ const newSignature = snapshotsSignature(sorted);
757
+ const hasChanged = newSignature !== lastSnapshotsSignature;
758
+ // Skip update if nothing changed and not forced
759
+ if (!forceReload && !hasChanged && !isInitialLoad) {
760
+ return;
761
+ }
762
+ lastSnapshotsSignature = newSignature;
763
+ snapshots = sorted;
764
+ renderList(sorted);
765
+ if (!sorted.length)
766
+ return;
767
+ const selectedKey = selected ? snapshotKey(selected) : "";
768
+ const match = selectedKey
769
+ ? sorted.find((item) => snapshotKey(item) === selectedKey)
770
+ : null;
771
+ // Only reload detail if selection changed or forced
772
+ if (forceReload || !match || isInitialLoad) {
773
+ const next = match || sorted[0];
774
+ selectSnapshot(next);
775
+ }
776
+ else if (match) {
777
+ // Update selected reference but don't reload detail
778
+ selected = match;
779
+ renderList(sorted);
780
+ }
781
+ }
782
+ catch (err) {
783
+ listEl.innerHTML = "";
784
+ renderEmptyDetail("Unable to load archive snapshots.");
785
+ if (emptyEl)
786
+ emptyEl.classList.add("hidden");
787
+ flash("Failed to load archive snapshots.", "error");
788
+ }
789
+ finally {
790
+ if (showRefreshIndicator) {
791
+ setButtonLoading(refreshBtn, false);
792
+ }
793
+ }
794
+ }
795
+ function handleListClick(event) {
796
+ const target = event.target;
797
+ if (!target)
798
+ return;
799
+ const btn = target.closest(".archive-snapshot");
800
+ if (!btn)
801
+ return;
802
+ const snapshotId = btn.dataset.snapshotId;
803
+ const worktreeId = btn.dataset.worktreeId;
804
+ if (!snapshotId || !worktreeId)
805
+ return;
806
+ const match = snapshots.find((item) => item.snapshot_id === snapshotId && item.worktree_repo_id === worktreeId);
807
+ selectSnapshot(match || { snapshot_id: snapshotId, worktree_repo_id: worktreeId });
808
+ }
809
+ export function initArchive() {
810
+ if (initialized)
811
+ return;
812
+ initialized = true;
813
+ if (!listEl || !detailEl)
814
+ return;
815
+ listEl.addEventListener("click", handleListClick);
816
+ refreshBtn?.addEventListener("click", () => {
817
+ void loadSnapshots(true); // Force reload on manual refresh
818
+ });
819
+ subscribe("repo:health", (payload) => {
820
+ const status = payload?.status || "";
821
+ if (status === "ok" || status === "degraded") {
822
+ void loadSnapshots(); // Non-forced: only updates if data changed
823
+ }
824
+ });
825
+ void loadSnapshots(true); // Initial load
826
+ }