codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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 (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.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
+ }