@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/README.md +4 -2
- package/package.json +1 -1
- package/public/app.js +620 -19
- package/public/index.html +30 -0
- package/public/service-worker.js +1 -1
- package/public/styles.css +362 -7
- package/tests/mobile-static.test.mjs +63 -5
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
|
|
1402
|
+
function browserNotificationSupported() {
|
|
1316
1403
|
return "Notification" in window && window.isSecureContext;
|
|
1317
1404
|
}
|
|
1318
1405
|
|
|
1319
|
-
function
|
|
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
|
-
|
|
3332
|
-
|
|
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
|
|
3583
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
3969
|
-
|
|
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)
|
|
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
|
|
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.
|
|
4336
|
-
|
|
4337
|
-
elements.dialogMessage
|
|
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 (
|
|
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();
|