@firstpick/pi-package-webui 0.1.3 → 0.1.4

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.
package/public/app.js CHANGED
@@ -27,6 +27,10 @@ const elements = {
27
27
  newSessionButton: $("#newSessionButton"),
28
28
  compactButton: $("#compactButton"),
29
29
  gitWorkflowButton: $("#gitWorkflowButton"),
30
+ publishButton: $("#publishButton"),
31
+ publishMenu: $("#publishMenu"),
32
+ releaseNpmButton: $("#releaseNpmButton"),
33
+ releaseAurButton: $("#releaseAurButton"),
30
34
  gitWorkflowPanel: $("#gitWorkflowPanel"),
31
35
  gitWorkflowTitle: $("#gitWorkflowTitle"),
32
36
  gitWorkflowHint: $("#gitWorkflowHint"),
@@ -41,6 +45,8 @@ const elements = {
41
45
  themeSelect: $("#themeSelect"),
42
46
  networkStatus: $("#networkStatus"),
43
47
  openNetworkButton: $("#openNetworkButton"),
48
+ agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
49
+ agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
44
50
  toggleSidePanelButton: $("#toggleSidePanelButton"),
45
51
  sidePanelExpandButton: $("#sidePanelExpandButton"),
46
52
  sidePanelBackdrop: $("#sidePanelBackdrop"),
@@ -75,6 +81,8 @@ let tabSeenCompletionSerials = new Map();
75
81
  let streamBubble = null;
76
82
  let streamText = null;
77
83
  let streamRawText = "";
84
+ let streamBubbleVisibleSince = 0;
85
+ let streamBubbleHideTimer = null;
78
86
  let streamThinkingBubble = null;
79
87
  let streamThinking = null;
80
88
  let runIndicatorBubble = null;
@@ -103,6 +111,8 @@ let commandSuggestions = [];
103
111
  let pathSuggestions = [];
104
112
  let suggestionMode = "none";
105
113
  let commandSuggestIndex = 0;
114
+ let lastPointerPosition = null;
115
+ let pathSuggestActiveQuery = null;
106
116
  let pathSuggestRequestSerial = 0;
107
117
  let pathSuggestAbortController = null;
108
118
  let latestStats = null;
@@ -116,6 +126,10 @@ let actionFeedbackSendBusy = false;
116
126
  let blockedTabNotificationKeys = new Set();
117
127
  let blockedTabNotificationPermissionRequested = false;
118
128
  let blockedTabNotificationFallbackNoted = false;
129
+ let agentDoneNotificationsEnabled = false;
130
+ let agentDoneNotificationPermissionRequested = false;
131
+ let agentDoneNotificationFallbackNoted = false;
132
+ let agentDoneNotificationKeys = new Set();
119
133
  let availableModels = [];
120
134
  let availableThemes = [];
121
135
  let currentThemeName = "catppuccin-mocha";
@@ -129,6 +143,7 @@ let lastChatProgrammaticScrollAt = 0;
129
143
  let chatUserScrollIntentUntil = 0;
130
144
  let mobileFooterExpanded = false;
131
145
  let footerModelPickerOpen = false;
146
+ let publishMenuOpen = false;
132
147
  let maxVisualViewportHeight = 0;
133
148
  let currentRunStartedAt = null;
134
149
  let currentRunStreamChars = 0;
@@ -137,6 +152,7 @@ const dialogQueue = [];
137
152
  const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
138
153
  const TAB_STORAGE_KEY = "pi-webui-active-tab";
139
154
  const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
155
+ const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
140
156
  const THEME_STORAGE_KEY = "pi-webui-theme";
141
157
  const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
142
158
  const DEFAULT_THEME_NAME = "catppuccin-mocha";
@@ -150,6 +166,8 @@ const CHAT_USER_SCROLL_INTENT_MS = 700;
150
166
  const RUN_INDICATOR_TICK_MS = 1000;
151
167
  const RUN_INDICATOR_START_GRACE_MS = 2500;
152
168
  const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
169
+ const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
170
+ const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
153
171
  const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
154
172
  const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
155
173
  const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
@@ -157,6 +175,7 @@ const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
157
175
  const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "idle", "working"];
158
176
  const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
159
177
  const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
178
+ const AGENT_DONE_NOTIFICATION_TAG_PREFIX = "pi-webui-agent-done";
160
179
  const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
161
180
  const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
162
181
  const statusEntries = new Map();
@@ -213,10 +232,77 @@ function readStoredSidePanelCollapsed() {
213
232
  }
214
233
  }
215
234
 
235
+ function readStoredAgentDoneNotificationsEnabled() {
236
+ try {
237
+ return localStorage.getItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY) === "1";
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+
243
+ function persistAgentDoneNotificationsEnabled(enabled) {
244
+ try {
245
+ localStorage.setItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY, enabled ? "1" : "0");
246
+ } catch {
247
+ // Ignore storage failures; the toggle should still work for this page load.
248
+ }
249
+ }
250
+
251
+ function agentDoneNotificationsStatusText() {
252
+ if (!browserNotificationSupported()) return "Unavailable here";
253
+ const permission = browserNotificationPermission();
254
+ if (permission === "denied") return "Permission denied";
255
+ if (agentDoneNotificationsEnabled) return permission === "granted" ? "On" : "Permission needed";
256
+ return permission === "granted" ? "Off · permission granted" : "Off";
257
+ }
258
+
259
+ function renderAgentDoneNotificationsToggle() {
260
+ if (!elements.agentDoneNotificationsToggle) return;
261
+ const supported = browserNotificationSupported();
262
+ const permission = browserNotificationPermission();
263
+ elements.agentDoneNotificationsToggle.checked = agentDoneNotificationsEnabled;
264
+ elements.agentDoneNotificationsToggle.disabled = !supported || permission === "denied";
265
+ elements.agentDoneNotificationsToggle.setAttribute("aria-describedby", "agentDoneNotificationsStatus");
266
+ if (elements.agentDoneNotificationsStatus) elements.agentDoneNotificationsStatus.textContent = agentDoneNotificationsStatusText();
267
+ }
268
+
269
+ async function setAgentDoneNotificationsEnabled(enabled, { requestPermission = false, announce = false } = {}) {
270
+ let next = !!enabled;
271
+ if (next) {
272
+ if (!browserNotificationSupported()) {
273
+ addEvent("agent-done notifications require HTTPS or localhost", "warn");
274
+ next = false;
275
+ } else if (browserNotificationPermission() === "denied") {
276
+ addEvent("agent-done notifications are blocked by browser permission", "warn");
277
+ next = false;
278
+ } else if (requestPermission && browserNotificationPermission() !== "granted") {
279
+ next = await ensureAgentDoneNotificationPermission();
280
+ if (!next) addEvent("agent-done notifications not enabled; browser permission was not granted", "warn");
281
+ } else if (browserNotificationPermission() !== "granted") {
282
+ next = false;
283
+ }
284
+ }
285
+ agentDoneNotificationsEnabled = next;
286
+ persistAgentDoneNotificationsEnabled(next);
287
+ renderAgentDoneNotificationsToggle();
288
+ if (announce) addEvent(next ? "agent-done notifications enabled" : "agent-done notifications disabled", next ? "info" : "warn");
289
+ return next;
290
+ }
291
+
292
+ function restoreAgentDoneNotificationsSetting() {
293
+ agentDoneNotificationsEnabled = readStoredAgentDoneNotificationsEnabled();
294
+ if (agentDoneNotificationsEnabled && (!browserNotificationSupported() || browserNotificationPermission() !== "granted")) {
295
+ agentDoneNotificationsEnabled = false;
296
+ persistAgentDoneNotificationsEnabled(false);
297
+ }
298
+ renderAgentDoneNotificationsToggle();
299
+ }
300
+
216
301
  function setComposerActionsOpen(open) {
217
302
  const shouldOpen = open && isMobileView();
218
303
  document.body.classList.toggle("composer-actions-open", shouldOpen);
219
304
  elements.composerActionsButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
305
+ if (!shouldOpen) setPublishMenuOpen(false);
220
306
  }
