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
@@ -1,17 +1,14 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  import { api, flash, statusPill, resolvePath, escapeHtml, confirmModal, inputModal, openModal, } from "./utils.js";
2
3
  import { registerAutoRefresh } from "./autoRefresh.js";
3
4
  import { HUB_BASE } from "./env.js";
5
+ import { preserveScroll } from "./preserve.js";
4
6
  let hubData = { repos: [], last_scan_at: null };
5
- const repoPrCache = new Map();
6
- const repoPrFetches = new Set();
7
7
  const prefetchedUrls = new Set();
8
+ let hubInboxHydrated = false;
8
9
  const HUB_CACHE_TTL_MS = 30000;
9
10
  const HUB_CACHE_KEY = `car:hub:${HUB_BASE || "/"}`;
10
11
  const HUB_USAGE_CACHE_KEY = `car:hub-usage:${HUB_BASE || "/"}`;
11
- const PR_CACHE_TTL_MS = 120000;
12
- const PR_FAILURE_TTL_MS = 15000;
13
- const PR_FETCH_CONCURRENCY = 3;
14
- const PR_PREFETCH_MARGIN = "200px";
15
12
  const HUB_REFRESH_ACTIVE_MS = 5000;
16
13
  const HUB_REFRESH_IDLE_MS = 30000;
17
14
  let lastHubAutoRefreshAt = 0;
@@ -20,13 +17,14 @@ const lastScanEl = document.getElementById("hub-last-scan");
20
17
  const totalEl = document.getElementById("hub-count-total");
21
18
  const runningEl = document.getElementById("hub-count-running");
22
19
  const missingEl = document.getElementById("hub-count-missing");
23
- const hubUsageList = document.getElementById("hub-usage-list");
24
20
  const hubUsageMeta = document.getElementById("hub-usage-meta");
25
21
  const hubUsageRefresh = document.getElementById("hub-usage-refresh");
26
22
  const hubUsageChartCanvas = document.getElementById("hub-usage-chart-canvas");
27
23
  const hubUsageChartRange = document.getElementById("hub-usage-chart-range");
28
24
  const hubUsageChartSegment = document.getElementById("hub-usage-chart-segment");
29
25
  const hubVersionEl = document.getElementById("hub-version");
26
+ const hubInboxList = document.getElementById("hub-inbox-list");
27
+ const hubInboxRefresh = document.getElementById("hub-inbox-refresh");
30
28
  const UPDATE_STATUS_SEEN_KEY = "car_update_status_seen";
31
29
  const HUB_JOB_POLL_INTERVAL_MS = 1200;
32
30
  const HUB_JOB_TIMEOUT_MS = 180000;
@@ -37,10 +35,8 @@ const hubUsageChartState = {
37
35
  };
38
36
  let hubUsageSeriesRetryTimer = null;
39
37
  let hubUsageSummaryRetryTimer = null;
