@firstpick/pi-package-webui 0.4.2 → 0.4.3
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 +6 -4
- package/WEBUI_TUI_NATIVE_PARITY.json +2 -2
- package/bin/pi-webui.mjs +94 -13
- package/package.json +5 -3
- package/public/app.js +473 -47
- package/public/index.html +10 -4
- package/public/styles.css +176 -55
- package/tests/http-endpoints-harness.test.mjs +2 -0
- package/tests/mobile-static.test.mjs +59 -10
- package/tests/remote-auth-settings-harness.test.mjs +81 -0
package/public/app.js
CHANGED
|
@@ -85,6 +85,7 @@ const elements = {
|
|
|
85
85
|
optionsCommandPaletteButton: $("#optionsCommandPaletteButton"),
|
|
86
86
|
optionsResumeButton: $("#optionsResumeButton"),
|
|
87
87
|
optionsReloadButton: $("#optionsReloadButton"),
|
|
88
|
+
optionsRemoteButton: $("#optionsRemoteButton"),
|
|
88
89
|
optionsNameButton: $("#optionsNameButton"),
|
|
89
90
|
optionsCloneButton: $("#optionsCloneButton"),
|
|
90
91
|
optionsSettingsButton: $("#optionsSettingsButton"),
|
|
@@ -197,6 +198,7 @@ const elements = {
|
|
|
197
198
|
commandPaletteInput: $("#commandPaletteInput"),
|
|
198
199
|
commandPaletteList: $("#commandPaletteList"),
|
|
199
200
|
commandPaletteHint: $("#commandPaletteHint"),
|
|
201
|
+
commandPaletteCloseButton: $("#commandPaletteCloseButton"),
|
|
200
202
|
editRetryDialog: $("#editRetryDialog"),
|
|
201
203
|
editRetryMessage: $("#editRetryMessage"),
|
|
202
204
|
editRetryText: $("#editRetryText"),
|
|
@@ -316,6 +318,8 @@ let updateStatusRefreshTimer = null;
|
|
|
316
318
|
let updateNotificationHideTimer = null;
|
|
317
319
|
let backendOfflineNoticeShown = false;
|
|
318
320
|
let latestMessages = [];
|
|
321
|
+
let latestMessagesSessionKey = "";
|
|
322
|
+
const tabMessagesCache = new Map();
|
|
319
323
|
let promptHistoryByTab = new Map();
|
|
320
324
|
let promptHistoryNavigation = null;
|
|
321
325
|
let transientMessages = [];
|
|
@@ -340,6 +344,8 @@ let toolOutputGloballyExpanded = false;
|
|
|
340
344
|
let agentDoneNotificationPermissionRequested = false;
|
|
341
345
|
let agentDoneNotificationFallbackNoted = false;
|
|
342
346
|
let agentDoneNotificationKeys = new Set();
|
|
347
|
+
let pendingAgentDoneNotificationTimers = new Map();
|
|
348
|
+
let autoRetryingTabs = new Set();
|
|
343
349
|
let availableModels = [];
|
|
344
350
|
let availableThemes = [];
|
|
345
351
|
let currentThemeName = "catppuccin-mocha";
|
|
@@ -374,7 +380,17 @@ let workspaceDashboardCollapsed = false;
|
|
|
374
380
|
let commandPaletteIndex = 0;
|
|
375
381
|
let commandPaletteItems = [];
|
|
376
382
|
let activeEditRetry = null;
|
|
383
|
+
let activePointerActivation = null;
|
|
384
|
+
let pointerActivationTimeout = null;
|
|
385
|
+
let deferredChatFollowScroll = false;
|
|
386
|
+
const deferredUiRenderCallbacks = new Map();
|
|
377
387
|
let abortLongPressTimer = null;
|
|
388
|
+
let abortLongPressTickTimer = null;
|
|
389
|
+
let abortLongPressResetTimer = null;
|
|
390
|
+
let abortLongPressStartedAt = 0;
|
|
391
|
+
let abortLongPressDeadlineAt = 0;
|
|
392
|
+
let abortLongPressSource = "long-press";
|
|
393
|
+
let abortLongPressReleasePending = false;
|
|
378
394
|
let abortLongPressHandled = false;
|
|
379
395
|
const dialogQueue = [];
|
|
380
396
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
@@ -412,6 +428,8 @@ const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
|
412
428
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
413
429
|
const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
|
|
414
430
|
const WORKSPACE_DASHBOARD_STORAGE_KEY = "pi-webui-workspace-dashboard-collapsed";
|
|
431
|
+
const POINTER_ACTIVATION_SELECTOR = "button, a[href], input, select, textarea, summary, [role='button'], [tabindex]:not([tabindex='-1'])";
|
|
432
|
+
const POINTER_ACTIVATION_RENDER_DEFER_MAX_MS = 1200;
|
|
415
433
|
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
416
434
|
const ATTACHMENT_MAX_FILES = 12;
|
|
417
435
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
@@ -444,7 +462,9 @@ const UPDATE_STATUS_INITIAL_DELAY_MS = 1800;
|
|
|
444
462
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
445
463
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
446
464
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
447
|
-
const ABORT_LONG_PRESS_MS =
|
|
465
|
+
const ABORT_LONG_PRESS_MS = 3000;
|
|
466
|
+
const ABORT_LONG_PRESS_TICK_MS = 100;
|
|
467
|
+
const ABORT_LONG_PRESS_RELEASE_GRACE_MS = 350;
|
|
448
468
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
449
469
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
450
470
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
@@ -454,6 +474,7 @@ const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+
|
|
|
454
474
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
455
475
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
456
476
|
const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
|
|
477
|
+
const AGENT_DONE_NOTIFICATION_RETRY_GRACE_MS = 1200;
|
|
457
478
|
const FOREGROUND_RECONCILE_DELAY_MS = 120;
|
|
458
479
|
const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "working", "idle"];
|
|
459
480
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
@@ -484,6 +505,7 @@ const optionalFeatureAvailability = {
|
|
|
484
505
|
tuiSkillsCommand: false,
|
|
485
506
|
todoProgressWidget: false,
|
|
486
507
|
tuiToolsCommand: false,
|
|
508
|
+
remoteWebui: false,
|
|
487
509
|
themeBundle: false,
|
|
488
510
|
};
|
|
489
511
|
const OPTIONAL_FEATURES = [
|
|
@@ -536,6 +558,13 @@ const OPTIONAL_FEATURES = [
|
|
|
536
558
|
capabilityLabel: "RPC /tools from tools extension",
|
|
537
559
|
description: "Terminal-native active-tool manager alongside WebUI-native /tools toggles.",
|
|
538
560
|
},
|
|
561
|
+
{
|
|
562
|
+
id: "remoteWebui",
|
|
563
|
+
label: "Remote WebUI",
|
|
564
|
+
packageName: "@firstpick/pi-package-remote-webui",
|
|
565
|
+
capabilityLabel: "/remote",
|
|
566
|
+
description: "Trusted-LAN QR helper for opening the Web UI from mobile browsers.",
|
|
567
|
+
},
|
|
539
568
|
{
|
|
540
569
|
id: "gitFooterStatus",
|
|
541
570
|
label: "Git footer status",
|
|
@@ -568,6 +597,7 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
568
597
|
["safety-guard", "safetyGuard"],
|
|
569
598
|
["skills", "tuiSkillsCommand"],
|
|
570
599
|
["tools", "tuiToolsCommand"],
|
|
600
|
+
["remote", "remoteWebui"],
|
|
571
601
|
["stats", "statsCommand"],
|
|
572
602
|
["git-footer-refresh", "gitFooterStatus"],
|
|
573
603
|
["todo-progress-status", "todoProgressWidget"],
|
|
@@ -826,6 +856,82 @@ function delay(ms) {
|
|
|
826
856
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
827
857
|
}
|
|
828
858
|
|
|
859
|
+
function activationControlFromEvent(event) {
|
|
860
|
+
const target = event?.target instanceof Element ? event.target : null;
|
|
861
|
+
const control = target?.closest?.(POINTER_ACTIVATION_SELECTOR);
|
|
862
|
+
if (!control || control === document.body || control === document.documentElement) return null;
|
|
863
|
+
if (control.disabled || control.getAttribute("aria-disabled") === "true") return null;
|
|
864
|
+
return control;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function shouldDeferUiRenderForPointerActivation() {
|
|
868
|
+
return Boolean(
|
|
869
|
+
activePointerActivation
|
|
870
|
+
&& performance.now() - activePointerActivation.startedAt <= POINTER_ACTIVATION_RENDER_DEFER_MAX_MS,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function deferUiRenderDuringPointerActivation(key, callback) {
|
|
875
|
+
if (!shouldDeferUiRenderForPointerActivation()) return false;
|
|
876
|
+
deferredUiRenderCallbacks.set(key, callback);
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function deferChatFollowScrollDuringPointerActivation({ force = false } = {}) {
|
|
881
|
+
if (force || !shouldDeferUiRenderForPointerActivation()) return false;
|
|
882
|
+
deferredChatFollowScroll = true;
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function flushDeferredUiRenders() {
|
|
887
|
+
const callbacks = [...deferredUiRenderCallbacks.values()];
|
|
888
|
+
deferredUiRenderCallbacks.clear();
|
|
889
|
+
const shouldScroll = deferredChatFollowScroll;
|
|
890
|
+
deferredChatFollowScroll = false;
|
|
891
|
+
|
|
892
|
+
for (const callback of callbacks) {
|
|
893
|
+
try {
|
|
894
|
+
callback();
|
|
895
|
+
} catch (error) {
|
|
896
|
+
console.error("deferred Web UI render failed", error);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (shouldScroll) scrollChatToBottom();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function beginPointerActivation(event) {
|
|
903
|
+
if (event?.button !== undefined && event.button !== 0) return;
|
|
904
|
+
const control = activationControlFromEvent(event);
|
|
905
|
+
if (!control) return;
|
|
906
|
+
clearTimeout(pointerActivationTimeout);
|
|
907
|
+
const activation = { pointerId: event.pointerId, startedAt: performance.now(), control };
|
|
908
|
+
activePointerActivation = activation;
|
|
909
|
+
pointerActivationTimeout = setTimeout(() => {
|
|
910
|
+
if (activePointerActivation === activation) activePointerActivation = null;
|
|
911
|
+
pointerActivationTimeout = null;
|
|
912
|
+
flushDeferredUiRenders();
|
|
913
|
+
}, POINTER_ACTIVATION_RENDER_DEFER_MAX_MS);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function finishPointerActivation(event) {
|
|
917
|
+
if (!activePointerActivation) return;
|
|
918
|
+
if (event?.pointerId !== undefined && activePointerActivation.pointerId !== event.pointerId) return;
|
|
919
|
+
const activation = activePointerActivation;
|
|
920
|
+
clearTimeout(pointerActivationTimeout);
|
|
921
|
+
pointerActivationTimeout = null;
|
|
922
|
+
setTimeout(() => {
|
|
923
|
+
if (activePointerActivation === activation) activePointerActivation = null;
|
|
924
|
+
flushDeferredUiRenders();
|
|
925
|
+
}, 0);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function cancelPointerActivation() {
|
|
929
|
+
clearTimeout(pointerActivationTimeout);
|
|
930
|
+
pointerActivationTimeout = null;
|
|
931
|
+
activePointerActivation = null;
|
|
932
|
+
flushDeferredUiRenders();
|
|
933
|
+
}
|
|
934
|
+
|
|
829
935
|
function isMobileView() {
|
|
830
936
|
return mobileViewMedia?.matches || false;
|
|
831
937
|
}
|
|
@@ -834,6 +940,36 @@ function isSidePanelOverlayView() {
|
|
|
834
940
|
return sidePanelOverlayMedia?.matches || false;
|
|
835
941
|
}
|
|
836
942
|
|
|
943
|
+
function mobileDropdownViewportHeight() {
|
|
944
|
+
return window.visualViewport?.height || window.innerHeight || document.documentElement.clientHeight || 0;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function mobileDropdownConfigs() {
|
|
948
|
+
return [
|
|
949
|
+
{ menu: elements.publishButton?.parentElement, button: elements.publishButton, panel: elements.publishButton?.parentElement?.querySelector(".composer-publish-menu-panel") },
|
|
950
|
+
{ menu: elements.nativeCommandMenuButton?.parentElement, button: elements.nativeCommandMenuButton, panel: elements.nativeCommandMenuButton?.parentElement?.querySelector(".composer-publish-menu-panel") },
|
|
951
|
+
{ menu: elements.optionsMenuButton?.parentElement, button: elements.optionsMenuButton, panel: elements.optionsMenu },
|
|
952
|
+
{ menu: elements.appRunnerMenu, button: elements.appRunnerMenuButton, panel: elements.appRunnerMenuPanel },
|
|
953
|
+
];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function updateMobileDropdownScrollBounds() {
|
|
957
|
+
const viewportHeight = mobileDropdownViewportHeight();
|
|
958
|
+
for (const { menu, button, panel } of mobileDropdownConfigs()) {
|
|
959
|
+
if (!panel) continue;
|
|
960
|
+
panel.style.removeProperty("--mobile-dropdown-max-height");
|
|
961
|
+
if (!isMobileView() || !menu?.classList.contains("open") || !viewportHeight) continue;
|
|
962
|
+
const anchorRect = (button || menu).getBoundingClientRect();
|
|
963
|
+
const availableAbove = Math.floor(anchorRect.top - 8);
|
|
964
|
+
const boundedHeight = Math.max(72, Math.min(viewportHeight - 16, availableAbove));
|
|
965
|
+
panel.style.setProperty("--mobile-dropdown-max-height", `${boundedHeight}px`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function scheduleMobileDropdownScrollBoundsUpdate() {
|
|
970
|
+
requestAnimationFrame(updateMobileDropdownScrollBounds);
|
|
971
|
+
}
|
|
972
|
+
|
|
837
973
|
function readStoredSidePanelCollapsed() {
|
|
838
974
|
try {
|
|
839
975
|
const stored = localStorage.getItem(SIDE_PANEL_STORAGE_KEY);
|
|
@@ -1592,6 +1728,7 @@ function setComposerActionsOpen(open) {
|
|
|
1592
1728
|
setOptionsMenuOpen(false);
|
|
1593
1729
|
setBusyPromptBehaviorMenuOpen(false);
|
|
1594
1730
|
}
|
|
1731
|
+
scheduleMobileDropdownScrollBoundsUpdate();
|
|
1595
1732
|
}
|
|
1596
1733
|
|
|
1597
1734
|
function isUserBashActive(tabId = activeTabId) {
|
|
@@ -1643,11 +1780,17 @@ function updateComposerModeButtons() {
|
|
|
1643
1780
|
button.hidden = !runActive;
|
|
1644
1781
|
button.disabled = !runActive;
|
|
1645
1782
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
elements.abortButton.
|
|
1649
|
-
elements.abortButton.
|
|
1650
|
-
|
|
1783
|
+
const abortHoldActive = isAbortLongPressActive();
|
|
1784
|
+
if (!abortAvailable && !abortHoldActive) resetAbortLongPressAffordance();
|
|
1785
|
+
elements.abortButton.hidden = !abortAvailable && !abortHoldActive;
|
|
1786
|
+
elements.abortButton.disabled = (!abortAvailable && !abortHoldActive) || abortRequestInFlight;
|
|
1787
|
+
if (abortHoldActive) {
|
|
1788
|
+
renderAbortLongPressAffordance();
|
|
1789
|
+
} else {
|
|
1790
|
+
elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
1791
|
+
elements.abortButton.title = abortAvailable ? abortButtonReadyTitle() : "Abort is available while Pi is running";
|
|
1792
|
+
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
1793
|
+
}
|
|
1651
1794
|
renderBusyPromptBehaviorTag();
|
|
1652
1795
|
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
1653
1796
|
}
|
|
@@ -1851,6 +1994,7 @@ function updateVisualViewportVars() {
|
|
|
1851
1994
|
syncMobileChatToBottomForInput();
|
|
1852
1995
|
}
|
|
1853
1996
|
updateFooterModelPickerPosition();
|
|
1997
|
+
updateMobileDropdownScrollBounds();
|
|
1854
1998
|
}
|
|
1855
1999
|
|
|
1856
2000
|
function installViewportHandlers() {
|
|
@@ -3726,11 +3870,17 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
3726
3870
|
if (!liveIds.has(tabId)) {
|
|
3727
3871
|
tabActivities.delete(tabId);
|
|
3728
3872
|
tabSeenCompletionSerials.delete(tabId);
|
|
3873
|
+
autoRetryingTabs.delete(tabId);
|
|
3874
|
+
suppressPendingAgentDoneNotificationsForTab(tabId, { markSeen: false });
|
|
3729
3875
|
actionFeedbackByTab.delete(tabId);
|
|
3730
3876
|
skillUsageByTab.delete(tabId);
|
|
3877
|
+
tabMessagesCache.delete(tabId);
|
|
3731
3878
|
clearGitWorkflowForTab(tabId);
|
|
3732
3879
|
}
|
|
3733
3880
|
}
|
|
3881
|
+
for (const tabId of tabMessagesCache.keys()) {
|
|
3882
|
+
if (!liveIds.has(tabId)) tabMessagesCache.delete(tabId);
|
|
3883
|
+
}
|
|
3734
3884
|
pruneSkillUsageForKnownTabs(liveIds);
|
|
3735
3885
|
}
|
|
3736
3886
|
|
|
@@ -3894,6 +4044,18 @@ function ingestEventTabActivity(event) {
|
|
|
3894
4044
|
if (changed) renderTabs();
|
|
3895
4045
|
}
|
|
3896
4046
|
|
|
4047
|
+
function trackAutoRetryStateFromEvent(event) {
|
|
4048
|
+
const tabId = event?.tabId || activeTabId;
|
|
4049
|
+
if (!tabId) return;
|
|
4050
|
+
if (event.type === "auto_retry_start") {
|
|
4051
|
+
autoRetryingTabs.add(tabId);
|
|
4052
|
+
suppressPendingAgentDoneNotificationsForTab(tabId);
|
|
4053
|
+
markTabWorkingLocally(tabId);
|
|
4054
|
+
} else if (event.type === "auto_retry_end") {
|
|
4055
|
+
autoRetryingTabs.delete(tabId);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
|
|
3897
4059
|
function rememberActiveTab() {
|
|
3898
4060
|
try {
|
|
3899
4061
|
if (activeTabId) localStorage.setItem(TAB_STORAGE_KEY, activeTabId);
|
|
@@ -3985,6 +4147,7 @@ function resetActiveTabUi() {
|
|
|
3985
4147
|
latestStatsOverlayPayload = null;
|
|
3986
4148
|
latestWorkspace = null;
|
|
3987
4149
|
latestMessages = [];
|
|
4150
|
+
latestMessagesSessionKey = "";
|
|
3988
4151
|
clearRunIndicatorActivity({ render: false });
|
|
3989
4152
|
statusEntries.clear();
|
|
3990
4153
|
widgets.clear();
|
|
@@ -4016,8 +4179,10 @@ function resetActiveTabUi() {
|
|
|
4016
4179
|
renderAppRunnerControls();
|
|
4017
4180
|
renderWidgets();
|
|
4018
4181
|
renderGitWorkflow();
|
|
4019
|
-
|
|
4020
|
-
|
|
4182
|
+
if (!restoreCachedMessagesForActiveTab()) {
|
|
4183
|
+
renderFooter();
|
|
4184
|
+
renderFeedbackTray();
|
|
4185
|
+
}
|
|
4021
4186
|
}
|
|
4022
4187
|
|
|
4023
4188
|
function tabGroupStatusRank(state) {
|
|
@@ -4256,6 +4421,7 @@ function moveNewTabMenuFocus(delta) {
|
|
|
4256
4421
|
}
|
|
4257
4422
|
|
|
4258
4423
|
function renderTabs() {
|
|
4424
|
+
if (deferUiRenderDuringPointerActivation("tabs", renderTabs)) return;
|
|
4259
4425
|
const active = activeTab();
|
|
4260
4426
|
const activeIndicator = active ? tabIndicator(active) : null;
|
|
4261
4427
|
elements.terminalTabsToggleButton.textContent = active ? `${activeIndicator.glyph} ${active.title}${tabs.length > 1 ? ` · ${tabs.length}` : ""}` : "Tabs";
|
|
@@ -4280,7 +4446,7 @@ function renderTabs() {
|
|
|
4280
4446
|
updateDocumentTitle();
|
|
4281
4447
|
renderWorkspaceDashboard();
|
|
4282
4448
|
renderContextMeter();
|
|
4283
|
-
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
4449
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
|
|
4284
4450
|
syncTabPolling();
|
|
4285
4451
|
}
|
|
4286
4452
|
|
|
@@ -4311,6 +4477,7 @@ async function switchTab(tabId) {
|
|
|
4311
4477
|
footerBranchPickerOpen = false;
|
|
4312
4478
|
footerBranchPickerRequestSerial += 1;
|
|
4313
4479
|
saveActiveDraft();
|
|
4480
|
+
cacheMessagesForTab(activeTabId);
|
|
4314
4481
|
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
4315
4482
|
resetActiveTabUi();
|
|
4316
4483
|
renderTabs();
|
|
@@ -4423,6 +4590,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
4423
4590
|
clearAttachments(id);
|
|
4424
4591
|
clearGitWorkflowForTab(id);
|
|
4425
4592
|
appRunnerDataByTab.delete(id);
|
|
4593
|
+
tabMessagesCache.delete(id);
|
|
4426
4594
|
}
|
|
4427
4595
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
4428
4596
|
|
|
@@ -4689,6 +4857,36 @@ function agentDoneNotificationKey(tabId, activity = {}) {
|
|
|
4689
4857
|
return `${tabId}:${Number.isFinite(serial) && serial > 0 ? serial : "done"}`;
|
|
4690
4858
|
}
|
|
4691
4859
|
|
|
4860
|
+
function isAutoRetryingTab(tabId) {
|
|
4861
|
+
return !!tabId && autoRetryingTabs.has(tabId);
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
function clearPendingAgentDoneNotification(key, { markSeen = false } = {}) {
|
|
4865
|
+
const pending = pendingAgentDoneNotificationTimers.get(key);
|
|
4866
|
+
if (!pending) return false;
|
|
4867
|
+
clearTimeout(pending.timer);
|
|
4868
|
+
pendingAgentDoneNotificationTimers.delete(key);
|
|
4869
|
+
if (markSeen) agentDoneNotificationKeys.add(key);
|
|
4870
|
+
return true;
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
function suppressPendingAgentDoneNotificationsForTab(tabId, { markSeen = true } = {}) {
|
|
4874
|
+
if (!tabId) return;
|
|
4875
|
+
for (const [key, pending] of pendingAgentDoneNotificationTimers) {
|
|
4876
|
+
if (pending.tabId === tabId) clearPendingAgentDoneNotification(key, { markSeen });
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
function queueAgentDoneBrowserNotification({ key, tabId, title, body }) {
|
|
4881
|
+
clearPendingAgentDoneNotification(key);
|
|
4882
|
+
const timer = setTimeout(() => {
|
|
4883
|
+
pendingAgentDoneNotificationTimers.delete(key);
|
|
4884
|
+
if (isAutoRetryingTab(tabId)) return;
|
|
4885
|
+
showAgentDoneBrowserNotification({ tabId, title, body });
|
|
4886
|
+
}, AGENT_DONE_NOTIFICATION_RETRY_GRACE_MS);
|
|
4887
|
+
pendingAgentDoneNotificationTimers.set(key, { tabId, timer });
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4692
4890
|
function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
|
|
4693
4891
|
if (!agentDoneNotificationsEnabled) return;
|
|
4694
4892
|
const tabId = typeof tabOrId === "string" ? tabOrId : tabOrId?.id || activeTabId;
|
|
@@ -4699,9 +4897,11 @@ function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
|
|
|
4699
4897
|
const key = agentDoneNotificationKey(tabId, normalizedActivity);
|
|
4700
4898
|
if (agentDoneNotificationKeys.has(key)) return;
|
|
4701
4899
|
agentDoneNotificationKeys.add(key);
|
|
4900
|
+
if (isAutoRetryingTab(tabId)) return;
|
|
4702
4901
|
|
|
4703
4902
|
const displayTitle = tabTitle || tab?.title || "terminal";
|
|
4704
|
-
|
|
4903
|
+
queueAgentDoneBrowserNotification({
|
|
4904
|
+
key,
|
|
4705
4905
|
tabId,
|
|
4706
4906
|
title: "Pi finished work",
|
|
4707
4907
|
body: `${displayTitle} finished its agent run.`,
|
|
@@ -6183,6 +6383,7 @@ async function requestManualCompaction({ triggerButton = null } = {}) {
|
|
|
6183
6383
|
}
|
|
6184
6384
|
|
|
6185
6385
|
function renderContextMeter() {
|
|
6386
|
+
if (deferUiRenderDuringPointerActivation("context-meter", renderContextMeter)) return;
|
|
6186
6387
|
const root = elements.contextMeterBar;
|
|
6187
6388
|
if (!root) return;
|
|
6188
6389
|
const tab = activeTab();
|
|
@@ -6239,6 +6440,7 @@ function dashboardAction(label, handler, className = "") {
|
|
|
6239
6440
|
}
|
|
6240
6441
|
|
|
6241
6442
|
function renderWorkspaceDashboard() {
|
|
6443
|
+
if (deferUiRenderDuringPointerActivation("workspace-dashboard", renderWorkspaceDashboard)) return;
|
|
6242
6444
|
const root = elements.workspaceDashboard;
|
|
6243
6445
|
if (!root) return;
|
|
6244
6446
|
const tab = activeTab();
|
|
@@ -7037,6 +7239,7 @@ async function changeActiveTabCwd() {
|
|
|
7037
7239
|
}
|
|
7038
7240
|
|
|
7039
7241
|
function renderFooter() {
|
|
7242
|
+
if (deferUiRenderDuringPointerActivation("footer", renderFooter)) return;
|
|
7040
7243
|
const gitFooterPayload = parseGitFooterWebuiPayload();
|
|
7041
7244
|
if (gitFooterPayload) {
|
|
7042
7245
|
renderGitFooterPayload(footerPayloadWithLiveModel(gitFooterPayload));
|
|
@@ -7269,6 +7472,7 @@ function initializeCodexUsage() {
|
|
|
7269
7472
|
}
|
|
7270
7473
|
|
|
7271
7474
|
function renderStatus() {
|
|
7475
|
+
if (deferUiRenderDuringPointerActivation("status", renderStatus)) return;
|
|
7272
7476
|
const state = currentState;
|
|
7273
7477
|
updateComposerModeButtons();
|
|
7274
7478
|
const running = state?.isStreaming ? "running" : "idle";
|
|
@@ -8710,7 +8914,22 @@ function handleStatsWebuiStatus(statusText) {
|
|
|
8710
8914
|
if (payload.open || elements.statsOverlayDialog?.open) renderStatsOverlay();
|
|
8711
8915
|
}
|
|
8712
8916
|
|
|
8917
|
+
function remoteWebuiWidgetLines(lines = []) {
|
|
8918
|
+
return (Array.isArray(lines) ? lines : [])
|
|
8919
|
+
.map(stripAnsi)
|
|
8920
|
+
.map((line) => String(line ?? ""))
|
|
8921
|
+
.filter((line, index, array) => line.trim() || (index > 0 && index < array.length - 1));
|
|
8922
|
+
}
|
|
8923
|
+
|
|
8924
|
+
function mirrorRemoteWebuiWidgetToTranscript(widgetKey, lines = [], request = {}) {
|
|
8925
|
+
if (widgetKey !== "pi-remote-webui" || request.replayed) return;
|
|
8926
|
+
const content = remoteWebuiWidgetLines(lines).join("\n").trimEnd();
|
|
8927
|
+
if (!content) return;
|
|
8928
|
+
addTransientMessage({ role: "extension", title: "/remote", content, level: "info", widgetKey });
|
|
8929
|
+
}
|
|
8930
|
+
|
|
8713
8931
|
function renderWidgets() {
|
|
8932
|
+
if (deferUiRenderDuringPointerActivation("widgets", renderWidgets)) return;
|
|
8714
8933
|
elements.widgetArea.replaceChildren();
|
|
8715
8934
|
const releaseOutput = renderReleaseNpmOutputWidget();
|
|
8716
8935
|
if (releaseOutput) elements.widgetArea.append(releaseOutput);
|
|
@@ -8726,8 +8945,9 @@ function renderWidgets() {
|
|
|
8726
8945
|
for (const [key, value] of widgets) {
|
|
8727
8946
|
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
8728
8947
|
if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
|
|
8729
|
-
if (widgetFeatureId && key
|
|
8948
|
+
if (widgetFeatureId && optionalFeatureWidgetHasSpecializedRenderer(key)) continue;
|
|
8730
8949
|
const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
|
|
8950
|
+
if (key === "pi-remote-webui") continue;
|
|
8731
8951
|
const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
|
|
8732
8952
|
if (specialized) {
|
|
8733
8953
|
elements.widgetArea.append(specialized);
|
|
@@ -10330,6 +10550,7 @@ function renderQueueGroup(label, items, tone) {
|
|
|
10330
10550
|
}
|
|
10331
10551
|
|
|
10332
10552
|
function renderQueue(event) {
|
|
10553
|
+
if (deferUiRenderDuringPointerActivation("queue", () => renderQueue(event))) return;
|
|
10333
10554
|
const snapshot = normalizeQueuedMessages(event);
|
|
10334
10555
|
const tabId = event?.tabId || activeTabId;
|
|
10335
10556
|
if (tabId) latestQueuedMessagesByTab.set(tabId, snapshot);
|
|
@@ -11281,6 +11502,7 @@ function renderActionFeedbackControls(bubble, message, messageIndex) {
|
|
|
11281
11502
|
}
|
|
11282
11503
|
|
|
11283
11504
|
function renderFeedbackTray() {
|
|
11505
|
+
if (deferUiRenderDuringPointerActivation("feedback-tray", renderFeedbackTray)) return;
|
|
11284
11506
|
const items = queuedActionFeedback();
|
|
11285
11507
|
const hasItems = items.length > 0;
|
|
11286
11508
|
elements.feedbackTray.hidden = !hasItems;
|
|
@@ -11753,6 +11975,7 @@ function findStickyUserPromptTarget(targets = userPromptTargets()) {
|
|
|
11753
11975
|
}
|
|
11754
11976
|
|
|
11755
11977
|
function updateStickyUserPromptButton() {
|
|
11978
|
+
if (deferUiRenderDuringPointerActivation("sticky-user-prompt", updateStickyUserPromptButton)) return;
|
|
11756
11979
|
const button = elements.stickyUserPromptButton;
|
|
11757
11980
|
if (!button) return;
|
|
11758
11981
|
const targets = userPromptTargets();
|
|
@@ -12902,6 +13125,7 @@ function pruneDisconnectedLiveToolCards() {
|
|
|
12902
13125
|
}
|
|
12903
13126
|
|
|
12904
13127
|
function renderAllMessages({ preserveScroll = false, forceRebuild = false } = {}) {
|
|
13128
|
+
if (deferUiRenderDuringPointerActivation("messages", () => renderAllMessages({ preserveScroll, forceRebuild }))) return;
|
|
12905
13129
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
12906
13130
|
const previousScrollTop = elements.chat.scrollTop;
|
|
12907
13131
|
const transcriptItems = orderedTranscriptItems();
|
|
@@ -13073,6 +13297,7 @@ function scheduleChatFollowScroll() {
|
|
|
13073
13297
|
}
|
|
13074
13298
|
|
|
13075
13299
|
function scrollChatToBottom({ force = false } = {}) {
|
|
13300
|
+
if (deferChatFollowScrollDuringPointerActivation({ force })) return;
|
|
13076
13301
|
if (force) autoFollowChat = true;
|
|
13077
13302
|
if (!autoFollowChat) {
|
|
13078
13303
|
updateJumpToLatestButton();
|
|
@@ -13133,6 +13358,7 @@ function setPublishMenuOpen(open) {
|
|
|
13133
13358
|
elements.publishButton.setAttribute("aria-expanded", publishMenuOpen ? "true" : "false");
|
|
13134
13359
|
elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
|
|
13135
13360
|
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
13361
|
+
scheduleMobileDropdownScrollBoundsUpdate();
|
|
13136
13362
|
}
|
|
13137
13363
|
|
|
13138
13364
|
function setNativeCommandMenuOpen(open) {
|
|
@@ -13140,6 +13366,7 @@ function setNativeCommandMenuOpen(open) {
|
|
|
13140
13366
|
elements.nativeCommandMenuButton.setAttribute("aria-expanded", nativeCommandMenuOpen ? "true" : "false");
|
|
13141
13367
|
elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
|
|
13142
13368
|
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
13369
|
+
scheduleMobileDropdownScrollBoundsUpdate();
|
|
13143
13370
|
}
|
|
13144
13371
|
|
|
13145
13372
|
function setAppRunnerMenuOpen(open) {
|
|
@@ -13147,6 +13374,7 @@ function setAppRunnerMenuOpen(open) {
|
|
|
13147
13374
|
elements.appRunnerMenuButton?.setAttribute("aria-expanded", appRunnerMenuOpen ? "true" : "false");
|
|
13148
13375
|
elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
|
|
13149
13376
|
elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
|
|
13377
|
+
scheduleMobileDropdownScrollBoundsUpdate();
|
|
13150
13378
|
}
|
|
13151
13379
|
|
|
13152
13380
|
function setOptionsMenuOpen(open) {
|
|
@@ -13154,6 +13382,7 @@ function setOptionsMenuOpen(open) {
|
|
|
13154
13382
|
elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
|
|
13155
13383
|
elements.optionsMenuButton.classList.toggle("menu-open", optionsMenuOpen);
|
|
13156
13384
|
elements.optionsMenuButton.parentElement?.classList.toggle("open", optionsMenuOpen);
|
|
13385
|
+
scheduleMobileDropdownScrollBoundsUpdate();
|
|
13157
13386
|
}
|
|
13158
13387
|
|
|
13159
13388
|
function optionalFeatureIdForCommand(name) {
|
|
@@ -13318,6 +13547,7 @@ function updateOptionalFeatureAvailability() {
|
|
|
13318
13547
|
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
13319
13548
|
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
13320
13549
|
optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
|
|
13550
|
+
optionalFeatureAvailability.remoteWebui = hasAvailableCommand("remote") || optionalFeatureAvailability.remoteWebui || statusEntries.has("pi-remote-webui") || widgets.has("pi-remote-webui");
|
|
13321
13551
|
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
13322
13552
|
requestGitFooterWebuiPayload();
|
|
13323
13553
|
renderOptionalFeatureControls();
|
|
@@ -13342,9 +13572,14 @@ function optionalFeatureWidgetFeatureId(key) {
|
|
|
13342
13572
|
if (key.startsWith("release-npm:")) return "releaseNpm";
|
|
13343
13573
|
if (key.startsWith("release-aur:")) return "releaseAur";
|
|
13344
13574
|
if (key === "todo-progress") return "todoProgressWidget";
|
|
13575
|
+
if (key === "pi-remote-webui") return "remoteWebui";
|
|
13345
13576
|
return null;
|
|
13346
13577
|
}
|
|
13347
13578
|
|
|
13579
|
+
function optionalFeatureWidgetHasSpecializedRenderer(key) {
|
|
13580
|
+
return key.startsWith("release-npm:") || key.startsWith("release-aur:");
|
|
13581
|
+
}
|
|
13582
|
+
|
|
13348
13583
|
function renderOptionalFeaturePanel() {
|
|
13349
13584
|
if (!elements.optionalFeaturesBox) return;
|
|
13350
13585
|
elements.optionalFeaturesBox.replaceChildren();
|
|
@@ -13449,6 +13684,16 @@ function renderOptionalFeatureControls() {
|
|
|
13449
13684
|
);
|
|
13450
13685
|
}
|
|
13451
13686
|
|
|
13687
|
+
const hasRemoteWebuiCommand = isOptionalFeatureEnabled("remoteWebui") && hasAvailableCommand("remote");
|
|
13688
|
+
if (elements.optionsRemoteButton) {
|
|
13689
|
+
elements.optionsRemoteButton.hidden = !hasRemoteWebuiCommand;
|
|
13690
|
+
setOptionalControlState(
|
|
13691
|
+
elements.optionsRemoteButton,
|
|
13692
|
+
hasRemoteWebuiCommand,
|
|
13693
|
+
optionalFeatureUnavailableMessage("remoteWebui"),
|
|
13694
|
+
);
|
|
13695
|
+
}
|
|
13696
|
+
|
|
13452
13697
|
renderOptionalFeaturePanel();
|
|
13453
13698
|
}
|
|
13454
13699
|
|
|
@@ -14751,6 +14996,10 @@ async function refreshState(tabContext = activeTabContext()) {
|
|
|
14751
14996
|
if (!isCurrentTabContext(tabContext)) return;
|
|
14752
14997
|
const previousState = currentState;
|
|
14753
14998
|
currentState = response.data || null;
|
|
14999
|
+
if (latestMessages.length) {
|
|
15000
|
+
latestMessagesSessionKey = resolveMessagesSessionKey(tabContext.tabId);
|
|
15001
|
+
cacheMessagesForTab(tabContext.tabId, latestMessages, latestMessagesSessionKey);
|
|
15002
|
+
}
|
|
14754
15003
|
const shouldRefreshGitFooter = gitFooterRelevantStateChanged(previousState, currentState);
|
|
14755
15004
|
syncActiveTabActivityFromState(currentState);
|
|
14756
15005
|
syncRunIndicatorFromState(currentState);
|
|
@@ -14904,7 +15153,32 @@ async function refreshFooterData(tabContext = activeTabContext()) {
|
|
|
14904
15153
|
|
|
14905
15154
|
// Session key of the last applied transcript fetch; deltas are only
|
|
14906
15155
|
// attempted while the tab+session is unchanged.
|
|
14907
|
-
|
|
15156
|
+
function resolveMessagesSessionKey(tabId = activeTabId) {
|
|
15157
|
+
if (!tabId) return "";
|
|
15158
|
+
const stateSessionId = tabId === activeTabId ? currentState?.sessionId : null;
|
|
15159
|
+
if (stateSessionId) return `${tabId}|${stateSessionId}`;
|
|
15160
|
+
if (latestMessagesSessionKey.startsWith(`${tabId}|`)) return latestMessagesSessionKey;
|
|
15161
|
+
const cached = tabMessagesCache.get(tabId);
|
|
15162
|
+
if (cached?.sessionKey?.startsWith(`${tabId}|`)) return cached.sessionKey;
|
|
15163
|
+
return `${tabId}|`;
|
|
15164
|
+
}
|
|
15165
|
+
|
|
15166
|
+
function cacheMessagesForTab(tabId = activeTabId, messages = latestMessages, sessionKey = latestMessagesSessionKey) {
|
|
15167
|
+
if (!tabId || !Array.isArray(messages)) return;
|
|
15168
|
+
const stateSessionKey = tabId === activeTabId && currentState?.sessionId ? `${tabId}|${currentState.sessionId}` : "";
|
|
15169
|
+
const resolvedSessionKey = stateSessionKey || (sessionKey?.startsWith(`${tabId}|`) ? sessionKey : resolveMessagesSessionKey(tabId));
|
|
15170
|
+
tabMessagesCache.set(tabId, { messages, sessionKey: resolvedSessionKey });
|
|
15171
|
+
}
|
|
15172
|
+
|
|
15173
|
+
function restoreCachedMessagesForActiveTab() {
|
|
15174
|
+
if (!activeTabId) return false;
|
|
15175
|
+
const cached = tabMessagesCache.get(activeTabId);
|
|
15176
|
+
if (!cached || !Array.isArray(cached.messages)) return false;
|
|
15177
|
+
latestMessages = cached.messages;
|
|
15178
|
+
latestMessagesSessionKey = cached.sessionKey || resolveMessagesSessionKey(activeTabId);
|
|
15179
|
+
renderMessages(latestMessages);
|
|
15180
|
+
return true;
|
|
15181
|
+
}
|
|
14908
15182
|
|
|
14909
15183
|
function messagesLookEqual(a, b) {
|
|
14910
15184
|
return !!a && !!b && a.role === b.role && String(a.timestamp || "") === String(b.timestamp || "")
|
|
@@ -14933,7 +15207,7 @@ function mergeMessagesDelta(previous, data) {
|
|
|
14933
15207
|
async function refreshMessages(tabContext = activeTabContext()) {
|
|
14934
15208
|
if (!tabContext.tabId) return;
|
|
14935
15209
|
const previousMessages = latestMessages;
|
|
14936
|
-
const sessionKey =
|
|
15210
|
+
const sessionKey = resolveMessagesSessionKey(tabContext.tabId);
|
|
14937
15211
|
let nextMessages = null;
|
|
14938
15212
|
if (previousMessages.length > 1 && sessionKey === latestMessagesSessionKey) {
|
|
14939
15213
|
// Delta fetch with a one-message overlap: the last known message is
|
|
@@ -14949,6 +15223,7 @@ async function refreshMessages(tabContext = activeTabContext()) {
|
|
|
14949
15223
|
}
|
|
14950
15224
|
latestMessages = nextMessages;
|
|
14951
15225
|
latestMessagesSessionKey = sessionKey;
|
|
15226
|
+
cacheMessagesForTab(tabContext.tabId, latestMessages, latestMessagesSessionKey);
|
|
14952
15227
|
const preserveLiveStream = liveStreamRenderActive();
|
|
14953
15228
|
if (!preserveLiveStream) resetStreamBubble();
|
|
14954
15229
|
renderMessages(latestMessages);
|
|
@@ -14989,7 +15264,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
14989
15264
|
syncModelSelectToState();
|
|
14990
15265
|
renderFooter();
|
|
14991
15266
|
renderFeedbackTray();
|
|
14992
|
-
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
15267
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
|
|
14993
15268
|
}
|
|
14994
15269
|
|
|
14995
15270
|
function syncModelSelectToState() {
|
|
@@ -15466,7 +15741,7 @@ async function refreshCommands(tabContext = activeTabContext()) {
|
|
|
15466
15741
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
15467
15742
|
updateOptionalFeatureAvailability();
|
|
15468
15743
|
renderCommands();
|
|
15469
|
-
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
15744
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
|
|
15470
15745
|
}
|
|
15471
15746
|
|
|
15472
15747
|
function paletteText(value) {
|
|
@@ -15563,12 +15838,14 @@ function setCommandPaletteIndex(index) {
|
|
|
15563
15838
|
renderCommandPaletteList();
|
|
15564
15839
|
}
|
|
15565
15840
|
|
|
15566
|
-
function renderCommandPaletteList() {
|
|
15841
|
+
function renderCommandPaletteList({ preserveScroll = false } = {}) {
|
|
15567
15842
|
const list = elements.commandPaletteList;
|
|
15568
15843
|
if (!list) return;
|
|
15844
|
+
const scrollTop = preserveScroll ? list.scrollTop : 0;
|
|
15569
15845
|
list.replaceChildren();
|
|
15570
15846
|
if (!commandPaletteItems.length) {
|
|
15571
15847
|
list.append(make("div", "command-palette-empty muted", "No matching actions."));
|
|
15848
|
+
if (preserveScroll) list.scrollTop = scrollTop;
|
|
15572
15849
|
return;
|
|
15573
15850
|
}
|
|
15574
15851
|
commandPaletteItems.forEach((item, index) => {
|
|
@@ -15584,14 +15861,18 @@ function renderCommandPaletteList() {
|
|
|
15584
15861
|
);
|
|
15585
15862
|
list.append(button);
|
|
15586
15863
|
});
|
|
15864
|
+
if (preserveScroll) {
|
|
15865
|
+
list.scrollTop = scrollTop;
|
|
15866
|
+
return;
|
|
15867
|
+
}
|
|
15587
15868
|
const active = list.children[commandPaletteIndex];
|
|
15588
15869
|
active?.scrollIntoView({ block: "nearest" });
|
|
15589
15870
|
}
|
|
15590
15871
|
|
|
15591
|
-
function renderCommandPalette() {
|
|
15872
|
+
function renderCommandPalette({ preserveScroll = false } = {}) {
|
|
15592
15873
|
commandPaletteItems = filteredCommandPaletteItems();
|
|
15593
15874
|
if (commandPaletteIndex >= commandPaletteItems.length) commandPaletteIndex = 0;
|
|
15594
|
-
renderCommandPaletteList();
|
|
15875
|
+
renderCommandPaletteList({ preserveScroll });
|
|
15595
15876
|
}
|
|
15596
15877
|
|
|
15597
15878
|
function openCommandPalette(initialQuery = "") {
|
|
@@ -16167,7 +16448,7 @@ function hasQueuedDialogRequest(id) {
|
|
|
16167
16448
|
|
|
16168
16449
|
function removeQueuedDialogRequests(ids = []) {
|
|
16169
16450
|
const idSet = new Set(ids.map((id) => String(id)).filter(Boolean));
|
|
16170
|
-
if (idSet.size === 0) return;
|
|
16451
|
+
if (idSet.size === 0) return false;
|
|
16171
16452
|
for (let i = dialogQueue.length - 1; i >= 0; i -= 1) {
|
|
16172
16453
|
if (idSet.has(String(dialogQueue[i]?.id || ""))) dialogQueue.splice(i, 1);
|
|
16173
16454
|
}
|
|
@@ -16175,7 +16456,9 @@ function removeQueuedDialogRequests(ids = []) {
|
|
|
16175
16456
|
if (elements.dialog.open) elements.dialog.close();
|
|
16176
16457
|
activeDialog = null;
|
|
16177
16458
|
showNextDialog();
|
|
16459
|
+
return true;
|
|
16178
16460
|
}
|
|
16461
|
+
return false;
|
|
16179
16462
|
}
|
|
16180
16463
|
|
|
16181
16464
|
function handleExtensionUiRequest(request) {
|
|
@@ -16201,12 +16484,20 @@ function handleExtensionUiRequest(request) {
|
|
|
16201
16484
|
renderStatus();
|
|
16202
16485
|
return;
|
|
16203
16486
|
}
|
|
16204
|
-
case "setWidget":
|
|
16205
|
-
|
|
16206
|
-
|
|
16487
|
+
case "setWidget": {
|
|
16488
|
+
const widgetKey = request.widgetKey || request.id;
|
|
16489
|
+
if (widgetKey === "pi-remote-webui") {
|
|
16490
|
+
widgets.delete(widgetKey);
|
|
16491
|
+
if (Array.isArray(request.widgetLines)) mirrorRemoteWebuiWidgetToTranscript(widgetKey, request.widgetLines, request);
|
|
16492
|
+
} else if (Array.isArray(request.widgetLines)) {
|
|
16493
|
+
widgets.set(widgetKey, request);
|
|
16494
|
+
} else {
|
|
16495
|
+
widgets.delete(widgetKey);
|
|
16496
|
+
}
|
|
16207
16497
|
updateOptionalFeatureAvailability();
|
|
16208
16498
|
renderWidgets();
|
|
16209
16499
|
return;
|
|
16500
|
+
}
|
|
16210
16501
|
case "setTitle":
|
|
16211
16502
|
if (request.title) document.title = request.title;
|
|
16212
16503
|
return;
|
|
@@ -16239,6 +16530,7 @@ function handleExtensionUiRequest(request) {
|
|
|
16239
16530
|
async function sendDialogResponse(payload) {
|
|
16240
16531
|
const { tabId = activeTabId, ...body } = payload;
|
|
16241
16532
|
const tabContext = activeTabContext(tabId);
|
|
16533
|
+
const responseId = String(body.id || "");
|
|
16242
16534
|
try {
|
|
16243
16535
|
const response = await api("/api/extension-ui-response", { method: "POST", body, tabId });
|
|
16244
16536
|
if (!applyResponseTab(response) && decrementTabPendingBlockerCount(tabId)) renderTabs();
|
|
@@ -16246,6 +16538,7 @@ async function sendDialogResponse(payload) {
|
|
|
16246
16538
|
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
16247
16539
|
} finally {
|
|
16248
16540
|
if (!isCurrentTabContext(tabContext)) return;
|
|
16541
|
+
if (responseId && activeDialog && String(activeDialog.id || "") !== responseId) return;
|
|
16249
16542
|
if (elements.dialog.open) elements.dialog.close();
|
|
16250
16543
|
activeDialog = null;
|
|
16251
16544
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Continuing after your response…");
|
|
@@ -16336,6 +16629,7 @@ function handleInactiveTabEvent(event) {
|
|
|
16336
16629
|
|
|
16337
16630
|
function handleEvent(event) {
|
|
16338
16631
|
ingestEventTabActivity(event);
|
|
16632
|
+
trackAutoRetryStateFromEvent(event);
|
|
16339
16633
|
trackSkillsFromEvent(event);
|
|
16340
16634
|
if (!eventTargetsActiveTab(event)) {
|
|
16341
16635
|
handleInactiveTabEvent(event);
|
|
@@ -16390,6 +16684,14 @@ function handleEvent(event) {
|
|
|
16390
16684
|
removeQueuedDialogRequests(event.ids || []);
|
|
16391
16685
|
addEvent(`cancelled ${event.ids?.length || 0} pending extension UI request(s)`, "warn");
|
|
16392
16686
|
break;
|
|
16687
|
+
case "webui_extension_ui_resolved": {
|
|
16688
|
+
const closedActiveDialog = removeQueuedDialogRequests([event.id]);
|
|
16689
|
+
if (closedActiveDialog) {
|
|
16690
|
+
addEvent("extension UI request resolved");
|
|
16691
|
+
if (runIndicatorIsActive() && !activeDialog) setRunIndicatorActivity("Continuing after extension UI response…");
|
|
16692
|
+
}
|
|
16693
|
+
break;
|
|
16694
|
+
}
|
|
16393
16695
|
case "webui_app_runner_update":
|
|
16394
16696
|
setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
|
|
16395
16697
|
renderAppRunnerControls();
|
|
@@ -16894,6 +17196,7 @@ elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu(
|
|
|
16894
17196
|
elements.optionsCommandPaletteButton.addEventListener("click", () => openCommandPalette());
|
|
16895
17197
|
elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
|
|
16896
17198
|
elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
|
|
17199
|
+
elements.optionsRemoteButton.addEventListener("click", () => runNativeCommandMenu("/remote"));
|
|
16897
17200
|
elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
|
|
16898
17201
|
elements.optionsCloneButton.addEventListener("click", () => runNativeCommandMenu("/clone"));
|
|
16899
17202
|
elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandMenu("/settings"));
|
|
@@ -16954,6 +17257,7 @@ elements.commandPaletteDialog?.addEventListener("cancel", (event) => {
|
|
|
16954
17257
|
event.preventDefault();
|
|
16955
17258
|
closeCommandPalette();
|
|
16956
17259
|
});
|
|
17260
|
+
elements.commandPaletteCloseButton?.addEventListener("click", closeCommandPalette);
|
|
16957
17261
|
elements.commandPaletteInput?.addEventListener("input", () => {
|
|
16958
17262
|
commandPaletteIndex = 0;
|
|
16959
17263
|
renderCommandPalette();
|
|
@@ -16986,11 +17290,104 @@ elements.editRetryCancelButton?.addEventListener("click", closeEditRetryDialog);
|
|
|
16986
17290
|
elements.editRetryForkButton?.addEventListener("click", () => submitEditRetry({ send: false }));
|
|
16987
17291
|
elements.editRetrySendButton?.addEventListener("click", () => submitEditRetry({ send: true }));
|
|
16988
17292
|
|
|
16989
|
-
function
|
|
17293
|
+
function abortButtonHoldSeconds() {
|
|
17294
|
+
return String(Math.round(ABORT_LONG_PRESS_MS / 1000));
|
|
17295
|
+
}
|
|
17296
|
+
|
|
17297
|
+
function abortButtonReadyTitle() {
|
|
17298
|
+
return `Hold Esc or the Abort button for ${abortButtonHoldSeconds()} seconds to abort the active Pi run`;
|
|
17299
|
+
}
|
|
17300
|
+
|
|
17301
|
+
function clearAbortLongPressResetTimer() {
|
|
17302
|
+
clearTimeout(abortLongPressResetTimer);
|
|
17303
|
+
abortLongPressResetTimer = null;
|
|
17304
|
+
}
|
|
17305
|
+
|
|
17306
|
+
function clearAbortLongPressCompletionTimers() {
|
|
16990
17307
|
clearTimeout(abortLongPressTimer);
|
|
17308
|
+
clearInterval(abortLongPressTickTimer);
|
|
16991
17309
|
abortLongPressTimer = null;
|
|
17310
|
+
abortLongPressTickTimer = null;
|
|
17311
|
+
}
|
|
17312
|
+
|
|
17313
|
+
function isAbortLongPressActive() {
|
|
17314
|
+
return abortLongPressStartedAt > 0;
|
|
17315
|
+
}
|
|
17316
|
+
|
|
17317
|
+
function abortLongPressRemainingMs() {
|
|
17318
|
+
if (!abortLongPressStartedAt || !abortLongPressDeadlineAt) return ABORT_LONG_PRESS_MS;
|
|
17319
|
+
return Math.max(0, abortLongPressDeadlineAt - performance.now());
|
|
17320
|
+
}
|
|
17321
|
+
|
|
17322
|
+
function formatAbortLongPressRemaining(ms) {
|
|
17323
|
+
return (Math.ceil(Math.max(0, ms) / 100) / 10).toFixed(1);
|
|
17324
|
+
}
|
|
17325
|
+
|
|
17326
|
+
function abortLongPressLabel() {
|
|
17327
|
+
const remaining = formatAbortLongPressRemaining(abortLongPressRemainingMs());
|
|
17328
|
+
return abortLongPressSource === "escape" ? `Hold Esc ${remaining}s` : `Hold ${remaining}s`;
|
|
17329
|
+
}
|
|
17330
|
+
|
|
17331
|
+
function renderAbortLongPressAffordance() {
|
|
17332
|
+
const label = abortLongPressLabel();
|
|
17333
|
+
elements.abortButton.textContent = label;
|
|
17334
|
+
elements.abortButton.title = `${label} more to abort the active Pi run`;
|
|
17335
|
+
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
17336
|
+
}
|
|
17337
|
+
|
|
17338
|
+
function completeAbortLongPress() {
|
|
17339
|
+
if (!isAbortLongPressActive()) return;
|
|
17340
|
+
if (abortLongPressReleasePending) return;
|
|
17341
|
+
const source = abortLongPressSource;
|
|
17342
|
+
clearAbortLongPressResetTimer();
|
|
17343
|
+
clearAbortLongPressCompletionTimers();
|
|
17344
|
+
abortLongPressHandled = true;
|
|
17345
|
+
if (isAbortAvailable()) abortActiveRun({ source });
|
|
17346
|
+
else {
|
|
17347
|
+
resetAbortLongPressAffordance();
|
|
17348
|
+
updateComposerModeButtons();
|
|
17349
|
+
}
|
|
17350
|
+
}
|
|
17351
|
+
|
|
17352
|
+
function tickAbortLongPressAffordance() {
|
|
17353
|
+
if (!isAbortLongPressActive()) return;
|
|
17354
|
+
renderAbortLongPressAffordance();
|
|
17355
|
+
if (abortLongPressRemainingMs() <= 0) completeAbortLongPress();
|
|
17356
|
+
}
|
|
17357
|
+
|
|
17358
|
+
function resumeAbortLongPressAffordance() {
|
|
17359
|
+
if (!isAbortLongPressActive()) return;
|
|
17360
|
+
clearAbortLongPressResetTimer();
|
|
17361
|
+
abortLongPressReleasePending = false;
|
|
17362
|
+
tickAbortLongPressAffordance();
|
|
17363
|
+
}
|
|
17364
|
+
|
|
17365
|
+
function scheduleAbortLongPressReleaseReset() {
|
|
17366
|
+
if (!isAbortLongPressActive()) return;
|
|
17367
|
+
abortLongPressReleasePending = true;
|
|
17368
|
+
clearAbortLongPressResetTimer();
|
|
17369
|
+
abortLongPressResetTimer = setTimeout(() => {
|
|
17370
|
+
abortLongPressResetTimer = null;
|
|
17371
|
+
if (!abortLongPressReleasePending) return;
|
|
17372
|
+
resetAbortLongPressAffordance();
|
|
17373
|
+
updateComposerModeButtons();
|
|
17374
|
+
}, ABORT_LONG_PRESS_RELEASE_GRACE_MS);
|
|
17375
|
+
}
|
|
17376
|
+
|
|
17377
|
+
function resetAbortLongPressAffordance() {
|
|
17378
|
+
clearAbortLongPressResetTimer();
|
|
17379
|
+
clearAbortLongPressCompletionTimers();
|
|
17380
|
+
abortLongPressStartedAt = 0;
|
|
17381
|
+
abortLongPressDeadlineAt = 0;
|
|
17382
|
+
abortLongPressSource = "long-press";
|
|
17383
|
+
abortLongPressReleasePending = false;
|
|
16992
17384
|
elements.abortButton.classList.remove("long-pressing");
|
|
16993
|
-
|
|
17385
|
+
elements.abortButton.style.removeProperty("--abort-long-press-duration");
|
|
17386
|
+
if (!abortRequestInFlight) {
|
|
17387
|
+
elements.abortButton.textContent = "Abort";
|
|
17388
|
+
elements.abortButton.title = isAbortAvailable() ? abortButtonReadyTitle() : "Abort is available while Pi is running";
|
|
17389
|
+
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
17390
|
+
}
|
|
16994
17391
|
}
|
|
16995
17392
|
|
|
16996
17393
|
async function abortActiveRun({ source = "button" } = {}) {
|
|
@@ -17026,31 +17423,41 @@ async function abortActiveRun({ source = "button" } = {}) {
|
|
|
17026
17423
|
}
|
|
17027
17424
|
}
|
|
17028
17425
|
|
|
17029
|
-
function startAbortLongPress(event) {
|
|
17030
|
-
if (!isAbortAvailable() || abortRequestInFlight) return;
|
|
17031
|
-
if (event
|
|
17426
|
+
function startAbortLongPress(event, { source = "long-press" } = {}) {
|
|
17427
|
+
if (!isAbortAvailable() || abortRequestInFlight) return false;
|
|
17428
|
+
if (source !== "escape" && event?.button !== undefined && event.button !== 0) return false;
|
|
17429
|
+
if (isAbortLongPressActive()) {
|
|
17430
|
+
resumeAbortLongPressAffordance();
|
|
17431
|
+
return true;
|
|
17432
|
+
}
|
|
17032
17433
|
resetAbortLongPressAffordance();
|
|
17033
17434
|
abortLongPressHandled = false;
|
|
17435
|
+
abortLongPressReleasePending = false;
|
|
17436
|
+
abortLongPressSource = source;
|
|
17437
|
+
abortLongPressStartedAt = performance.now();
|
|
17438
|
+
abortLongPressDeadlineAt = abortLongPressStartedAt + ABORT_LONG_PRESS_MS;
|
|
17439
|
+
elements.abortButton.style.setProperty("--abort-long-press-duration", `${ABORT_LONG_PRESS_MS}ms`);
|
|
17034
17440
|
elements.abortButton.classList.add("long-pressing");
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
abortActiveRun({ source: "long-press" });
|
|
17040
|
-
}, ABORT_LONG_PRESS_MS);
|
|
17441
|
+
renderAbortLongPressAffordance();
|
|
17442
|
+
abortLongPressTickTimer = setInterval(tickAbortLongPressAffordance, ABORT_LONG_PRESS_TICK_MS);
|
|
17443
|
+
abortLongPressTimer = setTimeout(tickAbortLongPressAffordance, ABORT_LONG_PRESS_MS + 10);
|
|
17444
|
+
return true;
|
|
17041
17445
|
}
|
|
17042
17446
|
|
|
17043
17447
|
elements.abortButton.addEventListener("pointerdown", startAbortLongPress);
|
|
17044
17448
|
for (const eventName of ["pointerup", "pointerleave", "pointercancel", "blur"]) {
|
|
17045
17449
|
elements.abortButton.addEventListener(eventName, resetAbortLongPressAffordance);
|
|
17046
17450
|
}
|
|
17451
|
+
elements.abortButton.addEventListener("keydown", (event) => {
|
|
17452
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
17453
|
+
if (startAbortLongPress(event)) event.preventDefault();
|
|
17454
|
+
});
|
|
17455
|
+
elements.abortButton.addEventListener("keyup", (event) => {
|
|
17456
|
+
if (event.key === " " || event.key === "Enter") resetAbortLongPressAffordance();
|
|
17457
|
+
});
|
|
17047
17458
|
elements.abortButton.addEventListener("click", (event) => {
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
abortLongPressHandled = false;
|
|
17051
|
-
return;
|
|
17052
|
-
}
|
|
17053
|
-
abortActiveRun({ source: "button" });
|
|
17459
|
+
event.preventDefault();
|
|
17460
|
+
if (abortLongPressHandled) abortLongPressHandled = false;
|
|
17054
17461
|
});
|
|
17055
17462
|
elements.newSessionButton.addEventListener("click", async () => {
|
|
17056
17463
|
setComposerActionsOpen(false);
|
|
@@ -17166,6 +17573,10 @@ elements.chat.addEventListener("scroll", () => {
|
|
|
17166
17573
|
markTabOutputSeen();
|
|
17167
17574
|
updateStickyUserPromptButton();
|
|
17168
17575
|
}, { passive: true });
|
|
17576
|
+
document.addEventListener("pointerdown", beginPointerActivation, { capture: true, passive: true });
|
|
17577
|
+
document.addEventListener("pointerup", finishPointerActivation, { capture: true, passive: true });
|
|
17578
|
+
document.addEventListener("pointercancel", cancelPointerActivation, { capture: true, passive: true });
|
|
17579
|
+
window.addEventListener("blur", cancelPointerActivation, { passive: true });
|
|
17169
17580
|
document.addEventListener("pointerdown", (event) => {
|
|
17170
17581
|
if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
|
|
17171
17582
|
clearOpenTerminalTabGroup(openTerminalTabGroupKey);
|
|
@@ -17386,12 +17797,14 @@ window.addEventListener("keydown", (event) => {
|
|
|
17386
17797
|
window.addEventListener("keydown", handleNativeAppShortcut, { capture: true });
|
|
17387
17798
|
document.addEventListener("visibilitychange", () => {
|
|
17388
17799
|
if (document.visibilityState === "visible") scheduleForegroundReconcile("visibility resume", 0);
|
|
17800
|
+
else resetAbortLongPressAffordance();
|
|
17389
17801
|
});
|
|
17390
17802
|
window.addEventListener("pageshow", () => scheduleForegroundReconcile("page show", 0));
|
|
17391
17803
|
window.addEventListener("focus", () => scheduleForegroundReconcile("window focus"));
|
|
17392
17804
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
17393
17805
|
window.addEventListener("keydown", (event) => {
|
|
17394
17806
|
if (event.key !== "Escape") return;
|
|
17807
|
+
if (event.defaultPrevented) return;
|
|
17395
17808
|
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open) return;
|
|
17396
17809
|
if (publishMenuOpen) {
|
|
17397
17810
|
setPublishMenuOpen(false);
|
|
@@ -17437,6 +17850,20 @@ window.addEventListener("keydown", (event) => {
|
|
|
17437
17850
|
hideCommandSuggestions();
|
|
17438
17851
|
return;
|
|
17439
17852
|
}
|
|
17853
|
+
if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
17854
|
+
setSidePanelCollapsed(true);
|
|
17855
|
+
return;
|
|
17856
|
+
}
|
|
17857
|
+
if (isAbortAvailable()) {
|
|
17858
|
+
event.preventDefault();
|
|
17859
|
+
if (abortLongPressSource === "escape" && isAbortLongPressActive()) resumeAbortLongPressAffordance();
|
|
17860
|
+
else if (!event.repeat) startAbortLongPress(event, { source: "escape" });
|
|
17861
|
+
return;
|
|
17862
|
+
}
|
|
17863
|
+
if (event.repeat) {
|
|
17864
|
+
event.preventDefault();
|
|
17865
|
+
return;
|
|
17866
|
+
}
|
|
17440
17867
|
if (document.activeElement === elements.promptInput && !elements.promptInput.value.trim() && doubleEscapeAction !== "none") {
|
|
17441
17868
|
const now = Date.now();
|
|
17442
17869
|
if (now - lastEmptyPromptEscapeTime < 500) {
|
|
@@ -17447,14 +17874,13 @@ window.addEventListener("keydown", (event) => {
|
|
|
17447
17874
|
}
|
|
17448
17875
|
lastEmptyPromptEscapeTime = now;
|
|
17449
17876
|
}
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
}
|
|
17877
|
+
});
|
|
17878
|
+
window.addEventListener("keyup", (event) => {
|
|
17879
|
+
if (event.key === "Escape" && abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
|
|
17880
|
+
}, { capture: true });
|
|
17881
|
+
window.addEventListener("blur", () => {
|
|
17882
|
+
if (abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
|
|
17883
|
+
else resetAbortLongPressAffordance();
|
|
17458
17884
|
});
|
|
17459
17885
|
|
|
17460
17886
|
elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);
|