@firstpick/pi-package-webui 0.4.1 → 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/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"),
@@ -127,6 +128,8 @@ const elements = {
127
128
  backgroundClearButton: $("#backgroundClearButton"),
128
129
  backgroundStatus: $("#backgroundStatus"),
129
130
  networkStatus: $("#networkStatus"),
131
+ remoteAuthToggle: $("#remoteAuthToggle"),
132
+ remoteAuthStatus: $("#remoteAuthStatus"),
130
133
  openNetworkButton: $("#openNetworkButton"),
131
134
  serverActionSelect: $("#serverActionSelect"),
132
135
  runServerActionButton: $("#runServerActionButton"),
@@ -195,6 +198,7 @@ const elements = {
195
198
  commandPaletteInput: $("#commandPaletteInput"),
196
199
  commandPaletteList: $("#commandPaletteList"),
197
200
  commandPaletteHint: $("#commandPaletteHint"),
201
+ commandPaletteCloseButton: $("#commandPaletteCloseButton"),
198
202
  editRetryDialog: $("#editRetryDialog"),
199
203
  editRetryMessage: $("#editRetryMessage"),
200
204
  editRetryText: $("#editRetryText"),
@@ -314,6 +318,8 @@ let updateStatusRefreshTimer = null;
314
318
  let updateNotificationHideTimer = null;
315
319
  let backendOfflineNoticeShown = false;
316
320
  let latestMessages = [];
321
+ let latestMessagesSessionKey = "";
322
+ const tabMessagesCache = new Map();
317
323
  let promptHistoryByTab = new Map();
318
324
  let promptHistoryNavigation = null;
319
325
  let transientMessages = [];
@@ -338,6 +344,8 @@ let toolOutputGloballyExpanded = false;
338
344
  let agentDoneNotificationPermissionRequested = false;
339
345
  let agentDoneNotificationFallbackNoted = false;
340
346
  let agentDoneNotificationKeys = new Set();
347
+ let pendingAgentDoneNotificationTimers = new Map();
348
+ let autoRetryingTabs = new Set();
341
349
  let availableModels = [];
342
350
  let availableThemes = [];
343
351
  let currentThemeName = "catppuccin-mocha";
@@ -372,7 +380,17 @@ let workspaceDashboardCollapsed = false;
372
380
  let commandPaletteIndex = 0;
373
381
  let commandPaletteItems = [];
374
382
  let activeEditRetry = null;
383
+ let activePointerActivation = null;
384
+ let pointerActivationTimeout = null;
385
+ let deferredChatFollowScroll = false;
386
+ const deferredUiRenderCallbacks = new Map();
375
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;
376
394
  let abortLongPressHandled = false;
377
395
  const dialogQueue = [];
378
396
  const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
@@ -410,6 +428,8 @@ const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
410
428
  const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
411
429
  const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
412
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;
413
433
  const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
414
434
  const ATTACHMENT_MAX_FILES = 12;
415
435
  const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
@@ -442,7 +462,9 @@ const UPDATE_STATUS_INITIAL_DELAY_MS = 1800;
442
462
  const RUN_INDICATOR_TICK_MS = 1000;
443
463
  const RUN_INDICATOR_START_GRACE_MS = 2500;
444
464
  const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
445
- const ABORT_LONG_PRESS_MS = 700;
465
+ const ABORT_LONG_PRESS_MS = 3000;
466
+ const ABORT_LONG_PRESS_TICK_MS = 100;
467
+ const ABORT_LONG_PRESS_RELEASE_GRACE_MS = 350;
446
468
  const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
447
469
  const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
448
470
  const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
@@ -452,6 +474,7 @@ const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+
452
474
  const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
453
475
  const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
454
476
  const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
477
+ const AGENT_DONE_NOTIFICATION_RETRY_GRACE_MS = 1200;
455
478
  const FOREGROUND_RECONCILE_DELAY_MS = 120;
456
479
  const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "working", "idle"];
457
480
  const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
@@ -482,6 +505,7 @@ const optionalFeatureAvailability = {
482
505
  tuiSkillsCommand: false,
483
506
  todoProgressWidget: false,
484
507
  tuiToolsCommand: false,
508
+ remoteWebui: false,
485
509
  themeBundle: false,
486
510
  };
487
511
  const OPTIONAL_FEATURES = [
@@ -534,6 +558,13 @@ const OPTIONAL_FEATURES = [
534
558
  capabilityLabel: "RPC /tools from tools extension",
535
559
  description: "Terminal-native active-tool manager alongside WebUI-native /tools toggles.",
536
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
+ },
537
568
  {
538
569
  id: "gitFooterStatus",
539
570
  label: "Git footer status",
@@ -566,6 +597,7 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
566
597
  ["safety-guard", "safetyGuard"],
567
598
  ["skills", "tuiSkillsCommand"],
568
599
  ["tools", "tuiToolsCommand"],
600
+ ["remote", "remoteWebui"],
569
601
  ["stats", "statsCommand"],
570
602
  ["git-footer-refresh", "gitFooterStatus"],
571
603
  ["todo-progress-status", "todoProgressWidget"],
@@ -824,6 +856,82 @@ function delay(ms) {
824
856
  return new Promise((resolve) => setTimeout(resolve, ms));
825
857
  }
826
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
+
827
935
  function isMobileView() {
828
936
  return mobileViewMedia?.matches || false;
829
937
  }
@@ -832,6 +940,36 @@ function isSidePanelOverlayView() {
832
940
  return sidePanelOverlayMedia?.matches || false;
833
941
  }
834
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
+
835
973
  function readStoredSidePanelCollapsed() {
836
974
  try {
837
975
  const stored = localStorage.getItem(SIDE_PANEL_STORAGE_KEY);
@@ -1590,6 +1728,7 @@ function setComposerActionsOpen(open) {
1590
1728
  setOptionsMenuOpen(false);
1591
1729
  setBusyPromptBehaviorMenuOpen(false);
1592
1730
  }
1731
+ scheduleMobileDropdownScrollBoundsUpdate();
1593
1732
  }
1594
1733
 
1595
1734
  function isUserBashActive(tabId = activeTabId) {
@@ -1641,11 +1780,17 @@ function updateComposerModeButtons() {
1641
1780
  button.hidden = !runActive;
1642
1781
  button.disabled = !runActive;
1643
1782
  }
1644
- elements.abortButton.hidden = !abortAvailable;
1645
- elements.abortButton.disabled = !abortAvailable || abortRequestInFlight;
1646
- elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
1647
- elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
1648
- elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
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
+ }
1649
1794
  renderBusyPromptBehaviorTag();
1650
1795
  document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
1651
1796
  }
@@ -1849,6 +1994,7 @@ function updateVisualViewportVars() {
1849
1994
  syncMobileChatToBottomForInput();
1850
1995
  }
1851
1996
  updateFooterModelPickerPosition();
1997
+ updateMobileDropdownScrollBounds();
1852
1998
  }
1853
1999
 
1854
2000
  function installViewportHandlers() {
@@ -2267,6 +2413,10 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
2267
2413
  setBackendOffline(false);
2268
2414
  const data = await response.json().catch(() => ({}));
2269
2415
  if (!response.ok) {
2416
+ if (response.status === 401 && data.remoteAuthRequired) {
2417
+ const returnPath = `${window.location.pathname}${window.location.search || ""}` || "/";
2418
+ window.location.assign(`/remote-auth?return=${encodeURIComponent(returnPath)}`);
2419
+ }
2270
2420
  const error = new Error(data.error || data.message || JSON.stringify(data));
2271
2421
  error.statusCode = response.status;
2272
2422
  error.data = data;
@@ -3720,11 +3870,17 @@ function syncTabMetadata(nextTabs = []) {
3720
3870
  if (!liveIds.has(tabId)) {
3721
3871
  tabActivities.delete(tabId);
3722
3872
  tabSeenCompletionSerials.delete(tabId);
3873
+ autoRetryingTabs.delete(tabId);
3874
+ suppressPendingAgentDoneNotificationsForTab(tabId, { markSeen: false });
3723
3875
  actionFeedbackByTab.delete(tabId);
3724
3876
  skillUsageByTab.delete(tabId);
3877
+ tabMessagesCache.delete(tabId);
3725
3878
  clearGitWorkflowForTab(tabId);
3726
3879
  }
3727
3880
  }
3881
+ for (const tabId of tabMessagesCache.keys()) {
3882
+ if (!liveIds.has(tabId)) tabMessagesCache.delete(tabId);
3883
+ }
3728
3884
  pruneSkillUsageForKnownTabs(liveIds);
3729
3885
  }
3730
3886
 
@@ -3888,6 +4044,18 @@ function ingestEventTabActivity(event) {
3888
4044
  if (changed) renderTabs();
3889
4045
  }
3890
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
+
3891
4059
  function rememberActiveTab() {
3892
4060
  try {
3893
4061
  if (activeTabId) localStorage.setItem(TAB_STORAGE_KEY, activeTabId);
@@ -3979,6 +4147,7 @@ function resetActiveTabUi() {
3979
4147
  latestStatsOverlayPayload = null;
3980
4148
  latestWorkspace = null;
3981
4149
  latestMessages = [];
4150
+ latestMessagesSessionKey = "";
3982
4151
  clearRunIndicatorActivity({ render: false });
3983
4152
  statusEntries.clear();
3984
4153
  widgets.clear();
@@ -4010,8 +4179,10 @@ function resetActiveTabUi() {
4010
4179
  renderAppRunnerControls();
4011
4180
  renderWidgets();
4012
4181
  renderGitWorkflow();
4013
- renderFooter();
4014
- renderFeedbackTray();
4182
+ if (!restoreCachedMessagesForActiveTab()) {
4183
+ renderFooter();
4184
+ renderFeedbackTray();
4185
+ }
4015
4186
  }
4016
4187
 
4017
4188
  function tabGroupStatusRank(state) {
@@ -4250,6 +4421,7 @@ function moveNewTabMenuFocus(delta) {
4250
4421
  }
4251
4422
 
4252
4423
  function renderTabs() {
4424
+ if (deferUiRenderDuringPointerActivation("tabs", renderTabs)) return;
4253
4425
  const active = activeTab();
4254
4426
  const activeIndicator = active ? tabIndicator(active) : null;
4255
4427
  elements.terminalTabsToggleButton.textContent = active ? `${activeIndicator.glyph} ${active.title}${tabs.length > 1 ? ` · ${tabs.length}` : ""}` : "Tabs";
@@ -4274,7 +4446,7 @@ function renderTabs() {
4274
4446
  updateDocumentTitle();
4275
4447
  renderWorkspaceDashboard();
4276
4448
  renderContextMeter();
4277
- if (elements.commandPaletteDialog?.open) renderCommandPalette();
4449
+ if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
4278
4450
  syncTabPolling();
4279
4451
  }
4280
4452
 
@@ -4305,6 +4477,7 @@ async function switchTab(tabId) {
4305
4477
  footerBranchPickerOpen = false;
4306
4478
  footerBranchPickerRequestSerial += 1;
4307
4479
  saveActiveDraft();
4480
+ cacheMessagesForTab(activeTabId);
4308
4481
  const tabContext = setActiveTabId(tabId, { remember: true });
4309
4482
  resetActiveTabUi();
4310
4483
  renderTabs();
@@ -4417,6 +4590,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
4417
4590
  clearAttachments(id);
4418
4591
  clearGitWorkflowForTab(id);
4419
4592
  appRunnerDataByTab.delete(id);
4593
+ tabMessagesCache.delete(id);
4420
4594
  }
4421
4595
  clearOpenTerminalTabGroup(null, { force: true });
4422
4596
 
@@ -4683,6 +4857,36 @@ function agentDoneNotificationKey(tabId, activity = {}) {
4683
4857
  return `${tabId}:${Number.isFinite(serial) && serial > 0 ? serial : "done"}`;
4684
4858
  }
4685
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
+
4686
4890
  function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
4687
4891
  if (!agentDoneNotificationsEnabled) return;
4688
4892
  const tabId = typeof tabOrId === "string" ? tabOrId : tabOrId?.id || activeTabId;
@@ -4693,9 +4897,11 @@ function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
4693
4897
  const key = agentDoneNotificationKey(tabId, normalizedActivity);
4694
4898
  if (agentDoneNotificationKeys.has(key)) return;
4695
4899
  agentDoneNotificationKeys.add(key);
4900
+ if (isAutoRetryingTab(tabId)) return;
4696
4901
 
4697
4902
  const displayTitle = tabTitle || tab?.title || "terminal";
4698
- showAgentDoneBrowserNotification({
4903
+ queueAgentDoneBrowserNotification({
4904
+ key,
4699
4905
  tabId,
4700
4906
  title: "Pi finished work",
4701
4907
  body: `${displayTitle} finished its agent run.`,
@@ -6177,6 +6383,7 @@ async function requestManualCompaction({ triggerButton = null } = {}) {
6177
6383
  }
6178
6384
 
6179
6385
  function renderContextMeter() {
6386
+ if (deferUiRenderDuringPointerActivation("context-meter", renderContextMeter)) return;
6180
6387
  const root = elements.contextMeterBar;
6181
6388
  if (!root) return;
6182
6389
  const tab = activeTab();
@@ -6233,6 +6440,7 @@ function dashboardAction(label, handler, className = "") {
6233
6440
  }
6234
6441
 
6235
6442
  function renderWorkspaceDashboard() {
6443
+ if (deferUiRenderDuringPointerActivation("workspace-dashboard", renderWorkspaceDashboard)) return;
6236
6444
  const root = elements.workspaceDashboard;
6237
6445
  if (!root) return;
6238
6446
  const tab = activeTab();
@@ -7031,6 +7239,7 @@ async function changeActiveTabCwd() {
7031
7239
  }
7032
7240
 
7033
7241
  function renderFooter() {
7242
+ if (deferUiRenderDuringPointerActivation("footer", renderFooter)) return;
7034
7243
  const gitFooterPayload = parseGitFooterWebuiPayload();
7035
7244
  if (gitFooterPayload) {
7036
7245
  renderGitFooterPayload(footerPayloadWithLiveModel(gitFooterPayload));
@@ -7263,6 +7472,7 @@ function initializeCodexUsage() {
7263
7472
  }
7264
7473
 
7265
7474
  function renderStatus() {
7475
+ if (deferUiRenderDuringPointerActivation("status", renderStatus)) return;
7266
7476
  const state = currentState;
7267
7477
  updateComposerModeButtons();
7268
7478
  const running = state?.isStreaming ? "running" : "idle";
@@ -8704,7 +8914,22 @@ function handleStatsWebuiStatus(statusText) {
8704
8914
  if (payload.open || elements.statsOverlayDialog?.open) renderStatsOverlay();
8705
8915
  }
8706
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
+
8707
8931
  function renderWidgets() {
8932
+ if (deferUiRenderDuringPointerActivation("widgets", renderWidgets)) return;
8708
8933
  elements.widgetArea.replaceChildren();
8709
8934
  const releaseOutput = renderReleaseNpmOutputWidget();
8710
8935
  if (releaseOutput) elements.widgetArea.append(releaseOutput);
@@ -8720,8 +8945,9 @@ function renderWidgets() {
8720
8945
  for (const [key, value] of widgets) {
8721
8946
  const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
8722
8947
  if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
8723
- if (widgetFeatureId && key !== "todo-progress") continue;
8948
+ if (widgetFeatureId && optionalFeatureWidgetHasSpecializedRenderer(key)) continue;
8724
8949
  const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
8950
+ if (key === "pi-remote-webui") continue;
8725
8951
  const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
8726
8952
  if (specialized) {
8727
8953
  elements.widgetArea.append(specialized);
@@ -10324,6 +10550,7 @@ function renderQueueGroup(label, items, tone) {
10324
10550
  }
10325
10551
 
10326
10552
  function renderQueue(event) {
10553
+ if (deferUiRenderDuringPointerActivation("queue", () => renderQueue(event))) return;
10327
10554
  const snapshot = normalizeQueuedMessages(event);
10328
10555
  const tabId = event?.tabId || activeTabId;
10329
10556
  if (tabId) latestQueuedMessagesByTab.set(tabId, snapshot);
@@ -11275,6 +11502,7 @@ function renderActionFeedbackControls(bubble, message, messageIndex) {
11275
11502
  }
11276
11503
 
11277
11504
  function renderFeedbackTray() {
11505
+ if (deferUiRenderDuringPointerActivation("feedback-tray", renderFeedbackTray)) return;
11278
11506
  const items = queuedActionFeedback();
11279
11507
  const hasItems = items.length > 0;
11280
11508
  elements.feedbackTray.hidden = !hasItems;
@@ -11747,6 +11975,7 @@ function findStickyUserPromptTarget(targets = userPromptTargets()) {
11747
11975
  }
11748
11976
 
11749
11977
  function updateStickyUserPromptButton() {
11978
+ if (deferUiRenderDuringPointerActivation("sticky-user-prompt", updateStickyUserPromptButton)) return;
11750
11979
  const button = elements.stickyUserPromptButton;
11751
11980
  if (!button) return;
11752
11981
  const targets = userPromptTargets();
@@ -12896,6 +13125,7 @@ function pruneDisconnectedLiveToolCards() {
12896
13125
  }
12897
13126
 
12898
13127
  function renderAllMessages({ preserveScroll = false, forceRebuild = false } = {}) {
13128
+ if (deferUiRenderDuringPointerActivation("messages", () => renderAllMessages({ preserveScroll, forceRebuild }))) return;
12899
13129
  const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
12900
13130
  const previousScrollTop = elements.chat.scrollTop;
12901
13131
  const transcriptItems = orderedTranscriptItems();
@@ -13067,6 +13297,7 @@ function scheduleChatFollowScroll() {
13067
13297
  }
13068
13298
 
13069
13299
  function scrollChatToBottom({ force = false } = {}) {
13300
+ if (deferChatFollowScrollDuringPointerActivation({ force })) return;
13070
13301
  if (force) autoFollowChat = true;
13071
13302
  if (!autoFollowChat) {
13072
13303
  updateJumpToLatestButton();
@@ -13127,6 +13358,7 @@ function setPublishMenuOpen(open) {
13127
13358
  elements.publishButton.setAttribute("aria-expanded", publishMenuOpen ? "true" : "false");
13128
13359
  elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
13129
13360
  elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
13361
+ scheduleMobileDropdownScrollBoundsUpdate();
13130
13362
  }
13131
13363
 
13132
13364
  function setNativeCommandMenuOpen(open) {
@@ -13134,6 +13366,7 @@ function setNativeCommandMenuOpen(open) {
13134
13366
  elements.nativeCommandMenuButton.setAttribute("aria-expanded", nativeCommandMenuOpen ? "true" : "false");
13135
13367
  elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
13136
13368
  elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
13369
+ scheduleMobileDropdownScrollBoundsUpdate();
13137
13370
  }
13138
13371
 
13139
13372
  function setAppRunnerMenuOpen(open) {
@@ -13141,6 +13374,7 @@ function setAppRunnerMenuOpen(open) {
13141
13374
  elements.appRunnerMenuButton?.setAttribute("aria-expanded", appRunnerMenuOpen ? "true" : "false");
13142
13375
  elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
13143
13376
  elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
13377
+ scheduleMobileDropdownScrollBoundsUpdate();
13144
13378
  }
13145
13379
 
13146
13380
  function setOptionsMenuOpen(open) {
@@ -13148,6 +13382,7 @@ function setOptionsMenuOpen(open) {
13148
13382
  elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
13149
13383
  elements.optionsMenuButton.classList.toggle("menu-open", optionsMenuOpen);
13150
13384
  elements.optionsMenuButton.parentElement?.classList.toggle("open", optionsMenuOpen);
13385
+ scheduleMobileDropdownScrollBoundsUpdate();
13151
13386
  }
13152
13387
 
13153
13388
  function optionalFeatureIdForCommand(name) {
@@ -13312,6 +13547,7 @@ function updateOptionalFeatureAvailability() {
13312
13547
  optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
13313
13548
  optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
13314
13549
  optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
13550
+ optionalFeatureAvailability.remoteWebui = hasAvailableCommand("remote") || optionalFeatureAvailability.remoteWebui || statusEntries.has("pi-remote-webui") || widgets.has("pi-remote-webui");
13315
13551
  optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
13316
13552
  requestGitFooterWebuiPayload();
13317
13553
  renderOptionalFeatureControls();
@@ -13336,9 +13572,14 @@ function optionalFeatureWidgetFeatureId(key) {
13336
13572
  if (key.startsWith("release-npm:")) return "releaseNpm";
13337
13573
  if (key.startsWith("release-aur:")) return "releaseAur";
13338
13574
  if (key === "todo-progress") return "todoProgressWidget";
13575
+ if (key === "pi-remote-webui") return "remoteWebui";
13339
13576
  return null;
13340
13577
  }
13341
13578
 
13579
+ function optionalFeatureWidgetHasSpecializedRenderer(key) {
13580
+ return key.startsWith("release-npm:") || key.startsWith("release-aur:");
13581
+ }
13582
+
13342
13583
  function renderOptionalFeaturePanel() {
13343
13584
  if (!elements.optionalFeaturesBox) return;
13344
13585
  elements.optionalFeaturesBox.replaceChildren();
@@ -13443,6 +13684,16 @@ function renderOptionalFeatureControls() {
13443
13684
  );
13444
13685
  }
13445
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
+
13446
13697
  renderOptionalFeaturePanel();
13447
13698
  }
13448
13699
 
@@ -14745,6 +14996,10 @@ async function refreshState(tabContext = activeTabContext()) {
14745
14996
  if (!isCurrentTabContext(tabContext)) return;
14746
14997
  const previousState = currentState;
14747
14998
  currentState = response.data || null;
14999
+ if (latestMessages.length) {
15000
+ latestMessagesSessionKey = resolveMessagesSessionKey(tabContext.tabId);
15001
+ cacheMessagesForTab(tabContext.tabId, latestMessages, latestMessagesSessionKey);
15002
+ }
14748
15003
  const shouldRefreshGitFooter = gitFooterRelevantStateChanged(previousState, currentState);
14749
15004
  syncActiveTabActivityFromState(currentState);
14750
15005
  syncRunIndicatorFromState(currentState);
@@ -14838,7 +15093,22 @@ function renderNetworkStatus() {
14838
15093
  if (networkUrls.length === 0) list.append(make("div", "network-status-empty", "No LAN address detected."));
14839
15094
  }
14840
15095
 
14841
- elements.networkStatus.replaceChildren(heading, detail, list);
15096
+ const auth = network?.auth || {};
15097
+ const authText = auth.enabled
15098
+ ? auth.pin
15099
+ ? `Remote PIN auth on · PIN ${auth.pin}`
15100
+ : "Remote PIN auth on"
15101
+ : "Remote PIN auth off";
15102
+ const authDetail = make("div", "network-status-detail", authText);
15103
+
15104
+ elements.networkStatus.replaceChildren(heading, detail, list, authDetail);
15105
+ elements.remoteAuthToggle.checked = !!auth.enabled;
15106
+ elements.remoteAuthToggle.disabled = rebinding;
15107
+ elements.remoteAuthStatus.textContent = auth.enabled
15108
+ ? auth.pin
15109
+ ? `PIN ${auth.pin}`
15110
+ : "On"
15111
+ : "Off";
14842
15112
  elements.openNetworkButton.disabled = rebinding;
14843
15113
  elements.openNetworkButton.textContent = opening ? "Opening…" : closing ? "Closing…" : open ? "Close for network" : "Open to network";
14844
15114
  }
@@ -14854,6 +15124,28 @@ async function refreshNetworkStatus() {
14854
15124
  renderNetworkStatus();
14855
15125
  }
14856
15126
 
15127
+ async function toggleRemoteAuth() {
15128
+ const enable = !latestNetwork?.auth?.enabled;
15129
+ const message = enable
15130
+ ? "Enable remote PIN authentication?\n\nA random 4-digit PIN will be required for non-local browser clients. The PIN is shown in Controls."
15131
+ : "Disable remote PIN authentication?\n\nNon-local browser clients will no longer need a PIN while the network listener is open.";
15132
+ if (!confirm(message)) {
15133
+ renderNetworkStatus();
15134
+ return;
15135
+ }
15136
+
15137
+ elements.remoteAuthToggle.disabled = true;
15138
+ try {
15139
+ const response = await api("/api/remote-auth/settings", { method: "POST", body: { enabled: enable }, scoped: false });
15140
+ latestNetwork = response.data?.network || { ...(latestNetwork || {}), auth: response.data?.auth };
15141
+ addEvent(enable ? "remote PIN auth enabled" : "remote PIN auth disabled", enable ? "warn" : "info");
15142
+ } catch (error) {
15143
+ addEvent(error.message || String(error), "error");
15144
+ } finally {
15145
+ renderNetworkStatus();
15146
+ }
15147
+ }
15148
+
14857
15149
  async function refreshFooterData(tabContext = activeTabContext()) {
14858
15150
  if (!tabContext.tabId) return;
14859
15151
  await Promise.allSettled([refreshStats(tabContext), refreshWorkspace(tabContext)]);
@@ -14861,7 +15153,32 @@ async function refreshFooterData(tabContext = activeTabContext()) {
14861
15153
 
14862
15154
  // Session key of the last applied transcript fetch; deltas are only
14863
15155
  // attempted while the tab+session is unchanged.
14864
- let latestMessagesSessionKey = "";
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
+ }
14865
15182
 
14866
15183
  function messagesLookEqual(a, b) {
14867
15184
  return !!a && !!b && a.role === b.role && String(a.timestamp || "") === String(b.timestamp || "")
@@ -14890,7 +15207,7 @@ function mergeMessagesDelta(previous, data) {
14890
15207
  async function refreshMessages(tabContext = activeTabContext()) {
14891
15208
  if (!tabContext.tabId) return;
14892
15209
  const previousMessages = latestMessages;
14893
- const sessionKey = `${tabContext.tabId}|${currentState?.sessionId || ""}`;
15210
+ const sessionKey = resolveMessagesSessionKey(tabContext.tabId);
14894
15211
  let nextMessages = null;
14895
15212
  if (previousMessages.length > 1 && sessionKey === latestMessagesSessionKey) {
14896
15213
  // Delta fetch with a one-message overlap: the last known message is
@@ -14906,6 +15223,7 @@ async function refreshMessages(tabContext = activeTabContext()) {
14906
15223
  }
14907
15224
  latestMessages = nextMessages;
14908
15225
  latestMessagesSessionKey = sessionKey;
15226
+ cacheMessagesForTab(tabContext.tabId, latestMessages, latestMessagesSessionKey);
14909
15227
  const preserveLiveStream = liveStreamRenderActive();
14910
15228
  if (!preserveLiveStream) resetStreamBubble();
14911
15229
  renderMessages(latestMessages);
@@ -14946,7 +15264,7 @@ async function refreshModels(tabContext = activeTabContext()) {
14946
15264
  syncModelSelectToState();
14947
15265
  renderFooter();
14948
15266
  renderFeedbackTray();
14949
- if (elements.commandPaletteDialog?.open) renderCommandPalette();
15267
+ if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
14950
15268
  }
14951
15269
 
14952
15270
  function syncModelSelectToState() {
@@ -15423,7 +15741,7 @@ async function refreshCommands(tabContext = activeTabContext()) {
15423
15741
  availableCommands = normalizeCommands(response.data?.commands || []);
15424
15742
  updateOptionalFeatureAvailability();
15425
15743
  renderCommands();
15426
- if (elements.commandPaletteDialog?.open) renderCommandPalette();
15744
+ if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
15427
15745
  }
15428
15746
 
15429
15747
  function paletteText(value) {
@@ -15520,12 +15838,14 @@ function setCommandPaletteIndex(index) {
15520
15838
  renderCommandPaletteList();
15521
15839
  }
15522
15840
 
15523
- function renderCommandPaletteList() {
15841
+ function renderCommandPaletteList({ preserveScroll = false } = {}) {
15524
15842
  const list = elements.commandPaletteList;
15525
15843
  if (!list) return;
15844
+ const scrollTop = preserveScroll ? list.scrollTop : 0;
15526
15845
  list.replaceChildren();
15527
15846
  if (!commandPaletteItems.length) {
15528
15847
  list.append(make("div", "command-palette-empty muted", "No matching actions."));
15848
+ if (preserveScroll) list.scrollTop = scrollTop;
15529
15849
  return;
15530
15850
  }
15531
15851
  commandPaletteItems.forEach((item, index) => {
@@ -15541,14 +15861,18 @@ function renderCommandPaletteList() {
15541
15861
  );
15542
15862
  list.append(button);
15543
15863
  });
15864
+ if (preserveScroll) {
15865
+ list.scrollTop = scrollTop;
15866
+ return;
15867
+ }
15544
15868
  const active = list.children[commandPaletteIndex];
15545
15869
  active?.scrollIntoView({ block: "nearest" });
15546
15870
  }
15547
15871
 
15548
- function renderCommandPalette() {
15872
+ function renderCommandPalette({ preserveScroll = false } = {}) {
15549
15873
  commandPaletteItems = filteredCommandPaletteItems();
15550
15874
  if (commandPaletteIndex >= commandPaletteItems.length) commandPaletteIndex = 0;
15551
- renderCommandPaletteList();
15875
+ renderCommandPaletteList({ preserveScroll });
15552
15876
  }
15553
15877
 
15554
15878
  function openCommandPalette(initialQuery = "") {
@@ -15636,7 +15960,7 @@ async function openToNetwork() {
15636
15960
  await closeNetworkAccess();
15637
15961
  return;
15638
15962
  }
15639
- if (!confirm("Open Pi Web UI to your local network?\n\nThe Web UI has no authentication and can control Pi/tools. Only do this on a trusted LAN.")) return;
15963
+ if (!confirm(`Open Pi Web UI to your local network?\n\nRemote PIN auth is ${latestNetwork?.auth?.enabled ? "ON" : "OFF"}. The Web UI can control Pi/tools, so only do this on a trusted LAN.`)) return;
15640
15964
 
15641
15965
  elements.openNetworkButton.disabled = true;
15642
15966
  elements.openNetworkButton.textContent = "Opening…";
@@ -16124,7 +16448,7 @@ function hasQueuedDialogRequest(id) {
16124
16448
 
16125
16449
  function removeQueuedDialogRequests(ids = []) {
16126
16450
  const idSet = new Set(ids.map((id) => String(id)).filter(Boolean));
16127
- if (idSet.size === 0) return;
16451
+ if (idSet.size === 0) return false;
16128
16452
  for (let i = dialogQueue.length - 1; i >= 0; i -= 1) {
16129
16453
  if (idSet.has(String(dialogQueue[i]?.id || ""))) dialogQueue.splice(i, 1);
16130
16454
  }
@@ -16132,7 +16456,9 @@ function removeQueuedDialogRequests(ids = []) {
16132
16456
  if (elements.dialog.open) elements.dialog.close();
16133
16457
  activeDialog = null;
16134
16458
  showNextDialog();
16459
+ return true;
16135
16460
  }
16461
+ return false;
16136
16462
  }
16137
16463
 
16138
16464
  function handleExtensionUiRequest(request) {
@@ -16158,12 +16484,20 @@ function handleExtensionUiRequest(request) {
16158
16484
  renderStatus();
16159
16485
  return;
16160
16486
  }
16161
- case "setWidget":
16162
- if (Array.isArray(request.widgetLines)) widgets.set(request.widgetKey || request.id, request);
16163
- else widgets.delete(request.widgetKey || request.id);
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
+ }
16164
16497
  updateOptionalFeatureAvailability();
16165
16498
  renderWidgets();
16166
16499
  return;
16500
+ }
16167
16501
  case "setTitle":
16168
16502
  if (request.title) document.title = request.title;
16169
16503
  return;
@@ -16196,6 +16530,7 @@ function handleExtensionUiRequest(request) {
16196
16530
  async function sendDialogResponse(payload) {
16197
16531
  const { tabId = activeTabId, ...body } = payload;
16198
16532
  const tabContext = activeTabContext(tabId);
16533
+ const responseId = String(body.id || "");
16199
16534
  try {
16200
16535
  const response = await api("/api/extension-ui-response", { method: "POST", body, tabId });
16201
16536
  if (!applyResponseTab(response) && decrementTabPendingBlockerCount(tabId)) renderTabs();
@@ -16203,6 +16538,7 @@ async function sendDialogResponse(payload) {
16203
16538
  if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
16204
16539
  } finally {
16205
16540
  if (!isCurrentTabContext(tabContext)) return;
16541
+ if (responseId && activeDialog && String(activeDialog.id || "") !== responseId) return;
16206
16542
  if (elements.dialog.open) elements.dialog.close();
16207
16543
  activeDialog = null;
16208
16544
  if (runIndicatorIsActive()) setRunIndicatorActivity("Continuing after your response…");
@@ -16293,6 +16629,7 @@ function handleInactiveTabEvent(event) {
16293
16629
 
16294
16630
  function handleEvent(event) {
16295
16631
  ingestEventTabActivity(event);
16632
+ trackAutoRetryStateFromEvent(event);
16296
16633
  trackSkillsFromEvent(event);
16297
16634
  if (!eventTargetsActiveTab(event)) {
16298
16635
  handleInactiveTabEvent(event);
@@ -16347,6 +16684,14 @@ function handleEvent(event) {
16347
16684
  removeQueuedDialogRequests(event.ids || []);
16348
16685
  addEvent(`cancelled ${event.ids?.length || 0} pending extension UI request(s)`, "warn");
16349
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
+ }
16350
16695
  case "webui_app_runner_update":
16351
16696
  setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
16352
16697
  renderAppRunnerControls();
@@ -16374,6 +16719,11 @@ function handleEvent(event) {
16374
16719
  renderNetworkStatus();
16375
16720
  break;
16376
16721
  }
16722
+ case "webui_remote_auth_changed":
16723
+ latestNetwork = { ...(latestNetwork || {}), auth: event.auth || {} };
16724
+ addEvent(`remote PIN auth ${event.auth?.enabled ? "enabled" : "disabled"}`, event.auth?.enabled ? "warn" : "info");
16725
+ renderNetworkStatus();
16726
+ break;
16377
16727
  case "pi_process_exit":
16378
16728
  addEvent(`pi rpc exited (${event.code ?? event.signal ?? "unknown"})`, "error");
16379
16729
  clearRunIndicatorActivity();
@@ -16846,6 +17196,7 @@ elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu(
16846
17196
  elements.optionsCommandPaletteButton.addEventListener("click", () => openCommandPalette());
16847
17197
  elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
16848
17198
  elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
17199
+ elements.optionsRemoteButton.addEventListener("click", () => runNativeCommandMenu("/remote"));
16849
17200
  elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
16850
17201
  elements.optionsCloneButton.addEventListener("click", () => runNativeCommandMenu("/clone"));
16851
17202
  elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandMenu("/settings"));
@@ -16906,6 +17257,7 @@ elements.commandPaletteDialog?.addEventListener("cancel", (event) => {
16906
17257
  event.preventDefault();
16907
17258
  closeCommandPalette();
16908
17259
  });
17260
+ elements.commandPaletteCloseButton?.addEventListener("click", closeCommandPalette);
16909
17261
  elements.commandPaletteInput?.addEventListener("input", () => {
16910
17262
  commandPaletteIndex = 0;
16911
17263
  renderCommandPalette();
@@ -16938,11 +17290,104 @@ elements.editRetryCancelButton?.addEventListener("click", closeEditRetryDialog);
16938
17290
  elements.editRetryForkButton?.addEventListener("click", () => submitEditRetry({ send: false }));
16939
17291
  elements.editRetrySendButton?.addEventListener("click", () => submitEditRetry({ send: true }));
16940
17292
 
16941
- function resetAbortLongPressAffordance() {
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() {
16942
17307
  clearTimeout(abortLongPressTimer);
17308
+ clearInterval(abortLongPressTickTimer);
16943
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;
16944
17384
  elements.abortButton.classList.remove("long-pressing");
16945
- if (!abortRequestInFlight) elements.abortButton.textContent = "Abort";
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
+ }
16946
17391
  }
16947
17392
 
16948
17393
  async function abortActiveRun({ source = "button" } = {}) {
@@ -16978,31 +17423,41 @@ async function abortActiveRun({ source = "button" } = {}) {
16978
17423
  }
16979
17424
  }
16980
17425
 
16981
- function startAbortLongPress(event) {
16982
- if (!isAbortAvailable() || abortRequestInFlight) return;
16983
- if (event.button !== undefined && event.button !== 0) return;
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
+ }
16984
17433
  resetAbortLongPressAffordance();
16985
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`);
16986
17440
  elements.abortButton.classList.add("long-pressing");
16987
- elements.abortButton.textContent = "Hold…";
16988
- abortLongPressTimer = setTimeout(() => {
16989
- abortLongPressTimer = null;
16990
- abortLongPressHandled = true;
16991
- abortActiveRun({ source: "long-press" });
16992
- }, 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;
16993
17445
  }
16994
17446
 
16995
17447
  elements.abortButton.addEventListener("pointerdown", startAbortLongPress);
16996
17448
  for (const eventName of ["pointerup", "pointerleave", "pointercancel", "blur"]) {
16997
17449
  elements.abortButton.addEventListener(eventName, resetAbortLongPressAffordance);
16998
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
+ });
16999
17458
  elements.abortButton.addEventListener("click", (event) => {
17000
- if (abortLongPressHandled) {
17001
- event.preventDefault();
17002
- abortLongPressHandled = false;
17003
- return;
17004
- }
17005
- abortActiveRun({ source: "button" });
17459
+ event.preventDefault();
17460
+ if (abortLongPressHandled) abortLongPressHandled = false;
17006
17461
  });
17007
17462
  elements.newSessionButton.addEventListener("click", async () => {
17008
17463
  setComposerActionsOpen(false);
@@ -17071,6 +17526,7 @@ if (elements.backgroundChooseButton && elements.backgroundInput) {
17071
17526
  if (elements.backgroundClearButton) {
17072
17527
  elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
17073
17528
  }
17529
+ elements.remoteAuthToggle.addEventListener("change", () => toggleRemoteAuth().catch((error) => addEvent(error.message || String(error), "error")));
17074
17530
  elements.openNetworkButton.addEventListener("click", openToNetwork);
17075
17531
  elements.serverActionSelect.addEventListener("change", updateServerActionButton);
17076
17532
  elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
@@ -17117,6 +17573,10 @@ elements.chat.addEventListener("scroll", () => {
17117
17573
  markTabOutputSeen();
17118
17574
  updateStickyUserPromptButton();
17119
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 });
17120
17580
  document.addEventListener("pointerdown", (event) => {
17121
17581
  if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
17122
17582
  clearOpenTerminalTabGroup(openTerminalTabGroupKey);
@@ -17337,12 +17797,14 @@ window.addEventListener("keydown", (event) => {
17337
17797
  window.addEventListener("keydown", handleNativeAppShortcut, { capture: true });
17338
17798
  document.addEventListener("visibilitychange", () => {
17339
17799
  if (document.visibilityState === "visible") scheduleForegroundReconcile("visibility resume", 0);
17800
+ else resetAbortLongPressAffordance();
17340
17801
  });
17341
17802
  window.addEventListener("pageshow", () => scheduleForegroundReconcile("page show", 0));
17342
17803
  window.addEventListener("focus", () => scheduleForegroundReconcile("window focus"));
17343
17804
  window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
17344
17805
  window.addEventListener("keydown", (event) => {
17345
17806
  if (event.key !== "Escape") return;
17807
+ if (event.defaultPrevented) return;
17346
17808
  if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open) return;
17347
17809
  if (publishMenuOpen) {
17348
17810
  setPublishMenuOpen(false);
@@ -17388,6 +17850,20 @@ window.addEventListener("keydown", (event) => {
17388
17850
  hideCommandSuggestions();
17389
17851
  return;
17390
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
+ }
17391
17867
  if (document.activeElement === elements.promptInput && !elements.promptInput.value.trim() && doubleEscapeAction !== "none") {
17392
17868
  const now = Date.now();
17393
17869
  if (now - lastEmptyPromptEscapeTime < 500) {
@@ -17398,14 +17874,13 @@ window.addEventListener("keydown", (event) => {
17398
17874
  }
17399
17875
  lastEmptyPromptEscapeTime = now;
17400
17876
  }
17401
- if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
17402
- setSidePanelCollapsed(true);
17403
- return;
17404
- }
17405
- if (isAbortAvailable()) {
17406
- event.preventDefault();
17407
- abortActiveRun({ source: "escape" });
17408
- }
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();
17409
17884
  });
17410
17885
 
17411
17886
  elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);