codex-autorunner 1.2.0__py3-none-any.whl → 1.3.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 (67) hide show
  1. codex_autorunner/bootstrap.py +26 -5
  2. codex_autorunner/core/about_car.py +12 -12
  3. codex_autorunner/core/config.py +178 -61
  4. codex_autorunner/core/context_awareness.py +1 -0
  5. codex_autorunner/core/filesystem.py +24 -0
  6. codex_autorunner/core/flows/controller.py +50 -12
  7. codex_autorunner/core/flows/runtime.py +8 -3
  8. codex_autorunner/core/hub.py +293 -16
  9. codex_autorunner/core/lifecycle_events.py +44 -5
  10. codex_autorunner/core/pma_context.py +188 -1
  11. codex_autorunner/core/pma_delivery.py +81 -0
  12. codex_autorunner/core/pma_dispatches.py +224 -0
  13. codex_autorunner/core/pma_lane_worker.py +122 -0
  14. codex_autorunner/core/pma_queue.py +167 -18
  15. codex_autorunner/core/pma_reactive.py +91 -0
  16. codex_autorunner/core/pma_safety.py +58 -0
  17. codex_autorunner/core/pma_sink.py +104 -0
  18. codex_autorunner/core/pma_transcripts.py +183 -0
  19. codex_autorunner/core/safe_paths.py +117 -0
  20. codex_autorunner/housekeeping.py +77 -23
  21. codex_autorunner/integrations/agents/codex_backend.py +18 -12
  22. codex_autorunner/integrations/agents/wiring.py +2 -0
  23. codex_autorunner/integrations/app_server/client.py +31 -0
  24. codex_autorunner/integrations/app_server/supervisor.py +3 -0
  25. codex_autorunner/integrations/telegram/adapter.py +1 -1
  26. codex_autorunner/integrations/telegram/config.py +1 -1
  27. codex_autorunner/integrations/telegram/constants.py +1 -1
  28. codex_autorunner/integrations/telegram/handlers/commands/execution.py +16 -15
  29. codex_autorunner/integrations/telegram/handlers/commands/files.py +5 -8
  30. codex_autorunner/integrations/telegram/handlers/commands/github.py +10 -6
  31. codex_autorunner/integrations/telegram/handlers/commands/shared.py +9 -8
  32. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +85 -2
  33. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +29 -8
  34. codex_autorunner/integrations/telegram/handlers/messages.py +8 -2
  35. codex_autorunner/integrations/telegram/helpers.py +30 -2
  36. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +54 -3
  37. codex_autorunner/static/archive.js +274 -81
  38. codex_autorunner/static/archiveApi.js +21 -0
  39. codex_autorunner/static/constants.js +1 -1
  40. codex_autorunner/static/docChatCore.js +2 -0
  41. codex_autorunner/static/hub.js +59 -0
  42. codex_autorunner/static/index.html +70 -54
  43. codex_autorunner/static/notificationBell.js +173 -0
  44. codex_autorunner/static/notifications.js +187 -36
  45. codex_autorunner/static/pma.js +96 -35
  46. codex_autorunner/static/styles.css +431 -4
  47. codex_autorunner/static/terminalManager.js +22 -3
  48. codex_autorunner/static/utils.js +5 -1
  49. codex_autorunner/surfaces/cli/cli.py +206 -129
  50. codex_autorunner/surfaces/cli/template_repos.py +157 -0
  51. codex_autorunner/surfaces/web/app.py +193 -5
  52. codex_autorunner/surfaces/web/routes/archive.py +197 -0
  53. codex_autorunner/surfaces/web/routes/file_chat.py +115 -87
  54. codex_autorunner/surfaces/web/routes/flows.py +125 -67
  55. codex_autorunner/surfaces/web/routes/pma.py +638 -57
  56. codex_autorunner/surfaces/web/schemas.py +11 -0
  57. codex_autorunner/tickets/agent_pool.py +6 -1
  58. codex_autorunner/tickets/outbox.py +27 -14
  59. codex_autorunner/tickets/replies.py +4 -10
  60. codex_autorunner/tickets/runner.py +1 -0
  61. codex_autorunner/workspace/paths.py +8 -3
  62. {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/METADATA +1 -1
  63. {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/RECORD +67 -57
  64. {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/WHEEL +0 -0
  65. {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/entry_points.txt +0 -0
  66. {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/licenses/LICENSE +0 -0
  67. {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/top_level.txt +0 -0
@@ -3,8 +3,10 @@ import { api, flash, statusPill, resolvePath, escapeHtml, confirmModal, inputMod
3
3
  import { registerAutoRefresh } from "./autoRefresh.js";
4
4
  import { HUB_BASE } from "./env.js";
5
5
  import { preserveScroll } from "./preserve.js";
6
+ import { initNotificationBell } from "./notificationBell.js";
6
7
  let hubData = { repos: [], last_scan_at: null };
7
8
  const prefetchedUrls = new Set();
9
+ let hubInboxHydrated = false;
8
10
  const HUB_CACHE_TTL_MS = 30000;
9
11
  const HUB_CACHE_KEY = `car:hub:${HUB_BASE || "/"}`;
10
12
  const HUB_USAGE_CACHE_KEY = `car:hub-usage:${HUB_BASE || "/"}`;
@@ -24,6 +26,8 @@ const hubUsageChartRange = document.getElementById("hub-usage-chart-range");
24
26
  const hubUsageChartSegment = document.getElementById("hub-usage-chart-segment");
25
27
  const hubVersionEl = document.getElementById("hub-version");
26
28
  const pmaVersionEl = document.getElementById("pma-version");
29
+ const hubInboxList = document.getElementById("hub-inbox-list");
30
+ const hubInboxRefresh = document.getElementById("hub-inbox-refresh");
27
31
  const UPDATE_STATUS_SEEN_KEY = "car_update_status_seen";
28
32
  const HUB_JOB_POLL_INTERVAL_MS = 1200;
29
33
  const HUB_JOB_TIMEOUT_MS = 180000;
@@ -83,7 +87,9 @@ function formatLastActivity(repo) {
83
87
  }
84
88
  function setButtonLoading(scanning) {
85
89
  const buttons = [
90
+ document.getElementById("hub-scan"),
86
91
  document.getElementById("hub-quick-scan"),
92
+ document.getElementById("hub-refresh"),
87
93
  ];
88
94
  buttons.forEach((btn) => {
89
95
  if (!btn)
@@ -915,6 +921,7 @@ async function refreshHub() {
915
921
  saveSessionCache(HUB_CACHE_KEY, hubData);
916
922
  renderSummary(data.repos || []);
917
923
  renderReposWithScroll(data.repos || []);
924
+ await loadHubInbox().catch(() => { });
918
925
  loadHubUsage({ silent: true }).catch(() => { });
919
926
  }
920
927
  catch (err) {
@@ -924,6 +931,46 @@ async function refreshHub() {
924
931
  setButtonLoading(false);
925
932
  }
926
933
  }
934
+ async function loadHubInbox(ctx) {
935
+ if (!hubInboxList)
936
+ return;
937
+ if (!hubInboxHydrated || ctx?.reason === "manual") {
938
+ hubInboxList.textContent = "Loading…";
939
+ }
940
+ try {
941
+ const payload = (await api("/hub/messages", { method: "GET" }));
942
+ const items = payload?.items || [];
943
+ const html = !items.length
944
+ ? '<div class="muted">No paused runs</div>'
945
+ : items
946
+ .map((item) => {
947
+ const title = item.message?.title || item.message?.mode || "Message";
948
+ const excerpt = item.message?.body ? item.message.body.slice(0, 160) : "";
949
+ const repoLabel = item.repo_display_name || item.repo_id;
950
+ const href = item.open_url || `/repos/${item.repo_id}/?tab=messages&run_id=${item.run_id}`;
951
+ return `
952
+ <a class="hub-inbox-item" href="${escapeHtml(resolvePath(href))}">
953
+ <div class="hub-inbox-item-header">
954
+ <span class="hub-inbox-repo">${escapeHtml(repoLabel)}</span>
955
+ <span class="pill pill-small pill-warn">paused</span>
956
+ </div>
957
+ <div class="hub-inbox-title">${escapeHtml(title)}</div>
958
+ <div class="hub-inbox-excerpt muted small">${escapeHtml(excerpt)}</div>
959
+ </a>
960
+ `;
961
+ })
962
+ .join("");
963
+ preserveScroll(hubInboxList, () => {
964
+ hubInboxList.innerHTML = html;
965
+ }, { restoreOnNextFrame: true });
966
+ hubInboxHydrated = true;
967
+ }
968
+ catch (_err) {
969
+ preserveScroll(hubInboxList, () => {
970
+ hubInboxList.innerHTML = "";
971
+ }, { restoreOnNextFrame: true });
972
+ }
973
+ }
927
974
  async function triggerHubScan() {
928
975
  setButtonLoading(true);
929
976
  try {
@@ -1138,14 +1185,22 @@ async function handleRepoAction(repoId, action) {
1138
1185
  }
1139
1186
  function attachHubHandlers() {
1140
1187
  initHubSettings();
1188
+ const scanBtn = document.getElementById("hub-scan");
1189
+ const refreshBtn = document.getElementById("hub-refresh");
1141
1190
  const quickScanBtn = document.getElementById("hub-quick-scan");
1142
1191
  const newRepoBtn = document.getElementById("hub-new-repo");
1143
1192
  const createCancelBtn = document.getElementById("create-repo-cancel");
1144
1193
  const createSubmitBtn = document.getElementById("create-repo-submit");
1145
1194
  const createRepoId = document.getElementById("create-repo-id");
1195
+ if (scanBtn) {
1196
+ scanBtn.addEventListener("click", () => triggerHubScan());
1197
+ }
1146
1198
  if (quickScanBtn) {
1147
1199
  quickScanBtn.addEventListener("click", () => triggerHubScan());
1148
1200
  }
1201
+ if (refreshBtn) {
1202
+ refreshBtn.addEventListener("click", () => refreshHub());
1203
+ }
1149
1204
  if (hubUsageRefresh) {
1150
1205
  hubUsageRefresh.addEventListener("click", () => loadHubUsage());
1151
1206
  }
@@ -1290,6 +1345,10 @@ export function initHub() {
1290
1345
  return;
1291
1346
  attachHubHandlers();
1292
1347
  initHubUsageChartControls();
1348
+ initNotificationBell();
1349
+ hubInboxRefresh?.addEventListener("click", () => {
1350
+ void loadHubInbox({ reason: "manual" });
1351
+ });
1293
1352
  const cachedHub = loadSessionCache(HUB_CACHE_KEY, HUB_CACHE_TTL_MS);
1294
1353
  if (cachedHub) {
1295
1354
  hubData = cachedHub;
@@ -23,15 +23,15 @@
23
23
  </div>
24
24
  <div class="hub-hero-actions">
25
25
  <button class="primary sm" id="hub-new-repo">+ New</button>
26
- <div class="notifications-bell" data-notifications-root="hub">
27
- <button class="ghost sm icon-btn notifications-bell-btn" data-notifications-trigger title="Notifications"
28
- aria-label="Notifications">
29
- <span aria-hidden="true">🔔</span>
30
- <span class="notifications-bell-badge hidden" data-notifications-badge></span>
31
- </button>
32
- <div class="notifications-dropdown hidden" data-notifications-dropdown role="menu"
33
- aria-label="Pending dispatches"></div>
34
- </div>
26
+ <button class="sm" id="hub-scan">Scan</button>
27
+ <button class="ghost sm" id="hub-refresh">Refresh</button>
28
+ <button class="ghost sm icon-btn notification-bell" id="hub-notification-bell" title="Dispatches">
29
+ <svg class="notification-bell-icon" viewBox="0 0 24 24" aria-hidden="true">
30
+ <path d="M6 9.5a6 6 0 0 1 12 0v3.5l1.6 2.2H4.4L6 13z" />
31
+ <path d="M9.8 18.2a2.2 2.2 0 0 0 4.4 0" />
32
+ </svg>
33
+ <span class="notification-badge hidden"></span>
34
+ </button>
35
35
  <button class="ghost sm icon-btn" id="hub-settings" title="Settings">⚙</button>
36
36
  </div>
37
37
  <div class="hub-mode-toggle" role="tablist" aria-label="Hub mode">
@@ -57,6 +57,15 @@
57
57
  <p class="muted small">missing</p>
58
58
  </div>
59
59
  </section>
60
+ <section class="hub-inbox">
61
+ <div class="hub-panel-header">
62
+ <span class="label">Inbox</span>
63
+ <div class="hub-panel-actions">
64
+ <button class="ghost sm" id="hub-inbox-refresh">Refresh</button>
65
+ </div>
66
+ </div>
67
+ <div class="hub-inbox-list" id="hub-inbox-list">Loading…</div>
68
+ </section>
60
69
  <section class="hub-usage-chart">
61
70
  <div class="hub-usage-chart-header">
62
71
  <span class="label">Usage Trend</span>
@@ -89,7 +98,7 @@
89
98
  </div>
90
99
  </section>
91
100
  </div>
92
- <div class="hub-shell hidden" id="pma-shell">
101
+ <div class="hub-shell hidden" id="pma-shell" data-pma-view="chat">
93
102
  <header class="hub-hero pma-hero">
94
103
  <div class="hub-hero-text pma-hero-text">
95
104
  <h1>Project Manager</h1>
@@ -97,15 +106,13 @@
97
106
  <span class="hub-version" id="pma-version">v–</span>
98
107
  </div>
99
108
  <div class="hub-hero-actions pma-hero-actions">
100
- <div class="notifications-bell" data-notifications-root="pma">
101
- <button class="ghost sm icon-btn notifications-bell-btn" data-notifications-trigger title="Notifications"
102
- aria-label="Notifications">
103
- <span aria-hidden="true">🔔</span>
104
- <span class="notifications-bell-badge hidden" data-notifications-badge></span>
105
- </button>
106
- <div class="notifications-dropdown hidden" data-notifications-dropdown role="menu"
107
- aria-label="Pending dispatches"></div>
108
- </div>
109
+ <button class="ghost sm icon-btn notification-bell" id="pma-notification-bell" title="Dispatches">
110
+ <svg class="notification-bell-icon" viewBox="0 0 24 24" aria-hidden="true">
111
+ <path d="M6 9.5a6 6 0 0 1 12 0v3.5l1.6 2.2H4.4L6 13z" />
112
+ <path d="M9.8 18.2a2.2 2.2 0 0 0 4.4 0" />
113
+ </svg>
114
+ <span class="notification-badge hidden"></span>
115
+ </button>
109
116
  <button class="ghost sm icon-btn" id="pma-settings" title="Settings">⚙</button>
110
117
  </div>
111
118
  <div class="hub-mode-toggle" role="tablist" aria-label="Hub mode">
@@ -123,42 +130,44 @@
123
130
  <select id="pma-chat-agent-select" title="Agent"></select>
124
131
  <select id="pma-chat-model-select" title="Model"></select>
125
132
  <select id="pma-chat-reasoning-select" title="Reasoning"></select>
133
+ <div class="pma-view-toggle" role="tablist" aria-label="PMA view">
134
+ <button class="pma-view-btn active" data-view="chat" role="tab" aria-selected="true" type="button">Chat</button>
135
+ <button class="pma-view-btn" data-view="memory" role="tab" aria-selected="false" type="button">Memory</button>
136
+ </div>
126
137
  </div>
127
138
  <div class="pma-chat-actions">
139
+ <button class="ghost sm icon-btn" id="pma-scan-repos-btn" title="Rescan hub repositories">↻</button>
128
140
  <button class="ghost sm hidden" id="pma-chat-cancel" title="Cancel">✕</button>
129
141
  <button class="pma-new-thread-btn" id="pma-chat-new-thread" title="Start a new chat thread">New
130
142
  thread</button>
131
143
  <span class="pma-status-pill" id="pma-chat-status">idle</span>
132
144
  </div>
133
145
  </div>
134
- <div class="pma-thread-info hidden" id="pma-thread-info">
135
- <div class="pma-thread-info-header">
136
- <span class="pma-thread-info-label">Thread</span>
137
- <span class="pill pill-small pill-idle" id="pma-thread-info-status">idle</span>
138
- </div>
139
- <div class="pma-thread-info-details">
140
- <div class="pma-thread-info-row">
141
- <span class="pma-thread-info-key muted">Agent</span>
142
- <span class="pma-thread-info-value" id="pma-thread-info-agent">–</span>
143
- </div>
144
- <div class="pma-thread-info-row">
145
- <span class="pma-thread-info-key muted">Thread ID</span>
146
- <span class="pma-thread-info-value" id="pma-thread-info-thread-id" title="Click to copy">–</span>
147
- </div>
148
- <div class="pma-thread-info-row">
149
- <span class="pma-thread-info-key muted">Turn ID</span>
150
- <span class="pma-thread-info-value" id="pma-thread-info-turn-id" title="Click to copy">–</span>
151
- </div>
152
- </div>
153
- </div>
154
- <div class="pma-repo-actions" id="pma-repo-actions">
155
- <button class="ghost sm" id="pma-scan-repos-btn">Scan repos</button>
156
- </div>
157
146
  </section>
158
- <section class="pma-chat-section">
147
+ <section class="pma-chat-section" id="pma-chat-section">
159
148
  <div class="pma-chat-main">
160
149
  <div class="pma-chat-stream" id="pma-chat-stream">
161
150
  <div class="pma-chat-error hidden" id="pma-chat-error"></div>
151
+ <div class="pma-thread-info hidden" id="pma-thread-info">
152
+ <div class="pma-thread-info-header">
153
+ <span class="pma-thread-info-label">Thread</span>
154
+ <span class="pill pill-small pill-idle" id="pma-thread-info-status">idle</span>
155
+ </div>
156
+ <div class="pma-thread-info-details">
157
+ <div class="pma-thread-info-row">
158
+ <span class="pma-thread-info-key muted">Agent</span>
159
+ <span class="pma-thread-info-value" id="pma-thread-info-agent">–</span>
160
+ </div>
161
+ <div class="pma-thread-info-row">
162
+ <span class="pma-thread-info-key muted">Thread ID</span>
163
+ <span class="pma-thread-info-value" id="pma-thread-info-thread-id" title="Click to copy">–</span>
164
+ </div>
165
+ <div class="pma-thread-info-row">
166
+ <span class="pma-thread-info-key muted">Turn ID</span>
167
+ <span class="pma-thread-info-value" id="pma-thread-info-turn-id" title="Click to copy">–</span>
168
+ </div>
169
+ </div>
170
+ </div>
162
171
  <div class="pma-history-header" id="pma-chat-history-header">
163
172
  <span class="pma-history-label">History</span>
164
173
  </div>
@@ -176,10 +185,12 @@
176
185
  <div class="pma-attachments-area" id="pma-attachments-area">
177
186
  <div class="pma-files-row">
178
187
  <span class="pma-files-label">In</span>
188
+ <button class="pma-icon-btn-small hidden pma-clear-btn" id="pma-inbox-clear" title="Clear inbox">🗑</button>
179
189
  <div class="pma-file-list filebox-list" id="pma-inbox-files"></div>
180
190
  </div>
181
191
  <div class="pma-files-row">
182
192
  <span class="pma-files-label">Out</span>
193
+ <button class="pma-icon-btn-small hidden pma-clear-btn" id="pma-outbox-clear" title="Clear outbox">🗑</button>
183
194
  <div class="pma-file-list filebox-list" id="pma-outbox-files"></div>
184
195
  <button class="pma-icon-btn-small" id="pma-outbox-refresh" title="Refresh files">↻</button>
185
196
  </div>
@@ -1042,6 +1053,22 @@
1042
1053
  </div>
1043
1054
  </div>
1044
1055
  </div>
1056
+ <!-- Dispatch Notifications Modal -->
1057
+ <div class="modal-overlay" hidden="" id="notification-modal">
1058
+ <div aria-describedby="notification-modal-body" aria-labelledby="notification-modal-title" aria-modal="true"
1059
+ class="modal-dialog notification-dialog" role="dialog" tabindex="-1">
1060
+ <div class="modal-header notification-modal-header">
1061
+ <span class="label" id="notification-modal-title">Dispatches</span>
1062
+ <div class="notification-modal-actions">
1063
+ <button class="ghost sm" id="notification-refresh">Refresh</button>
1064
+ <button class="ghost sm icon-btn" id="notification-close" title="Close">✕</button>
1065
+ </div>
1066
+ </div>
1067
+ <div class="modal-body" id="notification-modal-body">
1068
+ <div class="notification-list" id="notification-list">Loading…</div>
1069
+ </div>
1070
+ </div>
1071
+ </div>
1045
1072
  <!-- Repo Settings Modal -->
1046
1073
  <div class="modal-overlay" hidden="" id="repo-settings-modal">
1047
1074
  <div aria-describedby="repo-settings-modal-body" aria-labelledby="repo-settings-modal-title" aria-modal="true"
@@ -1153,17 +1180,6 @@
1153
1180
  </div>
1154
1181
  </div>
1155
1182
  </div>
1156
- <!-- Notifications Modal -->
1157
- <div class="modal-overlay hidden" id="notifications-modal">
1158
- <div aria-labelledby="notifications-modal-title" aria-modal="true" class="modal-dialog notifications-modal-dialog"
1159
- role="dialog" tabindex="-1">
1160
- <div class="notifications-modal-header">
1161
- <span class="label" id="notifications-modal-title">Pending dispatch</span>
1162
- <button class="ghost sm icon-btn" id="notifications-modal-close" title="Close">×</button>
1163
- </div>
1164
- <div class="notifications-modal-body-wrapper" id="notifications-modal-body"></div>
1165
- </div>
1166
- </div>
1167
1183
  <!-- Reason Details Modal -->
1168
1184
  <div class="modal-overlay" hidden="" id="reason-modal">
1169
1185
  <div aria-describedby="reason-modal-content" aria-labelledby="reason-modal-title" aria-modal="true"
@@ -0,0 +1,173 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { api, escapeHtml, flash, openModal, resolvePath } from "./utils.js";
3
+ let bellInitialized = false;
4
+ let modalOpen = false;
5
+ let closeModal = null;
6
+ function getBellButtons() {
7
+ return Array.from(document.querySelectorAll(".notification-bell"));
8
+ }
9
+ function setBadges(count) {
10
+ getBellButtons().forEach((btn) => {
11
+ const badge = btn.querySelector(".notification-badge");
12
+ if (!badge)
13
+ return;
14
+ if (count > 0) {
15
+ badge.textContent = String(count);
16
+ badge.classList.remove("hidden");
17
+ }
18
+ else {
19
+ badge.textContent = "";
20
+ badge.classList.add("hidden");
21
+ }
22
+ });
23
+ }
24
+ function itemTitle(item) {
25
+ const payload = item.dispatch || item.message || {};
26
+ return payload.title || payload.mode || "Message";
27
+ }
28
+ function itemBody(item) {
29
+ const payload = item.dispatch || item.message || {};
30
+ return payload.body || "";
31
+ }
32
+ function renderList(items) {
33
+ const listEl = document.getElementById("notification-list");
34
+ if (!listEl)
35
+ return;
36
+ if (!items.length) {
37
+ listEl.innerHTML = '<div class="muted">No dispatches</div>';
38
+ return;
39
+ }
40
+ const html = items
41
+ .map((item) => {
42
+ const title = itemTitle(item);
43
+ const excerpt = itemBody(item).slice(0, 180);
44
+ const repoLabel = item.repo_display_name || item.repo_id;
45
+ const href = item.open_url || `/repos/${item.repo_id}/?tab=inbox&run_id=${item.run_id}`;
46
+ const seq = item.seq ? `#${item.seq}` : "";
47
+ return `
48
+ <div class="notification-item">
49
+ <div class="notification-item-header">
50
+ <span class="notification-repo">${escapeHtml(repoLabel)} <span class="muted">(${item.run_id.slice(0, 8)}${seq})</span></span>
51
+ <span class="pill pill-small pill-warn">paused</span>
52
+ </div>
53
+ <div class="notification-title">${escapeHtml(title)}</div>
54
+ <div class="notification-excerpt">${escapeHtml(excerpt)}</div>
55
+ <div class="notification-actions">
56
+ <a class="notification-action" href="${escapeHtml(resolvePath(href))}">Open run</a>
57
+ <button class="notification-action" data-action="copy-run-id" data-run-id="${escapeHtml(item.run_id)}">Copy ID</button>
58
+ ${item.repo_id ? `<button class="notification-action" data-action="copy-repo-id" data-repo-id="${escapeHtml(item.repo_id)}">Copy repo</button>` : ""}
59
+ </div>
60
+ </div>
61
+ `;
62
+ })
63
+ .join("");
64
+ listEl.innerHTML = html;
65
+ }
66
+ async function fetchNotifications() {
67
+ const payload = (await api("/hub/messages", { method: "GET" }));
68
+ return payload?.items || [];
69
+ }
70
+ async function refreshNotifications(options = {}) {
71
+ const { silent = true, render = false } = options;
72
+ try {
73
+ const items = await fetchNotifications();
74
+ setBadges(items.length);
75
+ if (modalOpen || render) {
76
+ renderList(items);
77
+ }
78
+ }
79
+ catch (err) {
80
+ if (!silent) {
81
+ flash(err.message || "Failed to load dispatches", "error");
82
+ }
83
+ setBadges(0);
84
+ if (modalOpen || render) {
85
+ renderList([]);
86
+ }
87
+ }
88
+ }
89
+ function openNotificationsModal() {
90
+ const modal = document.getElementById("notification-modal");
91
+ const closeBtn = document.getElementById("notification-close");
92
+ if (!modal)
93
+ return;
94
+ if (closeModal)
95
+ closeModal();
96
+ closeModal = openModal(modal, {
97
+ initialFocus: closeBtn || modal,
98
+ onRequestClose: () => {
99
+ modalOpen = false;
100
+ if (closeModal) {
101
+ const close = closeModal;
102
+ closeModal = null;
103
+ close();
104
+ }
105
+ },
106
+ });
107
+ modalOpen = true;
108
+ void refreshNotifications({ render: true, silent: true });
109
+ }
110
+ function attachModalHandlers() {
111
+ const modal = document.getElementById("notification-modal");
112
+ if (!modal)
113
+ return;
114
+ const closeBtn = document.getElementById("notification-close");
115
+ const refreshBtn = document.getElementById("notification-refresh");
116
+ closeBtn?.addEventListener("click", () => {
117
+ if (closeModal) {
118
+ const close = closeModal;
119
+ closeModal = null;
120
+ modalOpen = false;
121
+ close();
122
+ }
123
+ });
124
+ refreshBtn?.addEventListener("click", () => {
125
+ void refreshNotifications({ render: true, silent: false });
126
+ });
127
+ const listEl = document.getElementById("notification-list");
128
+ listEl?.addEventListener("click", (event) => {
129
+ const target = event.target;
130
+ if (!target)
131
+ return;
132
+ const action = target.dataset.action || "";
133
+ if (action === "copy-run-id") {
134
+ const runId = target.dataset.runId || "";
135
+ if (runId) {
136
+ void navigator.clipboard.writeText(runId).then(() => {
137
+ flash("Copied run ID", "info");
138
+ });
139
+ }
140
+ }
141
+ if (action === "copy-repo-id") {
142
+ const repoId = target.dataset.repoId || "";
143
+ if (repoId) {
144
+ void navigator.clipboard.writeText(repoId).then(() => {
145
+ flash("Copied repo ID", "info");
146
+ });
147
+ }
148
+ }
149
+ });
150
+ }
151
+ export function initNotificationBell() {
152
+ if (bellInitialized)
153
+ return;
154
+ bellInitialized = true;
155
+ const buttons = getBellButtons();
156
+ if (!buttons.length)
157
+ return;
158
+ buttons.forEach((btn) => {
159
+ btn.addEventListener("click", () => {
160
+ openNotificationsModal();
161
+ });
162
+ });
163
+ attachModalHandlers();
164
+ void refreshNotifications({ render: false, silent: true });
165
+ window.setInterval(() => {
166
+ if (document.hidden)
167
+ return;
168
+ void refreshNotifications({ render: false, silent: true });
169
+ }, 15000);
170
+ }
171
+ export const __notificationBellTest = {
172
+ refreshNotifications,
173
+ };