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
@@ -2,6 +2,7 @@
2
2
  import { api, flash, statusPill, resolvePath, escapeHtml, confirmModal, inputModal, openModal, } from "./utils.js";
3
3
  import { registerAutoRefresh } from "./autoRefresh.js";
4
4
  import { HUB_BASE } from "./env.js";
5
+ import { preserveScroll } from "./preserve.js";
5
6
  let hubData = { repos: [], last_scan_at: null };
6
7
  const prefetchedUrls = new Set();
7
8
  const HUB_CACHE_TTL_MS = 30000;
@@ -12,18 +13,17 @@ const HUB_REFRESH_IDLE_MS = 30000;
12
13
  let lastHubAutoRefreshAt = 0;
13
14
  const repoListEl = document.getElementById("hub-repo-list");
14
15
  const lastScanEl = document.getElementById("hub-last-scan");
16
+ const pmaLastScanEl = document.getElementById("pma-last-scan");
15
17
  const totalEl = document.getElementById("hub-count-total");
16
18
  const runningEl = document.getElementById("hub-count-running");
17
19
  const missingEl = document.getElementById("hub-count-missing");
18
- const hubUsageList = document.getElementById("hub-usage-list");
19
20
  const hubUsageMeta = document.getElementById("hub-usage-meta");
20
21
  const hubUsageRefresh = document.getElementById("hub-usage-refresh");
21
22
  const hubUsageChartCanvas = document.getElementById("hub-usage-chart-canvas");
22
23
  const hubUsageChartRange = document.getElementById("hub-usage-chart-range");
23
24
  const hubUsageChartSegment = document.getElementById("hub-usage-chart-segment");
24
25
  const hubVersionEl = document.getElementById("hub-version");
25
- const hubInboxList = document.getElementById("hub-inbox-list");
26
- const hubInboxRefresh = document.getElementById("hub-inbox-refresh");
26
+ const pmaVersionEl = document.getElementById("pma-version");
27
27
  const UPDATE_STATUS_SEEN_KEY = "car_update_status_seen";
28
28
  const HUB_JOB_POLL_INTERVAL_MS = 1200;
29
29
  const HUB_JOB_TIMEOUT_MS = 180000;
@@ -34,6 +34,8 @@ const hubUsageChartState = {
34
34
  };
35
35
  let hubUsageSeriesRetryTimer = null;
36
36
  let hubUsageSummaryRetryTimer = null;