221
307
 
222
308
  function isRunActive() {
@@ -1211,6 +1297,7 @@ async function refreshTabs({ selectStored = false } = {}) {
1211
1297
  tabs = response.data?.tabs || [];
1212
1298
  syncTabMetadata(tabs);
1213
1299
  syncBlockedTabNotificationsFromTabs(tabs, previousTabs);
1300
+ syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
1214
1301
  const stored = selectStored ? restoreStoredTabId() : null;
1215
1302
  if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
1216
1303
  activeTabId = (stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null;
@@ -1312,15 +1399,23 @@ function addEvent(message, level = "info") {
1312
1399
  while (elements.eventLog.children.length > 120) elements.eventLog.lastElementChild?.remove();
1313
1400
  }
1314
1401
 
1315
- function blockedTabNotificationSupported() {
1402
+ function browserNotificationSupported() {
1316
1403
  return "Notification" in window && window.isSecureContext;
1317
1404
  }
1318
1405
 
1319
- function blockedTabNotificationPermission() {
1406
+ function browserNotificationPermission() {
1320
1407
  if (!("Notification" in window)) return "unsupported";
1321
1408
  return Notification.permission || "default";
1322
1409
  }
1323
1410
 
1411
+ function blockedTabNotificationSupported() {
1412
+ return browserNotificationSupported();
1413
+ }
1414
+
1415
+ function blockedTabNotificationPermission() {
1416
+ return browserNotificationPermission();
1417
+ }
1418
+
1324
1419
  async function ensureBlockedTabNotificationPermission() {
1325
1420
  if (!blockedTabNotificationSupported()) return false;
1326
1421
  if (Notification.permission === "granted") return true;
@@ -1339,6 +1434,24 @@ async function ensureBlockedTabNotificationPermission() {
1339
1434
  return false;
1340
1435
  }
1341
1436
 
1437
+ async function ensureAgentDoneNotificationPermission() {
1438
+ if (!browserNotificationSupported()) return false;
1439
+ if (Notification.permission === "granted") return true;
1440
+ if (Notification.permission === "denied" || agentDoneNotificationPermissionRequested || typeof Notification.requestPermission !== "function") return false;
1441
+
1442
+ agentDoneNotificationPermissionRequested = true;
1443
+ try {
1444
+ const permission = await Notification.requestPermission();
1445
+ if (permission === "granted") {
1446
+ addEvent("browser notifications enabled for completed agent work", "info");
1447
+ return true;
1448
+ }
1449
+ } catch (error) {
1450
+ addEvent(`agent-done notification permission request failed: ${error.message}`, "warn");
1451
+ }
1452
+ return false;
1453
+ }
1454
+
1342
1455
  function noteBlockedTabNotificationFallback(reason) {
1343
1456
  if (blockedTabNotificationFallbackNoted) return;
1344
1457
  blockedTabNotificationFallbackNoted = true;
@@ -1423,6 +1536,97 @@ function notifyBlockedTab(tabOrId, { request = null, count } = {}) {
1423
1536
  showBlockedTabBrowserNotification({ tabId, title, body, method, count: pendingCount });
1424
1537
  }
1425
1538
 
1539
+ function noteAgentDoneNotificationFallback(reason) {
1540
+ if (agentDoneNotificationFallbackNoted) return;
1541
+ agentDoneNotificationFallbackNoted = true;
1542
+ addEvent(`browser notifications unavailable for completed agent work: ${reason}`, "warn");
1543
+ }
1544
+
1545
+ async function showAgentDoneBrowserNotification({ tabId, title, body }) {
1546
+ if (!agentDoneNotificationsEnabled) return false;
1547
+ if (!browserNotificationSupported()) {
1548
+ noteAgentDoneNotificationFallback("requires HTTPS or localhost");
1549
+ renderAgentDoneNotificationsToggle();
1550
+ return false;
1551
+ }
1552
+ if (!(await ensureAgentDoneNotificationPermission())) {
1553
+ const permission = browserNotificationPermission();
1554
+ noteAgentDoneNotificationFallback(permission === "denied" ? "permission denied" : "permission not granted");
1555
+ if (permission !== "granted") {
1556
+ agentDoneNotificationsEnabled = false;
1557
+ persistAgentDoneNotificationsEnabled(false);
1558
+ }
1559
+ renderAgentDoneNotificationsToggle();
1560
+ return false;
1561
+ }
1562
+
1563
+ const options = {
1564
+ body,
1565
+ tag: `${AGENT_DONE_NOTIFICATION_TAG_PREFIX}:${tabId}`,
1566
+ renotify: true,
1567
+ requireInteraction: false,
1568
+ icon: BLOCKED_TAB_NOTIFICATION_ICON,
1569
+ badge: BLOCKED_TAB_NOTIFICATION_ICON,
1570
+ data: { tabId, url: location.href },
1571
+ };
1572
+
1573
+ try {
1574
+ let registration = null;
1575
+ if ("serviceWorker" in navigator) {
1576
+ registration = await Promise.race([navigator.serviceWorker.ready, delay(1200).then(() => null)]).catch(() => null);
1577
+ }
1578
+ if (registration?.showNotification) {
1579
+ await registration.showNotification(title, options);
1580
+ return true;
1581
+ }
1582
+
1583
+ const notification = new Notification(title, options);
1584
+ notification.onclick = () => {
1585
+ window.focus();
1586
+ if (tabId && tabId !== activeTabId) switchTab(tabId).catch((error) => addEvent(error.message, "error"));
1587
+ notification.close();
1588
+ };
1589
+ return true;
1590
+ } catch (error) {
1591
+ noteAgentDoneNotificationFallback(error.message || "notification failed");
1592
+ return false;
1593
+ }
1594
+ }
1595
+
1596
+ function agentDoneNotificationKey(tabId, activity = {}) {
1597
+ const serial = Number(activity?.completionSerial);
1598
+ return `${tabId}:${Number.isFinite(serial) && serial > 0 ? serial : "done"}`;
1599
+ }
1600
+
1601
+ function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
1602
+ if (!agentDoneNotificationsEnabled) return;
1603
+ const tabId = typeof tabOrId === "string" ? tabOrId : tabOrId?.id || activeTabId;
1604
+ if (!tabId) return;
1605
+ const tab = typeof tabOrId === "object" && tabOrId !== null ? tabOrId : tabs.find((item) => item.id === tabId);
1606
+ const normalizedActivity = normalizeTabActivity(activity || tab?.activity || activityForTab(tab));
1607
+ if (!normalizedActivity.completionSerial) return;
1608
+ const key = agentDoneNotificationKey(tabId, normalizedActivity);
1609
+ if (agentDoneNotificationKeys.has(key)) return;
1610
+ agentDoneNotificationKeys.add(key);
1611
+
1612
+ const displayTitle = tabTitle || tab?.title || "terminal";
1613
+ showAgentDoneBrowserNotification({
1614
+ tabId,
1615
+ title: "Pi finished work",
1616
+ body: `${displayTitle} finished its agent run.`,
1617
+ });
1618
+ }
1619
+
1620
+ function syncAgentDoneNotificationsFromTabs(nextTabs = [], previousTabs = []) {
1621
+ if (!agentDoneNotificationsEnabled || previousTabs.length === 0) return;
1622
+ const previousSerials = new Map(previousTabs.filter((tab) => tab?.id).map((tab) => [tab.id, normalizeTabActivity(tab.activity).completionSerial]));
1623
+ for (const tab of nextTabs) {
1624
+ if (!tab?.id || !previousSerials.has(tab.id)) continue;
1625
+ const activity = normalizeTabActivity(tab.activity);
1626
+ if (!activity.isWorking && activity.completionSerial > previousSerials.get(tab.id)) notifyAgentDone(tab, { activity });
1627
+ }
1628
+ }
1629
+
1426
1630
  function syncBlockedTabNotificationsFromTabs(nextTabs = [], previousTabs = []) {
1427
1631
  if (previousTabs.length === 0) return;
1428
1632
  const previousCounts = new Map(previousTabs.filter((tab) => tab?.id).map((tab) => [tab.id, tabPendingBlockerCount(tab)]));
@@ -2185,6 +2389,51 @@ function isGuardrailDialogPrompt(prompt) {
2185
2389
  return /(?:dangerous|high-risk|protected).*(?:command|file)|safety rule|execute anyway\?/i.test(`${plainTitle}\n${plainMessage}`);
2186
2390
  }
2187
2391
 
2392
+ function releaseDialogPromptParts(prompt) {
2393
+ const combined = [prompt.title, prompt.message].filter((part) => stripAnsi(part).trim()).join("\n").trimEnd();
2394
+ const lines = combined.split("\n");
2395
+ const questionIndex = lines.findIndex((line) => /^(Publish eligible packages now\?|Publish to AUR\?|Publish newly created\/converged AUR package\?)$/i.test(stripAnsi(line).trim()));
2396
+ const question = questionIndex === -1 ? "Publish eligible packages now?" : stripAnsi(lines[questionIndex]).trim();
2397
+ const isNpmReleasePrompt = /Release preflight summary:/i.test(combined) && /Publish eligible packages now\?/i.test(combined);
2398
+ const isAurReleasePrompt = /AUR release summary:/i.test(combined) && questionIndex !== -1;
2399
+ if (!isNpmReleasePrompt && !isAurReleasePrompt) return null;
2400
+
2401
+ const summaryLines = questionIndex === -1 ? lines : [...lines.slice(0, questionIndex), ...lines.slice(questionIndex + 1)];
2402
+ const message = summaryLines.join("\n").replace(/\n+$/, "");
2403
+ return {
2404
+ title: question,
2405
+ message,
2406
+ plainMessage: stripAnsi(message),
2407
+ };
2408
+ }
2409
+
2410
+ function releaseDialogLineClass(plainLine, section) {
2411
+ const text = plainLine.trim();
2412
+ if (!text) return "release-dialog-spacer";
2413
+ if (/^(Release preflight summary|AUR release summary|Version changes|Bump summary|Will publish|Will skip|Blocked|Other|Publish targets after confirmation|Missing local package dirs):$/i.test(text)) {
2414
+ return "release-dialog-heading";
2415
+ }
2416
+ if (/^none$/i.test(text)) return "release-dialog-muted";
2417
+ if (/->\s*error\b|\bfailed\b|\bmissing\b|\berrors?:\s*[1-9]/i.test(text) || /^blocked$/i.test(section)) return "release-dialog-danger";
2418
+ if (/publish-(?:first|update)|would bump up|first release/i.test(text) || /^(will publish|publish targets after confirmation)$/i.test(section)) return "release-dialog-success";
2419
+ if (/\bskip(?:ped)?\b|\bunchanged\b|would reduce down|already published/i.test(text) || /^will skip$/i.test(section)) return "release-dialog-warning";
2420
+ return "";
2421
+ }
2422
+
2423
+ function renderReleaseDialogMessage(parent, text) {
2424
+ parent.replaceChildren();
2425
+ let section = "";
2426
+ for (const line of String(text || "").split("\n")) {
2427
+ const plainLine = stripAnsi(line);
2428
+ const heading = plainLine.trim().match(/^(Release preflight summary|Version changes|Bump summary|Will publish|Will skip|Blocked|Other|Publish targets after confirmation|Missing local package dirs):$/i);
2429
+ const rowClass = ["release-dialog-line", releaseDialogLineClass(plainLine, section)].filter(Boolean).join(" ");
2430
+ const row = make("span", rowClass);
2431
+ renderAnsiText(row, line || " ");
2432
+ parent.append(row);
2433
+ if (heading) section = heading[1].toLowerCase();
2434
+ }
2435
+ }
2436
+
2188
2437
  function stripTodoProgressLines(text, { streaming = false } = {}) {
2189
2438
  let inFence = false;
2190
2439
  const kept = [];
@@ -2272,9 +2521,202 @@ function renderTodoProgressWidget(_key, lines) {
2272
2521
  return node;
2273
2522
  }
2274
2523
 
2524
+ function getWidgetLines(key) {
2525
+ const value = widgets.get(key);
2526
+ return Array.isArray(value?.widgetLines) ? value.widgetLines : [];
2527
+ }
2528
+
2529
+ function releaseNpmFooterDetails(lines) {
2530
+ const primary = cleanStatusText(lines[0] || "").replace(/^release-(?:npm|aur):\s*/i, "");
2531
+ const parts = primary.split(/\s+·\s+/).map((part) => part.trim()).filter(Boolean);
2532
+ return {
2533
+ phase: parts[0] || "release workflow",
2534
+ mode: parts[1] || "",
2535
+ elapsed: parts[2] || "",
2536
+ controls: lines.slice(1).map(cleanStatusText).filter(Boolean).join(" · "),
2537
+ };
2538
+ }
2539
+
2540
+ function releaseNpmLineTone(line) {
2541
+ const clean = stripAnsi(line).trim();
2542
+ if (/^\$\s+/.test(clean)) return "command";
2543
+ if (/^==>/.test(clean)) return "target";
2544
+ if (/^(PASS|✓|Published)\b/i.test(clean)) return "pass";
2545
+ if (/^(FAIL|ERROR|ERR|✗)\b/i.test(clean)) return "fail";
2546
+ if (/^(WARN|warning)\b/i.test(clean)) return "warn";
2547
+ if (/^(INFO|npm notice|notice)\b/i.test(clean)) return "info";
2548
+ if (/^RELEASE_NPM_EVENT\b/.test(clean)) return "event";
2549
+ return "";
2550
+ }
2551
+
2552
+ function appendReleaseNpmTerminalLine(parent, line) {
2553
+ const tone = releaseNpmLineTone(line);
2554
+ const row = make("div", `release-npm-line${tone ? ` ${tone}` : ""}`);
2555
+ if (String(line ?? "") === "") row.textContent = "\u00a0";
2556
+ else renderAnsiText(row, line);
2557
+ parent.append(row);
2558
+ }
2559
+
2560
+ async function sendReleaseNpmCommand(command) {
2561
+ try {
2562
+ await api("/api/prompt", { method: "POST", body: { message: command }, tabId: activeTabId });
2563
+ addEvent(`${command} sent`, "info");
2564
+ scheduleRefreshState();
2565
+ } catch (error) {
2566
+ addEvent(error.message, "error");
2567
+ addTransientMessage({ role: "error", title: command, content: error.message, level: "error" });
2568
+ }
2569
+ }
2570
+
2571
+ function releaseNpmActionButton(label, command, className = "") {
2572
+ const button = make("button", `release-npm-action ${className}`.trim(), label);
2573
+ button.type = "button";
2574
+ button.addEventListener("click", () => sendReleaseNpmCommand(command));
2575
+ return button;
2576
+ }
2577
+
2578
+ function renderReleaseNpmOutputWidget() {
2579
+ const outputLines = getWidgetLines("release-npm:output");
2580
+ const footerLines = getWidgetLines("release-npm:footer");
2581
+ if (outputLines.length === 0 && footerLines.length === 0) return null;
2582
+
2583
+ const details = releaseNpmFooterDetails(footerLines);
2584
+ const node = make("section", "widget release-npm-widget release-npm-live-widget");
2585
+ node.setAttribute("aria-label", "npm release output");
2586
+
2587
+ const header = make("div", "release-npm-header");
2588
+ const titleWrap = make("div", "release-npm-title-wrap");
2589
+ titleWrap.append(make("span", "release-npm-kicker", "npm release"), make("strong", "release-npm-title", details.phase));
2590
+
2591
+ const meta = make("div", "release-npm-meta");
2592
+ if (details.mode) meta.append(make("span", "release-npm-pill", details.mode));
2593
+ if (details.elapsed) meta.append(make("span", "release-npm-pill elapsed", details.elapsed));
2594
+
2595
+ const actions = make("div", "release-npm-actions");
2596
+ actions.append(
2597
+ releaseNpmActionButton("Toggle output", "/release-toggle"),
2598
+ releaseNpmActionButton("Abort", "/release-abort", "danger"),
2599
+ );
2600
+ header.append(titleWrap, meta, actions);
2601
+
2602
+ const terminal = make("div", "release-npm-terminal");
2603
+ terminal.setAttribute("role", "log");
2604
+ terminal.setAttribute("aria-live", "polite");
2605
+ for (const line of (outputLines.length ? outputLines : ["Waiting for release output..."])) {
2606
+ appendReleaseNpmTerminalLine(terminal, line);
2607
+ }
2608
+
2609
+ const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
2610
+ node.append(header, terminal, controls);
2611
+ requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
2612
+ return node;
2613
+ }
2614
+
2615
+ function renderReleaseNpmLogWidget() {
2616
+ const lines = getWidgetLines("release-npm:logs");
2617
+ if (lines.length === 0) return null;
2618
+
2619
+ const node = make("section", "widget release-npm-widget release-npm-log-widget");
2620
+ node.setAttribute("aria-label", "npm release log");
2621
+ const header = make("div", "release-npm-header");
2622
+ const titleWrap = make("div", "release-npm-title-wrap");
2623
+ titleWrap.append(
2624
+ make("span", "release-npm-kicker", "saved log"),
2625
+ make("strong", "release-npm-title", stripAnsi(lines[0] || "release-npm log")),
2626
+ );
2627
+ const meta = make("div", "release-npm-meta");
2628
+ if (lines[1]) meta.append(make("span", "release-npm-pill", stripAnsi(lines[1])));
2629
+ const actions = make("div", "release-npm-actions");
2630
+ actions.append(releaseNpmActionButton("Close log", "/release-npm-logs close"));
2631
+ header.append(titleWrap, meta, actions);
2632
+
2633
+ const terminal = make("div", "release-npm-terminal");
2634
+ for (const line of lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim())) {
2635
+ appendReleaseNpmTerminalLine(terminal, line);
2636
+ }
2637
+ node.append(header, terminal);
2638
+ requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
2639
+ return node;
2640
+ }
2641
+
2642
+ function renderReleaseAurOutputWidget() {
2643
+ const outputLines = getWidgetLines("release-aur:output");
2644
+ const footerLines = getWidgetLines("release-aur:footer");
2645
+ if (outputLines.length === 0 && footerLines.length === 0) return null;
2646
+
2647
+ const details = releaseNpmFooterDetails(footerLines);
2648
+ const node = make("section", "widget release-npm-widget release-aur-widget release-aur-live-widget");
2649
+ node.setAttribute("aria-label", "AUR release output");
2650
+
2651
+ const header = make("div", "release-npm-header");
2652
+ const titleWrap = make("div", "release-npm-title-wrap");
2653
+ titleWrap.append(make("span", "release-npm-kicker", "AUR release"), make("strong", "release-npm-title", details.phase));
2654
+
2655
+ const meta = make("div", "release-npm-meta");
2656
+ if (details.mode) meta.append(make("span", "release-npm-pill", details.mode));
2657
+ if (details.elapsed) meta.append(make("span", "release-npm-pill elapsed", details.elapsed));
2658
+
2659
+ const actions = make("div", "release-npm-actions");
2660
+ actions.append(
2661
+ releaseNpmActionButton("Toggle output", "/release-aur toggle"),
2662
+ releaseNpmActionButton("Abort", "/release-aur abort", "danger"),
2663
+ );
2664
+ header.append(titleWrap, meta, actions);
2665
+
2666
+ const terminal = make("div", "release-npm-terminal");
2667
+ terminal.setAttribute("role", "log");
2668
+ terminal.setAttribute("aria-live", "polite");
2669
+ for (const line of (outputLines.length ? outputLines : ["Waiting for release-aur output..."])) {
2670
+ appendReleaseNpmTerminalLine(terminal, line);
2671
+ }
2672
+
2673
+ const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
2674
+ node.append(header, terminal, controls);
2675
+ requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
2676
+ return node;
2677
+ }
2678
+
2679
+ function renderReleaseAurLogWidget() {
2680
+ const lines = getWidgetLines("release-aur:logs");
2681
+ if (lines.length === 0) return null;
2682
+
2683
+ const node = make("section", "widget release-npm-widget release-aur-widget release-aur-log-widget");
2684
+ node.setAttribute("aria-label", "AUR release log");
2685
+ const header = make("div", "release-npm-header");
2686
+ const titleWrap = make("div", "release-npm-title-wrap");
2687
+ titleWrap.append(
2688
+ make("span", "release-npm-kicker", "saved AUR log"),
2689
+ make("strong", "release-npm-title", stripAnsi(lines[0] || "release-aur log")),
2690
+ );
2691
+ const meta = make("div", "release-npm-meta");
2692
+ if (lines[1]) meta.append(make("span", "release-npm-pill", stripAnsi(lines[1])));
2693
+ const actions = make("div", "release-npm-actions");
2694
+ actions.append(releaseNpmActionButton("Close log", "/release-aur logs close"));
2695
+ header.append(titleWrap, meta, actions);
2696
+
2697
+ const terminal = make("div", "release-npm-terminal");
2698
+ for (const line of lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim())) {
2699
+ appendReleaseNpmTerminalLine(terminal, line);
2700
+ }
2701
+ node.append(header, terminal);
2702
+ requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
2703
+ return node;
2704
+ }
2705
+
2275
2706
  function renderWidgets() {
2276
2707
  elements.widgetArea.replaceChildren();
2708
+ const releaseOutput = renderReleaseNpmOutputWidget();
2709
+ if (releaseOutput) elements.widgetArea.append(releaseOutput);
2710
+ const releaseLog = renderReleaseNpmLogWidget();
2711
+ if (releaseLog) elements.widgetArea.append(releaseLog);
2712
+ const releaseAurOutput = renderReleaseAurOutputWidget();
2713
+ if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
2714
+ const releaseAurLog = renderReleaseAurLogWidget();
2715
+ if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
2716
+
2717
+ const releaseWidgetKeys = new Set(["release-npm:output", "release-npm:footer", "release-npm:logs", "release-aur:output", "release-aur:footer", "release-aur:logs"]);
2277
2718
  for (const [key, value] of widgets) {
2719
+ if (releaseWidgetKeys.has(key)) continue;
2278
2720
  const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
2279
2721
  const specialized = key === "todo-progress" ? renderTodoProgressWidget(key, lines) : null;
2280
2722
  if (specialized) {
@@ -3324,12 +3766,31 @@ function scheduleAbortStateChecks() {
3324
3766
  }
3325
3767
  }
3326
3768
 
3769
+ function messageTimestampMs(message) {
3770
+ const timestamp = message?.timestamp;
3771
+ const date = typeof timestamp === "number" ? new Date(timestamp) : new Date(String(timestamp || ""));
3772
+ const time = date.getTime();
3773
+ return Number.isFinite(time) ? time : 0;
3774
+ }
3775
+
3776
+ function orderedTranscriptItems() {
3777
+ const items = [];
3778
+ latestMessages.forEach((message, index) => {
3779
+ items.push({ message, messageIndex: index, transient: false, timestampMs: messageTimestampMs(message), order: index });
3780
+ });
3781
+ transientMessages.forEach((message, index) => {
3782
+ items.push({ message, messageIndex: index, transient: true, timestampMs: messageTimestampMs(message), order: latestMessages.length + index });
3783
+ });
3784
+ return items.sort((a, b) => a.timestampMs - b.timestampMs || a.order - b.order);
3785
+ }
3786
+
3327
3787
  function renderAllMessages({ preserveScroll = false } = {}) {
3328
3788
  const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
3329
3789
  const previousScrollTop = elements.chat.scrollTop;
3330
3790
  resetChatOutput();
3331
- latestMessages.forEach((message, index) => appendTranscriptMessage(message, { messageIndex: index }));
3332
- transientMessages.forEach((message, index) => appendTranscriptMessage(message, { messageIndex: index, transient: true }));
3791
+ for (const item of orderedTranscriptItems()) {
3792
+ appendTranscriptMessage(item.message, { messageIndex: item.messageIndex, transient: item.transient });
3793
+ }
3333
3794
  renderRunIndicator({ scroll: false });
3334
3795
  updateStickyUserPromptButton();
3335
3796
  if (shouldFollow) scrollChatToBottom({ force: true });
@@ -3353,6 +3814,21 @@ function addTransientMessage({ role = "notice", title, content, level = "info" }
3353
3814
  renderAllMessages();
3354
3815
  }
3355
3816
 
3817
+ function addAbortTranscriptNotice({ activeRun = false, errorMessage = "" } = {}) {
3818
+ if (errorMessage) {
3819
+ addTransientMessage({ role: "error", title: "Abort failed", content: `Abort request failed: ${errorMessage}`, level: "error" });
3820
+ return;
3821
+ }
3822
+ addTransientMessage({
3823
+ role: "native",
3824
+ title: activeRun ? "Agent aborted" : "Abort requested",
3825
+ content: activeRun
3826
+ ? "⛔ Agent run aborted by user from the Web UI. Pi was told to stop; this transcript marks the run as aborted."
3827
+ : "⛔ Abort requested from the Web UI, but no active agent run was visible in this tab.",
3828
+ level: activeRun ? "warn" : "info",
3829
+ });
3830
+ }
3831
+
3356
3832
  function isChatNearBottom() {
3357
3833
  const remaining = elements.chat.scrollHeight - elements.chat.scrollTop - elements.chat.clientHeight;
3358
3834
  return remaining <= CHAT_BOTTOM_THRESHOLD_PX;
@@ -3457,6 +3933,19 @@ function sendPromptFromModeButton(kind, button) {
3457
3933
  sendPrompt(kind);
3458
3934
  }
3459
3935
 
3936
+ function setPublishMenuOpen(open) {
3937
+ publishMenuOpen = !!open;
3938
+ elements.publishButton.setAttribute("aria-expanded", publishMenuOpen ? "true" : "false");
3939
+ elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
3940
+ elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
3941
+ }
3942
+
3943
+ function runPublishWorkflow(command) {
3944
+ setComposerActionsOpen(false);
3945
+ setPublishMenuOpen(false);
3946
+ sendPrompt("prompt", command);
3947
+ }
3948
+
3460
3949
  function shouldSendPromptFromEnter(event) {
3461
3950
  if (event.key !== "Enter" || event.shiftKey || event.isComposing) return false;
3462
3951
  if (event.ctrlKey || event.metaKey) return true;
@@ -3471,11 +3960,39 @@ function renderMessages(messages) {
3471
3960
  renderFeedbackTray();
3472
3961
  }
3473
3962
 
3963
+ function cancelStreamBubbleHide() {
3964
+ clearTimeout(streamBubbleHideTimer);
3965
+ streamBubbleHideTimer = null;
3966
+ }
3967
+
3968
+ function removeStreamBubble() {
3969
+ cancelStreamBubbleHide();
3970
+ streamBubble?.remove();
3971
+ streamBubble = null;
3972
+ streamText = null;
3973
+ streamBubbleVisibleSince = 0;
3974
+ renderRunIndicator({ scroll: false });
3975
+ }
3976
+
3977
+ function scheduleStreamBubbleHide() {
3978
+ if (!streamBubble) return;
3979
+ const visibleForMs = streamBubbleVisibleSince ? performance.now() - streamBubbleVisibleSince : STREAM_OUTPUT_MIN_VISIBLE_MS;
3980
+ const delayMs = Math.max(STREAM_OUTPUT_HIDE_DELAY_MS, STREAM_OUTPUT_MIN_VISIBLE_MS - visibleForMs);
3981
+ clearTimeout(streamBubbleHideTimer);
3982
+ streamBubbleHideTimer = setTimeout(() => {
3983
+ streamBubbleHideTimer = null;
3984
+ if (stripTodoProgressLines(streamRawText, { streaming: true }) || !streamBubble) return;
3985
+ removeStreamBubble();
3986
+ }, delayMs);
3987
+ }
3988
+
3474
3989
  function ensureStreamBubble() {
3990
+ cancelStreamBubbleHide();
3475
3991
  if (streamBubble) return;
3476
3992
  const created = appendMessage({ role: "assistant", title: "Assistant", timestamp: Date.now(), content: "" }, { streaming: true });
3477
3993
  streamBubble = created.bubble;
3478
3994
  streamText = appendText(created.body, "");
3995
+ streamBubbleVisibleSince = performance.now();
3479
3996
  renderRunIndicator({ scroll: false });
3480
3997
  scrollChatToBottom();
3481
3998
  }
@@ -3495,9 +4012,11 @@ function showStreamingThinking(placeholder = "Thinking…") {
3495
4012
  }
3496
4013
 
3497
4014
  function resetStreamBubble() {
4015
+ cancelStreamBubbleHide();
3498
4016
  streamBubble = null;
3499
4017
  streamText = null;
3500
4018
  streamRawText = "";
4019
+ streamBubbleVisibleSince = 0;
3501
4020
  streamThinkingBubble = null;
3502
4021
  streamThinking = null;
3503
4022
  }
@@ -3579,11 +4098,8 @@ function handleMessageUpdate(event) {
3579
4098
  if (assistantText) {
3580
4099
  ensureStreamBubble();
3581
4100
  streamText.textContent = assistantText;
3582
- } else if (streamBubble) {
3583
- streamBubble.remove();
3584
- streamBubble = null;
3585
- streamText = null;
3586
- renderRunIndicator({ scroll: false });
4101
+ } else {
4102
+ scheduleStreamBubbleHide();
3587
4103
  }
3588
4104
  renderFooter();
3589
4105
  scrollChatToBottom();
@@ -3858,12 +4374,14 @@ function abortPathSuggestionRequest() {
3858
4374
 
3859
4375
  function cancelPathSuggestionRequest() {
3860
4376
  pathSuggestRequestSerial++;
4377
+ pathSuggestActiveQuery = null;
3861
4378
  abortPathSuggestionRequest();
3862
4379
  }
3863
4380
 
3864
4381
  function hideCommandSuggestions() {
3865
4382
  cancelPathSuggestionRequest();
3866
4383
  elements.commandSuggest.hidden = true;
4384
+ elements.commandSuggest.removeAttribute("aria-busy");
3867
4385
  elements.commandSuggest.replaceChildren();
3868
4386
  commandSuggestions = [];
3869
4387
  pathSuggestions = [];
@@ -3884,6 +4402,33 @@ function setActiveCommandSuggestion(index) {
3884
4402
  }
3885
4403
  }
3886
4404
 
4405
+ function pointerPositionFromEvent(event) {
4406
+ if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return null;
4407
+ return { x: event.clientX, y: event.clientY };
4408
+ }
4409
+
4410
+ function rememberPointerPosition(event) {
4411
+ lastPointerPosition = pointerPositionFromEvent(event);
4412
+ }
4413
+
4414
+ function commandSuggestionPointerActuallyMoved(event) {
4415
+ const movementX = Number.isFinite(event.movementX) ? event.movementX : 0;
4416
+ const movementY = Number.isFinite(event.movementY) ? event.movementY : 0;
4417
+ if (movementX !== 0 || movementY !== 0) return true;
4418
+
4419
+ const position = pointerPositionFromEvent(event);
4420
+ return Boolean(
4421
+ position &&
4422
+ lastPointerPosition &&
4423
+ (position.x !== lastPointerPosition.x || position.y !== lastPointerPosition.y),
4424
+ );
4425
+ }
4426
+
4427
+ function setActiveCommandSuggestionFromPointerMove(index, event) {
4428
+ if (!commandSuggestionPointerActuallyMoved(event)) return;
4429
+ setActiveCommandSuggestion(index);
4430
+ }
4431
+
3887
4432
  function renderCommandSuggestionItems(trigger, { keepIndex = false } = {}) {
3888
4433
  suggestionMode = "command";
3889
4434
  pathSuggestions = [];
@@ -3901,7 +4446,7 @@ function renderCommandSuggestionItems(trigger, { keepIndex = false } = {}) {
3901
4446
  item.type = "button";
3902
4447
  item.setAttribute("role", "option");
3903
4448
  item.addEventListener("mousedown", (event) => event.preventDefault());
3904
- item.addEventListener("mouseenter", () => setActiveCommandSuggestion(index));
4449
+ item.addEventListener("pointermove", (event) => setActiveCommandSuggestionFromPointerMove(index, event));
3905
4450
  item.addEventListener("click", () => insertCommandSuggestion(index));
3906
4451
 
3907
4452
  item.append(
@@ -3943,7 +4488,7 @@ function renderPathSuggestionItems(trigger, { keepIndex = false } = {}) {
3943
4488
  item.type = "button";
3944
4489
  item.setAttribute("role", "option");
3945
4490
  item.addEventListener("mousedown", (event) => event.preventDefault());
3946
- item.addEventListener("mouseenter", () => setActiveCommandSuggestion(index));
4491
+ item.addEventListener("pointermove", (event) => setActiveCommandSuggestionFromPointerMove(index, event));
3947
4492
  item.addEventListener("click", () => insertPathSuggestion(index));
3948
4493
 
3949
4494
  item.append(
@@ -3959,15 +4504,25 @@ function renderPathSuggestionItems(trigger, { keepIndex = false } = {}) {
3959
4504
  }
3960
4505
 
3961
4506
  async function renderPathSuggestions(trigger, { keepIndex = false } = {}) {
4507
+ if (suggestionMode === "path" && pathSuggestActiveQuery === trigger.query && !elements.commandSuggest.hidden) {
4508
+ if (keepIndex && activeSuggestionCount() > 0) setActiveCommandSuggestion(commandSuggestIndex);
4509
+ return;
4510
+ }
4511
+
4512
+ const keepExistingPathMenu = suggestionMode === "path" && !elements.commandSuggest.hidden && elements.commandSuggest.childElementCount > 0;
3962
4513
  abortPathSuggestionRequest();
3963
4514
  const requestSerial = ++pathSuggestRequestSerial;
3964
4515
  const controller = new AbortController();
4516
+ pathSuggestActiveQuery = trigger.query;
3965
4517
  pathSuggestAbortController = controller;
3966
4518
  suggestionMode = "path";
3967
4519
  commandSuggestions = [];
3968
- pathSuggestions = [];
3969
- elements.commandSuggest.replaceChildren(make("div", "command-suggest-empty", "Finding paths…"));
4520
+ if (!keepExistingPathMenu) {
4521
+ pathSuggestions = [];
4522
+ elements.commandSuggest.replaceChildren(make("div", "command-suggest-empty", "Finding paths…"));
4523
+ }
3970
4524
  elements.commandSuggest.hidden = false;
4525
+ elements.commandSuggest.setAttribute("aria-busy", "true");
3971
4526
 
3972
4527
  try {
3973
4528
  const response = await api(`/api/path-suggestions?query=${encodeURIComponent(trigger.query)}`, { signal: controller.signal });
@@ -3980,7 +4535,10 @@ async function renderPathSuggestions(trigger, { keepIndex = false } = {}) {
3980
4535
  elements.commandSuggest.replaceChildren(make("div", "command-suggest-empty", `Path suggestions unavailable: ${error.message}`));
3981
4536
  elements.commandSuggest.hidden = false;
3982
4537
  } finally {
3983
- if (requestSerial === pathSuggestRequestSerial) pathSuggestAbortController = null;
4538
+ if (requestSerial === pathSuggestRequestSerial) {
4539
+ pathSuggestAbortController = null;
4540
+ elements.commandSuggest.removeAttribute("aria-busy");
4541
+ }
3984
4542
  }
3985
4543
  }
3986
4544
 
@@ -4330,11 +4888,16 @@ function showNextDialog() {
4330
4888
  const request = activeDialog;
4331
4889
 
4332
4890
  const prompt = normalizeDialogPrompt(request);
4333
- const isGuardrailDialog = isGuardrailDialogPrompt(prompt);
4891
+ const releasePrompt = request.method === "select" ? releaseDialogPromptParts(prompt) : null;
4892
+ const displayPrompt = releasePrompt || prompt;
4893
+ const isGuardrailDialog = isGuardrailDialogPrompt(displayPrompt);
4894
+ const isReleaseDialog = !!releasePrompt;
4334
4895
  elements.dialog.classList.toggle("guardrail-dialog", isGuardrailDialog);
4335
- elements.dialogTitle.textContent = prompt.title;
4336
- renderAnsiText(elements.dialogMessage, prompt.message);
4337
- elements.dialogMessage.hidden = !prompt.plainMessage;
4896
+ elements.dialog.classList.toggle("release-dialog", isReleaseDialog);
4897
+ elements.dialogTitle.textContent = displayPrompt.title;
4898
+ if (isReleaseDialog) renderReleaseDialogMessage(elements.dialogMessage, displayPrompt.message);
4899
+ else renderAnsiText(elements.dialogMessage, displayPrompt.message);
4900
+ elements.dialogMessage.hidden = !displayPrompt.plainMessage;
4338
4901
  elements.dialogBody.replaceChildren();
4339
4902
  elements.dialogActions.replaceChildren();
4340
4903
 
@@ -4348,6 +4911,8 @@ function showNextDialog() {
4348
4911
  button.type = "button";
4349
4912
  if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
4350
4913
  if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
4914
+ if (isReleaseDialog && /^Yes$/i.test(optionLabel)) button.classList.add("primary", "release-publish-action");
4915
+ if (isReleaseDialog && /^No$/i.test(optionLabel)) button.classList.add("release-cancel-action");
4351
4916
  button.addEventListener("click", () => sendDialogResponse({ type: "extension_ui_response", id: request.id, value: optionLabel, tabId: request.tabId }));
4352
4917
  options.append(button);
4353
4918
  }
@@ -4459,6 +5024,7 @@ function handleEvent(event) {
4459
5024
  break;
4460
5025
  case "agent_end":
4461
5026
  addEvent("agent finished");
5027
+ notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
4462
5028
  currentRunStartedAt = null;
4463
5029
  if (currentState) currentState = { ...currentState, isStreaming: false };
4464
5030
  clearRunIndicatorActivity();
@@ -4567,14 +5133,31 @@ elements.gitWorkflowButton.addEventListener("click", () => {
4567
5133
  setComposerActionsOpen(false);
4568
5134
  startGitWorkflow();
4569
5135
  });
5136
+ const publishMenuContainer = elements.publishButton.parentElement;
5137
+ elements.publishButton.addEventListener("click", () => {
5138
+ setPublishMenuOpen(true);
5139
+ });
5140
+ publishMenuContainer?.addEventListener("pointerenter", () => setPublishMenuOpen(true));
5141
+ publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
5142
+ publishMenuContainer?.addEventListener("focusin", () => setPublishMenuOpen(true));
5143
+ publishMenuContainer?.addEventListener("focusout", () => {
5144
+ setTimeout(() => {
5145
+ if (!publishMenuContainer?.contains(document.activeElement)) setPublishMenuOpen(false);
5146
+ }, 0);
5147
+ });
5148
+ elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
5149
+ elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
4570
5150
  elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
4571
5151
  elements.abortButton.addEventListener("click", async () => {
5152
+ const hadActiveRun = runIndicatorIsActive();
4572
5153
  try {
4573
- if (runIndicatorIsActive()) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
5154
+ if (hadActiveRun) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
4574
5155
  await api("/api/abort", { method: "POST", body: {} });
5156
+ addAbortTranscriptNotice({ activeRun: hadActiveRun });
4575
5157
  scheduleAbortStateChecks();
4576
5158
  } catch (error) {
4577
5159
  addEvent(error.message, "error");
5160
+ addAbortTranscriptNotice({ errorMessage: error.message });
4578
5161
  }
4579
5162
  });
4580
5163
  elements.newSessionButton.addEventListener("click", async () => {
@@ -4630,6 +5213,15 @@ elements.setThinkingButton.addEventListener("click", async () => {
4630
5213
  });
4631
5214
  elements.themeSelect.addEventListener("change", () => setThemeByName(elements.themeSelect.value, { persist: true, announce: true }));
4632
5215
  elements.openNetworkButton.addEventListener("click", openToNetwork);
5216
+ elements.agentDoneNotificationsToggle.addEventListener("change", () => {
5217
+ setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
5218
+ requestPermission: elements.agentDoneNotificationsToggle.checked,
5219
+ announce: true,
5220
+ }).catch((error) => {
5221
+ addEvent(error.message, "error");
5222
+ renderAgentDoneNotificationsToggle();
5223
+ });
5224
+ });
4633
5225
  elements.toggleSidePanelButton.addEventListener("click", () => {
4634
5226
  setSidePanelCollapsed(true);
4635
5227
  });
@@ -4658,6 +5250,9 @@ document.addEventListener("pointerdown", (event) => {
4658
5250
  if (document.body.classList.contains("composer-actions-open") && !elements.composer.contains(event.target)) {
4659
5251
  setComposerActionsOpen(false);
4660
5252
  }
5253
+ if (publishMenuOpen && !event.target?.closest?.(".composer-publish-menu")) {
5254
+ setPublishMenuOpen(false);
5255
+ }
4661
5256
  if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
4662
5257
  setMobileTabsExpanded(false);
4663
5258
  }
@@ -4669,9 +5264,14 @@ document.addEventListener("pointermove", (event) => {
4669
5264
  if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
4670
5265
  clearOpenTerminalTabGroup(openTerminalTabGroupKey);
4671
5266
  }
5267
+ rememberPointerPosition(event);
4672
5268
  }, { passive: true });
4673
5269
  window.addEventListener("keydown", (event) => {
4674
5270
  if (event.key !== "Escape") return;
5271
+ if (publishMenuOpen) {
5272
+ setPublishMenuOpen(false);
5273
+ return;
5274
+ }
4675
5275
  if (document.body.classList.contains("composer-actions-open")) {
4676
5276
  setComposerActionsOpen(false);
4677
5277
  return;
@@ -4762,6 +5362,7 @@ loadLastUserPromptCache();
4762
5362
  installViewportHandlers();
4763
5363
  initializeThemes().catch((error) => addEvent(`failed to load themes: ${error.message}`, "warn"));
4764
5364
  initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
5365
+ restoreAgentDoneNotificationsSetting();
4765
5366
  restoreSidePanelState();
4766
5367
  bindMobileViewChanges();
4767
5368
  registerPwaServiceWorker();