@firstpick/pi-package-webui 0.1.3 → 0.1.5
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 +29 -8
- package/bin/pi-webui.mjs +183 -13
- package/package.json +34 -4
- package/public/app.js +1181 -55
- package/public/index.html +33 -0
- package/public/service-worker.js +1 -1
- package/public/styles.css +507 -9
- package/tests/mobile-static.test.mjs +151 -12
package/public/app.js
CHANGED
|
@@ -5,6 +5,7 @@ const elements = {
|
|
|
5
5
|
tabBar: $("#tabBar"),
|
|
6
6
|
terminalTabsToggleButton: $("#terminalTabsToggleButton"),
|
|
7
7
|
newTabButton: $("#newTabButton"),
|
|
8
|
+
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
8
9
|
statusBar: $("#statusBar"),
|
|
9
10
|
widgetArea: $("#widgetArea"),
|
|
10
11
|
stickyUserPromptButton: $("#stickyUserPromptButton"),
|
|
@@ -27,6 +28,10 @@ const elements = {
|
|
|
27
28
|
newSessionButton: $("#newSessionButton"),
|
|
28
29
|
compactButton: $("#compactButton"),
|
|
29
30
|
gitWorkflowButton: $("#gitWorkflowButton"),
|
|
31
|
+
publishButton: $("#publishButton"),
|
|
32
|
+
publishMenu: $("#publishMenu"),
|
|
33
|
+
releaseNpmButton: $("#releaseNpmButton"),
|
|
34
|
+
releaseAurButton: $("#releaseAurButton"),
|
|
30
35
|
gitWorkflowPanel: $("#gitWorkflowPanel"),
|
|
31
36
|
gitWorkflowTitle: $("#gitWorkflowTitle"),
|
|
32
37
|
gitWorkflowHint: $("#gitWorkflowHint"),
|
|
@@ -41,6 +46,9 @@ const elements = {
|
|
|
41
46
|
themeSelect: $("#themeSelect"),
|
|
42
47
|
networkStatus: $("#networkStatus"),
|
|
43
48
|
openNetworkButton: $("#openNetworkButton"),
|
|
49
|
+
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
50
|
+
agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
|
|
51
|
+
optionalFeaturesBox: $("#optionalFeaturesBox"),
|
|
44
52
|
toggleSidePanelButton: $("#toggleSidePanelButton"),
|
|
45
53
|
sidePanelExpandButton: $("#sidePanelExpandButton"),
|
|
46
54
|
sidePanelBackdrop: $("#sidePanelBackdrop"),
|
|
@@ -75,6 +83,10 @@ let tabSeenCompletionSerials = new Map();
|
|
|
75
83
|
let streamBubble = null;
|
|
76
84
|
let streamText = null;
|
|
77
85
|
let streamRawText = "";
|
|
86
|
+
let streamBubbleVisibleSince = 0;
|
|
87
|
+
let streamBubbleHideTimer = null;
|
|
88
|
+
let streamTextRenderTimer = null;
|
|
89
|
+
let streamToolCallSeen = false;
|
|
78
90
|
let streamThinkingBubble = null;
|
|
79
91
|
let streamThinking = null;
|
|
80
92
|
let runIndicatorBubble = null;
|
|
@@ -103,6 +115,8 @@ let commandSuggestions = [];
|
|
|
103
115
|
let pathSuggestions = [];
|
|
104
116
|
let suggestionMode = "none";
|
|
105
117
|
let commandSuggestIndex = 0;
|
|
118
|
+
let lastPointerPosition = null;
|
|
119
|
+
let pathSuggestActiveQuery = null;
|
|
106
120
|
let pathSuggestRequestSerial = 0;
|
|
107
121
|
let pathSuggestAbortController = null;
|
|
108
122
|
let latestStats = null;
|
|
@@ -110,12 +124,18 @@ let latestWorkspace = null;
|
|
|
110
124
|
let latestNetwork = null;
|
|
111
125
|
let latestMessages = [];
|
|
112
126
|
let transientMessages = [];
|
|
127
|
+
let actionEntrySeenKeysByTab = new Map();
|
|
128
|
+
let actionEntryAnimationPrimedTabs = new Set();
|
|
113
129
|
let lastUserPromptByTab = new Map();
|
|
114
130
|
let actionFeedbackByTab = new Map();
|
|
115
131
|
let actionFeedbackSendBusy = false;
|
|
116
132
|
let blockedTabNotificationKeys = new Set();
|
|
117
133
|
let blockedTabNotificationPermissionRequested = false;
|
|
118
134
|
let blockedTabNotificationFallbackNoted = false;
|
|
135
|
+
let agentDoneNotificationsEnabled = false;
|
|
136
|
+
let agentDoneNotificationPermissionRequested = false;
|
|
137
|
+
let agentDoneNotificationFallbackNoted = false;
|
|
138
|
+
let agentDoneNotificationKeys = new Set();
|
|
119
139
|
let availableModels = [];
|
|
120
140
|
let availableThemes = [];
|
|
121
141
|
let currentThemeName = "catppuccin-mocha";
|
|
@@ -129,6 +149,7 @@ let lastChatProgrammaticScrollAt = 0;
|
|
|
129
149
|
let chatUserScrollIntentUntil = 0;
|
|
130
150
|
let mobileFooterExpanded = false;
|
|
131
151
|
let footerModelPickerOpen = false;
|
|
152
|
+
let publishMenuOpen = false;
|
|
132
153
|
let maxVisualViewportHeight = 0;
|
|
133
154
|
let currentRunStartedAt = null;
|
|
134
155
|
let currentRunStreamChars = 0;
|
|
@@ -137,7 +158,9 @@ const dialogQueue = [];
|
|
|
137
158
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
138
159
|
const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
139
160
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
161
|
+
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
140
162
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
163
|
+
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
141
164
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
142
165
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
143
166
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
@@ -150,6 +173,9 @@ const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
|
150
173
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
151
174
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
152
175
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
176
|
+
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
177
|
+
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
178
|
+
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
153
179
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
154
180
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
155
181
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
@@ -157,10 +183,84 @@ const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
|
|
|
157
183
|
const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "idle", "working"];
|
|
158
184
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
159
185
|
const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
186
|
+
const AGENT_DONE_NOTIFICATION_TAG_PREFIX = "pi-webui-agent-done";
|
|
160
187
|
const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
161
188
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
162
189
|
const statusEntries = new Map();
|
|
163
190
|
const widgets = new Map();
|
|
191
|
+
// Optional feature detection intentionally checks loaded Pi capabilities (RPC-visible
|
|
192
|
+
// commands and live widget events), not npm package folders. This keeps local dev
|
|
193
|
+
// symlinks and independently installed packages working.
|
|
194
|
+
const optionalFeatureAvailability = {
|
|
195
|
+
gitWorkflow: false,
|
|
196
|
+
releaseNpm: false,
|
|
197
|
+
releaseAur: false,
|
|
198
|
+
statsCommand: false,
|
|
199
|
+
gitFooterStatus: false,
|
|
200
|
+
todoProgressWidget: false,
|
|
201
|
+
themeBundle: false,
|
|
202
|
+
};
|
|
203
|
+
const OPTIONAL_FEATURES = [
|
|
204
|
+
{
|
|
205
|
+
id: "gitWorkflow",
|
|
206
|
+
label: "Guided Git workflow",
|
|
207
|
+
packageName: "@firstpick/pi-prompts-git-pr",
|
|
208
|
+
capabilityLabel: "/git-staged-msg",
|
|
209
|
+
description: "Generate staged commit messages for the guided Git workflow.",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "releaseNpm",
|
|
213
|
+
label: "NPM Release",
|
|
214
|
+
packageName: "@firstpick/pi-extension-release-npm",
|
|
215
|
+
capabilityLabel: "/release-npm",
|
|
216
|
+
description: "Publish menu action and live npm release widgets.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "releaseAur",
|
|
220
|
+
label: "AUR Release",
|
|
221
|
+
packageName: "@firstpick/pi-extension-release-aur",
|
|
222
|
+
capabilityLabel: "/release-aur",
|
|
223
|
+
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "todoProgressWidget",
|
|
227
|
+
label: "Todo progress widget",
|
|
228
|
+
packageName: "@firstpick/pi-extension-todo-progress",
|
|
229
|
+
capabilityLabel: "/todo-progress-status or todo-progress widget event",
|
|
230
|
+
description: "Styled live checklist rendering for assistant todo updates.",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "gitFooterStatus",
|
|
234
|
+
label: "Git footer status",
|
|
235
|
+
packageName: "@firstpick/pi-extension-git-footer-status",
|
|
236
|
+
capabilityLabel: "/git-footer-refresh or git-footer status event",
|
|
237
|
+
description: "Enhanced Pi footer/status telemetry when loaded by Pi.",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "statsCommand",
|
|
241
|
+
label: "Stats command",
|
|
242
|
+
packageName: "@firstpick/pi-extension-stats",
|
|
243
|
+
capabilityLabel: "/stats",
|
|
244
|
+
description: "Token and cost usage analytics commands.",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "themeBundle",
|
|
248
|
+
label: "Theme bundle",
|
|
249
|
+
packageName: "@firstpick/pi-themes-bundle",
|
|
250
|
+
capabilityLabel: "/api/themes returned themes",
|
|
251
|
+
description: "Additional browser theme-picker and Pi theme resources.",
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
|
|
255
|
+
const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
256
|
+
["git-staged-msg", "gitWorkflow"],
|
|
257
|
+
["release-npm", "releaseNpm"],
|
|
258
|
+
["release-aur", "releaseAur"],
|
|
259
|
+
["stats", "statsCommand"],
|
|
260
|
+
["git-footer-refresh", "gitFooterStatus"],
|
|
261
|
+
["todo-progress-status", "todoProgressWidget"],
|
|
262
|
+
]);
|
|
263
|
+
const optionalFeatureInstallInProgress = new Set();
|
|
164
264
|
const gitWorkflow = {
|
|
165
265
|
active: false,
|
|
166
266
|
step: "idle",
|
|
@@ -213,10 +313,77 @@ function readStoredSidePanelCollapsed() {
|
|
|
213
313
|
}
|
|
214
314
|
}
|
|
215
315
|
|
|
316
|
+
function readStoredAgentDoneNotificationsEnabled() {
|
|
317
|
+
try {
|
|
318
|
+
return localStorage.getItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY) === "1";
|
|
319
|
+
} catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function persistAgentDoneNotificationsEnabled(enabled) {
|
|
325
|
+
try {
|
|
326
|
+
localStorage.setItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY, enabled ? "1" : "0");
|
|
327
|
+
} catch {
|
|
328
|
+
// Ignore storage failures; the toggle should still work for this page load.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function agentDoneNotificationsStatusText() {
|
|
333
|
+
if (!browserNotificationSupported()) return "Unavailable here";
|
|
334
|
+
const permission = browserNotificationPermission();
|
|
335
|
+
if (permission === "denied") return "Permission denied";
|
|
336
|
+
if (agentDoneNotificationsEnabled) return permission === "granted" ? "On" : "Permission needed";
|
|
337
|
+
return permission === "granted" ? "Off · permission granted" : "Off";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function renderAgentDoneNotificationsToggle() {
|
|
341
|
+
if (!elements.agentDoneNotificationsToggle) return;
|
|
342
|
+
const supported = browserNotificationSupported();
|
|
343
|
+
const permission = browserNotificationPermission();
|
|
344
|
+
elements.agentDoneNotificationsToggle.checked = agentDoneNotificationsEnabled;
|
|
345
|
+
elements.agentDoneNotificationsToggle.disabled = !supported || permission === "denied";
|
|
346
|
+
elements.agentDoneNotificationsToggle.setAttribute("aria-describedby", "agentDoneNotificationsStatus");
|
|
347
|
+
if (elements.agentDoneNotificationsStatus) elements.agentDoneNotificationsStatus.textContent = agentDoneNotificationsStatusText();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function setAgentDoneNotificationsEnabled(enabled, { requestPermission = false, announce = false } = {}) {
|
|
351
|
+
let next = !!enabled;
|
|
352
|
+
if (next) {
|
|
353
|
+
if (!browserNotificationSupported()) {
|
|
354
|
+
addEvent("agent-done notifications require HTTPS or localhost", "warn");
|
|
355
|
+
next = false;
|
|
356
|
+
} else if (browserNotificationPermission() === "denied") {
|
|
357
|
+
addEvent("agent-done notifications are blocked by browser permission", "warn");
|
|
358
|
+
next = false;
|
|
359
|
+
} else if (requestPermission && browserNotificationPermission() !== "granted") {
|
|
360
|
+
next = await ensureAgentDoneNotificationPermission();
|
|
361
|
+
if (!next) addEvent("agent-done notifications not enabled; browser permission was not granted", "warn");
|
|
362
|
+
} else if (browserNotificationPermission() !== "granted") {
|
|
363
|
+
next = false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
agentDoneNotificationsEnabled = next;
|
|
367
|
+
persistAgentDoneNotificationsEnabled(next);
|
|
368
|
+
renderAgentDoneNotificationsToggle();
|
|
369
|
+
if (announce) addEvent(next ? "agent-done notifications enabled" : "agent-done notifications disabled", next ? "info" : "warn");
|
|
370
|
+
return next;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function restoreAgentDoneNotificationsSetting() {
|
|
374
|
+
agentDoneNotificationsEnabled = readStoredAgentDoneNotificationsEnabled();
|
|
375
|
+
if (agentDoneNotificationsEnabled && (!browserNotificationSupported() || browserNotificationPermission() !== "granted")) {
|
|
376
|
+
agentDoneNotificationsEnabled = false;
|
|
377
|
+
persistAgentDoneNotificationsEnabled(false);
|
|
378
|
+
}
|
|
379
|
+
renderAgentDoneNotificationsToggle();
|
|
380
|
+
}
|
|
381
|
+
|
|
216
382
|
function setComposerActionsOpen(open) {
|
|
217
383
|
const shouldOpen = open && isMobileView();
|
|
218
384
|
document.body.classList.toggle("composer-actions-open", shouldOpen);
|
|
219
385
|
elements.composerActionsButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
|
|
386
|
+
if (!shouldOpen) setPublishMenuOpen(false);
|
|
220
387
|
}
|
|
221
388
|
|
|
222
389
|
function isRunActive() {
|
|
@@ -411,6 +578,49 @@ function storeThemeName(name) {
|
|
|
411
578
|
}
|
|
412
579
|
}
|
|
413
580
|
|
|
581
|
+
function loadDisabledOptionalFeatures() {
|
|
582
|
+
try {
|
|
583
|
+
const parsed = JSON.parse(localStorage.getItem(OPTIONAL_FEATURES_STORAGE_KEY) || "[]");
|
|
584
|
+
return Array.isArray(parsed) ? parsed.filter((id) => OPTIONAL_FEATURE_BY_ID.has(id)) : [];
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let disabledOptionalFeatures = new Set(loadDisabledOptionalFeatures());
|
|
591
|
+
|
|
592
|
+
function storeDisabledOptionalFeatures() {
|
|
593
|
+
try {
|
|
594
|
+
localStorage.setItem(OPTIONAL_FEATURES_STORAGE_KEY, JSON.stringify([...disabledOptionalFeatures].sort()));
|
|
595
|
+
} catch {
|
|
596
|
+
// Optional feature toggles should still work for this page load.
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isOptionalFeatureDetected(featureId) {
|
|
601
|
+
return optionalFeatureAvailability[featureId] === true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function isOptionalFeatureDisabled(featureId) {
|
|
605
|
+
return disabledOptionalFeatures.has(featureId);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function isOptionalFeatureEnabled(featureId) {
|
|
609
|
+
return isOptionalFeatureDetected(featureId) && !isOptionalFeatureDisabled(featureId);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function setOptionalFeatureDisabled(featureId, disabled) {
|
|
613
|
+
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
614
|
+
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
615
|
+
else disabledOptionalFeatures.delete(featureId);
|
|
616
|
+
storeDisabledOptionalFeatures();
|
|
617
|
+
renderOptionalFeatureControls();
|
|
618
|
+
renderThemeSelect();
|
|
619
|
+
renderWidgets();
|
|
620
|
+
renderStatus();
|
|
621
|
+
refreshCommands().catch((error) => addEvent(error.message || String(error), "error"));
|
|
622
|
+
}
|
|
623
|
+
|
|
414
624
|
function displayThemeName(name) {
|
|
415
625
|
return String(name || "")
|
|
416
626
|
.split(/[-_]+/)
|
|
@@ -599,6 +809,13 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
599
809
|
function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {}) {
|
|
600
810
|
if (!elements.themeSelect) return;
|
|
601
811
|
elements.themeSelect.replaceChildren();
|
|
812
|
+
if (isOptionalFeatureDisabled("themeBundle")) {
|
|
813
|
+
const option = make("option", undefined, "Theme feature disabled");
|
|
814
|
+
option.value = "";
|
|
815
|
+
elements.themeSelect.append(option);
|
|
816
|
+
elements.themeSelect.disabled = true;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
602
819
|
if (!availableThemes.length) {
|
|
603
820
|
const option = make("option", undefined, unavailableLabel);
|
|
604
821
|
option.value = "";
|
|
@@ -616,6 +833,7 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
616
833
|
}
|
|
617
834
|
|
|
618
835
|
function setThemeByName(name, options = {}) {
|
|
836
|
+
if (!isOptionalFeatureEnabled("themeBundle")) return;
|
|
619
837
|
const theme = availableThemes.find((item) => item.name === name);
|
|
620
838
|
if (!theme) return;
|
|
621
839
|
applyTheme(theme, options);
|
|
@@ -627,16 +845,20 @@ async function initializeThemes() {
|
|
|
627
845
|
response = await api("/api/themes", { scoped: false });
|
|
628
846
|
} catch (error) {
|
|
629
847
|
availableThemes = [];
|
|
848
|
+
optionalFeatureAvailability.themeBundle = false;
|
|
849
|
+
renderOptionalFeatureControls();
|
|
630
850
|
const label = error.statusCode === 404 ? "Restart Web UI to load themes" : "Theme bundle unavailable";
|
|
631
851
|
renderThemeSelect({ unavailableLabel: label });
|
|
632
852
|
throw error;
|
|
633
853
|
}
|
|
634
854
|
availableThemes = Array.isArray(response.data?.themes) ? response.data.themes : [];
|
|
855
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
856
|
+
renderOptionalFeatureControls();
|
|
635
857
|
const stored = storedThemeName();
|
|
636
858
|
currentThemeName = availableThemes.some((theme) => theme.name === stored) ? stored : DEFAULT_THEME_NAME;
|
|
637
859
|
renderThemeSelect();
|
|
638
860
|
setThemeByName(currentThemeName, { persist: false });
|
|
639
|
-
if (!availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) applyTheme(availableThemes[0], { persist: false });
|
|
861
|
+
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) applyTheme(availableThemes[0], { persist: false });
|
|
640
862
|
if (!availableThemes.length) addEvent("theme bundle unavailable; using built-in default theme", "warn");
|
|
641
863
|
}
|
|
642
864
|
|
|
@@ -958,6 +1180,7 @@ function resetActiveTabUi() {
|
|
|
958
1180
|
widgets.clear();
|
|
959
1181
|
transientMessages = [];
|
|
960
1182
|
availableCommands = [];
|
|
1183
|
+
resetOptionalFeatureAvailability();
|
|
961
1184
|
commandSuggestions = [];
|
|
962
1185
|
pathSuggestions = [];
|
|
963
1186
|
suggestionMode = "none";
|
|
@@ -1113,7 +1336,7 @@ function shouldRenderTerminalTabGroup(group, groupCount) {
|
|
|
1113
1336
|
return groupCount > 1 && group.tabs.length > 1 && Boolean(group.cwd);
|
|
1114
1337
|
}
|
|
1115
1338
|
|
|
1116
|
-
function renderTerminalTabGroup(group) {
|
|
1339
|
+
function renderTerminalTabGroup(group, groupCount = 1) {
|
|
1117
1340
|
const groupTabs = group.tabs;
|
|
1118
1341
|
const activeGroupTab = groupTabs.find((tab) => tab.id === activeTabId) || groupTabs[0];
|
|
1119
1342
|
const isActive = groupTabs.some((tab) => tab.id === activeTabId);
|
|
@@ -1143,6 +1366,18 @@ function renderTerminalTabGroup(group) {
|
|
|
1143
1366
|
button.addEventListener("click", () => switchTab(activeGroupTab.id));
|
|
1144
1367
|
wrapper.append(button);
|
|
1145
1368
|
|
|
1369
|
+
if (groupCount > 1) {
|
|
1370
|
+
const close = make("button", "terminal-tab-close terminal-tab-group-close", "×");
|
|
1371
|
+
close.type = "button";
|
|
1372
|
+
close.title = `Close ${displayCwd} group`;
|
|
1373
|
+
close.setAttribute("aria-label", `Close ${displayCwd} group`);
|
|
1374
|
+
close.addEventListener("click", (event) => {
|
|
1375
|
+
event.stopPropagation();
|
|
1376
|
+
closeTerminalTabGroup(group);
|
|
1377
|
+
});
|
|
1378
|
+
wrapper.append(close);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1146
1381
|
const menu = make("div", "terminal-tab-group-menu");
|
|
1147
1382
|
menu.setAttribute("role", "group");
|
|
1148
1383
|
menu.setAttribute("aria-label", `${displayCwd} tabs`);
|
|
@@ -1193,12 +1428,13 @@ function renderTabs() {
|
|
|
1193
1428
|
if (openTerminalTabGroupKey && !renderedGroupKeys.has(openTerminalTabGroupKey)) openTerminalTabGroupKey = null;
|
|
1194
1429
|
for (const group of groups) {
|
|
1195
1430
|
if (shouldRenderTerminalTabGroup(group, groups.length)) {
|
|
1196
|
-
elements.tabBar.append(renderTerminalTabGroup(group));
|
|
1431
|
+
elements.tabBar.append(renderTerminalTabGroup(group, groups.length));
|
|
1197
1432
|
} else {
|
|
1198
1433
|
for (const tab of group.tabs) elements.tabBar.append(renderTerminalTab(tab));
|
|
1199
1434
|
}
|
|
1200
1435
|
}
|
|
1201
1436
|
elements.tabBar.append(elements.newTabButton);
|
|
1437
|
+
elements.closeAllTabsButton.disabled = tabs.length === 0;
|
|
1202
1438
|
updateTerminalTabGroupOpenState();
|
|
1203
1439
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
1204
1440
|
updateDocumentTitle();
|
|
@@ -1211,6 +1447,7 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
1211
1447
|
tabs = response.data?.tabs || [];
|
|
1212
1448
|
syncTabMetadata(tabs);
|
|
1213
1449
|
syncBlockedTabNotificationsFromTabs(tabs, previousTabs);
|
|
1450
|
+
syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
|
|
1214
1451
|
const stored = selectStored ? restoreStoredTabId() : null;
|
|
1215
1452
|
if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
|
|
1216
1453
|
activeTabId = (stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null;
|
|
@@ -1258,21 +1495,58 @@ async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = eleme
|
|
|
1258
1495
|
}
|
|
1259
1496
|
}
|
|
1260
1497
|
|
|
1261
|
-
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1498
|
+
function tabHasActiveAgent(tab) {
|
|
1499
|
+
const activity = activityForTab(tab);
|
|
1500
|
+
const indicator = tabIndicator(tab);
|
|
1501
|
+
return !!activity.isWorking || indicator.state === "working" || indicator.state === "blocked";
|
|
1502
|
+
}
|
|
1265
1503
|
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1504
|
+
function confirmCloseTerminalTabs(targetTabs, label) {
|
|
1505
|
+
const count = targetTabs.length;
|
|
1506
|
+
const noun = count === 1 ? "tab" : "tabs";
|
|
1507
|
+
const activeAgentTabs = targetTabs.filter(tabHasActiveAgent);
|
|
1508
|
+
const tabList = targetTabs.map((tab) => `- ${tab.title}`).join("\n");
|
|
1509
|
+
const activeList = activeAgentTabs.map((tab) => `- ${tab.title} (${tabIndicator(tab).label})`).join("\n");
|
|
1510
|
+
const base = [
|
|
1511
|
+
`Close ${label || `${count} terminal ${noun}`}?`,
|
|
1512
|
+
"",
|
|
1513
|
+
`This terminates ${count === 1 ? "its isolated Pi process" : "their isolated Pi processes"}.`,
|
|
1514
|
+
count > 1 ? `\nTabs to close:\n${tabList}` : "",
|
|
1515
|
+
].filter(Boolean).join("\n");
|
|
1516
|
+
const warning = activeAgentTabs.length
|
|
1517
|
+
? [
|
|
1518
|
+
`WARNING: ${activeAgentTabs.length} ${activeAgentTabs.length === 1 ? "tab has an agent" : "tabs have agents"} still running or waiting for input:`,
|
|
1519
|
+
activeList,
|
|
1520
|
+
"",
|
|
1521
|
+
base,
|
|
1522
|
+
"",
|
|
1523
|
+
"Close anyway?",
|
|
1524
|
+
].join("\n")
|
|
1525
|
+
: base;
|
|
1526
|
+
return confirm(warning);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } = {}) {
|
|
1530
|
+
const targetIds = [...new Set(tabIds.filter(Boolean))];
|
|
1531
|
+
const targetTabs = targetIds.map((id) => tabs.find((item) => item.id === id)).filter(Boolean);
|
|
1532
|
+
if (!targetTabs.length) return;
|
|
1533
|
+
if (!confirmCloseTerminalTabs(targetTabs, label)) return;
|
|
1534
|
+
|
|
1535
|
+
const closedActiveTab = targetTabs.some((tab) => tab.id === activeTabId);
|
|
1536
|
+
const fallbackTabId = tabs.find((item) => !targetIds.includes(item.id))?.id || null;
|
|
1268
1537
|
try {
|
|
1269
|
-
if (
|
|
1270
|
-
const response = await api(
|
|
1271
|
-
|
|
1538
|
+
if (closedActiveTab) eventSource?.close();
|
|
1539
|
+
const response = await api("/api/tabs/close", { method: "POST", body: { ids: targetIds }, scoped: false });
|
|
1540
|
+
const closedIds = response.data?.closedIds || targetIds;
|
|
1541
|
+
tabs = response.data?.tabs || tabs.filter((item) => !closedIds.includes(item.id));
|
|
1272
1542
|
syncTabMetadata(tabs);
|
|
1273
|
-
tabDrafts.delete(
|
|
1274
|
-
|
|
1275
|
-
|
|
1543
|
+
for (const id of closedIds) tabDrafts.delete(id);
|
|
1544
|
+
clearOpenTerminalTabGroup(null, { force: true });
|
|
1545
|
+
|
|
1546
|
+
if (closedActiveTab || !tabs.some((item) => item.id === activeTabId)) {
|
|
1547
|
+
activeTabId = (response.data?.activeTabId && tabs.some((item) => item.id === response.data.activeTabId)
|
|
1548
|
+
? response.data.activeTabId
|
|
1549
|
+
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null;
|
|
1276
1550
|
rememberActiveTab();
|
|
1277
1551
|
resetActiveTabUi();
|
|
1278
1552
|
renderTabs();
|
|
@@ -1286,11 +1560,27 @@ async function closeTerminalTab(tabId) {
|
|
|
1286
1560
|
} else {
|
|
1287
1561
|
renderTabs();
|
|
1288
1562
|
}
|
|
1563
|
+
addEvent(`closed ${closedIds.length || targetTabs.length} terminal ${closedIds.length === 1 ? "tab" : "tabs"}`, "warn");
|
|
1289
1564
|
} catch (error) {
|
|
1290
1565
|
addEvent(error.message, "error");
|
|
1291
1566
|
}
|
|
1292
1567
|
}
|
|
1293
1568
|
|
|
1569
|
+
async function closeTerminalTab(tabId) {
|
|
1570
|
+
const tab = tabs.find((item) => item.id === tabId);
|
|
1571
|
+
if (!tab) return;
|
|
1572
|
+
await closeTerminalTabs([tabId], { label: tab.title });
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async function closeTerminalTabGroup(group) {
|
|
1576
|
+
const title = tabGroupTitle(group.cwd, group.tabs[0]?.title || "cwd");
|
|
1577
|
+
await closeTerminalTabs(group.tabs.map((tab) => tab.id), { label: `${title} group` });
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function closeAllTerminalTabs() {
|
|
1581
|
+
await closeTerminalTabs(tabs.map((tab) => tab.id), { label: "all terminal tabs" });
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1294
1584
|
async function initializeTabs() {
|
|
1295
1585
|
await refreshTabs({ selectStored: true });
|
|
1296
1586
|
resetActiveTabUi();
|
|
@@ -1312,15 +1602,23 @@ function addEvent(message, level = "info") {
|
|
|
1312
1602
|
while (elements.eventLog.children.length > 120) elements.eventLog.lastElementChild?.remove();
|
|
1313
1603
|
}
|
|
1314
1604
|
|
|
1315
|
-
function
|
|
1605
|
+
function browserNotificationSupported() {
|
|
1316
1606
|
return "Notification" in window && window.isSecureContext;
|
|
1317
1607
|
}
|
|
1318
1608
|
|
|
1319
|
-
function
|
|
1609
|
+
function browserNotificationPermission() {
|
|
1320
1610
|
if (!("Notification" in window)) return "unsupported";
|
|
1321
1611
|
return Notification.permission || "default";
|
|
1322
1612
|
}
|
|
1323
1613
|
|
|
1614
|
+
function blockedTabNotificationSupported() {
|
|
1615
|
+
return browserNotificationSupported();
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function blockedTabNotificationPermission() {
|
|
1619
|
+
return browserNotificationPermission();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1324
1622
|
async function ensureBlockedTabNotificationPermission() {
|
|
1325
1623
|
if (!blockedTabNotificationSupported()) return false;
|
|
1326
1624
|
if (Notification.permission === "granted") return true;
|
|
@@ -1339,6 +1637,24 @@ async function ensureBlockedTabNotificationPermission() {
|
|
|
1339
1637
|
return false;
|
|
1340
1638
|
}
|
|
1341
1639
|
|
|
1640
|
+
async function ensureAgentDoneNotificationPermission() {
|
|
1641
|
+
if (!browserNotificationSupported()) return false;
|
|
1642
|
+
if (Notification.permission === "granted") return true;
|
|
1643
|
+
if (Notification.permission === "denied" || agentDoneNotificationPermissionRequested || typeof Notification.requestPermission !== "function") return false;
|
|
1644
|
+
|
|
1645
|
+
agentDoneNotificationPermissionRequested = true;
|
|
1646
|
+
try {
|
|
1647
|
+
const permission = await Notification.requestPermission();
|
|
1648
|
+
if (permission === "granted") {
|
|
1649
|
+
addEvent("browser notifications enabled for completed agent work", "info");
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
addEvent(`agent-done notification permission request failed: ${error.message}`, "warn");
|
|
1654
|
+
}
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1342
1658
|
function noteBlockedTabNotificationFallback(reason) {
|
|
1343
1659
|
if (blockedTabNotificationFallbackNoted) return;
|
|
1344
1660
|
blockedTabNotificationFallbackNoted = true;
|
|
@@ -1423,6 +1739,97 @@ function notifyBlockedTab(tabOrId, { request = null, count } = {}) {
|
|
|
1423
1739
|
showBlockedTabBrowserNotification({ tabId, title, body, method, count: pendingCount });
|
|
1424
1740
|
}
|
|
1425
1741
|
|
|
1742
|
+
function noteAgentDoneNotificationFallback(reason) {
|
|
1743
|
+
if (agentDoneNotificationFallbackNoted) return;
|
|
1744
|
+
agentDoneNotificationFallbackNoted = true;
|
|
1745
|
+
addEvent(`browser notifications unavailable for completed agent work: ${reason}`, "warn");
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async function showAgentDoneBrowserNotification({ tabId, title, body }) {
|
|
1749
|
+
if (!agentDoneNotificationsEnabled) return false;
|
|
1750
|
+
if (!browserNotificationSupported()) {
|
|
1751
|
+
noteAgentDoneNotificationFallback("requires HTTPS or localhost");
|
|
1752
|
+
renderAgentDoneNotificationsToggle();
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
if (!(await ensureAgentDoneNotificationPermission())) {
|
|
1756
|
+
const permission = browserNotificationPermission();
|
|
1757
|
+
noteAgentDoneNotificationFallback(permission === "denied" ? "permission denied" : "permission not granted");
|
|
1758
|
+
if (permission !== "granted") {
|
|
1759
|
+
agentDoneNotificationsEnabled = false;
|
|
1760
|
+
persistAgentDoneNotificationsEnabled(false);
|
|
1761
|
+
}
|
|
1762
|
+
renderAgentDoneNotificationsToggle();
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const options = {
|
|
1767
|
+
body,
|
|
1768
|
+
tag: `${AGENT_DONE_NOTIFICATION_TAG_PREFIX}:${tabId}`,
|
|
1769
|
+
renotify: true,
|
|
1770
|
+
requireInteraction: false,
|
|
1771
|
+
icon: BLOCKED_TAB_NOTIFICATION_ICON,
|
|
1772
|
+
badge: BLOCKED_TAB_NOTIFICATION_ICON,
|
|
1773
|
+
data: { tabId, url: location.href },
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
try {
|
|
1777
|
+
let registration = null;
|
|
1778
|
+
if ("serviceWorker" in navigator) {
|
|
1779
|
+
registration = await Promise.race([navigator.serviceWorker.ready, delay(1200).then(() => null)]).catch(() => null);
|
|
1780
|
+
}
|
|
1781
|
+
if (registration?.showNotification) {
|
|
1782
|
+
await registration.showNotification(title, options);
|
|
1783
|
+
return true;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const notification = new Notification(title, options);
|
|
1787
|
+
notification.onclick = () => {
|
|
1788
|
+
window.focus();
|
|
1789
|
+
if (tabId && tabId !== activeTabId) switchTab(tabId).catch((error) => addEvent(error.message, "error"));
|
|
1790
|
+
notification.close();
|
|
1791
|
+
};
|
|
1792
|
+
return true;
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
noteAgentDoneNotificationFallback(error.message || "notification failed");
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function agentDoneNotificationKey(tabId, activity = {}) {
|
|
1800
|
+
const serial = Number(activity?.completionSerial);
|
|
1801
|
+
return `${tabId}:${Number.isFinite(serial) && serial > 0 ? serial : "done"}`;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
|
|
1805
|
+
if (!agentDoneNotificationsEnabled) return;
|
|
1806
|
+
const tabId = typeof tabOrId === "string" ? tabOrId : tabOrId?.id || activeTabId;
|
|
1807
|
+
if (!tabId) return;
|
|
1808
|
+
const tab = typeof tabOrId === "object" && tabOrId !== null ? tabOrId : tabs.find((item) => item.id === tabId);
|
|
1809
|
+
const normalizedActivity = normalizeTabActivity(activity || tab?.activity || activityForTab(tab));
|
|
1810
|
+
if (!normalizedActivity.completionSerial) return;
|
|
1811
|
+
const key = agentDoneNotificationKey(tabId, normalizedActivity);
|
|
1812
|
+
if (agentDoneNotificationKeys.has(key)) return;
|
|
1813
|
+
agentDoneNotificationKeys.add(key);
|
|
1814
|
+
|
|
1815
|
+
const displayTitle = tabTitle || tab?.title || "terminal";
|
|
1816
|
+
showAgentDoneBrowserNotification({
|
|
1817
|
+
tabId,
|
|
1818
|
+
title: "Pi finished work",
|
|
1819
|
+
body: `${displayTitle} finished its agent run.`,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function syncAgentDoneNotificationsFromTabs(nextTabs = [], previousTabs = []) {
|
|
1824
|
+
if (!agentDoneNotificationsEnabled || previousTabs.length === 0) return;
|
|
1825
|
+
const previousSerials = new Map(previousTabs.filter((tab) => tab?.id).map((tab) => [tab.id, normalizeTabActivity(tab.activity).completionSerial]));
|
|
1826
|
+
for (const tab of nextTabs) {
|
|
1827
|
+
if (!tab?.id || !previousSerials.has(tab.id)) continue;
|
|
1828
|
+
const activity = normalizeTabActivity(tab.activity);
|
|
1829
|
+
if (!activity.isWorking && activity.completionSerial > previousSerials.get(tab.id)) notifyAgentDone(tab, { activity });
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1426
1833
|
function syncBlockedTabNotificationsFromTabs(nextTabs = [], previousTabs = []) {
|
|
1427
1834
|
if (previousTabs.length === 0) return;
|
|
1428
1835
|
const previousCounts = new Map(previousTabs.filter((tab) => tab?.id).map((tab) => [tab.id, tabPendingBlockerCount(tab)]));
|
|
@@ -1587,6 +1994,7 @@ function formatStatusEntry(key, value) {
|
|
|
1587
1994
|
const cleanKey = cleanStatusText(key);
|
|
1588
1995
|
const cleanValue = cleanStatusText(value);
|
|
1589
1996
|
if (!cleanValue) return "";
|
|
1997
|
+
if (cleanKey === "git-footer" && !isOptionalFeatureEnabled("gitFooterStatus")) return "";
|
|
1590
1998
|
if (cleanKey === "plan-mode") return `Plan: ${cleanValue}`;
|
|
1591
1999
|
if (cleanKey === "extension") return cleanValue;
|
|
1592
2000
|
return `${cleanKey}: ${cleanValue}`;
|
|
@@ -2185,7 +2593,53 @@ function isGuardrailDialogPrompt(prompt) {
|
|
|
2185
2593
|
return /(?:dangerous|high-risk|protected).*(?:command|file)|safety rule|execute anyway\?/i.test(`${plainTitle}\n${plainMessage}`);
|
|
2186
2594
|
}
|
|
2187
2595
|
|
|
2596
|
+
function releaseDialogPromptParts(prompt) {
|
|
2597
|
+
const combined = [prompt.title, prompt.message].filter((part) => stripAnsi(part).trim()).join("\n").trimEnd();
|
|
2598
|
+
const lines = combined.split("\n");
|
|
2599
|
+
const questionIndex = lines.findIndex((line) => /^(Publish eligible packages now\?|Publish to AUR\?|Publish newly created\/converged AUR package\?)$/i.test(stripAnsi(line).trim()));
|
|
2600
|
+
const question = questionIndex === -1 ? "Publish eligible packages now?" : stripAnsi(lines[questionIndex]).trim();
|
|
2601
|
+
const isNpmReleasePrompt = /Release preflight summary:/i.test(combined) && /Publish eligible packages now\?/i.test(combined);
|
|
2602
|
+
const isAurReleasePrompt = /AUR release summary:/i.test(combined) && questionIndex !== -1;
|
|
2603
|
+
if (!isNpmReleasePrompt && !isAurReleasePrompt) return null;
|
|
2604
|
+
|
|
2605
|
+
const summaryLines = questionIndex === -1 ? lines : [...lines.slice(0, questionIndex), ...lines.slice(questionIndex + 1)];
|
|
2606
|
+
const message = summaryLines.join("\n").replace(/\n+$/, "");
|
|
2607
|
+
return {
|
|
2608
|
+
title: question,
|
|
2609
|
+
message,
|
|
2610
|
+
plainMessage: stripAnsi(message),
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
function releaseDialogLineClass(plainLine, section) {
|
|
2615
|
+
const text = plainLine.trim();
|
|
2616
|
+
if (!text) return "release-dialog-spacer";
|
|
2617
|
+
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)) {
|
|
2618
|
+
return "release-dialog-heading";
|
|
2619
|
+
}
|
|
2620
|
+
if (/^none$/i.test(text)) return "release-dialog-muted";
|
|
2621
|
+
if (/->\s*error\b|\bfailed\b|\bmissing\b|\berrors?:\s*[1-9]/i.test(text) || /^blocked$/i.test(section)) return "release-dialog-danger";
|
|
2622
|
+
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";
|
|
2623
|
+
if (/\bskip(?:ped)?\b|\bunchanged\b|would reduce down|already published/i.test(text) || /^will skip$/i.test(section)) return "release-dialog-warning";
|
|
2624
|
+
return "";
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
function renderReleaseDialogMessage(parent, text) {
|
|
2628
|
+
parent.replaceChildren();
|
|
2629
|
+
let section = "";
|
|
2630
|
+
for (const line of String(text || "").split("\n")) {
|
|
2631
|
+
const plainLine = stripAnsi(line);
|
|
2632
|
+
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);
|
|
2633
|
+
const rowClass = ["release-dialog-line", releaseDialogLineClass(plainLine, section)].filter(Boolean).join(" ");
|
|
2634
|
+
const row = make("span", rowClass);
|
|
2635
|
+
renderAnsiText(row, line || " ");
|
|
2636
|
+
parent.append(row);
|
|
2637
|
+
if (heading) section = heading[1].toLowerCase();
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2188
2641
|
function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
2642
|
+
if (!isOptionalFeatureEnabled("todoProgressWidget")) return String(text || "");
|
|
2189
2643
|
let inFence = false;
|
|
2190
2644
|
const kept = [];
|
|
2191
2645
|
const raw = String(text || "");
|
|
@@ -2272,11 +2726,209 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
2272
2726
|
return node;
|
|
2273
2727
|
}
|
|
2274
2728
|
|
|
2729
|
+
function getWidgetLines(key) {
|
|
2730
|
+
const value = widgets.get(key);
|
|
2731
|
+
return Array.isArray(value?.widgetLines) ? value.widgetLines : [];
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
function releaseNpmFooterDetails(lines) {
|
|
2735
|
+
const primary = cleanStatusText(lines[0] || "").replace(/^release-(?:npm|aur):\s*/i, "");
|
|
2736
|
+
const parts = primary.split(/\s+·\s+/).map((part) => part.trim()).filter(Boolean);
|
|
2737
|
+
return {
|
|
2738
|
+
phase: parts[0] || "release workflow",
|
|
2739
|
+
mode: parts[1] || "",
|
|
2740
|
+
elapsed: parts[2] || "",
|
|
2741
|
+
controls: lines.slice(1).map(cleanStatusText).filter(Boolean).join(" · "),
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function releaseNpmLineTone(line) {
|
|
2746
|
+
const clean = stripAnsi(line).trim();
|
|
2747
|
+
if (/^\$\s+/.test(clean)) return "command";
|
|
2748
|
+
if (/^==>/.test(clean)) return "target";
|
|
2749
|
+
if (/^(PASS|✓|Published)\b/i.test(clean)) return "pass";
|
|
2750
|
+
if (/^(FAIL|ERROR|ERR|✗)\b/i.test(clean)) return "fail";
|
|
2751
|
+
if (/^(WARN|warning)\b/i.test(clean)) return "warn";
|
|
2752
|
+
if (/^(INFO|npm notice|notice)\b/i.test(clean)) return "info";
|
|
2753
|
+
if (/^RELEASE_NPM_EVENT\b/.test(clean)) return "event";
|
|
2754
|
+
return "";
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
function appendReleaseNpmTerminalLine(parent, line) {
|
|
2758
|
+
const tone = releaseNpmLineTone(line);
|
|
2759
|
+
const row = make("div", `release-npm-line${tone ? ` ${tone}` : ""}`);
|
|
2760
|
+
if (String(line ?? "") === "") row.textContent = "\u00a0";
|
|
2761
|
+
else renderAnsiText(row, line);
|
|
2762
|
+
parent.append(row);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
async function sendReleaseNpmCommand(command) {
|
|
2766
|
+
try {
|
|
2767
|
+
await api("/api/prompt", { method: "POST", body: { message: command }, tabId: activeTabId });
|
|
2768
|
+
addEvent(`${command} sent`, "info");
|
|
2769
|
+
scheduleRefreshState();
|
|
2770
|
+
} catch (error) {
|
|
2771
|
+
addEvent(error.message, "error");
|
|
2772
|
+
addTransientMessage({ role: "error", title: command, content: error.message, level: "error" });
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
function releaseNpmActionButton(label, command, className = "") {
|
|
2777
|
+
const button = make("button", `release-npm-action ${className}`.trim(), label);
|
|
2778
|
+
button.type = "button";
|
|
2779
|
+
button.addEventListener("click", () => sendReleaseNpmCommand(command));
|
|
2780
|
+
return button;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function renderReleaseNpmOutputWidget() {
|
|
2784
|
+
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
2785
|
+
const outputLines = getWidgetLines("release-npm:output");
|
|
2786
|
+
const footerLines = getWidgetLines("release-npm:footer");
|
|
2787
|
+
if (outputLines.length === 0 && footerLines.length === 0) return null;
|
|
2788
|
+
|
|
2789
|
+
const details = releaseNpmFooterDetails(footerLines);
|
|
2790
|
+
const node = make("section", "widget release-npm-widget release-npm-live-widget");
|
|
2791
|
+
node.setAttribute("aria-label", "npm release output");
|
|
2792
|
+
|
|
2793
|
+
const header = make("div", "release-npm-header");
|
|
2794
|
+
const titleWrap = make("div", "release-npm-title-wrap");
|
|
2795
|
+
titleWrap.append(make("span", "release-npm-kicker", "npm release"), make("strong", "release-npm-title", details.phase));
|
|
2796
|
+
|
|
2797
|
+
const meta = make("div", "release-npm-meta");
|
|
2798
|
+
if (details.mode) meta.append(make("span", "release-npm-pill", details.mode));
|
|
2799
|
+
if (details.elapsed) meta.append(make("span", "release-npm-pill elapsed", details.elapsed));
|
|
2800
|
+
|
|
2801
|
+
const actions = make("div", "release-npm-actions");
|
|
2802
|
+
actions.append(
|
|
2803
|
+
releaseNpmActionButton("Toggle output", "/release-toggle"),
|
|
2804
|
+
releaseNpmActionButton("Abort", "/release-abort", "danger"),
|
|
2805
|
+
);
|
|
2806
|
+
header.append(titleWrap, meta, actions);
|
|
2807
|
+
|
|
2808
|
+
const terminal = make("div", "release-npm-terminal");
|
|
2809
|
+
terminal.setAttribute("role", "log");
|
|
2810
|
+
terminal.setAttribute("aria-live", "polite");
|
|
2811
|
+
for (const line of (outputLines.length ? outputLines : ["Waiting for release output..."])) {
|
|
2812
|
+
appendReleaseNpmTerminalLine(terminal, line);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
|
|
2816
|
+
node.append(header, terminal, controls);
|
|
2817
|
+
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
2818
|
+
return node;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function renderReleaseNpmLogWidget() {
|
|
2822
|
+
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
2823
|
+
const lines = getWidgetLines("release-npm:logs");
|
|
2824
|
+
if (lines.length === 0) return null;
|
|
2825
|
+
|
|
2826
|
+
const node = make("section", "widget release-npm-widget release-npm-log-widget");
|
|
2827
|
+
node.setAttribute("aria-label", "npm release log");
|
|
2828
|
+
const header = make("div", "release-npm-header");
|
|
2829
|
+
const titleWrap = make("div", "release-npm-title-wrap");
|
|
2830
|
+
titleWrap.append(
|
|
2831
|
+
make("span", "release-npm-kicker", "saved log"),
|
|
2832
|
+
make("strong", "release-npm-title", stripAnsi(lines[0] || "release-npm log")),
|
|
2833
|
+
);
|
|
2834
|
+
const meta = make("div", "release-npm-meta");
|
|
2835
|
+
if (lines[1]) meta.append(make("span", "release-npm-pill", stripAnsi(lines[1])));
|
|
2836
|
+
const actions = make("div", "release-npm-actions");
|
|
2837
|
+
actions.append(releaseNpmActionButton("Close log", "/release-npm-logs close"));
|
|
2838
|
+
header.append(titleWrap, meta, actions);
|
|
2839
|
+
|
|
2840
|
+
const terminal = make("div", "release-npm-terminal");
|
|
2841
|
+
for (const line of lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim())) {
|
|
2842
|
+
appendReleaseNpmTerminalLine(terminal, line);
|
|
2843
|
+
}
|
|
2844
|
+
node.append(header, terminal);
|
|
2845
|
+
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
2846
|
+
return node;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
function renderReleaseAurOutputWidget() {
|
|
2850
|
+
if (!isOptionalFeatureEnabled("releaseAur")) return null;
|
|
2851
|
+
const outputLines = getWidgetLines("release-aur:output");
|
|
2852
|
+
const footerLines = getWidgetLines("release-aur:footer");
|
|
2853
|
+
if (outputLines.length === 0 && footerLines.length === 0) return null;
|
|
2854
|
+
|
|
2855
|
+
const details = releaseNpmFooterDetails(footerLines);
|
|
2856
|
+
const node = make("section", "widget release-npm-widget release-aur-widget release-aur-live-widget");
|
|
2857
|
+
node.setAttribute("aria-label", "AUR release output");
|
|
2858
|
+
|
|
2859
|
+
const header = make("div", "release-npm-header");
|
|
2860
|
+
const titleWrap = make("div", "release-npm-title-wrap");
|
|
2861
|
+
titleWrap.append(make("span", "release-npm-kicker", "AUR release"), make("strong", "release-npm-title", details.phase));
|
|
2862
|
+
|
|
2863
|
+
const meta = make("div", "release-npm-meta");
|
|
2864
|
+
if (details.mode) meta.append(make("span", "release-npm-pill", details.mode));
|
|
2865
|
+
if (details.elapsed) meta.append(make("span", "release-npm-pill elapsed", details.elapsed));
|
|
2866
|
+
|
|
2867
|
+
const actions = make("div", "release-npm-actions");
|
|
2868
|
+
actions.append(
|
|
2869
|
+
releaseNpmActionButton("Toggle output", "/release-aur toggle"),
|
|
2870
|
+
releaseNpmActionButton("Abort", "/release-aur abort", "danger"),
|
|
2871
|
+
);
|
|
2872
|
+
header.append(titleWrap, meta, actions);
|
|
2873
|
+
|
|
2874
|
+
const terminal = make("div", "release-npm-terminal");
|
|
2875
|
+
terminal.setAttribute("role", "log");
|
|
2876
|
+
terminal.setAttribute("aria-live", "polite");
|
|
2877
|
+
for (const line of (outputLines.length ? outputLines : ["Waiting for release-aur output..."])) {
|
|
2878
|
+
appendReleaseNpmTerminalLine(terminal, line);
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
|
|
2882
|
+
node.append(header, terminal, controls);
|
|
2883
|
+
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
2884
|
+
return node;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function renderReleaseAurLogWidget() {
|
|
2888
|
+
if (!isOptionalFeatureEnabled("releaseAur")) return null;
|
|
2889
|
+
const lines = getWidgetLines("release-aur:logs");
|
|
2890
|
+
if (lines.length === 0) return null;
|
|
2891
|
+
|
|
2892
|
+
const node = make("section", "widget release-npm-widget release-aur-widget release-aur-log-widget");
|
|
2893
|
+
node.setAttribute("aria-label", "AUR release log");
|
|
2894
|
+
const header = make("div", "release-npm-header");
|
|
2895
|
+
const titleWrap = make("div", "release-npm-title-wrap");
|
|
2896
|
+
titleWrap.append(
|
|
2897
|
+
make("span", "release-npm-kicker", "saved AUR log"),
|
|
2898
|
+
make("strong", "release-npm-title", stripAnsi(lines[0] || "release-aur log")),
|
|
2899
|
+
);
|
|
2900
|
+
const meta = make("div", "release-npm-meta");
|
|
2901
|
+
if (lines[1]) meta.append(make("span", "release-npm-pill", stripAnsi(lines[1])));
|
|
2902
|
+
const actions = make("div", "release-npm-actions");
|
|
2903
|
+
actions.append(releaseNpmActionButton("Close log", "/release-aur logs close"));
|
|
2904
|
+
header.append(titleWrap, meta, actions);
|
|
2905
|
+
|
|
2906
|
+
const terminal = make("div", "release-npm-terminal");
|
|
2907
|
+
for (const line of lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim())) {
|
|
2908
|
+
appendReleaseNpmTerminalLine(terminal, line);
|
|
2909
|
+
}
|
|
2910
|
+
node.append(header, terminal);
|
|
2911
|
+
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
2912
|
+
return node;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2275
2915
|
function renderWidgets() {
|
|
2276
2916
|
elements.widgetArea.replaceChildren();
|
|
2917
|
+
const releaseOutput = renderReleaseNpmOutputWidget();
|
|
2918
|
+
if (releaseOutput) elements.widgetArea.append(releaseOutput);
|
|
2919
|
+
const releaseLog = renderReleaseNpmLogWidget();
|
|
2920
|
+
if (releaseLog) elements.widgetArea.append(releaseLog);
|
|
2921
|
+
const releaseAurOutput = renderReleaseAurOutputWidget();
|
|
2922
|
+
if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
|
|
2923
|
+
const releaseAurLog = renderReleaseAurLogWidget();
|
|
2924
|
+
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
2925
|
+
|
|
2277
2926
|
for (const [key, value] of widgets) {
|
|
2927
|
+
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
2928
|
+
if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
|
|
2929
|
+
if (widgetFeatureId && key !== "todo-progress") continue;
|
|
2278
2930
|
const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
|
|
2279
|
-
const specialized = key === "todo-progress" ? renderTodoProgressWidget(key, lines) : null;
|
|
2931
|
+
const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
|
|
2280
2932
|
if (specialized) {
|
|
2281
2933
|
elements.widgetArea.append(specialized);
|
|
2282
2934
|
continue;
|
|
@@ -2424,6 +3076,11 @@ function failGitWorkflow(error, step = gitWorkflow.step) {
|
|
|
2424
3076
|
}
|
|
2425
3077
|
|
|
2426
3078
|
function startGitWorkflow() {
|
|
3079
|
+
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
3080
|
+
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
3081
|
+
refreshCommands().catch((error) => addEvent(error.message || String(error), "error"));
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
2427
3084
|
if (gitWorkflow.active && !["done", "cancelled", "error"].includes(gitWorkflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
2428
3085
|
gitWorkflow.runId += 1;
|
|
2429
3086
|
setGitWorkflow({
|
|
@@ -2864,6 +3521,14 @@ function assistantThinkingText(part) {
|
|
|
2864
3521
|
return typeof part.content === "string" ? part.content : "";
|
|
2865
3522
|
}
|
|
2866
3523
|
|
|
3524
|
+
function isAssistantToolCallPart(part) {
|
|
3525
|
+
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
function assistantHasToolCallAfter(content, index) {
|
|
3529
|
+
return Array.isArray(content) && content.slice(index + 1).some(isAssistantToolCallPart);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
2867
3532
|
function assistantToolCallName(part) {
|
|
2868
3533
|
return String(part?.name || part?.toolName || part?.toolCall?.name || "unknown");
|
|
2869
3534
|
}
|
|
@@ -2900,14 +3565,15 @@ function assistantDisplayMessages(message) {
|
|
|
2900
3565
|
|
|
2901
3566
|
const displayMessages = [];
|
|
2902
3567
|
const finalParts = [];
|
|
2903
|
-
for (
|
|
3568
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
3569
|
+
const part = content[index];
|
|
2904
3570
|
const isThinkingPart = part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string");
|
|
2905
3571
|
if (isThinkingPart) {
|
|
2906
3572
|
const thinking = assistantThinkingText(part) || "No thinking content was exposed by the provider.";
|
|
2907
3573
|
displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
2908
3574
|
continue;
|
|
2909
3575
|
}
|
|
2910
|
-
if (part
|
|
3576
|
+
if (isAssistantToolCallPart(part)) {
|
|
2911
3577
|
const toolName = assistantToolCallName(part);
|
|
2912
3578
|
const args = assistantToolCallArguments(part);
|
|
2913
3579
|
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, arguments: args, content: args });
|
|
@@ -2915,7 +3581,7 @@ function assistantDisplayMessages(message) {
|
|
|
2915
3581
|
}
|
|
2916
3582
|
const finalPart = assistantFinalOutputPart(part);
|
|
2917
3583
|
if (finalPart) {
|
|
2918
|
-
finalParts.push(finalPart);
|
|
3584
|
+
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
2919
3585
|
continue;
|
|
2920
3586
|
}
|
|
2921
3587
|
if (part !== undefined && part !== null) {
|
|
@@ -3078,6 +3744,15 @@ function updateStickyUserPromptButton() {
|
|
|
3078
3744
|
);
|
|
3079
3745
|
}
|
|
3080
3746
|
|
|
3747
|
+
function toolResultPreviewText(message, lineLimit = 10) {
|
|
3748
|
+
const text = textFromContent(message?.content).replace(/\s+$/g, "");
|
|
3749
|
+
if (!text) return "(empty tool result)";
|
|
3750
|
+
const lines = text.split(/\r?\n/);
|
|
3751
|
+
const preview = lines.slice(0, lineLimit).join("\n");
|
|
3752
|
+
const remaining = Math.max(0, lines.length - lineLimit);
|
|
3753
|
+
return remaining > 0 ? `${preview}\n… ${remaining} more line${remaining === 1 ? "" : "s"}; expand for full output` : preview;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3081
3756
|
function jumpToStickyUserPrompt() {
|
|
3082
3757
|
const button = elements.stickyUserPromptButton;
|
|
3083
3758
|
const index = Number(button?.dataset.messageIndex);
|
|
@@ -3092,10 +3767,10 @@ function jumpToStickyUserPrompt() {
|
|
|
3092
3767
|
requestAnimationFrame(updateStickyUserPromptButton);
|
|
3093
3768
|
}
|
|
3094
3769
|
|
|
3095
|
-
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
3770
|
+
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
3096
3771
|
const role = String(message.role || "message");
|
|
3097
3772
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
3098
|
-
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}`);
|
|
3773
|
+
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
3099
3774
|
if (!transient && messageIndex >= 0) {
|
|
3100
3775
|
bubble.dataset.messageIndex = String(messageIndex);
|
|
3101
3776
|
if (role === "user") bubble.dataset.userPrompt = "true";
|
|
@@ -3115,7 +3790,8 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3115
3790
|
renderContent(body, message.content);
|
|
3116
3791
|
if (message.isError) bubble.classList.add("error");
|
|
3117
3792
|
} else if (message.role === "thinking") {
|
|
3118
|
-
|
|
3793
|
+
const thinkingText = message.thinking || textFromContent(message.content);
|
|
3794
|
+
if (thinkingText || !streaming) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
3119
3795
|
} else if (message.role === "toolCall") {
|
|
3120
3796
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
3121
3797
|
} else if (message.role === "assistantEvent") {
|
|
@@ -3129,6 +3805,11 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3129
3805
|
if (message.isError) details.open = true;
|
|
3130
3806
|
details.append(header, body);
|
|
3131
3807
|
bubble.append(details);
|
|
3808
|
+
if (message.role === "toolResult" && !message.isError) {
|
|
3809
|
+
const preview = make("div", "tool-result-preview");
|
|
3810
|
+
appendText(preview, toolResultPreviewText(message, 10), "code-block tool-result-preview-text");
|
|
3811
|
+
bubble.append(preview);
|
|
3812
|
+
}
|
|
3132
3813
|
} else {
|
|
3133
3814
|
bubble.append(header, body);
|
|
3134
3815
|
}
|
|
@@ -3137,9 +3818,9 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3137
3818
|
return { bubble, body };
|
|
3138
3819
|
}
|
|
3139
3820
|
|
|
3140
|
-
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
3821
|
+
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
3141
3822
|
if (streaming || transient || message?.role !== "assistant") {
|
|
3142
|
-
return appendMessage(message, { streaming, messageIndex, transient });
|
|
3823
|
+
return appendMessage(message, { streaming, messageIndex, transient, animateEntry });
|
|
3143
3824
|
}
|
|
3144
3825
|
|
|
3145
3826
|
let finalOutput = null;
|
|
@@ -3149,6 +3830,7 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
3149
3830
|
streaming: false,
|
|
3150
3831
|
messageIndex: displayMessage.role === "assistant" ? messageIndex : -1,
|
|
3151
3832
|
transient: false,
|
|
3833
|
+
animateEntry: animateEntry && isActionTranscriptMessage(displayMessage),
|
|
3152
3834
|
});
|
|
3153
3835
|
if (displayMessage.role === "assistant") finalOutput = created;
|
|
3154
3836
|
});
|
|
@@ -3199,7 +3881,7 @@ function formatRunIndicatorElapsed() {
|
|
|
3199
3881
|
|
|
3200
3882
|
function runIndicatorHeadline() {
|
|
3201
3883
|
if (currentState?.isCompacting && !currentState?.isStreaming) return "Pi is compacting context:";
|
|
3202
|
-
return "Agent is
|
|
3884
|
+
return "Agent is running: ";
|
|
3203
3885
|
}
|
|
3204
3886
|
|
|
3205
3887
|
function runIndicatorShowsElapsed() {
|
|
@@ -3233,7 +3915,7 @@ function ensureRunIndicatorBubble() {
|
|
|
3233
3915
|
if (runIndicatorBubble?.parentElement !== elements.chat) {
|
|
3234
3916
|
runIndicatorBubble = make("article", "message runIndicator run-indicator-message streaming");
|
|
3235
3917
|
runIndicatorBubble.setAttribute("aria-live", "polite");
|
|
3236
|
-
runIndicatorBubble.setAttribute("aria-label", "Agent is
|
|
3918
|
+
runIndicatorBubble.setAttribute("aria-label", "Agent is running:");
|
|
3237
3919
|
|
|
3238
3920
|
const body = make("div", "message-body");
|
|
3239
3921
|
const row = make("div", "run-indicator-row");
|
|
@@ -3324,12 +4006,87 @@ function scheduleAbortStateChecks() {
|
|
|
3324
4006
|
}
|
|
3325
4007
|
}
|
|
3326
4008
|
|
|
4009
|
+
function messageTimestampMs(message) {
|
|
4010
|
+
const timestamp = message?.timestamp;
|
|
4011
|
+
const date = typeof timestamp === "number" ? new Date(timestamp) : new Date(String(timestamp || ""));
|
|
4012
|
+
const time = date.getTime();
|
|
4013
|
+
return Number.isFinite(time) ? time : 0;
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
function isActionTranscriptMessage(message) {
|
|
4017
|
+
return ["assistantEvent", "bashExecution", "toolCall", "toolResult"].includes(message?.role);
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
function assistantMessageHasActionContent(message) {
|
|
4021
|
+
return message?.role === "assistant" && Array.isArray(message.content) && message.content.some(isAssistantToolCallPart);
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
function isActionEntryItem(item) {
|
|
4025
|
+
return isActionTranscriptMessage(item?.message) || assistantMessageHasActionContent(item?.message);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
function actionEntrySeenKeys(tabId = activeTabId) {
|
|
4029
|
+
if (!tabId) return new Set();
|
|
4030
|
+
let keys = actionEntrySeenKeysByTab.get(tabId);
|
|
4031
|
+
if (!keys) {
|
|
4032
|
+
keys = new Set();
|
|
4033
|
+
actionEntrySeenKeysByTab.set(tabId, keys);
|
|
4034
|
+
}
|
|
4035
|
+
return keys;
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
function actionEntryKey(item) {
|
|
4039
|
+
const message = item?.message || {};
|
|
4040
|
+
return [
|
|
4041
|
+
item?.transient ? "transient" : "message",
|
|
4042
|
+
item?.messageIndex ?? -1,
|
|
4043
|
+
message.role || "message",
|
|
4044
|
+
message.toolName || "",
|
|
4045
|
+
message.command || "",
|
|
4046
|
+
message.title || "",
|
|
4047
|
+
message.timestamp || "",
|
|
4048
|
+
textFromContent(message.content).slice(0, 240),
|
|
4049
|
+
].join("|");
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
function shouldAnimateActionEntry(item) {
|
|
4053
|
+
if (!activeTabId || !actionEntryAnimationPrimedTabs.has(activeTabId) || !isActionEntryItem(item)) return false;
|
|
4054
|
+
return !actionEntrySeenKeys(activeTabId).has(actionEntryKey(item));
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
function rememberActionEntries(items) {
|
|
4058
|
+
if (!activeTabId) return;
|
|
4059
|
+
const keys = actionEntrySeenKeys(activeTabId);
|
|
4060
|
+
for (const item of items) {
|
|
4061
|
+
if (isActionEntryItem(item)) keys.add(actionEntryKey(item));
|
|
4062
|
+
}
|
|
4063
|
+
actionEntryAnimationPrimedTabs.add(activeTabId);
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
function orderedTranscriptItems() {
|
|
4067
|
+
const items = [];
|
|
4068
|
+
latestMessages.forEach((message, index) => {
|
|
4069
|
+
items.push({ message, messageIndex: index, transient: false, timestampMs: messageTimestampMs(message), order: index });
|
|
4070
|
+
});
|
|
4071
|
+
transientMessages.forEach((message, index) => {
|
|
4072
|
+
items.push({ message, messageIndex: index, transient: true, timestampMs: messageTimestampMs(message), order: latestMessages.length + index });
|
|
4073
|
+
});
|
|
4074
|
+
return items.sort((a, b) => a.timestampMs - b.timestampMs || a.order - b.order);
|
|
4075
|
+
}
|
|
4076
|
+
|
|
3327
4077
|
function renderAllMessages({ preserveScroll = false } = {}) {
|
|
3328
4078
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
3329
4079
|
const previousScrollTop = elements.chat.scrollTop;
|
|
3330
4080
|
resetChatOutput();
|
|
3331
|
-
|
|
3332
|
-
|
|
4081
|
+
const transcriptItems = orderedTranscriptItems();
|
|
4082
|
+
for (const item of transcriptItems) {
|
|
4083
|
+
appendTranscriptMessage(item.message, {
|
|
4084
|
+
messageIndex: item.messageIndex,
|
|
4085
|
+
transient: item.transient,
|
|
4086
|
+
animateEntry: shouldAnimateActionEntry(item),
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
rememberActionEntries(transcriptItems);
|
|
3333
4090
|
renderRunIndicator({ scroll: false });
|
|
3334
4091
|
updateStickyUserPromptButton();
|
|
3335
4092
|
if (shouldFollow) scrollChatToBottom({ force: true });
|
|
@@ -3353,6 +4110,21 @@ function addTransientMessage({ role = "notice", title, content, level = "info" }
|
|
|
3353
4110
|
renderAllMessages();
|
|
3354
4111
|
}
|
|
3355
4112
|
|
|
4113
|
+
function addAbortTranscriptNotice({ activeRun = false, errorMessage = "" } = {}) {
|
|
4114
|
+
if (errorMessage) {
|
|
4115
|
+
addTransientMessage({ role: "error", title: "Abort failed", content: `Abort request failed: ${errorMessage}`, level: "error" });
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
addTransientMessage({
|
|
4119
|
+
role: "native",
|
|
4120
|
+
title: activeRun ? "Agent aborted" : "Abort requested",
|
|
4121
|
+
content: activeRun
|
|
4122
|
+
? "⛔ Agent run aborted by user from the Web UI. Pi was told to stop; this transcript marks the run as aborted."
|
|
4123
|
+
: "⛔ Abort requested from the Web UI, but no active agent run was visible in this tab.",
|
|
4124
|
+
level: activeRun ? "warn" : "info",
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
|
|
3356
4128
|
function isChatNearBottom() {
|
|
3357
4129
|
const remaining = elements.chat.scrollHeight - elements.chat.scrollTop - elements.chat.clientHeight;
|
|
3358
4130
|
return remaining <= CHAT_BOTTOM_THRESHOLD_PX;
|
|
@@ -3457,6 +4229,199 @@ function sendPromptFromModeButton(kind, button) {
|
|
|
3457
4229
|
sendPrompt(kind);
|
|
3458
4230
|
}
|
|
3459
4231
|
|
|
4232
|
+
function setPublishMenuOpen(open) {
|
|
4233
|
+
publishMenuOpen = !!open;
|
|
4234
|
+
elements.publishButton.setAttribute("aria-expanded", publishMenuOpen ? "true" : "false");
|
|
4235
|
+
elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
|
|
4236
|
+
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
function optionalFeatureIdForCommand(name) {
|
|
4240
|
+
if (OPTIONAL_COMMAND_FEATURES.has(name)) return OPTIONAL_COMMAND_FEATURES.get(name);
|
|
4241
|
+
if (name === "release-toggle" || name === "release-abort" || name === "release-npm-logs") return "releaseNpm";
|
|
4242
|
+
if (name === "release-aur" || name.startsWith("release-aur-")) return "releaseAur";
|
|
4243
|
+
if (name === "stats" || name.startsWith("stats-") || name === "calibrate") return "statsCommand";
|
|
4244
|
+
return null;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
function isCommandVisible(command) {
|
|
4248
|
+
const featureId = optionalFeatureIdForCommand(command.name);
|
|
4249
|
+
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
function visibleCommands() {
|
|
4253
|
+
return availableCommands.filter(isCommandVisible);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
function hasAvailableCommand(name) {
|
|
4257
|
+
return availableCommands.some((command) => command.name === name);
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
function optionalFeatureUnavailableMessage(featureId) {
|
|
4261
|
+
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
4262
|
+
if (!feature) return "Optional feature unavailable.";
|
|
4263
|
+
if (isOptionalFeatureDisabled(featureId)) return `${feature.label} is disabled in the Web UI optional-features panel.`;
|
|
4264
|
+
return `${feature.label} unavailable: ${feature.capabilityLabel} is not loaded. Install or enable ${feature.packageName}.`;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
function setOptionalControlState(button, available, unavailableTitle) {
|
|
4268
|
+
if (!button) return;
|
|
4269
|
+
if (!button.dataset.defaultTitle) button.dataset.defaultTitle = button.getAttribute("title") || "";
|
|
4270
|
+
button.disabled = !available;
|
|
4271
|
+
button.setAttribute("aria-disabled", available ? "false" : "true");
|
|
4272
|
+
button.classList.toggle("feature-unavailable", !available);
|
|
4273
|
+
button.setAttribute("title", available ? button.dataset.defaultTitle : unavailableTitle);
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
function resetOptionalFeatureAvailability() {
|
|
4277
|
+
for (const key of Object.keys(optionalFeatureAvailability)) optionalFeatureAvailability[key] = false;
|
|
4278
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
4279
|
+
renderOptionalFeatureControls();
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
function updateOptionalFeatureAvailability() {
|
|
4283
|
+
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
4284
|
+
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
4285
|
+
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
4286
|
+
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
4287
|
+
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer");
|
|
4288
|
+
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
4289
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
4290
|
+
renderOptionalFeatureControls();
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
function optionalFeatureStatus(featureId) {
|
|
4294
|
+
const detected = isOptionalFeatureDetected(featureId);
|
|
4295
|
+
const disabled = isOptionalFeatureDisabled(featureId);
|
|
4296
|
+
if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: "Detected and enabled in Web UI" };
|
|
4297
|
+
if (detected && disabled) return { label: "Disabled", className: "disabled", detail: "Detected, but disabled in Web UI" };
|
|
4298
|
+
return { label: "Install needed", className: "missing", detail: "Not detected in the active Pi tab" };
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
function optionalFeatureWidgetFeatureId(key) {
|
|
4302
|
+
if (key.startsWith("release-npm:")) return "releaseNpm";
|
|
4303
|
+
if (key.startsWith("release-aur:")) return "releaseAur";
|
|
4304
|
+
if (key === "todo-progress") return "todoProgressWidget";
|
|
4305
|
+
return null;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
function renderOptionalFeaturePanel() {
|
|
4309
|
+
if (!elements.optionalFeaturesBox) return;
|
|
4310
|
+
elements.optionalFeaturesBox.replaceChildren();
|
|
4311
|
+
elements.optionalFeaturesBox.classList.remove("muted");
|
|
4312
|
+
|
|
4313
|
+
for (const feature of OPTIONAL_FEATURES) {
|
|
4314
|
+
const detected = isOptionalFeatureDetected(feature.id);
|
|
4315
|
+
const enabled = isOptionalFeatureEnabled(feature.id);
|
|
4316
|
+
const installing = optionalFeatureInstallInProgress.has(feature.id);
|
|
4317
|
+
const status = optionalFeatureStatus(feature.id);
|
|
4318
|
+
const row = make("div", `optional-feature-row ${status.className}`);
|
|
4319
|
+
|
|
4320
|
+
const main = make("div", "optional-feature-main");
|
|
4321
|
+
const title = make("div", "optional-feature-title");
|
|
4322
|
+
title.append(make("strong", undefined, feature.label), make("span", `optional-feature-pill ${status.className}`, status.label));
|
|
4323
|
+
const detail = make("div", "optional-feature-detail", `${status.detail} · checks ${feature.capabilityLabel}`);
|
|
4324
|
+
const description = make("div", "optional-feature-description", feature.description);
|
|
4325
|
+
const packageLine = make("code", "optional-feature-package", feature.packageName);
|
|
4326
|
+
main.append(title, detail, description, packageLine);
|
|
4327
|
+
|
|
4328
|
+
const action = make("button", "optional-feature-action");
|
|
4329
|
+
action.type = "button";
|
|
4330
|
+
action.disabled = installing;
|
|
4331
|
+
if (installing) {
|
|
4332
|
+
action.textContent = "Installing…";
|
|
4333
|
+
} else if (detected) {
|
|
4334
|
+
action.textContent = enabled ? "Disable" : "Enable";
|
|
4335
|
+
action.addEventListener("click", () => setOptionalFeatureDisabled(feature.id, enabled));
|
|
4336
|
+
} else {
|
|
4337
|
+
action.textContent = "Install…";
|
|
4338
|
+
action.classList.add("install");
|
|
4339
|
+
action.addEventListener("click", () => installOptionalFeature(feature.id));
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
row.append(main, action);
|
|
4343
|
+
elements.optionalFeaturesBox.append(row);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function renderOptionalFeatureControls() {
|
|
4348
|
+
setOptionalControlState(
|
|
4349
|
+
elements.gitWorkflowButton,
|
|
4350
|
+
isOptionalFeatureEnabled("gitWorkflow"),
|
|
4351
|
+
optionalFeatureUnavailableMessage("gitWorkflow"),
|
|
4352
|
+
);
|
|
4353
|
+
|
|
4354
|
+
elements.releaseNpmButton.hidden = !isOptionalFeatureEnabled("releaseNpm");
|
|
4355
|
+
elements.releaseAurButton.hidden = !isOptionalFeatureEnabled("releaseAur");
|
|
4356
|
+
const hasPublishWorkflow = isOptionalFeatureEnabled("releaseNpm") || isOptionalFeatureEnabled("releaseAur");
|
|
4357
|
+
const publishContainer = elements.publishButton.parentElement;
|
|
4358
|
+
if (publishContainer) publishContainer.hidden = !hasPublishWorkflow;
|
|
4359
|
+
setOptionalControlState(
|
|
4360
|
+
elements.publishButton,
|
|
4361
|
+
hasPublishWorkflow,
|
|
4362
|
+
"Publish workflows unavailable: enable/install NPM Release and/or AUR Release in Optional features.",
|
|
4363
|
+
);
|
|
4364
|
+
if (!hasPublishWorkflow && publishMenuOpen) setPublishMenuOpen(false);
|
|
4365
|
+
|
|
4366
|
+
renderOptionalFeaturePanel();
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
function commandUnavailableMessage(commandName) {
|
|
4370
|
+
const featureId = optionalFeatureIdForCommand(commandName);
|
|
4371
|
+
if (featureId) return optionalFeatureUnavailableMessage(featureId);
|
|
4372
|
+
return `Command unavailable: /${commandName} is not loaded in the active Pi tab.`;
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
async function installOptionalFeature(featureId) {
|
|
4376
|
+
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
4377
|
+
if (!feature || optionalFeatureInstallInProgress.has(featureId)) return;
|
|
4378
|
+
|
|
4379
|
+
const warning = [
|
|
4380
|
+
`Install optional feature: ${feature.label}?`,
|
|
4381
|
+
"",
|
|
4382
|
+
`This will run npm install for ${feature.packageName} in the Web UI package install root.`,
|
|
4383
|
+
"It can download code from npm and modify the local Pi/Web UI npm installation.",
|
|
4384
|
+
"If this feature is already installed but disabled in Pi settings, cancel and enable it there instead.",
|
|
4385
|
+
"",
|
|
4386
|
+
"Continue?",
|
|
4387
|
+
].join("\n");
|
|
4388
|
+
if (!confirm(warning)) return;
|
|
4389
|
+
|
|
4390
|
+
optionalFeatureInstallInProgress.add(featureId);
|
|
4391
|
+
renderOptionalFeatureControls();
|
|
4392
|
+
addEvent(`installing optional feature ${feature.label} (${feature.packageName})…`, "warn");
|
|
4393
|
+
try {
|
|
4394
|
+
const response = await api("/api/optional-feature-install", { method: "POST", body: { featureId }, scoped: false });
|
|
4395
|
+
disabledOptionalFeatures.delete(featureId);
|
|
4396
|
+
storeDisabledOptionalFeatures();
|
|
4397
|
+
addEvent(response.data?.message || `installed ${feature.packageName}`, "info");
|
|
4398
|
+
if (confirm(`${feature.label} install finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
4399
|
+
sendPrompt("prompt", "/reload");
|
|
4400
|
+
} else {
|
|
4401
|
+
await Promise.allSettled([refreshCommands(), initializeThemes()]);
|
|
4402
|
+
renderOptionalFeatureControls();
|
|
4403
|
+
}
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
addEvent(error.message || String(error), "error");
|
|
4406
|
+
} finally {
|
|
4407
|
+
optionalFeatureInstallInProgress.delete(featureId);
|
|
4408
|
+
renderOptionalFeatureControls();
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
function runPublishWorkflow(command) {
|
|
4413
|
+
setComposerActionsOpen(false);
|
|
4414
|
+
setPublishMenuOpen(false);
|
|
4415
|
+
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
4416
|
+
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
4417
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
|
|
4418
|
+
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
4419
|
+
refreshCommands().catch((error) => addEvent(error.message || String(error), "error"));
|
|
4420
|
+
return;
|
|
4421
|
+
}
|
|
4422
|
+
sendPrompt("prompt", command);
|
|
4423
|
+
}
|
|
4424
|
+
|
|
3460
4425
|
function shouldSendPromptFromEnter(event) {
|
|
3461
4426
|
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return false;
|
|
3462
4427
|
if (event.ctrlKey || event.metaKey) return true;
|
|
@@ -3471,11 +4436,68 @@ function renderMessages(messages) {
|
|
|
3471
4436
|
renderFeedbackTray();
|
|
3472
4437
|
}
|
|
3473
4438
|
|
|
4439
|
+
function cancelStreamBubbleHide() {
|
|
4440
|
+
clearTimeout(streamBubbleHideTimer);
|
|
4441
|
+
streamBubbleHideTimer = null;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function cancelStreamingAssistantTextRender() {
|
|
4445
|
+
clearTimeout(streamTextRenderTimer);
|
|
4446
|
+
streamTextRenderTimer = null;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
function removeStreamBubble() {
|
|
4450
|
+
cancelStreamingAssistantTextRender();
|
|
4451
|
+
cancelStreamBubbleHide();
|
|
4452
|
+
streamBubble?.remove();
|
|
4453
|
+
streamBubble = null;
|
|
4454
|
+
streamText = null;
|
|
4455
|
+
streamBubbleVisibleSince = 0;
|
|
4456
|
+
renderRunIndicator({ scroll: false });
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function scheduleStreamBubbleHide() {
|
|
4460
|
+
if (!streamBubble) return;
|
|
4461
|
+
const visibleForMs = streamBubbleVisibleSince ? performance.now() - streamBubbleVisibleSince : STREAM_OUTPUT_MIN_VISIBLE_MS;
|
|
4462
|
+
const delayMs = Math.max(STREAM_OUTPUT_HIDE_DELAY_MS, STREAM_OUTPUT_MIN_VISIBLE_MS - visibleForMs);
|
|
4463
|
+
clearTimeout(streamBubbleHideTimer);
|
|
4464
|
+
streamBubbleHideTimer = setTimeout(() => {
|
|
4465
|
+
streamBubbleHideTimer = null;
|
|
4466
|
+
if (stripTodoProgressLines(streamRawText, { streaming: true }) || !streamBubble) return;
|
|
4467
|
+
removeStreamBubble();
|
|
4468
|
+
}, delayMs);
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function renderStreamingAssistantText() {
|
|
4472
|
+
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
4473
|
+
if (assistantText) {
|
|
4474
|
+
ensureStreamBubble();
|
|
4475
|
+
streamText.textContent = assistantText;
|
|
4476
|
+
} else {
|
|
4477
|
+
scheduleStreamBubbleHide();
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
function scheduleStreamingAssistantTextRender() {
|
|
4482
|
+
if (streamTextRenderTimer) return;
|
|
4483
|
+
streamTextRenderTimer = setTimeout(() => {
|
|
4484
|
+
streamTextRenderTimer = null;
|
|
4485
|
+
renderStreamingAssistantText();
|
|
4486
|
+
}, STREAM_OUTPUT_TOOLCALL_GUARD_MS);
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
function suppressStreamingAssistantTextBeforeToolCall() {
|
|
4490
|
+
streamRawText = "";
|
|
4491
|
+
removeStreamBubble();
|
|
4492
|
+
}
|
|
4493
|
+
|
|
3474
4494
|
function ensureStreamBubble() {
|
|
4495
|
+
cancelStreamBubbleHide();
|
|
3475
4496
|
if (streamBubble) return;
|
|
3476
4497
|
const created = appendMessage({ role: "assistant", title: "Assistant", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
3477
4498
|
streamBubble = created.bubble;
|
|
3478
4499
|
streamText = appendText(created.body, "");
|
|
4500
|
+
streamBubbleVisibleSince = performance.now();
|
|
3479
4501
|
renderRunIndicator({ scroll: false });
|
|
3480
4502
|
scrollChatToBottom();
|
|
3481
4503
|
}
|
|
@@ -3495,9 +4517,13 @@ function showStreamingThinking(placeholder = "Thinking…") {
|
|
|
3495
4517
|
}
|
|
3496
4518
|
|
|
3497
4519
|
function resetStreamBubble() {
|
|
4520
|
+
cancelStreamingAssistantTextRender();
|
|
4521
|
+
cancelStreamBubbleHide();
|
|
3498
4522
|
streamBubble = null;
|
|
3499
4523
|
streamText = null;
|
|
3500
4524
|
streamRawText = "";
|
|
4525
|
+
streamBubbleVisibleSince = 0;
|
|
4526
|
+
streamToolCallSeen = false;
|
|
3501
4527
|
streamThinkingBubble = null;
|
|
3502
4528
|
streamThinking = null;
|
|
3503
4529
|
}
|
|
@@ -3516,9 +4542,13 @@ function assistantTextFromMessage(message) {
|
|
|
3516
4542
|
const content = message?.content;
|
|
3517
4543
|
if (typeof content === "string") return content;
|
|
3518
4544
|
if (!Array.isArray(content)) return null;
|
|
3519
|
-
const parts =
|
|
3520
|
-
|
|
3521
|
-
|
|
4545
|
+
const parts = [];
|
|
4546
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
4547
|
+
const part = content[index];
|
|
4548
|
+
if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && !assistantHasToolCallAfter(content, index)) {
|
|
4549
|
+
parts.push(part.text);
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
3522
4552
|
return parts.length ? parts.join("\n\n") : "";
|
|
3523
4553
|
}
|
|
3524
4554
|
|
|
@@ -3574,20 +4604,14 @@ function handleMessageUpdate(event) {
|
|
|
3574
4604
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
3575
4605
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
3576
4606
|
else streamRawText += delta;
|
|
3577
|
-
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
3578
4607
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
3579
|
-
if (
|
|
3580
|
-
|
|
3581
|
-
streamText.textContent = assistantText;
|
|
3582
|
-
} else if (streamBubble) {
|
|
3583
|
-
streamBubble.remove();
|
|
3584
|
-
streamBubble = null;
|
|
3585
|
-
streamText = null;
|
|
3586
|
-
renderRunIndicator({ scroll: false });
|
|
3587
|
-
}
|
|
4608
|
+
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
4609
|
+
else scheduleStreamingAssistantTextRender();
|
|
3588
4610
|
renderFooter();
|
|
3589
4611
|
scrollChatToBottom();
|
|
3590
4612
|
} else if (update.type === "toolcall_start") {
|
|
4613
|
+
streamToolCallSeen = true;
|
|
4614
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
3591
4615
|
const name = runIndicatorToolName(update.name || update.toolName || update.toolCall?.name);
|
|
3592
4616
|
setRunIndicatorActivity(`Preparing tool call: ${name}…`);
|
|
3593
4617
|
addEvent(`tool call started in assistant message`, "info");
|
|
@@ -3839,7 +4863,7 @@ function scoreCommandSuggestion(command, query) {
|
|
|
3839
4863
|
}
|
|
3840
4864
|
|
|
3841
4865
|
function getCommandMatches(query) {
|
|
3842
|
-
return
|
|
4866
|
+
return visibleCommands()
|
|
3843
4867
|
.map((command) => ({ command, score: scoreCommandSuggestion(command, query) }))
|
|
3844
4868
|
.filter((item) => Number.isFinite(item.score))
|
|
3845
4869
|
.sort((a, b) => a.score - b.score || a.command.name.localeCompare(b.command.name))
|
|
@@ -3858,12 +4882,14 @@ function abortPathSuggestionRequest() {
|
|
|
3858
4882
|
|
|
3859
4883
|
function cancelPathSuggestionRequest() {
|
|
3860
4884
|
pathSuggestRequestSerial++;
|
|
4885
|
+
pathSuggestActiveQuery = null;
|
|
3861
4886
|
abortPathSuggestionRequest();
|
|
3862
4887
|
}
|
|
3863
4888
|
|
|
3864
4889
|
function hideCommandSuggestions() {
|
|
3865
4890
|
cancelPathSuggestionRequest();
|
|
3866
4891
|
elements.commandSuggest.hidden = true;
|
|
4892
|
+
elements.commandSuggest.removeAttribute("aria-busy");
|
|
3867
4893
|
elements.commandSuggest.replaceChildren();
|
|
3868
4894
|
commandSuggestions = [];
|
|
3869
4895
|
pathSuggestions = [];
|
|
@@ -3884,6 +4910,33 @@ function setActiveCommandSuggestion(index) {
|
|
|
3884
4910
|
}
|
|
3885
4911
|
}
|
|
3886
4912
|
|
|
4913
|
+
function pointerPositionFromEvent(event) {
|
|
4914
|
+
if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return null;
|
|
4915
|
+
return { x: event.clientX, y: event.clientY };
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
function rememberPointerPosition(event) {
|
|
4919
|
+
lastPointerPosition = pointerPositionFromEvent(event);
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
function commandSuggestionPointerActuallyMoved(event) {
|
|
4923
|
+
const movementX = Number.isFinite(event.movementX) ? event.movementX : 0;
|
|
4924
|
+
const movementY = Number.isFinite(event.movementY) ? event.movementY : 0;
|
|
4925
|
+
if (movementX !== 0 || movementY !== 0) return true;
|
|
4926
|
+
|
|
4927
|
+
const position = pointerPositionFromEvent(event);
|
|
4928
|
+
return Boolean(
|
|
4929
|
+
position &&
|
|
4930
|
+
lastPointerPosition &&
|
|
4931
|
+
(position.x !== lastPointerPosition.x || position.y !== lastPointerPosition.y),
|
|
4932
|
+
);
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
function setActiveCommandSuggestionFromPointerMove(index, event) {
|
|
4936
|
+
if (!commandSuggestionPointerActuallyMoved(event)) return;
|
|
4937
|
+
setActiveCommandSuggestion(index);
|
|
4938
|
+
}
|
|
4939
|
+
|
|
3887
4940
|
function renderCommandSuggestionItems(trigger, { keepIndex = false } = {}) {
|
|
3888
4941
|
suggestionMode = "command";
|
|
3889
4942
|
pathSuggestions = [];
|
|
@@ -3901,7 +4954,7 @@ function renderCommandSuggestionItems(trigger, { keepIndex = false } = {}) {
|
|
|
3901
4954
|
item.type = "button";
|
|
3902
4955
|
item.setAttribute("role", "option");
|
|
3903
4956
|
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
3904
|
-
item.addEventListener("
|
|
4957
|
+
item.addEventListener("pointermove", (event) => setActiveCommandSuggestionFromPointerMove(index, event));
|
|
3905
4958
|
item.addEventListener("click", () => insertCommandSuggestion(index));
|
|
3906
4959
|
|
|
3907
4960
|
item.append(
|
|
@@ -3943,7 +4996,7 @@ function renderPathSuggestionItems(trigger, { keepIndex = false } = {}) {
|
|
|
3943
4996
|
item.type = "button";
|
|
3944
4997
|
item.setAttribute("role", "option");
|
|
3945
4998
|
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
3946
|
-
item.addEventListener("
|
|
4999
|
+
item.addEventListener("pointermove", (event) => setActiveCommandSuggestionFromPointerMove(index, event));
|
|
3947
5000
|
item.addEventListener("click", () => insertPathSuggestion(index));
|
|
3948
5001
|
|
|
3949
5002
|
item.append(
|
|
@@ -3959,15 +5012,25 @@ function renderPathSuggestionItems(trigger, { keepIndex = false } = {}) {
|
|
|
3959
5012
|
}
|
|
3960
5013
|
|
|
3961
5014
|
async function renderPathSuggestions(trigger, { keepIndex = false } = {}) {
|
|
5015
|
+
if (suggestionMode === "path" && pathSuggestActiveQuery === trigger.query && !elements.commandSuggest.hidden) {
|
|
5016
|
+
if (keepIndex && activeSuggestionCount() > 0) setActiveCommandSuggestion(commandSuggestIndex);
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
const keepExistingPathMenu = suggestionMode === "path" && !elements.commandSuggest.hidden && elements.commandSuggest.childElementCount > 0;
|
|
3962
5021
|
abortPathSuggestionRequest();
|
|
3963
5022
|
const requestSerial = ++pathSuggestRequestSerial;
|
|
3964
5023
|
const controller = new AbortController();
|
|
5024
|
+
pathSuggestActiveQuery = trigger.query;
|
|
3965
5025
|
pathSuggestAbortController = controller;
|
|
3966
5026
|
suggestionMode = "path";
|
|
3967
5027
|
commandSuggestions = [];
|
|
3968
|
-
|
|
3969
|
-
|
|
5028
|
+
if (!keepExistingPathMenu) {
|
|
5029
|
+
pathSuggestions = [];
|
|
5030
|
+
elements.commandSuggest.replaceChildren(make("div", "command-suggest-empty", "Finding paths…"));
|
|
5031
|
+
}
|
|
3970
5032
|
elements.commandSuggest.hidden = false;
|
|
5033
|
+
elements.commandSuggest.setAttribute("aria-busy", "true");
|
|
3971
5034
|
|
|
3972
5035
|
try {
|
|
3973
5036
|
const response = await api(`/api/path-suggestions?query=${encodeURIComponent(trigger.query)}`, { signal: controller.signal });
|
|
@@ -3980,7 +5043,10 @@ async function renderPathSuggestions(trigger, { keepIndex = false } = {}) {
|
|
|
3980
5043
|
elements.commandSuggest.replaceChildren(make("div", "command-suggest-empty", `Path suggestions unavailable: ${error.message}`));
|
|
3981
5044
|
elements.commandSuggest.hidden = false;
|
|
3982
5045
|
} finally {
|
|
3983
|
-
if (requestSerial === pathSuggestRequestSerial)
|
|
5046
|
+
if (requestSerial === pathSuggestRequestSerial) {
|
|
5047
|
+
pathSuggestAbortController = null;
|
|
5048
|
+
elements.commandSuggest.removeAttribute("aria-busy");
|
|
5049
|
+
}
|
|
3984
5050
|
}
|
|
3985
5051
|
}
|
|
3986
5052
|
|
|
@@ -4064,6 +5130,7 @@ function insertPathSuggestion(index = commandSuggestIndex) {
|
|
|
4064
5130
|
async function refreshCommands() {
|
|
4065
5131
|
const response = await api("/api/commands");
|
|
4066
5132
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
5133
|
+
updateOptionalFeatureAvailability();
|
|
4067
5134
|
elements.commandsBox.replaceChildren();
|
|
4068
5135
|
if (!availableCommands.length) {
|
|
4069
5136
|
elements.commandsBox.textContent = "No RPC-visible commands.";
|
|
@@ -4071,8 +5138,15 @@ async function refreshCommands() {
|
|
|
4071
5138
|
hideCommandSuggestions();
|
|
4072
5139
|
return;
|
|
4073
5140
|
}
|
|
5141
|
+
const commandsToShow = visibleCommands();
|
|
5142
|
+
if (!commandsToShow.length) {
|
|
5143
|
+
elements.commandsBox.textContent = "No enabled commands visible. Re-enable optional features to show their commands.";
|
|
5144
|
+
elements.commandsBox.classList.add("muted");
|
|
5145
|
+
hideCommandSuggestions();
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
4074
5148
|
elements.commandsBox.classList.remove("muted");
|
|
4075
|
-
for (const command of
|
|
5149
|
+
for (const command of commandsToShow.slice(0, 80)) {
|
|
4076
5150
|
const item = make("button", "command-item");
|
|
4077
5151
|
item.type = "button";
|
|
4078
5152
|
item.title = `Send /${command.name}`;
|
|
@@ -4265,11 +5339,13 @@ function handleExtensionUiRequest(request) {
|
|
|
4265
5339
|
case "setStatus":
|
|
4266
5340
|
if (request.statusText) statusEntries.set(request.statusKey || "extension", request.statusText);
|
|
4267
5341
|
else statusEntries.delete(request.statusKey || "extension");
|
|
5342
|
+
updateOptionalFeatureAvailability();
|
|
4268
5343
|
renderStatus();
|
|
4269
5344
|
return;
|
|
4270
5345
|
case "setWidget":
|
|
4271
5346
|
if (Array.isArray(request.widgetLines)) widgets.set(request.widgetKey || request.id, request);
|
|
4272
5347
|
else widgets.delete(request.widgetKey || request.id);
|
|
5348
|
+
updateOptionalFeatureAvailability();
|
|
4273
5349
|
renderWidgets();
|
|
4274
5350
|
return;
|
|
4275
5351
|
case "setTitle":
|
|
@@ -4330,11 +5406,16 @@ function showNextDialog() {
|
|
|
4330
5406
|
const request = activeDialog;
|
|
4331
5407
|
|
|
4332
5408
|
const prompt = normalizeDialogPrompt(request);
|
|
4333
|
-
const
|
|
5409
|
+
const releasePrompt = request.method === "select" ? releaseDialogPromptParts(prompt) : null;
|
|
5410
|
+
const displayPrompt = releasePrompt || prompt;
|
|
5411
|
+
const isGuardrailDialog = isGuardrailDialogPrompt(displayPrompt);
|
|
5412
|
+
const isReleaseDialog = !!releasePrompt;
|
|
4334
5413
|
elements.dialog.classList.toggle("guardrail-dialog", isGuardrailDialog);
|
|
4335
|
-
elements.
|
|
4336
|
-
|
|
4337
|
-
elements.dialogMessage
|
|
5414
|
+
elements.dialog.classList.toggle("release-dialog", isReleaseDialog);
|
|
5415
|
+
elements.dialogTitle.textContent = displayPrompt.title;
|
|
5416
|
+
if (isReleaseDialog) renderReleaseDialogMessage(elements.dialogMessage, displayPrompt.message);
|
|
5417
|
+
else renderAnsiText(elements.dialogMessage, displayPrompt.message);
|
|
5418
|
+
elements.dialogMessage.hidden = !displayPrompt.plainMessage;
|
|
4338
5419
|
elements.dialogBody.replaceChildren();
|
|
4339
5420
|
elements.dialogActions.replaceChildren();
|
|
4340
5421
|
|
|
@@ -4348,6 +5429,8 @@ function showNextDialog() {
|
|
|
4348
5429
|
button.type = "button";
|
|
4349
5430
|
if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
|
|
4350
5431
|
if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
|
|
5432
|
+
if (isReleaseDialog && /^Yes$/i.test(optionLabel)) button.classList.add("primary", "release-publish-action");
|
|
5433
|
+
if (isReleaseDialog && /^No$/i.test(optionLabel)) button.classList.add("release-cancel-action");
|
|
4351
5434
|
button.addEventListener("click", () => sendDialogResponse({ type: "extension_ui_response", id: request.id, value: optionLabel, tabId: request.tabId }));
|
|
4352
5435
|
options.append(button);
|
|
4353
5436
|
}
|
|
@@ -4402,6 +5485,11 @@ function handleEvent(event) {
|
|
|
4402
5485
|
case "webui_tab_reloaded":
|
|
4403
5486
|
addEvent(`${event.tabTitle || "terminal"} reloaded`);
|
|
4404
5487
|
addTransientMessage({ role: "native", title: "/reload", content: `${event.tabTitle || "terminal"} reloaded. Keybindings, extensions, skills, prompts, and themes were refreshed by restarting the RPC tab${event.sessionFile ? ` and resuming ${event.sessionFile}` : ""}.`, level: "info" });
|
|
5488
|
+
statusEntries.clear();
|
|
5489
|
+
widgets.clear();
|
|
5490
|
+
resetOptionalFeatureAvailability();
|
|
5491
|
+
renderStatus();
|
|
5492
|
+
renderWidgets();
|
|
4405
5493
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
4406
5494
|
setTimeout(() => refreshAll().catch((error) => addEvent(error.message, "error")), 500);
|
|
4407
5495
|
break;
|
|
@@ -4459,6 +5547,7 @@ function handleEvent(event) {
|
|
|
4459
5547
|
break;
|
|
4460
5548
|
case "agent_end":
|
|
4461
5549
|
addEvent("agent finished");
|
|
5550
|
+
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
4462
5551
|
currentRunStartedAt = null;
|
|
4463
5552
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
4464
5553
|
clearRunIndicatorActivity();
|
|
@@ -4563,18 +5652,36 @@ elements.terminalTabsToggleButton.addEventListener("click", () => {
|
|
|
4563
5652
|
setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
|
|
4564
5653
|
});
|
|
4565
5654
|
elements.newTabButton.addEventListener("click", () => createTerminalTab());
|
|
5655
|
+
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
4566
5656
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
4567
5657
|
setComposerActionsOpen(false);
|
|
4568
5658
|
startGitWorkflow();
|
|
4569
5659
|
});
|
|
5660
|
+
const publishMenuContainer = elements.publishButton.parentElement;
|
|
5661
|
+
elements.publishButton.addEventListener("click", () => {
|
|
5662
|
+
setPublishMenuOpen(true);
|
|
5663
|
+
});
|
|
5664
|
+
publishMenuContainer?.addEventListener("pointerenter", () => setPublishMenuOpen(true));
|
|
5665
|
+
publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
|
|
5666
|
+
publishMenuContainer?.addEventListener("focusin", () => setPublishMenuOpen(true));
|
|
5667
|
+
publishMenuContainer?.addEventListener("focusout", () => {
|
|
5668
|
+
setTimeout(() => {
|
|
5669
|
+
if (!publishMenuContainer?.contains(document.activeElement)) setPublishMenuOpen(false);
|
|
5670
|
+
}, 0);
|
|
5671
|
+
});
|
|
5672
|
+
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
5673
|
+
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
4570
5674
|
elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
|
|
4571
5675
|
elements.abortButton.addEventListener("click", async () => {
|
|
5676
|
+
const hadActiveRun = runIndicatorIsActive();
|
|
4572
5677
|
try {
|
|
4573
|
-
if (
|
|
5678
|
+
if (hadActiveRun) setRunIndicatorActivity("Abort requested; checking whether Pi stopped…");
|
|
4574
5679
|
await api("/api/abort", { method: "POST", body: {} });
|
|
5680
|
+
addAbortTranscriptNotice({ activeRun: hadActiveRun });
|
|
4575
5681
|
scheduleAbortStateChecks();
|
|
4576
5682
|
} catch (error) {
|
|
4577
5683
|
addEvent(error.message, "error");
|
|
5684
|
+
addAbortTranscriptNotice({ errorMessage: error.message });
|
|
4578
5685
|
}
|
|
4579
5686
|
});
|
|
4580
5687
|
elements.newSessionButton.addEventListener("click", async () => {
|
|
@@ -4630,6 +5737,15 @@ elements.setThinkingButton.addEventListener("click", async () => {
|
|
|
4630
5737
|
});
|
|
4631
5738
|
elements.themeSelect.addEventListener("change", () => setThemeByName(elements.themeSelect.value, { persist: true, announce: true }));
|
|
4632
5739
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
5740
|
+
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
5741
|
+
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
5742
|
+
requestPermission: elements.agentDoneNotificationsToggle.checked,
|
|
5743
|
+
announce: true,
|
|
5744
|
+
}).catch((error) => {
|
|
5745
|
+
addEvent(error.message, "error");
|
|
5746
|
+
renderAgentDoneNotificationsToggle();
|
|
5747
|
+
});
|
|
5748
|
+
});
|
|
4633
5749
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
4634
5750
|
setSidePanelCollapsed(true);
|
|
4635
5751
|
});
|
|
@@ -4658,6 +5774,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
4658
5774
|
if (document.body.classList.contains("composer-actions-open") && !elements.composer.contains(event.target)) {
|
|
4659
5775
|
setComposerActionsOpen(false);
|
|
4660
5776
|
}
|
|
5777
|
+
if (publishMenuOpen && !event.target?.closest?.(".composer-publish-menu")) {
|
|
5778
|
+
setPublishMenuOpen(false);
|
|
5779
|
+
}
|
|
4661
5780
|
if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
|
|
4662
5781
|
setMobileTabsExpanded(false);
|
|
4663
5782
|
}
|
|
@@ -4669,9 +5788,14 @@ document.addEventListener("pointermove", (event) => {
|
|
|
4669
5788
|
if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
|
|
4670
5789
|
clearOpenTerminalTabGroup(openTerminalTabGroupKey);
|
|
4671
5790
|
}
|
|
5791
|
+
rememberPointerPosition(event);
|
|
4672
5792
|
}, { passive: true });
|
|
4673
5793
|
window.addEventListener("keydown", (event) => {
|
|
4674
5794
|
if (event.key !== "Escape") return;
|
|
5795
|
+
if (publishMenuOpen) {
|
|
5796
|
+
setPublishMenuOpen(false);
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
4675
5799
|
if (document.body.classList.contains("composer-actions-open")) {
|
|
4676
5800
|
setComposerActionsOpen(false);
|
|
4677
5801
|
return;
|
|
@@ -4758,10 +5882,12 @@ elements.promptInput.addEventListener("blur", () => {
|
|
|
4758
5882
|
resizePromptInput();
|
|
4759
5883
|
focusPromptInput({ defer: true });
|
|
4760
5884
|
updateComposerModeButtons();
|
|
5885
|
+
updateOptionalFeatureAvailability();
|
|
4761
5886
|
loadLastUserPromptCache();
|
|
4762
5887
|
installViewportHandlers();
|
|
4763
5888
|
initializeThemes().catch((error) => addEvent(`failed to load themes: ${error.message}`, "warn"));
|
|
4764
5889
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
5890
|
+
restoreAgentDoneNotificationsSetting();
|
|
4765
5891
|
restoreSidePanelState();
|
|
4766
5892
|
bindMobileViewChanges();
|
|
4767
5893
|
registerPwaServiceWorker();
|