37
+ let hubUsageIndex = {};
38
+ let hubUsageUnmatched = null;
37
39
  function saveSessionCache(key, value) {
38
40
  try {
39
41
  const payload = { at: Date.now(), value };
@@ -81,9 +83,7 @@ function formatLastActivity(repo) {
81
83
  }
82
84
  function setButtonLoading(scanning) {
83
85
  const buttons = [
84
- document.getElementById("hub-scan"),
85
86
  document.getElementById("hub-quick-scan"),
86
- document.getElementById("hub-refresh"),
87
87
  ];
88
88
  buttons.forEach((btn) => {
89
89
  if (!btn)
@@ -153,6 +153,9 @@ function renderSummary(repos) {
153
153
  if (lastScanEl) {
154
154
  lastScanEl.textContent = formatTimeCompact(hubData.last_scan_at);
155
155
  }
156
+ if (pmaLastScanEl) {
157
+ pmaLastScanEl.textContent = formatTimeCompact(hubData.last_scan_at);
158
+ }
156
159
  }
157
160
  function formatTokensCompact(val) {
158
161
  if (val === null || val === undefined)
@@ -176,49 +179,30 @@ function formatTokensAxis(val) {
176
179
  return `${(num / 1000).toFixed(1)}k`;
177
180
  return Math.round(num).toString();
178
181
  }
179
- function renderHubUsage(data) {
180
- if (!hubUsageList)
182
+ function getRepoUsage(repoId) {
183
+ const usage = hubUsageIndex[repoId];
184
+ if (!usage)
185
+ return { label: "—", hasData: false };
186
+ const totals = usage.totals || {};
187
+ return {
188
+ label: formatTokensCompact(totals.total_tokens),
189
+ hasData: true,
190
+ };
191
+ }
192
+ function indexHubUsage(data) {
193
+ hubUsageIndex = {};
194
+ hubUsageUnmatched = data?.unmatched || null;
195
+ if (!data?.repos)
181
196
  return;
197
+ data.repos.forEach((repo) => {
198
+ if (repo?.id)
199
+ hubUsageIndex[repo.id] = repo;
200
+ });
201
+ }
202
+ function renderHubUsageMeta(data) {
182
203
  if (hubUsageMeta) {
183
204
  hubUsageMeta.textContent = data?.codex_home || "–";
184
205
  }
185
- if (!data || !data.repos) {
186
- hubUsageList.innerHTML =
187
- '<span class="muted small">Usage unavailable</span>';
188
- return;
189
- }
190
- if (!data.repos.length && (!data.unmatched || !data.unmatched.events)) {
191
- hubUsageList.innerHTML = '<span class="muted small">No token events</span>';
192
- return;
193
- }
194
- hubUsageList.innerHTML = "";
195
- const entries = [...data.repos].sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
196
- entries.forEach((repo) => {
197
- const div = document.createElement("div");
198
- div.className = "hub-usage-chip";
199
- const totals = repo.totals || {};
200
- const cached = totals.cached_input_tokens || 0;
201
- const cachePercent = totals.input_tokens
202
- ? Math.round((cached / totals.input_tokens) * 100)
203
- : 0;
204
- div.innerHTML = `
205
- <span class="hub-usage-chip-name">${escapeHtml(repo.id)}</span>
206
- <span class="hub-usage-chip-total">${escapeHtml(formatTokensCompact(totals.total_tokens))}</span>
207
- <span class="hub-usage-chip-meta">${escapeHtml(`${repo.events ?? 0}ev · ${cachePercent}%↻`)}</span>
208
- `;
209
- hubUsageList.appendChild(div);
210
- });
211
- if (data.unmatched && data.unmatched.events) {
212
- const div = document.createElement("div");
213
- div.className = "hub-usage-chip hub-usage-chip-unmatched";
214
- const totals = data.unmatched.totals || {};
215
- div.innerHTML = `
216
- <span class="hub-usage-chip-name">other</span>
217
- <span class="hub-usage-chip-total">${escapeHtml(formatTokensCompact(totals.total_tokens))}</span>
218
- <span class="hub-usage-chip-meta">${escapeHtml(`${data.unmatched.events}ev`)}</span>
219
- `;
220
- hubUsageList.appendChild(div);
221
- }
222
206
  }
223
207
  function scheduleHubUsageSummaryRetry() {
224
208
  clearHubUsageSummaryRetry();
@@ -234,30 +218,23 @@ function clearHubUsageSummaryRetry() {
234
218
  }
235
219
  function handleHubUsagePayload(data, { cachedUsage, allowRetry }) {
236
220
  const hasSummary = data && Array.isArray(data.repos);
221
+ const effective = hasSummary ? data : cachedUsage;
222
+ if (effective) {
223
+ indexHubUsage(effective);
224
+ renderHubUsageMeta(effective);
225
+ renderReposWithScroll(hubData.repos || []);
226
+ }
237
227
  if (data?.status === "loading") {
238
- if (hasSummary) {
239
- renderHubUsage(data);
240
- }
241
- else if (cachedUsage) {
242
- renderHubUsage(cachedUsage);
243
- }
244
- else {
245
- renderHubUsage(data);
246
- }
247
228
  if (allowRetry)
248
229
  scheduleHubUsageSummaryRetry();
249
- return hasSummary;
230
+ return Boolean(hasSummary);
250
231
  }
251
232
  if (hasSummary) {
252
- renderHubUsage(data);
253
233
  clearHubUsageSummaryRetry();
254
234
  return true;
255
235
  }
256
- if (cachedUsage) {
257
- renderHubUsage(cachedUsage);
258
- }
259
- else {
260
- renderHubUsage(null);
236
+ if (!effective && !data) {
237
+ renderReposWithScroll(hubData.repos || []);
261
238
  }
262
239
  return false;
263
240
  }
@@ -279,10 +256,7 @@ async function loadHubUsage({ silent = false, allowRetry = true } = {}) {
279
256
  catch (err) {
280
257
  const cachedUsage = loadSessionCache(HUB_USAGE_CACHE_KEY, HUB_CACHE_TTL_MS);
281
258
  if (cachedUsage) {
282
- renderHubUsage(cachedUsage);
283
- }
284
- else {
285
- renderHubUsage(null);
259
+ handleHubUsagePayload(cachedUsage, { cachedUsage, allowRetry: false });
286
260
  }
287
261
  if (!silent) {
288
262
  flash(err.message || "Failed to load usage", "error");
@@ -635,7 +609,7 @@ async function handleSystemUpdate(btnId, targetSelectId) {
635
609
  }
636
610
  }
637
611
  function initHubSettings() {
638
- const settingsBtn = document.getElementById("hub-settings");
612
+ const settingsBtns = Array.from(document.querySelectorAll("#hub-settings, #pma-settings"));
639
613
  const modal = document.getElementById("hub-settings-modal");
640
614
  const closeBtn = document.getElementById("hub-settings-close");
641
615
  const updateBtn = document.getElementById("hub-update-btn");
@@ -648,14 +622,16 @@ function initHubSettings() {
648
622
  close();
649
623
  }
650
624
  };
651
- if (settingsBtn && modal) {
652
- settingsBtn.addEventListener("click", () => {
653
- const triggerEl = document.activeElement;
654
- hideModal();
655
- closeModal = openModal(modal, {
656
- initialFocus: closeBtn || updateBtn || modal,
657
- returnFocusTo: triggerEl,
658
- onRequestClose: hideModal,
625
+ if (modal && settingsBtns.length > 0) {
626
+ settingsBtns.forEach((settingsBtn) => {
627
+ settingsBtn.addEventListener("click", () => {
628
+ const triggerEl = document.activeElement;
629
+ hideModal();
630
+ closeModal = openModal(modal, {
631
+ initialFocus: closeBtn || updateBtn || modal,
632
+ returnFocusTo: triggerEl,
633
+ onRequestClose: hideModal,
634
+ });
659
635
  });
660
636
  });
661
637
  }
@@ -828,6 +804,32 @@ function renderRepos(repos) {
828
804
  const infoLine = infoItems.length > 0
829
805
  ? `<span class="hub-repo-info-line">${escapeHtml(infoItems.join(" · "))}</span>`
830
806
  : "";
807
+ const usageInfo = getRepoUsage(repo.id);
808
+ const usageLine = `
809
+ <div class="hub-repo-usage-line${usageInfo.hasData ? "" : " muted"}">
810
+ <span class="pill pill-small hub-usage-pill">
811
+ ${escapeHtml(usageInfo.label)}
812
+ </span>
813
+ </div>`;
814
+ // Ticket flow progress line
815
+ let ticketFlowLine = "";
816
+ const tf = repo.ticket_flow;
817
+ if (tf && tf.total_count > 0) {
818
+ const percent = Math.round((tf.done_count / tf.total_count) * 100);
819
+ const isActive = tf.status === "running" || tf.status === "paused";
820
+ const statusSuffix = tf.status === "paused"
821
+ ? " · paused"
822
+ : tf.current_step
823
+ ? ` · step ${tf.current_step}`
824
+ : "";
825
+ ticketFlowLine = `
826
+ <div class="hub-repo-flow-line${isActive ? " active" : ""}">
827
+ <div class="hub-flow-bar">
828
+ <div class="hub-flow-fill" style="width:${percent}%"></div>
829
+ </div>
830
+ <span class="hub-flow-text">${tf.done_count}/${tf.total_count}${statusSuffix}</span>
831
+ </div>`;
832
+ }
831
833
  card.innerHTML = `
832
834
  <div class="hub-repo-row">
833
835
  <div class="hub-repo-left">
@@ -841,6 +843,8 @@ function renderRepos(repos) {
841
843
  <div class="hub-repo-subline">
842
844
  ${infoLine}
843
845
  </div>
846
+ ${usageLine}
847
+ ${ticketFlowLine}
844
848
  </div>
845
849
  <div class="hub-repo-right">
846
850
  ${actions || ""}
@@ -889,6 +893,18 @@ function renderRepos(repos) {
889
893
  .sort((a, b) => String(a.id).localeCompare(String(b.id)))
890
894
  .forEach((wt) => renderRepoCard(wt, { isWorktreeRow: true }));
891
895
  }
896
+ if (hubUsageUnmatched && hubUsageUnmatched.events) {
897
+ const note = document.createElement("div");
898
+ note.className = "hub-usage-unmatched-note muted small";
899
+ const total = formatTokensCompact(hubUsageUnmatched.totals?.total_tokens);
900
+ note.textContent = `Other: ${total} · ${hubUsageUnmatched.events}ev (unattributed)`;
901
+ repoListEl.appendChild(note);
902
+ }
903
+ }
904
+ function renderReposWithScroll(repos) {
905
+ preserveScroll(repoListEl, () => {
906
+ renderRepos(repos);
907
+ }, { restoreOnNextFrame: true });
892
908
  }
893
909
  async function refreshHub() {
894
910
  setButtonLoading(true);
@@ -898,9 +914,8 @@ async function refreshHub() {
898
914
  markHubRefreshed();
899
915
  saveSessionCache(HUB_CACHE_KEY, hubData);
900
916
  renderSummary(data.repos || []);
901
- renderRepos(data.repos || []);
902
- await loadHubInbox().catch(() => { });
903
- await loadHubUsage().catch(() => { });
917
+ renderReposWithScroll(data.repos || []);
918
+ loadHubUsage({ silent: true }).catch(() => { });
904
919
  }
905
920
  catch (err) {
906
921
  flash(err.message || "Hub request failed", "error");
@@ -909,40 +924,6 @@ async function refreshHub() {
909
924
  setButtonLoading(false);
910
925
  }
911
926
  }
912
- async function loadHubInbox() {
913
- if (!hubInboxList)
914
- return;
915
- hubInboxList.innerHTML = "Loading…";
916
- try {
917
- const payload = (await api("/hub/messages", { method: "GET" }));
918
- const items = payload?.items || [];
919
- if (!items.length) {
920
- hubInboxList.innerHTML = '<div class="muted">No paused runs</div>';
921
- return;
922
- }
923
- hubInboxList.innerHTML = items
924
- .map((item) => {
925
- const title = item.message?.title || item.message?.mode || "Message";
926
- const excerpt = item.message?.body ? item.message.body.slice(0, 160) : "";
927
- const repoLabel = item.repo_display_name || item.repo_id;
928
- const href = item.open_url || `/repos/${item.repo_id}/?tab=messages&run_id=${item.run_id}`;
929
- return `
930
- <a class="hub-inbox-item" href="${escapeHtml(resolvePath(href))}">
931
- <div class="hub-inbox-item-header">
932
- <span class="hub-inbox-repo">${escapeHtml(repoLabel)}</span>
933
- <span class="pill pill-small pill-warn">paused</span>
934
- </div>
935
- <div class="hub-inbox-title">${escapeHtml(title)}</div>
936
- <div class="hub-inbox-excerpt muted small">${escapeHtml(excerpt)}</div>
937
- </a>
938
- `;
939
- })
940
- .join("");
941
- }
942
- catch (_err) {
943
- hubInboxList.innerHTML = '';
944
- }
945
- }
946
927
  async function triggerHubScan() {
947
928
  setButtonLoading(true);
948
929
  try {
@@ -1070,7 +1051,12 @@ async function handleRepoAction(repoId, action) {
1070
1051
  if (!ok)
1071
1052
  return;
1072
1053
  await startHubJob("/hub/jobs/worktrees/cleanup", {
1073
- body: { worktree_repo_id: repoId },
1054
+ body: {
1055
+ worktree_repo_id: repoId,
1056
+ archive: true,
1057
+ force_archive: false,
1058
+ archive_note: null,
1059
+ },
1074
1060
  startedMessage: "Worktree cleanup queued",
1075
1061
  });
1076
1062
  flash(`Removed worktree: ${repoId}`, "success");
@@ -1152,22 +1138,14 @@ async function handleRepoAction(repoId, action) {
1152
1138
  }
1153
1139
  function attachHubHandlers() {
1154
1140
  initHubSettings();
1155
- const scanBtn = document.getElementById("hub-scan");
1156
- const refreshBtn = document.getElementById("hub-refresh");
1157
1141
  const quickScanBtn = document.getElementById("hub-quick-scan");
1158
1142
  const newRepoBtn = document.getElementById("hub-new-repo");
1159
1143
  const createCancelBtn = document.getElementById("create-repo-cancel");
1160
1144
  const createSubmitBtn = document.getElementById("create-repo-submit");
1161
1145
  const createRepoId = document.getElementById("create-repo-id");
1162
- if (scanBtn) {
1163
- scanBtn.addEventListener("click", () => triggerHubScan());
1164
- }
1165
1146
  if (quickScanBtn) {
1166
1147
  quickScanBtn.addEventListener("click", () => triggerHubScan());
1167
1148
  }
1168
- if (refreshBtn) {
1169
- refreshBtn.addEventListener("click", () => refreshHub());
1170
- }
1171
1149
  if (hubUsageRefresh) {
1172
1150
  hubUsageRefresh.addEventListener("click", () => loadHubUsage());
1173
1151
  }
@@ -1245,7 +1223,7 @@ async function silentRefreshHub() {
1245
1223
  markHubRefreshed();
1246
1224
  saveSessionCache(HUB_CACHE_KEY, hubData);
1247
1225
  renderSummary(data.repos || []);
1248
- renderRepos(data.repos || []);
1226
+ renderReposWithScroll(data.repos || []);
1249
1227
  await loadHubUsage({ silent: true, allowRetry: false });
1250
1228
  }
1251
1229
  catch (err) {
@@ -1267,15 +1245,20 @@ async function dynamicRefreshHub() {
1267
1245
  await silentRefreshHub();
1268
1246
  }
1269
1247
  async function loadHubVersion() {
1270
- if (!hubVersionEl)
1271
- return;
1272
1248
  try {
1273
1249
  const data = await api("/hub/version", { method: "GET" });
1274
1250
  const version = data.asset_version || "";
1275
- hubVersionEl.textContent = version ? `v${version}` : "v–";
1251
+ const formatted = version ? `v${version}` : "v–";
1252
+ if (hubVersionEl)
1253
+ hubVersionEl.textContent = formatted;
1254
+ if (pmaVersionEl)
1255
+ pmaVersionEl.textContent = formatted;
1276
1256
  }
1277
1257
  catch (_err) {
1278
- hubVersionEl.textContent = "v–";
1258
+ if (hubVersionEl)
1259
+ hubVersionEl.textContent = "v–";
1260
+ if (pmaVersionEl)
1261
+ pmaVersionEl.textContent = "v–";
1279
1262
  }
1280
1263
  }
1281
1264
  async function checkUpdateStatus() {
@@ -1307,25 +1290,26 @@ export function initHub() {
1307
1290
  return;
1308
1291
  attachHubHandlers();
1309
1292
  initHubUsageChartControls();
1310
- hubInboxRefresh?.addEventListener("click", () => {
1311
- void loadHubInbox();
1312
- });
1313
1293
  const cachedHub = loadSessionCache(HUB_CACHE_KEY, HUB_CACHE_TTL_MS);
1314
1294
  if (cachedHub) {
1315
1295
  hubData = cachedHub;
1316
1296
  renderSummary(cachedHub.repos || []);
1317
- renderRepos(cachedHub.repos || []);
1297
+ renderReposWithScroll(cachedHub.repos || []);
1318
1298
  }
1319
1299
  const cachedUsage = loadSessionCache(HUB_USAGE_CACHE_KEY, HUB_CACHE_TTL_MS);
1320
1300
  if (cachedUsage) {
1321
- renderHubUsage(cachedUsage);
1301
+ indexHubUsage(cachedUsage);
1302
+ renderHubUsageMeta(cachedUsage);
1322
1303
  }
1323
1304
  loadHubUsageSeries();
1324
1305
  refreshHub();
1325
1306
  loadHubVersion();
1326
1307
  checkUpdateStatus();
1327
1308
  registerAutoRefresh("hub-repos", {
1328
- callback: async () => { await dynamicRefreshHub(); },
1309
+ callback: async (ctx) => {
1310
+ void ctx;
1311
+ await dynamicRefreshHub();
1312
+ },
1329
1313
  tabId: null,
1330
1314
  interval: HUB_REFRESH_ACTIVE_MS,
1331
1315
  refreshOnActivation: true,
@@ -1334,5 +1318,4 @@ export function initHub() {
1334
1318
  }
1335
1319
  export const __hubTest = {
1336
1320
  renderRepos,
1337
- renderHubUsage,
1338
1321
  };