40
- const repoPrPending = new Set();
41
- const repoPrQueue = [];
42
- let repoPrActive = 0;
43
- let repoPrObserver = null;
38
+ let hubUsageIndex = {};
39
+ let hubUsageUnmatched = null;
44
40
  function saveSessionCache(key, value) {
45
41
  try {
46
42
  const payload = { at: Date.now(), value };
@@ -183,49 +179,37 @@ function formatTokensAxis(val) {
183
179
  return `${(num / 1000).toFixed(1)}k`;
184
180
  return Math.round(num).toString();
185
181
  }
186
- function renderHubUsage(data) {
187
- if (!hubUsageList)
182
+ function getRepoUsage(repoId) {
183
+ const usage = hubUsageIndex[repoId];
184
+ if (!usage)
185
+ return { label: "—", meta: "", hasData: false };
186
+ const totals = usage.totals || {};
187
+ const cached = totals.cached_input_tokens || 0;
188
+ const input = totals.input_tokens || 0;
189
+ const cachePercent = input ? Math.round((cached / input) * 100) : 0;
190
+ const meta = usage.events === undefined
191
+ ? ""
192
+ : `${usage.events}ev${input ? ` · ${cachePercent}%↻` : ""}`;
193
+ return {
194
+ label: formatTokensCompact(totals.total_tokens),
195
+ meta,
196
+ hasData: true,
197
+ };
198
+ }
199
+ function indexHubUsage(data) {
200
+ hubUsageIndex = {};
201
+ hubUsageUnmatched = data?.unmatched || null;
202
+ if (!data?.repos)
188
203
  return;
204
+ data.repos.forEach((repo) => {
205
+ if (repo?.id)
206
+ hubUsageIndex[repo.id] = repo;
207
+ });
208
+ }
209
+ function renderHubUsageMeta(data) {
189
210
  if (hubUsageMeta) {
190
211
  hubUsageMeta.textContent = data?.codex_home || "–";
191
212
  }
192
- if (!data || !data.repos) {
193
- hubUsageList.innerHTML =
194
- '<span class="muted small">Usage unavailable</span>';
195
- return;
196
- }
197
- if (!data.repos.length && (!data.unmatched || !data.unmatched.events)) {
198
- hubUsageList.innerHTML = '<span class="muted small">No token events</span>';
199
- return;
200
- }
201
- hubUsageList.innerHTML = "";
202
- const entries = [...data.repos].sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
203
- entries.forEach((repo) => {
204
- const div = document.createElement("div");
205
- div.className = "hub-usage-chip";
206
- const totals = repo.totals || {};
207
- const cached = totals.cached_input_tokens || 0;
208
- const cachePercent = totals.input_tokens
209
- ? Math.round((cached / totals.input_tokens) * 100)
210
- : 0;
211
- div.innerHTML = `
212
- <span class="hub-usage-chip-name">${escapeHtml(repo.id)}</span>
213
- <span class="hub-usage-chip-total">${escapeHtml(formatTokensCompact(totals.total_tokens))}</span>
214
- <span class="hub-usage-chip-meta">${escapeHtml(`${repo.events ?? 0}ev · ${cachePercent}%↻`)}</span>
215
- `;
216
- hubUsageList.appendChild(div);
217
- });
218
- if (data.unmatched && data.unmatched.events) {
219
- const div = document.createElement("div");
220
- div.className = "hub-usage-chip hub-usage-chip-unmatched";
221
- const totals = data.unmatched.totals || {};
222
- div.innerHTML = `
223
- <span class="hub-usage-chip-name">other</span>
224
- <span class="hub-usage-chip-total">${escapeHtml(formatTokensCompact(totals.total_tokens))}</span>
225
- <span class="hub-usage-chip-meta">${escapeHtml(`${data.unmatched.events}ev`)}</span>
226
- `;
227
- hubUsageList.appendChild(div);
228
- }
229
213
  }
230
214
  function scheduleHubUsageSummaryRetry() {
231
215
  clearHubUsageSummaryRetry();
@@ -241,30 +225,23 @@ function clearHubUsageSummaryRetry() {
241
225
  }
242
226
  function handleHubUsagePayload(data, { cachedUsage, allowRetry }) {
243
227
  const hasSummary = data && Array.isArray(data.repos);
228
+ const effective = hasSummary ? data : cachedUsage;
229
+ if (effective) {
230
+ indexHubUsage(effective);
231
+ renderHubUsageMeta(effective);
232
+ renderReposWithScroll(hubData.repos || []);
233
+ }
244
234
  if (data?.status === "loading") {
245
- if (hasSummary) {
246
- renderHubUsage(data);
247
- }
248
- else if (cachedUsage) {
249
- renderHubUsage(cachedUsage);
250
- }
251
- else {
252
- renderHubUsage(data);
253
- }
254
235
  if (allowRetry)
255
236
  scheduleHubUsageSummaryRetry();
256
- return hasSummary;
237
+ return Boolean(hasSummary);
257
238
  }
258
239
  if (hasSummary) {
259
- renderHubUsage(data);
260
240
  clearHubUsageSummaryRetry();
261
241
  return true;
262
242
  }
263
- if (cachedUsage) {
264
- renderHubUsage(cachedUsage);
265
- }
266
- else {
267
- renderHubUsage(null);
243
+ if (!effective && !data) {
244
+ renderReposWithScroll(hubData.repos || []);
268
245
  }
269
246
  return false;
270
247
  }
@@ -286,10 +263,7 @@ async function loadHubUsage({ silent = false, allowRetry = true } = {}) {
286
263
  catch (err) {
287
264
  const cachedUsage = loadSessionCache(HUB_USAGE_CACHE_KEY, HUB_CACHE_TTL_MS);
288
265
  if (cachedUsage) {
289
- renderHubUsage(cachedUsage);
290
- }
291
- else {
292
- renderHubUsage(null);
266
+ handleHubUsagePayload(cachedUsage, { cachedUsage, allowRetry: false });
293
267
  }
294
268
  if (!silent) {
295
269
  flash(err.message || "Failed to load usage", "error");
@@ -758,76 +732,9 @@ function inferBaseId(repo) {
758
732
  }
759
733
  return null;
760
734
  }
761
- function initRepoPrObserver() {
762
- if (!("IntersectionObserver" in window))
763
- return null;
764
- if (repoPrObserver)
765
- return repoPrObserver;
766
- repoPrObserver = new IntersectionObserver((entries) => {
767
- entries.forEach((entry) => {
768
- if (!entry.isIntersecting)
769
- return;
770
- const target = entry.target;
771
- const repoId = target?.dataset?.repoId;
772
- if (repoId) {
773
- const repo = (hubData.repos || []).find((item) => item.id === repoId);
774
- if (repo)
775
- scheduleRepoPrFetch(repo);
776
- }
777
- if (target)
778
- repoPrObserver?.unobserve(target);
779
- });
780
- }, { rootMargin: PR_PREFETCH_MARGIN });
781
- return repoPrObserver;
782
- }
783
- function scheduleRepoPrFetch(repo) {
784
- if (!repo || repo.mounted !== true)
785
- return;
786
- const cached = repoPrCache.get(repo.id);
787
- if (cached &&
788
- typeof cached.fetchedAt === "number" &&
789
- Date.now() - cached.fetchedAt <
790
- (cached.failed ? PR_FAILURE_TTL_MS : PR_CACHE_TTL_MS)) {
791
- return;
792
- }
793
- if (repoPrFetches.has(repo.id) || repoPrPending.has(repo.id))
794
- return;
795
- repoPrPending.add(repo.id);
796
- repoPrQueue.push(repo);
797
- pumpRepoPrQueue();
798
- }
799
- function pumpRepoPrQueue() {
800
- while (repoPrActive < PR_FETCH_CONCURRENCY && repoPrQueue.length) {
801
- const repo = repoPrQueue.shift();
802
- if (!repo || repoPrFetches.has(repo.id))
803
- continue;
804
- repoPrPending.delete(repo.id);
805
- repoPrActive += 1;
806
- repoPrFetches.add(repo.id);
807
- api(`/repos/${repo.id}/api/github/pr`, { method: "GET" })
808
- .then((pr) => {
809
- repoPrCache.set(repo.id, { data: pr, fetchedAt: Date.now() });
810
- })
811
- .catch(() => {
812
- repoPrCache.set(repo.id, {
813
- data: null,
814
- fetchedAt: Date.now(),
815
- failed: true,
816
- });
817
- })
818
- .finally(() => {
819
- repoPrFetches.delete(repo.id);
820
- repoPrActive -= 1;
821
- pumpRepoPrQueue();
822
- renderRepos(hubData.repos || []);
823
- });
824
- }
825
- }
826
735
  function renderRepos(repos) {
827
736
  if (!repoListEl)
828
737
  return;
829
- if (repoPrObserver)
830
- repoPrObserver.disconnect();
831
738
  repoListEl.innerHTML = "";
832
739
  if (!repos.length) {
833
740
  repoListEl.innerHTML =
@@ -902,12 +809,16 @@ function renderRepos(repos) {
902
809
  const infoLine = infoItems.length > 0
903
810
  ? `<span class="hub-repo-info-line">${escapeHtml(infoItems.join(" · "))}</span>`
904
811
  : "";
905
- const prInfo = repoPrCache.get(repo.id)?.data;
906
- const prPill = prInfo?.links?.files
907
- ? `<a class="pill pill-small hub-pr-pill" href="${escapeHtml(prInfo.links.files)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml(prInfo.pr?.title || "Open PR files")}">PR${prInfo.pr?.number
908
- ? ` #${escapeHtml(prInfo.pr.number)}`
909
- : ""}</a>`
910
- : "";
812
+ const usageInfo = getRepoUsage(repo.id);
813
+ const usageLine = `
814
+ <div class="hub-repo-usage-line${usageInfo.hasData ? "" : " muted"}">
815
+ <span class="pill pill-small hub-usage-pill">
816
+ ${escapeHtml(usageInfo.label)}
817
+ </span>
818
+ ${usageInfo.meta
819
+ ? `<span class="hub-usage-pill-meta">${escapeHtml(usageInfo.meta)}</span>`
820
+ : ""}
821
+ </div>`;
911
822
  card.innerHTML = `
912
823
  <div class="hub-repo-row">
913
824
  <div class="hub-repo-left">
@@ -920,8 +831,8 @@ function renderRepos(repos) {
920
831
  <span class="hub-repo-title">${escapeHtml(repo.display_name)}</span>
921
832
  <div class="hub-repo-subline">
922
833
  ${infoLine}
923
- ${prPill}
924
834
  </div>
835
+ ${usageLine}
925
836
  </div>
926
837
  <div class="hub-repo-right">
927
838
  ${actions || ""}
@@ -935,15 +846,6 @@ function renderRepos(repos) {
935
846
  statusPill(statusEl, repo.status);
936
847
  }
937
848
  repoListEl.appendChild(card);
938
- if (repo.mounted === true) {
939
- const observer = initRepoPrObserver();
940
- if (observer) {
941
- observer.observe(card);
942
- }
943
- else {
944
- scheduleRepoPrFetch(repo);
945
- }
946
- }
947
849
  };
948
850
  orderedGroups.forEach((group) => {
949
851
  const repo = group.base;
@@ -979,25 +881,18 @@ function renderRepos(repos) {
979
881
  .sort((a, b) => String(a.id).localeCompare(String(b.id)))
980
882
  .forEach((wt) => renderRepoCard(wt, { isWorktreeRow: true }));
981
883
  }
982
- }
983
- async function refreshRepoPrCache(repos) {
984
- const mounted = repos.filter((r) => r && r.mounted === true);
985
- if (!mounted.length)
986
- return;
987
- const observer = initRepoPrObserver();
988
- if (observer && repoListEl) {
989
- mounted.forEach((repo) => {
990
- const card = repoListEl.querySelector(`[data-repo-id="${repo.id}"]`);
991
- if (card) {
992
- observer.observe(card);
993
- }
994
- else {
995
- scheduleRepoPrFetch(repo);
996
- }
997
- });
998
- return;
884
+ if (hubUsageUnmatched && hubUsageUnmatched.events) {
885
+ const note = document.createElement("div");
886
+ note.className = "hub-usage-unmatched-note muted small";
887
+ const total = formatTokensCompact(hubUsageUnmatched.totals?.total_tokens);
888
+ note.textContent = `Other: ${total} · ${hubUsageUnmatched.events}ev (unattributed)`;
889
+ repoListEl.appendChild(note);
999
890
  }
1000
- mounted.forEach((repo) => scheduleRepoPrFetch(repo));
891
+ }
892
+ function renderReposWithScroll(repos) {
893
+ preserveScroll(repoListEl, () => {
894
+ renderRepos(repos);
895
+ }, { restoreOnNextFrame: true });
1001
896
  }
1002
897
  async function refreshHub() {
1003
898
  setButtonLoading(true);
@@ -1007,9 +902,9 @@ async function refreshHub() {
1007
902
  markHubRefreshed();
1008
903
  saveSessionCache(HUB_CACHE_KEY, hubData);
1009
904
  renderSummary(data.repos || []);
1010
- renderRepos(data.repos || []);
1011
- await refreshRepoPrCache(data.repos || []).catch(() => { });
1012
- await loadHubUsage().catch(() => { });
905
+ renderReposWithScroll(data.repos || []);
906
+ await loadHubInbox().catch(() => { });
907
+ loadHubUsage({ silent: true }).catch(() => { });
1013
908
  }
1014
909
  catch (err) {
1015
910
  flash(err.message || "Hub request failed", "error");
@@ -1018,6 +913,46 @@ async function refreshHub() {
1018
913
  setButtonLoading(false);
1019
914
  }
1020
915
  }
916
+ async function loadHubInbox(ctx) {
917
+ if (!hubInboxList)
918
+ return;
919
+ if (!hubInboxHydrated || ctx?.reason === "manual") {
920
+ hubInboxList.textContent = "Loading…";
921
+ }
922
+ try {
923
+ const payload = (await api("/hub/messages", { method: "GET" }));
924
+ const items = payload?.items || [];
925
+ const html = !items.length
926
+ ? '<div class="muted">No paused runs</div>'
927
+ : items
928
+ .map((item) => {
929
+ const title = item.message?.title || item.message?.mode || "Message";
930
+ const excerpt = item.message?.body ? item.message.body.slice(0, 160) : "";
931
+ const repoLabel = item.repo_display_name || item.repo_id;
932
+ const href = item.open_url || `/repos/${item.repo_id}/?tab=messages&run_id=${item.run_id}`;
933
+ return `
934
+ <a class="hub-inbox-item" href="${escapeHtml(resolvePath(href))}">
935
+ <div class="hub-inbox-item-header">
936
+ <span class="hub-inbox-repo">${escapeHtml(repoLabel)}</span>
937
+ <span class="pill pill-small pill-warn">paused</span>
938
+ </div>
939
+ <div class="hub-inbox-title">${escapeHtml(title)}</div>
940
+ <div class="hub-inbox-excerpt muted small">${escapeHtml(excerpt)}</div>
941
+ </a>
942
+ `;
943
+ })
944
+ .join("");
945
+ preserveScroll(hubInboxList, () => {
946
+ hubInboxList.innerHTML = html;
947
+ }, { restoreOnNextFrame: true });
948
+ hubInboxHydrated = true;
949
+ }
950
+ catch (_err) {
951
+ preserveScroll(hubInboxList, () => {
952
+ hubInboxList.innerHTML = "";
953
+ }, { restoreOnNextFrame: true });
954
+ }
955
+ }
1021
956
  async function triggerHubScan() {
1022
957
  setButtonLoading(true);
1023
958
  try {
@@ -1145,7 +1080,12 @@ async function handleRepoAction(repoId, action) {
1145
1080
  if (!ok)
1146
1081
  return;
1147
1082
  await startHubJob("/hub/jobs/worktrees/cleanup", {
1148
- body: { worktree_repo_id: repoId },
1083
+ body: {
1084
+ worktree_repo_id: repoId,
1085
+ archive: true,
1086
+ force_archive: false,
1087
+ archive_note: null,
1088
+ },
1149
1089
  startedMessage: "Worktree cleanup queued",
1150
1090
  });
1151
1091
  flash(`Removed worktree: ${repoId}`, "success");
@@ -1266,11 +1206,6 @@ function attachHubHandlers() {
1266
1206
  if (repoListEl) {
1267
1207
  repoListEl.addEventListener("click", (event) => {
1268
1208
  const target = event.target;
1269
- const prLink = target instanceof HTMLElement && target.closest("a.hub-pr-pill");
1270
- if (prLink) {
1271
- event.stopPropagation();
1272
- return;
1273
- }
1274
1209
  const btn = target instanceof HTMLElement && target.closest("button[data-action]");
1275
1210
  if (btn) {
1276
1211
  event.stopPropagation();
@@ -1325,7 +1260,7 @@ async function silentRefreshHub() {
1325
1260
  markHubRefreshed();
1326
1261
  saveSessionCache(HUB_CACHE_KEY, hubData);
1327
1262
  renderSummary(data.repos || []);
1328
- renderRepos(data.repos || []);
1263
+ renderReposWithScroll(data.repos || []);
1329
1264
  await loadHubUsage({ silent: true, allowRetry: false });
1330
1265
  }
1331
1266
  catch (err) {
@@ -1387,22 +1322,29 @@ export function initHub() {
1387
1322
  return;
1388
1323
  attachHubHandlers();
1389
1324
  initHubUsageChartControls();
1325
+ hubInboxRefresh?.addEventListener("click", () => {
1326
+ void loadHubInbox({ reason: "manual" });
1327
+ });
1390
1328
  const cachedHub = loadSessionCache(HUB_CACHE_KEY, HUB_CACHE_TTL_MS);
1391
1329
  if (cachedHub) {
1392
1330
  hubData = cachedHub;
1393
1331
  renderSummary(cachedHub.repos || []);
1394
- renderRepos(cachedHub.repos || []);
1332
+ renderReposWithScroll(cachedHub.repos || []);
1395
1333
  }
1396
1334
  const cachedUsage = loadSessionCache(HUB_USAGE_CACHE_KEY, HUB_CACHE_TTL_MS);
1397
1335
  if (cachedUsage) {
1398
- renderHubUsage(cachedUsage);
1336
+ indexHubUsage(cachedUsage);
1337
+ renderHubUsageMeta(cachedUsage);
1399
1338
  }
1400
1339
  loadHubUsageSeries();
1401
1340
  refreshHub();
1402
1341
  loadHubVersion();
1403
1342
  checkUpdateStatus();
1404
1343
  registerAutoRefresh("hub-repos", {
1405
- callback: async () => { await dynamicRefreshHub(); },
1344
+ callback: async (ctx) => {
1345
+ void ctx;
1346
+ await dynamicRefreshHub();
1347
+ },
1406
1348
  tabId: null,
1407
1349
  interval: HUB_REFRESH_ACTIVE_MS,
1408
1350
  refreshOnActivation: true,
@@ -1411,5 +1353,4 @@ export function initHub() {
1411
1353
  }
1412
1354
  export const __hubTest = {
1413
1355
  renderRepos,
1414
- renderHubUsage,
1415
1356
  };