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
@@ -1,13 +1,15 @@
1
1
  // GENERATED FILE - do not edit directly. Source: static_src/
2
- import { api, escapeHtml, openModal, resolvePath } from "./utils.js";
2
+ import { api, confirmModal, escapeHtml, flash, inputModal, openModal, resolvePath } from "./utils.js";
3
3
  import { registerAutoRefresh } from "./autoRefresh.js";
4
4
  let notificationsInitialized = false;
5
- let notificationItems = [];
5
+ const notificationItemsByRoot = {};
6
6
  let activeRoot = null;
7
7
  let closeModalFn = null;
8
8
  let documentListenerInstalled = false;
9
9
  let modalElements = null;
10
10
  let isRefreshing = false;
11
+ const DROPDOWN_MARGIN = 8;
12
+ const DROPDOWN_OFFSET = 6;
11
13
  const NOTIFICATIONS_REFRESH_ID = "notifications";
12
14
  const NOTIFICATIONS_REFRESH_MS = 15000;
13
15
  function getModalElements() {
@@ -27,10 +29,11 @@ function getRootElements(root) {
27
29
  const dropdown = root.querySelector("[data-notifications-dropdown]");
28
30
  if (!trigger || !badge || !dropdown)
29
31
  return null;
30
- return { root, trigger, badge, dropdown };
32
+ const key = root.getAttribute("data-notifications-root") || "hub";
33
+ return { root, trigger, badge, dropdown, key };
31
34
  }
32
- function setBadgeCount(count) {
33
- const roots = document.querySelectorAll("[data-notifications-root]");
35
+ function setBadgeCount(rootKey, count) {
36
+ const roots = document.querySelectorAll(`[data-notifications-root="${rootKey}"]`);
34
37
  roots.forEach((root) => {
35
38
  const elements = getRootElements(root);
36
39
  if (!elements)
@@ -40,7 +43,7 @@ function setBadgeCount(count) {
40
43
  elements.trigger.setAttribute("aria-label", count > 0 ? `Notifications (${count})` : "Notifications");
41
44
  });
42
45
  }
43
- function normalizeItem(item) {
46
+ function normalizeHubItem(item) {
44
47
  const repoId = String(item.repo_id || "");
45
48
  const repoDisplay = item.repo_display_name || repoId;
46
49
  const mode = item.dispatch?.mode || "";
@@ -51,6 +54,7 @@ function normalizeItem(item) {
51
54
  const runId = String(item.run_id || "");
52
55
  const openUrl = item.open_url || `/repos/${repoId}/?tab=inbox&run_id=${runId}`;
53
56
  return {
57
+ kind: "hub",
54
58
  repoId,
55
59
  repoDisplay,
56
60
  runId,
@@ -61,21 +65,42 @@ function normalizeItem(item) {
61
65
  body,
62
66
  isHandoff,
63
67
  openUrl,
68
+ pillLabel: isHandoff ? "handoff" : "paused",
64
69
  };
65
70
  }
71
+ function normalizePmaItem(item) {
72
+ const title = (item.title || "PMA dispatch").trim();
73
+ const body = item.body || "";
74
+ const priority = (item.priority || "info").toLowerCase();
75
+ const isHandoff = priority === "action";
76
+ return {
77
+ kind: "pma",
78
+ title,
79
+ body,
80
+ isHandoff,
81
+ openUrl: "/?view=pma",
82
+ pillLabel: priority,
83
+ priority,
84
+ links: item.links || [],
85
+ };
86
+ }
87
+ function getItemsForRoot(rootKey) {
88
+ return notificationItemsByRoot[rootKey] || [];
89
+ }
66
90
  function renderDropdown(root) {
67
91
  if (!root)
68
92
  return;
69
- if (!notificationItems.length) {
93
+ const items = getItemsForRoot(root.key);
94
+ if (!items.length) {
70
95
  root.dropdown.innerHTML = '<div class="notifications-empty muted small">No pending dispatches</div>';
71
96
  return;
72
97
  }
73
- const html = notificationItems
98
+ const html = items
74
99
  .map((item, index) => {
75
- const pill = item.isHandoff ? "handoff" : "paused";
100
+ const pill = item.pillLabel || (item.isHandoff ? "handoff" : "paused");
76
101
  return `
77
102
  <button class="notifications-item" type="button" data-index="${index}">
78
- <span class="notifications-item-repo">${escapeHtml(item.repoDisplay)}</span>
103
+ <span class="notifications-item-repo">${escapeHtml(item.repoDisplay || "PMA")}</span>
79
104
  <span class="notifications-item-title">${escapeHtml(item.title)}</span>
80
105
  <span class="pill pill-small pill-warn notifications-item-pill">${escapeHtml(pill)}</span>
81
106
  </button>
@@ -93,10 +118,40 @@ function closeDropdown() {
93
118
  if (!activeRoot)
94
119
  return;
95
120
  activeRoot.dropdown.classList.add("hidden");
121
+ activeRoot.dropdown.style.position = "";
122
+ activeRoot.dropdown.style.left = "";
123
+ activeRoot.dropdown.style.right = "";
124
+ activeRoot.dropdown.style.top = "";
125
+ activeRoot.dropdown.style.visibility = "";
96
126
  activeRoot.trigger.setAttribute("aria-expanded", "false");
97
127
  activeRoot = null;
98
128
  removeDocumentListener();
99
129
  }
130
+ function positionDropdown(root) {
131
+ const { trigger, dropdown } = root;
132
+ const triggerRect = trigger.getBoundingClientRect();
133
+ dropdown.style.position = "fixed";
134
+ dropdown.style.left = "0";
135
+ dropdown.style.right = "auto";
136
+ dropdown.style.top = "0";
137
+ dropdown.style.visibility = "hidden";
138
+ const dropdownRect = dropdown.getBoundingClientRect();
139
+ const width = dropdownRect.width || 240;
140
+ const height = dropdownRect.height || 0;
141
+ const viewportWidth = window.innerWidth;
142
+ const viewportHeight = window.innerHeight;
143
+ let left = triggerRect.right - width;
144
+ left = Math.min(Math.max(left, DROPDOWN_MARGIN), viewportWidth - width - DROPDOWN_MARGIN);
145
+ const preferredTop = triggerRect.bottom + DROPDOWN_OFFSET;
146
+ const fallbackTop = triggerRect.top - DROPDOWN_OFFSET - height;
147
+ let top = preferredTop;
148
+ if (preferredTop + height > viewportHeight - DROPDOWN_MARGIN) {
149
+ top = Math.max(DROPDOWN_MARGIN, fallbackTop);
150
+ }
151
+ dropdown.style.left = `${Math.max(DROPDOWN_MARGIN, left)}px`;
152
+ dropdown.style.top = `${Math.max(DROPDOWN_MARGIN, top)}px`;
153
+ dropdown.style.visibility = "";
154
+ }
100
155
  function openDropdown(root) {
101
156
  if (activeRoot && activeRoot !== root) {
102
157
  activeRoot.dropdown.classList.add("hidden");
@@ -105,6 +160,7 @@ function openDropdown(root) {
105
160
  activeRoot = root;
106
161
  renderDropdown(root);
107
162
  root.dropdown.classList.remove("hidden");
163
+ positionDropdown(root);
108
164
  root.trigger.setAttribute("aria-expanded", "true");
109
165
  installDocumentListener();
110
166
  }
@@ -141,35 +197,104 @@ function closeNotificationsModal() {
141
197
  closeModalFn();
142
198
  closeModalFn = null;
143
199
  }
200
+ function isSameNotification(a, b) {
201
+ return (a.kind === b.kind &&
202
+ a.repoId === b.repoId &&
203
+ a.runId === b.runId &&
204
+ a.seq === b.seq);
205
+ }
144
206
  function openNotificationsModal(item, returnFocusTo) {
145
207
  const modal = getModalElements();
146
208
  if (!modal)
147
209
  return;
148
210
  closeNotificationsModal();
149
- const runLabel = item.seq ? `${item.runId.slice(0, 8)} (#${item.seq})` : item.runId.slice(0, 8);
150
- const modeLabel = item.mode ? ` (${item.mode})` : "";
151
211
  const body = item.body?.trim() ? escapeHtml(item.body) : '<span class="muted">No message body.</span>';
152
- modal.body.innerHTML = `
153
- <div class="notifications-modal-meta">
154
- <div class="notifications-modal-row">
155
- <span class="notifications-modal-label">Repo</span>
156
- <span class="notifications-modal-value">${escapeHtml(item.repoDisplay)}</span>
212
+ if (item.kind === "pma") {
213
+ const priority = item.priority || "info";
214
+ const links = (item.links || [])
215
+ .map((link) => `<a href="${escapeHtml(link.href)}" target="_blank" rel="noopener">${escapeHtml(link.label)}</a>`)
216
+ .join("");
217
+ const linkBlock = links ? `<div class="notifications-modal-links">${links}</div>` : "";
218
+ modal.body.innerHTML = `
219
+ <div class="notifications-modal-meta">
220
+ <div class="notifications-modal-row">
221
+ <span class="notifications-modal-label">Dispatch</span>
222
+ <span class="notifications-modal-value">${escapeHtml(item.title)}</span>
223
+ </div>
224
+ <div class="notifications-modal-row">
225
+ <span class="notifications-modal-label">Priority</span>
226
+ <span class="notifications-modal-value">${escapeHtml(priority)}</span>
227
+ </div>
157
228
  </div>
158
- <div class="notifications-modal-row">
159
- <span class="notifications-modal-label">Run</span>
160
- <span class="notifications-modal-value mono">${escapeHtml(runLabel)}</span>
229
+ <div class="notifications-modal-body">${body}</div>
230
+ ${linkBlock}
231
+ <div class="notifications-modal-actions">
232
+ <a class="primary sm notifications-open-run" href="${escapeHtml(resolvePath(item.openUrl))}">Open PMA</a>
233
+ </div>
234
+ `;
235
+ }
236
+ else {
237
+ const runId = item.runId || "";
238
+ const runLabel = item.seq ? `${runId.slice(0, 8)} (#${item.seq})` : runId.slice(0, 8);
239
+ const modeLabel = item.mode ? ` (${item.mode})` : "";
240
+ modal.body.innerHTML = `
241
+ <div class="notifications-modal-meta">
242
+ <div class="notifications-modal-row">
243
+ <span class="notifications-modal-label">Repo</span>
244
+ <span class="notifications-modal-value">${escapeHtml(item.repoDisplay || "")}</span>
245
+ </div>
246
+ <div class="notifications-modal-row">
247
+ <span class="notifications-modal-label">Run</span>
248
+ <span class="notifications-modal-value mono">${escapeHtml(runLabel)}</span>
249
+ </div>
250
+ <div class="notifications-modal-row">
251
+ <span class="notifications-modal-label">Dispatch</span>
252
+ <span class="notifications-modal-value">${escapeHtml(item.title)}${escapeHtml(modeLabel)}</span>
253
+ </div>
161
254
  </div>
162
- <div class="notifications-modal-row">
163
- <span class="notifications-modal-label">Dispatch</span>
164
- <span class="notifications-modal-value">${escapeHtml(item.title)}${escapeHtml(modeLabel)}</span>
255
+ <div class="notifications-modal-body">${body}</div>
256
+ <div class="notifications-modal-actions">
257
+ <a class="primary sm notifications-open-run" href="${escapeHtml(resolvePath(item.openUrl))}">Open run</a>
258
+ ${item.seq ? '<button class="ghost sm notifications-dismiss" type="button">Dismiss</button>' : ""}
165
259
  </div>
166
- </div>
167
- <div class="notifications-modal-body">${body}</div>
168
- <div class="notifications-modal-actions">
169
- <a class="primary sm notifications-open-run" href="${escapeHtml(resolvePath(item.openUrl))}">Open run</a>
170
- </div>
171
- <div class="notifications-modal-placeholder">Reply here (coming soon).</div>
172
- `;
260
+ <div class="notifications-modal-placeholder">Reply here (coming soon).</div>
261
+ `;
262
+ const dismissBtn = modal.body.querySelector(".notifications-dismiss");
263
+ if (dismissBtn && item.seq) {
264
+ dismissBtn.addEventListener("click", async () => {
265
+ const confirmed = await confirmModal("Dismiss this inbox item?", {
266
+ confirmText: "Dismiss",
267
+ danger: false,
268
+ });
269
+ if (!confirmed)
270
+ return;
271
+ const reason = await inputModal("Dismiss reason (optional)", {
272
+ placeholder: "obsolete, resolved elsewhere, ...",
273
+ confirmText: "Dismiss",
274
+ allowEmpty: true,
275
+ });
276
+ if (reason === null)
277
+ return;
278
+ await api("/hub/messages/dismiss", {
279
+ method: "POST",
280
+ body: {
281
+ repo_id: item.repoId,
282
+ run_id: item.runId,
283
+ seq: item.seq,
284
+ reason,
285
+ },
286
+ });
287
+ const hubItems = getItemsForRoot("hub").filter((entry) => !isSameNotification(entry, item));
288
+ notificationItemsByRoot.hub = hubItems;
289
+ setBadgeCount("hub", hubItems.length);
290
+ if (activeRoot && activeRoot.key === "hub") {
291
+ renderDropdown(activeRoot);
292
+ }
293
+ closeNotificationsModal();
294
+ flash("Dispatch dismissed");
295
+ });
296
+ }
297
+ }
173
298
  closeModalFn = openModal(modal.overlay, {
174
299
  closeOnEscape: true,
175
300
  closeOnOverlay: true,
@@ -182,10 +307,28 @@ async function refreshNotifications(_ctx) {
182
307
  return;
183
308
  isRefreshing = true;
184
309
  try {
185
- const payload = (await api("/hub/messages", { method: "GET" }));
186
- const items = payload?.items || [];
187
- notificationItems = items.map(normalizeItem);
188
- setBadgeCount(notificationItems.length);
310
+ let hubPayload = null;
311
+ let pmaPayload = null;
312
+ try {
313
+ hubPayload = (await api("/hub/messages", { method: "GET" }));
314
+ }
315
+ catch {
316
+ hubPayload = null;
317
+ }
318
+ try {
319
+ pmaPayload = (await api("/hub/pma/dispatches?include_resolved=false", {
320
+ method: "GET",
321
+ }));
322
+ }
323
+ catch {
324
+ pmaPayload = { items: [] };
325
+ }
326
+ const hubItems = (hubPayload?.items || []).map(normalizeHubItem);
327
+ const pmaItems = (pmaPayload?.items || []).map(normalizePmaItem);
328
+ notificationItemsByRoot.hub = hubItems;
329
+ notificationItemsByRoot.pma = pmaItems;
330
+ setBadgeCount("hub", hubItems.length);
331
+ setBadgeCount("pma", pmaItems.length);
189
332
  if (activeRoot) {
190
333
  renderDropdown(activeRoot);
191
334
  }
@@ -215,12 +358,20 @@ function attachRoot(root) {
215
358
  const target = event.target?.closest(".notifications-item");
216
359
  if (!target)
217
360
  return;
361
+ event.preventDefault();
362
+ event.stopPropagation();
218
363
  const index = Number(target.dataset.index || "-1");
219
- const item = notificationItems[index];
364
+ const items = getItemsForRoot(root.key);
365
+ const item = items[index];
220
366
  if (!item)
221
367
  return;
222
368
  closeDropdown();
223
- openNotificationsModal(item, root.trigger);
369
+ const mouseEvent = event;
370
+ if (mouseEvent.shiftKey) {
371
+ openNotificationsModal(item, root.trigger);
372
+ return;
373
+ }
374
+ window.location.href = resolvePath(item.openUrl);
224
375
  });
225
376
  }
226
377
  function attachModalHandlers() {
@@ -2,12 +2,13 @@
2
2
  /**
3
3
  * PMA (Project Management Agent) - Hub-level chat interface
4
4
  */
5
- import { api, confirmModal, resolvePath, getAuthToken, flash } from "./utils.js";
5
+ import { api, resolvePath, getAuthToken, flash } from "./utils.js";
6
6
  import { createDocChat, } from "./docChatCore.js";
7
7
  import { initChatPasteUpload } from "./chatUploads.js";
8
8
  import { clearAgentSelectionStorage, getSelectedAgent, getSelectedModel, getSelectedReasoning, initAgentControls, refreshAgentControls, } from "./agentControls.js";
9
9
  import { createFileBoxWidget } from "./fileboxUi.js";
10
10
  import { extractContextRemainingPercent } from "./streamUtils.js";
11
+ import { initNotificationBell } from "./notificationBell.js";
11
12
  const pmaStyling = {
12
13
  eventClass: "chat-event",
13
14
  eventTitleClass: "chat-event-title",
@@ -44,6 +45,7 @@ let isUnloading = false;
44
45
  let unloadHandlerInstalled = false;
45
46
  let currentEventsController = null;
46
47
  const PMA_PENDING_TURN_KEY = "car.pma.pendingTurn";
48
+ const PMA_VIEW_KEY = "car.pma.view";
47
49
  const DEFAULT_PMA_LANE_ID = "pma:default";
48
50
  let fileBoxCtrl = null;
49
51
  let pendingUploadNames = [];
@@ -94,6 +96,29 @@ function clearPendingTurn() {
94
96
  // ignore
95
97
  }
96
98
  }
99
+ function loadPMAView() {
100
+ const raw = localStorage.getItem(PMA_VIEW_KEY);
101
+ if (raw === "memory")
102
+ return "memory";
103
+ return "chat";
104
+ }
105
+ function setPMAView(view, options = {}) {
106
+ const elements = getElements();
107
+ const { persist = true } = options;
108
+ if (persist) {
109
+ localStorage.setItem(PMA_VIEW_KEY, view);
110
+ }
111
+ if (elements.shell) {
112
+ elements.shell.setAttribute("data-pma-view", view);
113
+ }
114
+ document.querySelectorAll(".pma-view-btn").forEach((btn) => {
115
+ const isActive = btn.dataset.view === view;
116
+ btn.classList.toggle("active", isActive);
117
+ btn.setAttribute("aria-selected", isActive ? "true" : "false");
118
+ });
119
+ elements.chatSection?.classList.toggle("hidden", view !== "chat");
120
+ elements.docsSection?.classList.toggle("hidden", view !== "memory");
121
+ }
97
122
  async function initFileBoxUI() {
98
123
  const elements = getElements();
99
124
  if (!elements.inboxFiles || !elements.outboxFiles)
@@ -124,6 +149,7 @@ async function initFileBoxUI() {
124
149
  }
125
150
  pendingUploadNames = [];
126
151
  }
152
+ updateClearButtons(listing);
127
153
  },
128
154
  onUpload: (names) => {
129
155
  pendingUploadNames = names;
@@ -172,11 +198,15 @@ async function loadPMADocContent(name) {
172
198
  return payload?.content || "";
173
199
  }
174
200
  catch (err) {
201
+ const content = await bootstrapPMADoc(name);
202
+ if (content) {
203
+ return content;
204
+ }
175
205
  flash(`Failed to load ${name}`, "error");
176
206
  return "";
177
207
  }
178
208
  }
179
- async function loadPMADocDefaultContent(name) {
209
+ async function loadPMADocDefaultContent(name, options = {}) {
180
210
  try {
181
211
  const payload = (await api(`/hub/pma/docs/default/${encodeURIComponent(name)}`, {
182
212
  method: "GET",
@@ -184,10 +214,28 @@ async function loadPMADocDefaultContent(name) {
184
214
  return payload?.content || "";
185
215
  }
186
216
  catch (err) {
187
- flash(`Failed to load default ${name}`, "error");
217
+ if (!options.silent) {
218
+ flash(`Failed to load default ${name}`, "error");
219
+ }
188
220
  return "";
189
221
  }
190
222
  }
223
+ async function bootstrapPMADoc(name) {
224
+ const content = await loadPMADocDefaultContent(name, { silent: true });
225
+ if (!content)
226
+ return "";
227
+ try {
228
+ await api(`/hub/pma/docs/${encodeURIComponent(name)}`, {
229
+ method: "PUT",
230
+ body: { content },
231
+ });
232
+ await loadPMADocs();
233
+ return content;
234
+ }
235
+ catch {
236
+ return content;
237
+ }
238
+ }
191
239
  async function savePMADoc(name, content) {
192
240
  if (isSavingDoc)
193
241
  return;
@@ -282,9 +330,8 @@ async function snapshotActiveContext() {
282
330
  flash("Failed to snapshot active context", "error");
283
331
  }
284
332
  }
285
- async function resetActiveContext() {
286
- const confirmed = await confirmModal("Reset active context to default?");
287
- if (!confirmed)
333
+ function resetActiveContext() {
334
+ if (!confirm("Reset active context to default?"))
288
335
  return;
289
336
  const editor = document.getElementById("pma-docs-editor");
290
337
  if (!editor)
@@ -356,6 +403,7 @@ async function pollForTurnMeta(clientTurnId, options = {}) {
356
403
  function getElements() {
357
404
  return {
358
405
  shell: document.getElementById("pma-shell"),
406
+ chatSection: document.getElementById("pma-chat-section"),
359
407
  input: document.getElementById("pma-chat-input"),
360
408
  sendBtn: document.getElementById("pma-chat-send"),
361
409
  cancelBtn: document.getElementById("pma-chat-cancel"),
@@ -376,6 +424,8 @@ function getElements() {
376
424
  inboxFiles: document.getElementById("pma-inbox-files"),
377
425
  outboxFiles: document.getElementById("pma-outbox-files"),
378
426
  outboxRefresh: document.getElementById("pma-outbox-refresh"),
427
+ inboxClear: document.getElementById("pma-inbox-clear"),
428
+ outboxClear: document.getElementById("pma-outbox-clear"),
379
429
  threadInfo: document.getElementById("pma-thread-info"),
380
430
  threadInfoAgent: document.getElementById("pma-thread-info-agent"),
381
431
  threadInfoThreadId: document.getElementById("pma-thread-info-thread-id"),
@@ -471,6 +521,8 @@ async function initPMA() {
471
521
  await initFileBoxUI();
472
522
  await loadPMADocs();
473
523
  attachHandlers();
524
+ setPMAView(loadPMAView(), { persist: false });
525
+ initNotificationBell();
474
526
  // If we refreshed mid-turn, recover the final output from the server.
475
527
  await resumePendingTurn();
476
528
  // If the page refreshes/navigates while a turn is running, avoid showing a noisy
@@ -543,6 +595,23 @@ async function loadPMAThreadInfo() {
543
595
  elements.threadInfo?.classList.add("hidden");
544
596
  }
545
597
  }
598
+ function updateClearButtons(listing) {
599
+ const elements = getElements();
600
+ if (!elements.inboxClear || !elements.outboxClear)
601
+ return;
602
+ const inboxCount = listing?.inbox?.length ?? 0;
603
+ const outboxCount = listing?.outbox?.length ?? 0;
604
+ elements.inboxClear.classList.toggle("hidden", inboxCount <= 1);
605
+ elements.outboxClear.classList.toggle("hidden", outboxCount <= 1);
606
+ }
607
+ async function clearPMABox(box) {
608
+ const confirmed = window.confirm(`Clear ${box}? This will delete all files.`);
609
+ if (!confirmed)
610
+ return;
611
+ await api(`/hub/pma/files/${box}`, { method: "DELETE" });
612
+ flash(`Cleared ${box}`, "info");
613
+ await fileBoxCtrl?.refresh();
614
+ }
546
615
  async function sendMessage() {
547
616
  const elements = getElements();
548
617
  if (!elements.input || !pmaChat)
@@ -1006,6 +1075,14 @@ async function startNewThreadOnServer() {
1006
1075
  }
1007
1076
  function attachHandlers() {
1008
1077
  const elements = getElements();
1078
+ document.addEventListener("click", (event) => {
1079
+ const target = event.target;
1080
+ const btn = target?.closest?.(".pma-view-btn");
1081
+ if (!btn)
1082
+ return;
1083
+ const value = (btn.dataset.view || "chat");
1084
+ setPMAView(value);
1085
+ });
1009
1086
  if (elements.sendBtn) {
1010
1087
  elements.sendBtn.addEventListener("click", () => {
1011
1088
  void sendMessage();
@@ -1066,12 +1143,22 @@ function attachHandlers() {
1066
1143
  void fileBoxCtrl?.refresh();
1067
1144
  });
1068
1145
  }
1146
+ if (elements.inboxClear) {
1147
+ elements.inboxClear.addEventListener("click", () => {
1148
+ void clearPMABox("inbox");
1149
+ });
1150
+ }
1151
+ if (elements.outboxClear) {
1152
+ elements.outboxClear.addEventListener("click", () => {
1153
+ void clearPMABox("outbox");
1154
+ });
1155
+ }
1069
1156
  if (elements.scanReposBtn) {
1070
1157
  elements.scanReposBtn.addEventListener("click", async () => {
1158
+ const btn = elements.scanReposBtn;
1159
+ const originalText = btn.textContent || "";
1071
1160
  try {
1072
- const btn = elements.scanReposBtn;
1073
1161
  btn.disabled = true;
1074
- btn.textContent = "Scanning…";
1075
1162
  await api("/hub/repos/scan", { method: "POST" });
1076
1163
  flash("Repositories scanned", "info");
1077
1164
  }
@@ -1079,9 +1166,8 @@ function attachHandlers() {
1079
1166
  flash("Failed to scan repos", "error");
1080
1167
  }
1081
1168
  finally {
1082
- const btn = elements.scanReposBtn;
1083
1169
  btn.disabled = false;
1084
- btn.textContent = "Scan repos";
1170
+ btn.textContent = btn.textContent || originalText;
1085
1171
  }
1086
1172
  });
1087
1173
  }
@@ -1132,31 +1218,6 @@ function attachHandlers() {
1132
1218
  void snapshotActiveContext();
1133
1219
  });
1134
1220
  }
1135
- const pmaModeManual = document.getElementById("pma-mode-manual");
1136
- const pmaModePma = document.getElementById("pma-mode-pma");
1137
- const docsSection = document.getElementById("pma-docs-section");
1138
- if (pmaModeManual && pmaModePma) {
1139
- const handleModeChange = (mode) => {
1140
- if (!docsSection)
1141
- return;
1142
- if (mode === "manual") {
1143
- docsSection.classList.remove("hidden");
1144
- }
1145
- else {
1146
- docsSection.classList.add("hidden");
1147
- }
1148
- };
1149
- pmaModeManual.addEventListener("click", () => {
1150
- if (pmaModeManual.dataset.hubMode === "manual") {
1151
- handleModeChange("manual");
1152
- }
1153
- });
1154
- pmaModePma.addEventListener("click", () => {
1155
- if (pmaModePma.dataset.hubMode === "pma") {
1156
- handleModeChange("pma");
1157
- }
1158
- });
1159
- }
1160
1221
  if (elements.docsEditor) {
1161
1222
  elements.docsEditor.addEventListener("input", () => {
1162
1223
  elements.docsEditor.style.height = "auto";