@firstpick/pi-package-webui 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -58,6 +58,7 @@ const elements = {
58
58
  skillEditorCancelButton: $("#skillEditorCancelButton"),
59
59
  skillEditorSaveButton: $("#skillEditorSaveButton"),
60
60
  sendButton: $("#sendButton"),
61
+ btwButton: $("#btwButton"),
61
62
  commandSuggest: $("#commandSuggest"),
62
63
  attachmentTray: $("#attachmentTray"),
63
64
  attachButton: $("#attachButton"),
@@ -85,6 +86,7 @@ const elements = {
85
86
  optionsCommandPaletteButton: $("#optionsCommandPaletteButton"),
86
87
  optionsResumeButton: $("#optionsResumeButton"),
87
88
  optionsReloadButton: $("#optionsReloadButton"),
89
+ optionsRemoteButton: $("#optionsRemoteButton"),
88
90
  optionsNameButton: $("#optionsNameButton"),
89
91
  optionsCloneButton: $("#optionsCloneButton"),
90
92
  optionsSettingsButton: $("#optionsSettingsButton"),
@@ -197,6 +199,7 @@ const elements = {
197
199
  commandPaletteInput: $("#commandPaletteInput"),
198
200
  commandPaletteList: $("#commandPaletteList"),
199
201
  commandPaletteHint: $("#commandPaletteHint"),
202
+ commandPaletteCloseButton: $("#commandPaletteCloseButton"),
200
203
  editRetryDialog: $("#editRetryDialog"),
201
204
  editRetryMessage: $("#editRetryMessage"),
202
205
  editRetryText: $("#editRetryText"),
@@ -299,6 +302,11 @@ let statsOverlayLastScope = "14";
299
302
  let statsOverlayCalibrationMessage = "";
300
303
  let statsOverlayCalibrationBusy = "";
301
304
  let latestStatsOverlayPayload = null;
305
+ let latestBtwWidgetPayload = null;
306
+ let btwWidgetDismissedId = "";
307
+ let btwWidgetComposerOpen = false;
308
+ let btwWidgetInputDraft = "";
309
+ let btwWidgetFocusAfterRender = false;
302
310
  let latestWorkspace = null;
303
311
  let latestNetwork = null;
304
312
  let webuiVersion = "";
@@ -316,6 +324,8 @@ let updateStatusRefreshTimer = null;
316
324
  let updateNotificationHideTimer = null;
317
325
  let backendOfflineNoticeShown = false;
318
326
  let latestMessages = [];
327
+ let latestMessagesSessionKey = "";
328
+ const tabMessagesCache = new Map();
319
329
  let promptHistoryByTab = new Map();
320
330
  let promptHistoryNavigation = null;
321
331
  let transientMessages = [];
@@ -340,6 +350,8 @@ let toolOutputGloballyExpanded = false;
340
350
  let agentDoneNotificationPermissionRequested = false;
341
351
  let agentDoneNotificationFallbackNoted = false;
342
352
  let agentDoneNotificationKeys = new Set();
353
+ let pendingAgentDoneNotificationTimers = new Map();
354
+ let autoRetryingTabs = new Set();
343
355
  let availableModels = [];
344
356
  let availableThemes = [];
345
357
  let currentThemeName = "catppuccin-mocha";
@@ -374,7 +386,17 @@ let workspaceDashboardCollapsed = false;
374
386
  let commandPaletteIndex = 0;
375
387
  let commandPaletteItems = [];
376
388
  let activeEditRetry = null;
389
+ let activePointerActivation = null;
390
+ let pointerActivationTimeout = null;
391
+ let deferredChatFollowScroll = false;
392
+ const deferredUiRenderCallbacks = new Map();
377
393
  let abortLongPressTimer = null;
394
+ let abortLongPressTickTimer = null;
395
+ let abortLongPressResetTimer = null;
396
+ let abortLongPressStartedAt = 0;
397
+ let abortLongPressDeadlineAt = 0;
398
+ let abortLongPressSource = "long-press";
399
+ let abortLongPressReleasePending = false;
378
400
  let abortLongPressHandled = false;
379
401
  const dialogQueue = [];
380
402
  const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
@@ -407,11 +429,21 @@ const GIT_INIT_STACK_STORAGE_KEY = "pi-webui-git-init-stack";
407
429
  const STATS_WEBUI_STATUS_KEY = "stats-webui";
408
430
  const STATS_WEBUI_PAYLOAD_TYPE = "firstpick.pi-extension-stats.overlay";
409
431
  const STATS_WEBUI_PAYLOAD_VERSION = 1;
432
+ const BTW_WEBUI_STATUS_KEY = "btw-webui";
433
+ const BTW_OUTPUT_WIDGET_KEY = "btw:output";
434
+ const BTW_FOOTER_WIDGET_KEY = "btw:footer";
435
+ const BTW_WIDGET_PAYLOAD_PREFIX = "BTW_WEBUI_PAYLOAD ";
436
+ const BTW_WEBUI_PAYLOAD_TYPES = new Set(["firstpick.pi-extension-btw.overlay", "firstpick.pi-extension-btw.output"]);
437
+ const WORKFLOW_WIDGET_PAYLOAD_PREFIX = "WORKFLOW_WEBUI_PAYLOAD ";
438
+ const WORKFLOW_SUBPROCESS_PAYLOAD_TYPE = "firstpick.pi-extension-workflows.subprocess";
439
+ const WORKFLOW_SUBPROCESS_PAYLOAD_VERSION = 1;
410
440
  const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
411
441
  const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
412
442
  const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
413
443
  const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
414
444
  const WORKSPACE_DASHBOARD_STORAGE_KEY = "pi-webui-workspace-dashboard-collapsed";
445
+ const POINTER_ACTIVATION_SELECTOR = "button, a[href], input, select, textarea, summary, [role='button'], [tabindex]:not([tabindex='-1'])";
446
+ const POINTER_ACTIVATION_RENDER_DEFER_MAX_MS = 1200;
415
447
  const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
416
448
  const ATTACHMENT_MAX_FILES = 12;
417
449
  const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
@@ -444,7 +476,9 @@ const UPDATE_STATUS_INITIAL_DELAY_MS = 1800;
444
476
  const RUN_INDICATOR_TICK_MS = 1000;
445
477
  const RUN_INDICATOR_START_GRACE_MS = 2500;
446
478
  const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
447
- const ABORT_LONG_PRESS_MS = 700;
479
+ const ABORT_LONG_PRESS_MS = 3000;
480
+ const ABORT_LONG_PRESS_TICK_MS = 100;
481
+ const ABORT_LONG_PRESS_RELEASE_GRACE_MS = 350;
448
482
  const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
449
483
  const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
450
484
  const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
@@ -454,6 +488,7 @@ const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+
454
488
  const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
455
489
  const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
456
490
  const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
491
+ const AGENT_DONE_NOTIFICATION_RETRY_GRACE_MS = 1200;
457
492
  const FOREGROUND_RECONCILE_DELAY_MS = 120;
458
493
  const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "working", "idle"];
459
494
  const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
@@ -475,18 +510,28 @@ let liveToolRenderTimer = null;
475
510
  // commands and live widget events), not npm package folders. This keeps local dev
476
511
  // symlinks and independently installed packages working.
477
512
  const optionalFeatureAvailability = {
513
+ btwCommand: false,
478
514
  gitWorkflow: false,
479
515
  releaseNpm: false,
480
516
  releaseAur: false,
517
+ workflows: false,
481
518
  safetyGuard: false,
482
519
  statsCommand: false,
483
520
  gitFooterStatus: false,
484
521
  tuiSkillsCommand: false,
485
522
  todoProgressWidget: false,
486
523
  tuiToolsCommand: false,
524
+ remoteWebui: false,
487
525
  themeBundle: false,
488
526
  };
489
527
  const OPTIONAL_FEATURES = [
528
+ {
529
+ id: "btwCommand",
530
+ label: "/btw side questions",
531
+ packageName: "@firstpick/pi-extension-btw",
532
+ capabilityLabel: "/btw or btw:output widget event",
533
+ description: "Ephemeral side-question command with TUI overlay and browser output-widget rendering.",
534
+ },
490
535
  {
491
536
  id: "gitWorkflow",
492
537
  label: "Guided Git workflow",
@@ -508,6 +553,13 @@ const OPTIONAL_FEATURES = [
508
553
  capabilityLabel: "/release-aur",
509
554
  description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
510
555
  },
556
+ {
557
+ id: "workflows",
558
+ label: "Workflows",
559
+ packageName: "@firstpick/pi-extension-workflows",
560
+ capabilityLabel: "/workflow or workflow subprocess widget event",
561
+ description: "Modular workflow runner with live subprocess output shown in a non-blocking Web UI widget.",
562
+ },
511
563
  {
512
564
  id: "safetyGuard",
513
565
  label: "Safety guard",
@@ -536,6 +588,13 @@ const OPTIONAL_FEATURES = [
536
588
  capabilityLabel: "RPC /tools from tools extension",
537
589
  description: "Terminal-native active-tool manager alongside WebUI-native /tools toggles.",
538
590
  },
591
+ {
592
+ id: "remoteWebui",
593
+ label: "Remote WebUI",
594
+ packageName: "@firstpick/pi-package-remote-webui",
595
+ capabilityLabel: "/remote",
596
+ description: "Trusted-LAN QR helper for opening the Web UI from mobile browsers.",
597
+ },
539
598
  {
540
599
  id: "gitFooterStatus",
541
600
  label: "Git footer status",
@@ -560,20 +619,28 @@ const OPTIONAL_FEATURES = [
560
619
  ];
561
620
  const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
562
621
  const OPTIONAL_COMMAND_FEATURES = new Map([
622
+ ["btw", "btwCommand"],
623
+ ["btw-transfer", "btwCommand"],
624
+ ["btw-status", "btwCommand"],
563
625
  ["git-staged-msg", "gitWorkflow"],
564
626
  ["git-branch-name", "gitWorkflow"],
565
627
  ["pr", "gitWorkflow"],
566
628
  ["release-npm", "releaseNpm"],
567
629
  ["release-aur", "releaseAur"],
630
+ ["workflow", "workflows"],
631
+ ["workflow-clear", "workflows"],
568
632
  ["safety-guard", "safetyGuard"],
569
633
  ["skills", "tuiSkillsCommand"],
570
634
  ["tools", "tuiToolsCommand"],
635
+ ["remote", "remoteWebui"],
571
636
  ["stats", "statsCommand"],
572
637
  ["git-footer-refresh", "gitFooterStatus"],
573
638
  ["todo-progress-status", "todoProgressWidget"],
574
639
  ]);
575
640
  const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
576
641
  HIDDEN_COMMAND_NAMES.add("stats-webui");
642
+ HIDDEN_COMMAND_NAMES.add("btw-status");
643
+ HIDDEN_COMMAND_NAMES.add("btw-transfer");
577
644
  const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "name", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
578
645
  const SETTINGS_THINKING_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhigh"];
579
646
  const SETTINGS_TRANSPORT_OPTIONS = ["sse", "websocket", "websocket-cached", "auto"];
@@ -597,6 +664,7 @@ const optionalFeatureInstallInProgress = new Set();
597
664
  const optionalFeaturePackageStatuses = new Map();
598
665
  const optionalFeatureInstallMessages = new Map();
599
666
  const gitFooterPayloadRefreshInFlightByTab = new Set();
667
+ const gitFooterPiCalibrationInFlightByTab = new Set();
600
668
 
601
669
  function createGitWorkflowActionsDone(patch = {}) {
602
670
  return {
@@ -826,6 +894,82 @@ function delay(ms) {
826
894
  return new Promise((resolve) => setTimeout(resolve, ms));
827
895
  }
828
896
 
897
+ function activationControlFromEvent(event) {
898
+ const target = event?.target instanceof Element ? event.target : null;
899
+ const control = target?.closest?.(POINTER_ACTIVATION_SELECTOR);
900
+ if (!control || control === document.body || control === document.documentElement) return null;
901
+ if (control.disabled || control.getAttribute("aria-disabled") === "true") return null;
902
+ return control;
903
+ }
904
+
905
+ function shouldDeferUiRenderForPointerActivation() {
906
+ return Boolean(
907
+ activePointerActivation
908
+ && performance.now() - activePointerActivation.startedAt <= POINTER_ACTIVATION_RENDER_DEFER_MAX_MS,
909
+ );
910
+ }
911
+
912
+ function deferUiRenderDuringPointerActivation(key, callback) {
913
+ if (!shouldDeferUiRenderForPointerActivation()) return false;
914
+ deferredUiRenderCallbacks.set(key, callback);
915
+ return true;
916
+ }
917
+
918
+ function deferChatFollowScrollDuringPointerActivation({ force = false } = {}) {
919
+ if (force || !shouldDeferUiRenderForPointerActivation()) return false;
920
+ deferredChatFollowScroll = true;
921
+ return true;
922
+ }
923
+
924
+ function flushDeferredUiRenders() {
925
+ const callbacks = [...deferredUiRenderCallbacks.values()];
926
+ deferredUiRenderCallbacks.clear();
927
+ const shouldScroll = deferredChatFollowScroll;
928
+ deferredChatFollowScroll = false;
929
+
930
+ for (const callback of callbacks) {
931
+ try {
932
+ callback();
933
+ } catch (error) {
934
+ console.error("deferred Web UI render failed", error);
935
+ }
936
+ }
937
+ if (shouldScroll) scrollChatToBottom();
938
+ }
939
+
940
+ function beginPointerActivation(event) {
941
+ if (event?.button !== undefined && event.button !== 0) return;
942
+ const control = activationControlFromEvent(event);
943
+ if (!control) return;
944
+ clearTimeout(pointerActivationTimeout);
945
+ const activation = { pointerId: event.pointerId, startedAt: performance.now(), control };
946
+ activePointerActivation = activation;
947
+ pointerActivationTimeout = setTimeout(() => {
948
+ if (activePointerActivation === activation) activePointerActivation = null;
949
+ pointerActivationTimeout = null;
950
+ flushDeferredUiRenders();
951
+ }, POINTER_ACTIVATION_RENDER_DEFER_MAX_MS);
952
+ }
953
+
954
+ function finishPointerActivation(event) {
955
+ if (!activePointerActivation) return;
956
+ if (event?.pointerId !== undefined && activePointerActivation.pointerId !== event.pointerId) return;
957
+ const activation = activePointerActivation;
958
+ clearTimeout(pointerActivationTimeout);
959
+ pointerActivationTimeout = null;
960
+ setTimeout(() => {
961
+ if (activePointerActivation === activation) activePointerActivation = null;
962
+ flushDeferredUiRenders();
963
+ }, 0);
964
+ }
965
+
966
+ function cancelPointerActivation() {
967
+ clearTimeout(pointerActivationTimeout);
968
+ pointerActivationTimeout = null;
969
+ activePointerActivation = null;
970
+ flushDeferredUiRenders();
971
+ }
972
+
829
973
  function isMobileView() {
830
974
  return mobileViewMedia?.matches || false;
831
975
  }
@@ -834,6 +978,36 @@ function isSidePanelOverlayView() {
834
978
  return sidePanelOverlayMedia?.matches || false;
835
979
  }
836
980
 
981
+ function mobileDropdownViewportHeight() {
982
+ return window.visualViewport?.height || window.innerHeight || document.documentElement.clientHeight || 0;
983
+ }
984
+
985
+ function mobileDropdownConfigs() {
986
+ return [
987
+ { menu: elements.publishButton?.parentElement, button: elements.publishButton, panel: elements.publishButton?.parentElement?.querySelector(".composer-publish-menu-panel") },
988
+ { menu: elements.nativeCommandMenuButton?.parentElement, button: elements.nativeCommandMenuButton, panel: elements.nativeCommandMenuButton?.parentElement?.querySelector(".composer-publish-menu-panel") },
989
+ { menu: elements.optionsMenuButton?.parentElement, button: elements.optionsMenuButton, panel: elements.optionsMenu },
990
+ { menu: elements.appRunnerMenu, button: elements.appRunnerMenuButton, panel: elements.appRunnerMenuPanel },
991
+ ];
992
+ }
993
+
994
+ function updateMobileDropdownScrollBounds() {
995
+ const viewportHeight = mobileDropdownViewportHeight();
996
+ for (const { menu, button, panel } of mobileDropdownConfigs()) {
997
+ if (!panel) continue;
998
+ panel.style.removeProperty("--mobile-dropdown-max-height");
999
+ if (!isMobileView() || !menu?.classList.contains("open") || !viewportHeight) continue;
1000
+ const anchorRect = (button || menu).getBoundingClientRect();
1001
+ const availableAbove = Math.floor(anchorRect.top - 8);
1002
+ const boundedHeight = Math.max(72, Math.min(viewportHeight - 16, availableAbove));
1003
+ panel.style.setProperty("--mobile-dropdown-max-height", `${boundedHeight}px`);
1004
+ }
1005
+ }
1006
+
1007
+ function scheduleMobileDropdownScrollBoundsUpdate() {
1008
+ requestAnimationFrame(updateMobileDropdownScrollBounds);
1009
+ }
1010
+
837
1011
  function readStoredSidePanelCollapsed() {
838
1012
  try {
839
1013
  const stored = localStorage.getItem(SIDE_PANEL_STORAGE_KEY);
@@ -1592,6 +1766,7 @@ function setComposerActionsOpen(open) {
1592
1766
  setOptionsMenuOpen(false);
1593
1767
  setBusyPromptBehaviorMenuOpen(false);
1594
1768
  }
1769
+ scheduleMobileDropdownScrollBoundsUpdate();
1595
1770
  }
1596
1771
 
1597
1772
  function isUserBashActive(tabId = activeTabId) {
@@ -1643,11 +1818,17 @@ function updateComposerModeButtons() {
1643
1818
  button.hidden = !runActive;
1644
1819
  button.disabled = !runActive;
1645
1820
  }
1646
- elements.abortButton.hidden = !abortAvailable;
1647
- elements.abortButton.disabled = !abortAvailable || abortRequestInFlight;
1648
- elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
1649
- elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
1650
- elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
1821
+ const abortHoldActive = isAbortLongPressActive();
1822
+ if (!abortAvailable && !abortHoldActive) resetAbortLongPressAffordance();
1823
+ elements.abortButton.hidden = !abortAvailable && !abortHoldActive;
1824
+ elements.abortButton.disabled = (!abortAvailable && !abortHoldActive) || abortRequestInFlight;
1825
+ if (abortHoldActive) {
1826
+ renderAbortLongPressAffordance();
1827
+ } else {
1828
+ elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
1829
+ elements.abortButton.title = abortAvailable ? abortButtonReadyTitle() : "Abort is available while Pi is running";
1830
+ elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
1831
+ }
1651
1832
  renderBusyPromptBehaviorTag();
1652
1833
  document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
1653
1834
  }
@@ -1851,6 +2032,7 @@ function updateVisualViewportVars() {
1851
2032
  syncMobileChatToBottomForInput();
1852
2033
  }
1853
2034
  updateFooterModelPickerPosition();
2035
+ updateMobileDropdownScrollBounds();
1854
2036
  }
1855
2037
 
1856
2038
  function installViewportHandlers() {
@@ -3367,6 +3549,15 @@ function setOptionalFeatureDisabled(featureId, disabled) {
3367
3549
  statusEntries.delete(GIT_FOOTER_WEBUI_STATUS_KEY);
3368
3550
  clearGitFooterWebuiPayloadCache();
3369
3551
  }
3552
+ if (featureId === "btwCommand") {
3553
+ statusEntries.delete(BTW_WEBUI_STATUS_KEY);
3554
+ widgets.delete(BTW_OUTPUT_WIDGET_KEY);
3555
+ widgets.delete(BTW_FOOTER_WIDGET_KEY);
3556
+ latestBtwWidgetPayload = null;
3557
+ btwWidgetDismissedId = "";
3558
+ btwWidgetComposerOpen = false;
3559
+ btwWidgetInputDraft = "";
3560
+ }
3370
3561
  storeDisabledOptionalFeatures();
3371
3562
  renderOptionalFeatureDependentDisplays();
3372
3563
  const tabContext = activeTabContext();
@@ -3726,11 +3917,17 @@ function syncTabMetadata(nextTabs = []) {
3726
3917
  if (!liveIds.has(tabId)) {
3727
3918
  tabActivities.delete(tabId);
3728
3919
  tabSeenCompletionSerials.delete(tabId);
3920
+ autoRetryingTabs.delete(tabId);
3921
+ suppressPendingAgentDoneNotificationsForTab(tabId, { markSeen: false });
3729
3922
  actionFeedbackByTab.delete(tabId);
3730
3923
  skillUsageByTab.delete(tabId);
3924
+ tabMessagesCache.delete(tabId);
3731
3925
  clearGitWorkflowForTab(tabId);
3732
3926
  }
3733
3927
  }
3928
+ for (const tabId of tabMessagesCache.keys()) {
3929
+ if (!liveIds.has(tabId)) tabMessagesCache.delete(tabId);
3930
+ }
3734
3931
  pruneSkillUsageForKnownTabs(liveIds);
3735
3932
  }
3736
3933
 
@@ -3894,6 +4091,18 @@ function ingestEventTabActivity(event) {
3894
4091
  if (changed) renderTabs();
3895
4092
  }
3896
4093
 
4094
+ function trackAutoRetryStateFromEvent(event) {
4095
+ const tabId = event?.tabId || activeTabId;
4096
+ if (!tabId) return;
4097
+ if (event.type === "auto_retry_start") {
4098
+ autoRetryingTabs.add(tabId);
4099
+ suppressPendingAgentDoneNotificationsForTab(tabId);
4100
+ markTabWorkingLocally(tabId);
4101
+ } else if (event.type === "auto_retry_end") {
4102
+ autoRetryingTabs.delete(tabId);
4103
+ }
4104
+ }
4105
+
3897
4106
  function rememberActiveTab() {
3898
4107
  try {
3899
4108
  if (activeTabId) localStorage.setItem(TAB_STORAGE_KEY, activeTabId);
@@ -3983,8 +4192,13 @@ function resetActiveTabUi() {
3983
4192
  currentState = null;
3984
4193
  latestStats = null;
3985
4194
  latestStatsOverlayPayload = null;
4195
+ latestBtwWidgetPayload = null;
4196
+ btwWidgetDismissedId = "";
4197
+ btwWidgetComposerOpen = false;
4198
+ btwWidgetInputDraft = "";
3986
4199
  latestWorkspace = null;
3987
4200
  latestMessages = [];
4201
+ latestMessagesSessionKey = "";
3988
4202
  clearRunIndicatorActivity({ render: false });
3989
4203
  statusEntries.clear();
3990
4204
  widgets.clear();
@@ -4016,8 +4230,10 @@ function resetActiveTabUi() {
4016
4230
  renderAppRunnerControls();
4017
4231
  renderWidgets();
4018
4232
  renderGitWorkflow();
4019
- renderFooter();
4020
- renderFeedbackTray();
4233
+ if (!restoreCachedMessagesForActiveTab()) {
4234
+ renderFooter();
4235
+ renderFeedbackTray();
4236
+ }
4021
4237
  }
4022
4238
 
4023
4239
  function tabGroupStatusRank(state) {
@@ -4256,6 +4472,7 @@ function moveNewTabMenuFocus(delta) {
4256
4472
  }
4257
4473
 
4258
4474
  function renderTabs() {
4475
+ if (deferUiRenderDuringPointerActivation("tabs", renderTabs)) return;
4259
4476
  const active = activeTab();
4260
4477
  const activeIndicator = active ? tabIndicator(active) : null;
4261
4478
  elements.terminalTabsToggleButton.textContent = active ? `${activeIndicator.glyph} ${active.title}${tabs.length > 1 ? ` · ${tabs.length}` : ""}` : "Tabs";
@@ -4280,7 +4497,7 @@ function renderTabs() {
4280
4497
  updateDocumentTitle();
4281
4498
  renderWorkspaceDashboard();
4282
4499
  renderContextMeter();
4283
- if (elements.commandPaletteDialog?.open) renderCommandPalette();
4500
+ if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
4284
4501
  syncTabPolling();
4285
4502
  }
4286
4503
 
@@ -4311,6 +4528,7 @@ async function switchTab(tabId) {
4311
4528
  footerBranchPickerOpen = false;
4312
4529
  footerBranchPickerRequestSerial += 1;
4313
4530
  saveActiveDraft();
4531
+ cacheMessagesForTab(activeTabId);
4314
4532
  const tabContext = setActiveTabId(tabId, { remember: true });
4315
4533
  resetActiveTabUi();
4316
4534
  renderTabs();
@@ -4379,6 +4597,20 @@ function tabHasActiveAgent(tab) {
4379
4597
  return !!activity.isWorking || indicator.state === "working" || indicator.state === "blocked";
4380
4598
  }
4381
4599
 
4600
+ function activeTabHasConversationMessages(tab = activeTab()) {
4601
+ const tabId = tab?.id || activeTabId;
4602
+ if (!tabId) return false;
4603
+ if (tabId !== activeTabId && !latestMessagesSessionKey.startsWith(`${tabId}|`)) return false;
4604
+ return latestMessages.some((message) => ["user", "assistant"].includes(message?.role));
4605
+ }
4606
+
4607
+ function shouldOpenCwdChangeInNewTab(tab) {
4608
+ return !!tab?.conversationStarted
4609
+ || activeTabHasConversationMessages(tab)
4610
+ || stateHasVisibleWork(currentState)
4611
+ || tabHasActiveAgent(tab);
4612
+ }
4613
+
4382
4614
  function confirmCloseTerminalTabs(targetTabs, label) {
4383
4615
  const count = targetTabs.length;
4384
4616
  const noun = count === 1 ? "tab" : "tabs";
@@ -4423,6 +4655,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
4423
4655
  clearAttachments(id);
4424
4656
  clearGitWorkflowForTab(id);
4425
4657
  appRunnerDataByTab.delete(id);
4658
+ tabMessagesCache.delete(id);
4426
4659
  }
4427
4660
  clearOpenTerminalTabGroup(null, { force: true });
4428
4661
 
@@ -4689,6 +4922,36 @@ function agentDoneNotificationKey(tabId, activity = {}) {
4689
4922
  return `${tabId}:${Number.isFinite(serial) && serial > 0 ? serial : "done"}`;
4690
4923
  }
4691
4924
 
4925
+ function isAutoRetryingTab(tabId) {
4926
+ return !!tabId && autoRetryingTabs.has(tabId);
4927
+ }
4928
+
4929
+ function clearPendingAgentDoneNotification(key, { markSeen = false } = {}) {
4930
+ const pending = pendingAgentDoneNotificationTimers.get(key);
4931
+ if (!pending) return false;
4932
+ clearTimeout(pending.timer);
4933
+ pendingAgentDoneNotificationTimers.delete(key);
4934
+ if (markSeen) agentDoneNotificationKeys.add(key);
4935
+ return true;
4936
+ }
4937
+
4938
+ function suppressPendingAgentDoneNotificationsForTab(tabId, { markSeen = true } = {}) {
4939
+ if (!tabId) return;
4940
+ for (const [key, pending] of pendingAgentDoneNotificationTimers) {
4941
+ if (pending.tabId === tabId) clearPendingAgentDoneNotification(key, { markSeen });
4942
+ }
4943
+ }
4944
+
4945
+ function queueAgentDoneBrowserNotification({ key, tabId, title, body }) {
4946
+ clearPendingAgentDoneNotification(key);
4947
+ const timer = setTimeout(() => {
4948
+ pendingAgentDoneNotificationTimers.delete(key);
4949
+ if (isAutoRetryingTab(tabId)) return;
4950
+ showAgentDoneBrowserNotification({ tabId, title, body });
4951
+ }, AGENT_DONE_NOTIFICATION_RETRY_GRACE_MS);
4952
+ pendingAgentDoneNotificationTimers.set(key, { tabId, timer });
4953
+ }
4954
+
4692
4955
  function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
4693
4956
  if (!agentDoneNotificationsEnabled) return;
4694
4957
  const tabId = typeof tabOrId === "string" ? tabOrId : tabOrId?.id || activeTabId;
@@ -4699,9 +4962,11 @@ function notifyAgentDone(tabOrId, { activity = null, tabTitle = "" } = {}) {
4699
4962
  const key = agentDoneNotificationKey(tabId, normalizedActivity);
4700
4963
  if (agentDoneNotificationKeys.has(key)) return;
4701
4964
  agentDoneNotificationKeys.add(key);
4965
+ if (isAutoRetryingTab(tabId)) return;
4702
4966
 
4703
4967
  const displayTitle = tabTitle || tab?.title || "terminal";
4704
- showAgentDoneBrowserNotification({
4968
+ queueAgentDoneBrowserNotification({
4969
+ key,
4705
4970
  tabId,
4706
4971
  title: "Pi finished work",
4707
4972
  body: `${displayTitle} finished its agent run.`,
@@ -5068,6 +5333,58 @@ async function toggleFooterAutoCompaction(tabContext = activeTabContext()) {
5068
5333
  }
5069
5334
  }
5070
5335
 
5336
+ function scheduleGitFooterPiCalibrationRefresh(tabContext, delays = [600, 1600]) {
5337
+ for (const delayMs of delays) {
5338
+ setTimeout(() => {
5339
+ if (isCurrentTabContext(tabContext)) requestGitFooterWebuiPayload(tabContext, { force: true });
5340
+ }, delayMs);
5341
+ }
5342
+ }
5343
+
5344
+ async function runGitFooterPiCalibration(mode = "current", tabContext = activeTabContext()) {
5345
+ if (!tabContext.tabId) return;
5346
+ if (gitFooterPiCalibrationInFlightByTab.has(tabContext.tabId)) return;
5347
+ if (currentState?.isStreaming || currentState?.isCompacting) {
5348
+ addEvent("PI calibration can run after the active agent work finishes.", "warn");
5349
+ return;
5350
+ }
5351
+
5352
+ const commandName = resolveAvailableCommandName("calibrate", { rpcOnly: true });
5353
+ if (!commandName) {
5354
+ addEvent("PI calibration unavailable: /calibrate is not loaded in this Pi tab.", "warn");
5355
+ return;
5356
+ }
5357
+ if (mode === "probe" && !confirm("Start an isolated PI calibration probe? This sends one tiny model request and may incur provider token usage.")) return;
5358
+
5359
+ const command = mode === "probe" ? `/${commandName}` : `/${commandName} current`;
5360
+ gitFooterPiCalibrationInFlightByTab.add(tabContext.tabId);
5361
+ renderFooter();
5362
+ try {
5363
+ await sendPrompt("prompt", command, { targetTabId: tabContext.tabId, throwOnError: true });
5364
+ if (!isCurrentTabContext(tabContext)) return;
5365
+ addEvent(mode === "probe" ? "PI calibration probe started; refreshing git footer value after it records…" : "PI calibration requested; refreshing git footer value…", "info");
5366
+ scheduleGitFooterPiCalibrationRefresh(tabContext, mode === "probe" ? [5000, 14000] : [600, 1600]);
5367
+ } catch (error) {
5368
+ if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
5369
+ } finally {
5370
+ gitFooterPiCalibrationInFlightByTab.delete(tabContext.tabId);
5371
+ if (isCurrentTabContext(tabContext)) renderFooter();
5372
+ }
5373
+ }
5374
+
5375
+ function applyGitFooterPiCalibrationOptions(chip, options) {
5376
+ if (chip?.key !== "pi" || !FOOTER_PAYLOAD_ACTIONS.has(chip?.action)) return "";
5377
+ const tabContext = activeTabContext();
5378
+ const busy = !!tabContext.tabId && gitFooterPiCalibrationInFlightByTab.has(tabContext.tabId);
5379
+ const mode = chip.action === "calibrate-probe" ? "probe" : "current";
5380
+ options.onClick = () => runGitFooterPiCalibration(mode);
5381
+ if (busy) options.ariaBusy = true;
5382
+ if (busy) return "Calibrating PI estimate and refreshing this value…";
5383
+ return mode === "probe"
5384
+ ? "Click to start an isolated PI calibration probe, then refresh this value."
5385
+ : "Click to calibrate this uncalibrated PI estimate from the current session, then refresh this value.";
5386
+ }
5387
+
5071
5388
  function applyGitFooterContextToggleOptions(chip, options) {
5072
5389
  if (chip?.key !== "context") return "";
5073
5390
  options.onClick = () => toggleFooterAutoCompaction();
@@ -5249,6 +5566,7 @@ function footerMeta(label, value, className = "", options = {}) {
5249
5566
  }
5250
5567
 
5251
5568
  const FOOTER_PAYLOAD_TONES = new Set(["pink", "blue", "mauve", "yellow", "green", "teal"]);
5569
+ const FOOTER_PAYLOAD_ACTIONS = new Set(["calibrate-current", "calibrate-probe"]);
5252
5570
  const FOOTER_CHANGED_FILE_KINDS = new Set(["modified", "staged", "untracked", "conflicted"]);
5253
5571
  const FOOTER_CHANGED_FILE_KIND_ORDER = ["modified", "staged", "untracked", "conflicted"];
5254
5572
  const FOOTER_CHANGED_FILE_KIND_LABELS = {
@@ -5318,6 +5636,7 @@ function normalizeFooterPayloadChip(value, index) {
5318
5636
  tone: FOOTER_PAYLOAD_TONES.has(value.tone) ? value.tone : "",
5319
5637
  title: cleanFooterPayloadText(value.title, "", 4000),
5320
5638
  };
5639
+ if (FOOTER_PAYLOAD_ACTIONS.has(value.action)) chip.action = value.action;
5321
5640
  if (Array.isArray(value.files)) {
5322
5641
  const files = value.files.map(normalizeFooterPayloadChangedFile).filter(Boolean).slice(0, 80);
5323
5642
  if (files.length) chip.files = files;
@@ -5552,7 +5871,7 @@ function applyFooterChangedFilesDropdown(node, chip) {
5552
5871
 
5553
5872
  function renderGitFooterPayloadMetric(chip) {
5554
5873
  const options = { tooltipAlign: gitFooterTooltipAlign(chip) };
5555
- const action = applyGitFooterContextToggleOptions(chip, options);
5874
+ const action = applyGitFooterPiCalibrationOptions(chip, options) || applyGitFooterContextToggleOptions(chip, options);
5556
5875
  options.title = gitFooterPayloadTooltip(chip, { action });
5557
5876
  const node = footerMetric(chip.icon || "•", chip.label, chip.value, chip.tone ? `tone-${chip.tone}` : "", options);
5558
5877
  return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
@@ -6183,6 +6502,7 @@ async function requestManualCompaction({ triggerButton = null } = {}) {
6183
6502
  }
6184
6503
 
6185
6504
  function renderContextMeter() {
6505
+ if (deferUiRenderDuringPointerActivation("context-meter", renderContextMeter)) return;
6186
6506
  const root = elements.contextMeterBar;
6187
6507
  if (!root) return;
6188
6508
  const tab = activeTab();
@@ -6239,6 +6559,7 @@ function dashboardAction(label, handler, className = "") {
6239
6559
  }
6240
6560
 
6241
6561
  function renderWorkspaceDashboard() {
6562
+ if (deferUiRenderDuringPointerActivation("workspace-dashboard", renderWorkspaceDashboard)) return;
6242
6563
  const root = elements.workspaceDashboard;
6243
6564
  if (!root) return;
6244
6565
  const tab = activeTab();
@@ -7010,6 +7331,12 @@ async function changeActiveTabCwd() {
7010
7331
  const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
7011
7332
  const cwd = await pickCwd(tab, currentCwd);
7012
7333
  if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
7334
+
7335
+ if (shouldOpenCwdChangeInNewTab(tab)) {
7336
+ await createTerminalTab(cwd, { triggerButton: null });
7337
+ return;
7338
+ }
7339
+
7013
7340
  if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped. The conversation continues in the new directory.`)) return;
7014
7341
 
7015
7342
  saveActiveDraft();
@@ -7037,6 +7364,7 @@ async function changeActiveTabCwd() {
7037
7364
  }
7038
7365
 
7039
7366
  function renderFooter() {
7367
+ if (deferUiRenderDuringPointerActivation("footer", renderFooter)) return;
7040
7368
  const gitFooterPayload = parseGitFooterWebuiPayload();
7041
7369
  if (gitFooterPayload) {
7042
7370
  renderGitFooterPayload(footerPayloadWithLiveModel(gitFooterPayload));
@@ -7269,6 +7597,7 @@ function initializeCodexUsage() {
7269
7597
  }
7270
7598
 
7271
7599
  function renderStatus() {
7600
+ if (deferUiRenderDuringPointerActivation("status", renderStatus)) return;
7272
7601
  const state = currentState;
7273
7602
  updateComposerModeButtons();
7274
7603
  const running = state?.isStreaming ? "running" : "idle";
@@ -7511,6 +7840,10 @@ function releaseNpmLineTone(line) {
7511
7840
  if (/^(WARN|warning)\b/i.test(clean)) return "warn";
7512
7841
  if (/^(INFO|npm notice|notice)\b/i.test(clean)) return "info";
7513
7842
  if (/^RELEASE_NPM_EVENT\b/.test(clean)) return "event";
7843
+ if (/^\[[0-9:]+\]\s+\[[^\]]+\]\s+\$/.test(clean)) return "command";
7844
+ if (/\b(STDERR|failed|error|exited with code)\b/i.test(clean)) return "fail";
7845
+ if (/\b(completed|succeeded|agent completed|tool completed)\b/i.test(clean)) return "pass";
7846
+ if (/\b(started|running|auto retry|compaction)\b/i.test(clean)) return "info";
7514
7847
  return "";
7515
7848
  }
7516
7849
 
@@ -7720,6 +8053,74 @@ function renderReleaseAurLogWidget() {
7720
8053
  return node;
7721
8054
  }
7722
8055
 
8056
+ function parseWorkflowSubprocessPayload(lines) {
8057
+ const raw = String(lines?.[0] || "").trim();
8058
+ if (!raw) return null;
8059
+ const json = raw.startsWith(WORKFLOW_WIDGET_PAYLOAD_PREFIX) ? raw.slice(WORKFLOW_WIDGET_PAYLOAD_PREFIX.length) : raw;
8060
+ try {
8061
+ const payload = JSON.parse(json);
8062
+ if (payload?.type !== WORKFLOW_SUBPROCESS_PAYLOAD_TYPE || payload.version !== WORKFLOW_SUBPROCESS_PAYLOAD_VERSION) return null;
8063
+ return payload;
8064
+ } catch {
8065
+ return null;
8066
+ }
8067
+ }
8068
+
8069
+ function workflowSubprocessIsLive(payload) {
8070
+ return payload?.status === "queued" || payload?.status === "running" || Number(payload?.taskCounts?.running || 0) > 0;
8071
+ }
8072
+
8073
+ function workflowTaskCountLabel(payload) {
8074
+ const counts = payload?.taskCounts || {};
8075
+ const done = Number(counts.completed || 0);
8076
+ const total = Number(counts.total || 0);
8077
+ const failed = Number(counts.failed || 0);
8078
+ const cancelled = Number(counts.cancelled || 0);
8079
+ return `${done}/${total} done${failed ? ` · ${failed} failed` : ""}${cancelled ? ` · ${cancelled} cancelled` : ""}`;
8080
+ }
8081
+
8082
+ function renderWorkflowSubprocessWidget() {
8083
+ if (!isOptionalFeatureEnabled("workflows")) return null;
8084
+ const payload = parseWorkflowSubprocessPayload(getWidgetLines("workflow:subprocess"));
8085
+ if (!payload) return null;
8086
+
8087
+ const live = workflowSubprocessIsLive(payload);
8088
+ const node = make("section", `widget release-npm-widget workflow-widget ${live ? "workflow-live-widget" : "workflow-log-widget"}`);
8089
+ node.setAttribute("aria-label", "workflow subprocess output");
8090
+
8091
+ const header = make("div", "release-npm-header");
8092
+ const titleWrap = make("div", "release-npm-title-wrap");
8093
+ titleWrap.append(
8094
+ make("span", "release-npm-kicker", "workflow subprocesses"),
8095
+ make("strong", "release-npm-title", payload.workflowName || payload.workflowKey || "workflow"),
8096
+ );
8097
+
8098
+ const meta = make("div", "release-npm-meta");
8099
+ meta.append(make("span", `release-npm-pill workflow-status ${payload.status || "unknown"}`, payload.status || "unknown"));
8100
+ if (payload.activePhase) meta.append(make("span", "release-npm-pill", payload.activePhase));
8101
+ meta.append(make("span", "release-npm-pill elapsed", workflowTaskCountLabel(payload)));
8102
+ if (payload.truncated) meta.append(make("span", "release-npm-pill workflow-truncated", "truncated"));
8103
+
8104
+ const actions = make("div", "release-npm-actions");
8105
+ actions.append(releaseNpmActionButton("Status", "/workflow status"));
8106
+ if (live) actions.append(releaseNpmActionButton("Abort", "/workflow abort", "danger"));
8107
+ actions.append(releaseNpmActionButton("Clear", "/workflow-clear"));
8108
+ header.append(titleWrap, meta, actions);
8109
+
8110
+ const lines = Array.isArray(payload.lines) && payload.lines.length ? payload.lines : ["Waiting for workflow subprocess output..."];
8111
+ const streamHeader = releaseNpmStreamHeader(live ? "Live subprocess output" : "Subprocess output", lines.length, { live });
8112
+ const terminal = make("div", "release-npm-terminal");
8113
+ terminal.setAttribute("role", "log");
8114
+ terminal.setAttribute("aria-live", live ? "polite" : "off");
8115
+ for (const line of lines) appendReleaseNpmTerminalLine(terminal, line);
8116
+
8117
+ const controls = make("div", "release-npm-controls", "Workflow subprocess output is shown as a non-blocking Web UI widget. Use /workflow abort to stop an active run.");
8118
+ const outputDetails = renderReleaseNpmOutputDetails("workflow:subprocess", streamHeader, terminal, controls);
8119
+ node.append(header, outputDetails);
8120
+ requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
8121
+ return node;
8122
+ }
8123
+
7723
8124
  function activeAppRunnerData() {
7724
8125
  return activeTabId ? appRunnerDataByTab.get(activeTabId) || { runners: [], activeRun: null } : { runners: [], activeRun: null };
7725
8126
  }
@@ -8710,7 +9111,290 @@ function handleStatsWebuiStatus(statusText) {
8710
9111
  if (payload.open || elements.statsOverlayDialog?.open) renderStatsOverlay();
8711
9112
  }
8712
9113
 
9114
+ function parseBtwWebuiPayloadRaw(raw) {
9115
+ if (!raw) return null;
9116
+ const text = String(raw || "");
9117
+ const json = text.startsWith(BTW_WIDGET_PAYLOAD_PREFIX) ? text.slice(BTW_WIDGET_PAYLOAD_PREFIX.length) : text;
9118
+ try {
9119
+ const parsed = JSON.parse(json);
9120
+ if (!BTW_WEBUI_PAYLOAD_TYPES.has(parsed?.type)) return null;
9121
+ return parsed;
9122
+ } catch {
9123
+ return null;
9124
+ }
9125
+ }
9126
+
9127
+ function parseBtwWidgetPayload(lines = []) {
9128
+ const first = Array.isArray(lines) ? lines[0] : "";
9129
+ return parseBtwWebuiPayloadRaw(first);
9130
+ }
9131
+
9132
+ function currentBtwWidgetPayload() {
9133
+ if (isOptionalFeatureDisabled("btwCommand")) return null;
9134
+ const outputLines = widgets.get(BTW_OUTPUT_WIDGET_KEY)?.widgetLines || [];
9135
+ const payload = parseBtwWidgetPayload(outputLines) || parseBtwWebuiPayloadRaw(statusEntries.get(BTW_WEBUI_STATUS_KEY)) || latestBtwWidgetPayload;
9136
+ if (payload?.id && payload.id === btwWidgetDismissedId) return null;
9137
+ return payload;
9138
+ }
9139
+
9140
+ function btwStatusLabel(payload) {
9141
+ switch (payload?.status) {
9142
+ case "done": return "Done";
9143
+ case "error": return "Error";
9144
+ case "aborted": return "Aborted";
9145
+ case "streaming": return "Answering…";
9146
+ default: return "Starting…";
9147
+ }
9148
+ }
9149
+
9150
+ function btwAnswerLines(payload) {
9151
+ const text = payload?.error || payload?.answer || (payload?.status === "loading" ? "Starting side request…" : "Waiting for model output…");
9152
+ return String(text || "").replace(/\r\n?/g, "\n").split("\n");
9153
+ }
9154
+
9155
+ function focusBtwWidgetInput() {
9156
+ const input = document.querySelector(".btw-widget-input");
9157
+ if (!input) return;
9158
+ try {
9159
+ input.focus({ preventScroll: true });
9160
+ } catch {
9161
+ input.focus();
9162
+ }
9163
+ }
9164
+
9165
+ function openBtwComposerWidget() {
9166
+ btwWidgetComposerOpen = true;
9167
+ btwWidgetDismissedId = "";
9168
+ btwWidgetFocusAfterRender = true;
9169
+ setComposerActionsOpen(false);
9170
+ setPublishMenuOpen(false);
9171
+ setNativeCommandMenuOpen(false);
9172
+ setAppRunnerMenuOpen(false);
9173
+ setOptionsMenuOpen(false);
9174
+ renderWidgets();
9175
+ }
9176
+
9177
+ function closeBtwOutputWidget() {
9178
+ const payload = currentBtwWidgetPayload();
9179
+ if (payload?.id) btwWidgetDismissedId = payload.id;
9180
+ widgets.delete(BTW_OUTPUT_WIDGET_KEY);
9181
+ widgets.delete(BTW_FOOTER_WIDGET_KEY);
9182
+ statusEntries.delete(BTW_WEBUI_STATUS_KEY);
9183
+ latestBtwWidgetPayload = null;
9184
+ btwWidgetComposerOpen = false;
9185
+ btwWidgetInputDraft = "";
9186
+ renderWidgets();
9187
+ renderStatus();
9188
+ }
9189
+
9190
+ async function copyBtwWidgetAnswer(button) {
9191
+ const payload = currentBtwWidgetPayload();
9192
+ const answer = String(payload?.answer || payload?.error || "").trim();
9193
+ if (!answer) return;
9194
+ const original = button?.textContent || "Copy";
9195
+ try {
9196
+ await navigator.clipboard.writeText(answer);
9197
+ if (button) button.textContent = "Copied";
9198
+ setTimeout(() => { if (button) button.textContent = original; }, 1600);
9199
+ } catch (error) {
9200
+ addEvent(`copy /btw answer failed: ${error.message || String(error)}`, "error");
9201
+ }
9202
+ }
9203
+
9204
+ function base64UrlEncodeUtf8(value) {
9205
+ const bytes = new TextEncoder().encode(String(value || ""));
9206
+ let binary = "";
9207
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
9208
+ binary += String.fromCharCode(...bytes.slice(offset, offset + 0x8000));
9209
+ }
9210
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
9211
+ }
9212
+
9213
+ function btwTransferPayload(payload) {
9214
+ return {
9215
+ question: payload?.question || "",
9216
+ answer: payload?.answer || payload?.error || "",
9217
+ status: payload?.status || "done",
9218
+ model: payload?.model || "",
9219
+ generatedAt: payload?.generatedAt || 0,
9220
+ updatedAt: payload?.updatedAt || Date.now(),
9221
+ };
9222
+ }
9223
+
9224
+ function makeBtwTransferIcon() {
9225
+ const ns = "http://www.w3.org/2000/svg";
9226
+ const svg = document.createElementNS(ns, "svg");
9227
+ svg.setAttribute("class", "btw-transfer-icon");
9228
+ svg.setAttribute("viewBox", "0 0 24 24");
9229
+ svg.setAttribute("aria-hidden", "true");
9230
+ svg.setAttribute("focusable", "false");
9231
+ const bubble = document.createElementNS(ns, "path");
9232
+ bubble.setAttribute("d", "M4.5 5.75h8.75a2 2 0 0 1 2 2v3.5a2 2 0 0 1-2 2H9.8L6.5 15.6v-2.35h-2a2 2 0 0 1-2-2v-3.5a2 2 0 0 1 2-2Z");
9233
+ bubble.setAttribute("fill", "none");
9234
+ bubble.setAttribute("stroke", "currentColor");
9235
+ bubble.setAttribute("stroke-width", "1.9");
9236
+ bubble.setAttribute("stroke-linecap", "round");
9237
+ bubble.setAttribute("stroke-linejoin", "round");
9238
+ const arrow = document.createElementNS(ns, "path");
9239
+ arrow.setAttribute("d", "M13 17h7m0 0-2.8-2.8M20 17l-2.8 2.8");
9240
+ arrow.setAttribute("fill", "none");
9241
+ arrow.setAttribute("stroke", "currentColor");
9242
+ arrow.setAttribute("stroke-width", "2.15");
9243
+ arrow.setAttribute("stroke-linecap", "round");
9244
+ arrow.setAttribute("stroke-linejoin", "round");
9245
+ const line = document.createElementNS(ns, "path");
9246
+ line.setAttribute("d", "M6 9.4h5.6");
9247
+ line.setAttribute("fill", "none");
9248
+ line.setAttribute("stroke", "currentColor");
9249
+ line.setAttribute("stroke-width", "1.9");
9250
+ line.setAttribute("stroke-linecap", "round");
9251
+ svg.append(bubble, line, arrow);
9252
+ return svg;
9253
+ }
9254
+
9255
+ async function transferBtwContextToMain(button) {
9256
+ const payload = currentBtwWidgetPayload();
9257
+ const transferPayload = btwTransferPayload(payload);
9258
+ if (!transferPayload.question && !transferPayload.answer) return;
9259
+ const targetTabId = activeTabId;
9260
+ const liveSteer = !!currentState?.isStreaming;
9261
+ const original = button?.querySelector("span")?.textContent || "Transfer Context";
9262
+ const encoded = base64UrlEncodeUtf8(JSON.stringify(transferPayload));
9263
+ try {
9264
+ await sendPrompt("prompt", `/btw-transfer ${encoded}`, { targetTabId, throwOnError: true, streamingBehavior: liveSteer ? "steer" : undefined });
9265
+ const label = button?.querySelector("span");
9266
+ if (label) label.textContent = liveSteer ? "Steered" : "Transferred";
9267
+ addEvent(liveSteer
9268
+ ? "/btw context sent as live steering; it will be injected after the next agent action"
9269
+ : "/btw context transferred into the main conversation", "info");
9270
+ setTimeout(() => { if (label) label.textContent = original; }, 1800);
9271
+ } catch {
9272
+ // sendPrompt already reports the error.
9273
+ }
9274
+ }
9275
+
9276
+ function btwWidgetActionButton(label, handler, className = "") {
9277
+ const button = make("button", `release-npm-action ${className}`.trim(), label);
9278
+ button.type = "button";
9279
+ button.addEventListener("click", () => handler(button));
9280
+ return button;
9281
+ }
9282
+
9283
+ function renderBtwComposerForm() {
9284
+ const form = make("form", "btw-widget-composer");
9285
+ const input = make("textarea", "btw-widget-input");
9286
+ input.rows = 1;
9287
+ input.placeholder = "Ask a /btw side question…";
9288
+ input.value = btwWidgetInputDraft;
9289
+ input.setAttribute("aria-label", "Ask a /btw side question");
9290
+ input.addEventListener("input", () => { btwWidgetInputDraft = input.value; });
9291
+ input.addEventListener("keydown", (event) => {
9292
+ if (event.key === "Enter" && !event.shiftKey) {
9293
+ event.preventDefault();
9294
+ form.requestSubmit();
9295
+ }
9296
+ });
9297
+
9298
+ const submit = make("button", "release-npm-action btw-widget-send", "Ask /btw");
9299
+ submit.type = "submit";
9300
+ form.append(input, submit);
9301
+ form.addEventListener("submit", async (event) => {
9302
+ event.preventDefault();
9303
+ const question = input.value.trim();
9304
+ if (!question) {
9305
+ input.focus();
9306
+ return;
9307
+ }
9308
+ submit.disabled = true;
9309
+ const sent = await sendBtwQuestion(question);
9310
+ submit.disabled = false;
9311
+ if (!sent) return;
9312
+ btwWidgetInputDraft = "";
9313
+ input.value = "";
9314
+ input.focus({ preventScroll: true });
9315
+ });
9316
+ return form;
9317
+ }
9318
+
9319
+ function renderBtwOutputWidget() {
9320
+ const payload = currentBtwWidgetPayload();
9321
+ if (!payload && !btwWidgetComposerOpen) return null;
9322
+
9323
+ if (payload) latestBtwWidgetPayload = payload;
9324
+ const running = payload?.status === "loading" || payload?.status === "streaming";
9325
+ const lineCount = payload ? btwAnswerLines(payload).length : 0;
9326
+ const node = make("section", `widget release-npm-widget btw-widget${running ? " btw-live-widget" : " btw-done-widget"}`);
9327
+ node.setAttribute("aria-label", "/btw side-question output");
9328
+
9329
+ const header = make("div", "release-npm-header");
9330
+ const titleWrap = make("div", "release-npm-title-wrap");
9331
+ titleWrap.append(make("span", "release-npm-kicker", "/btw"), make("strong", "release-npm-title", payload ? btwStatusLabel(payload) : "Ready"));
9332
+
9333
+ const meta = make("div", "release-npm-meta");
9334
+ meta.append(make("span", `release-npm-pill btw-status ${payload?.status || "ready"}`.trim(), payload?.status || "ready"));
9335
+ if (payload?.model) meta.append(make("span", "release-npm-pill", payload.model));
9336
+
9337
+ const actions = make("div", "release-npm-actions");
9338
+ const transferButton = btwWidgetActionButton("", transferBtwContextToMain, "btw-transfer-action");
9339
+ transferButton.title = currentState?.isStreaming
9340
+ ? "Transfer this /btw question and answer as live steering after the next agent action"
9341
+ : "Transfer this /btw question and answer into the main conversation context";
9342
+ transferButton.append(makeBtwTransferIcon(), make("span", undefined, "Transfer Context"));
9343
+ transferButton.disabled = !payload || !(payload.answer || payload.error || payload.question);
9344
+ actions.append(
9345
+ transferButton,
9346
+ btwWidgetActionButton("Copy", copyBtwWidgetAnswer),
9347
+ btwWidgetActionButton("Close", closeBtwOutputWidget),
9348
+ );
9349
+ header.append(titleWrap, meta, actions);
9350
+
9351
+ const question = make("div", "btw-widget-question");
9352
+ question.append(make("span", "btw-widget-question-label", "Question"), make("span", "btw-widget-question-text", payload?.question || "Start or continue with the /btw input below."));
9353
+
9354
+ const streamHeader = releaseNpmStreamHeader(running ? "Live side answer" : "Side answer", lineCount, { live: running });
9355
+ const terminal = make("div", "release-npm-terminal btw-terminal");
9356
+ terminal.setAttribute("role", "log");
9357
+ terminal.setAttribute("aria-live", running ? "polite" : "off");
9358
+ for (const line of (payload ? btwAnswerLines(payload) : ["Type a side question below and press Enter to run it as /btw."])) appendReleaseNpmTerminalLine(terminal, line);
9359
+
9360
+ const note = payload?.status === "error"
9361
+ ? "The side request failed. The main conversation was not changed."
9362
+ : "Ephemeral answer · every message in this input is sent as /btw · not appended to the main transcript.";
9363
+ const controls = make("div", "release-npm-controls btw-controls", note);
9364
+ const outputDetails = renderReleaseNpmOutputDetails(`btw:${payload?.id || "composer"}`, streamHeader, terminal, controls);
9365
+ node.append(header, question, outputDetails, renderBtwComposerForm());
9366
+ requestAnimationFrame(() => {
9367
+ if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight;
9368
+ if (btwWidgetFocusAfterRender) {
9369
+ btwWidgetFocusAfterRender = false;
9370
+ focusBtwWidgetInput();
9371
+ }
9372
+ });
9373
+ return node;
9374
+ }
9375
+
9376
+ function handleBtwWebuiStatus(statusText) {
9377
+ const payload = parseBtwWebuiPayloadRaw(statusText);
9378
+ if (payload) latestBtwWidgetPayload = payload;
9379
+ renderWidgets();
9380
+ }
9381
+
9382
+ function remoteWebuiWidgetLines(lines = []) {
9383
+ return (Array.isArray(lines) ? lines : [])
9384
+ .map(stripAnsi)
9385
+ .map((line) => String(line ?? ""))
9386
+ .filter((line, index, array) => line.trim() || (index > 0 && index < array.length - 1));
9387
+ }
9388
+
9389
+ function mirrorRemoteWebuiWidgetToTranscript(widgetKey, lines = [], request = {}) {
9390
+ if (widgetKey !== "pi-remote-webui" || request.replayed) return;
9391
+ const content = remoteWebuiWidgetLines(lines).join("\n").trimEnd();
9392
+ if (!content) return;
9393
+ addTransientMessage({ role: "extension", title: "/remote", content, level: "info", widgetKey });
9394
+ }
9395
+
8713
9396
  function renderWidgets() {
9397
+ if (deferUiRenderDuringPointerActivation("widgets", renderWidgets)) return;
8714
9398
  elements.widgetArea.replaceChildren();
8715
9399
  const releaseOutput = renderReleaseNpmOutputWidget();
8716
9400
  if (releaseOutput) elements.widgetArea.append(releaseOutput);
@@ -8720,14 +9404,19 @@ function renderWidgets() {
8720
9404
  if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
8721
9405
  const releaseAurLog = renderReleaseAurLogWidget();
8722
9406
  if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
9407
+ const workflowSubprocessWidget = renderWorkflowSubprocessWidget();
9408
+ if (workflowSubprocessWidget) elements.widgetArea.append(workflowSubprocessWidget);
8723
9409
  const appRunnerWidget = renderAppRunnerWidget();
8724
9410
  if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
9411
+ const btwWidget = renderBtwOutputWidget();
9412
+ if (btwWidget) elements.widgetArea.append(btwWidget);
8725
9413
 
8726
9414
  for (const [key, value] of widgets) {
8727
9415
  const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
8728
9416
  if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
8729
- if (widgetFeatureId && key !== "todo-progress") continue;
9417
+ if (widgetFeatureId && optionalFeatureWidgetHasSpecializedRenderer(key)) continue;
8730
9418
  const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
9419
+ if (key === "pi-remote-webui") continue;
8731
9420
  const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
8732
9421
  if (specialized) {
8733
9422
  elements.widgetArea.append(specialized);
@@ -10330,6 +11019,7 @@ function renderQueueGroup(label, items, tone) {
10330
11019
  }
10331
11020
 
10332
11021
  function renderQueue(event) {
11022
+ if (deferUiRenderDuringPointerActivation("queue", () => renderQueue(event))) return;
10333
11023
  const snapshot = normalizeQueuedMessages(event);
10334
11024
  const tabId = event?.tabId || activeTabId;
10335
11025
  if (tabId) latestQueuedMessagesByTab.set(tabId, snapshot);
@@ -11281,6 +11971,7 @@ function renderActionFeedbackControls(bubble, message, messageIndex) {
11281
11971
  }
11282
11972
 
11283
11973
  function renderFeedbackTray() {
11974
+ if (deferUiRenderDuringPointerActivation("feedback-tray", renderFeedbackTray)) return;
11284
11975
  const items = queuedActionFeedback();
11285
11976
  const hasItems = items.length > 0;
11286
11977
  elements.feedbackTray.hidden = !hasItems;
@@ -11753,6 +12444,7 @@ function findStickyUserPromptTarget(targets = userPromptTargets()) {
11753
12444
  }
11754
12445
 
11755
12446
  function updateStickyUserPromptButton() {
12447
+ if (deferUiRenderDuringPointerActivation("sticky-user-prompt", updateStickyUserPromptButton)) return;
11756
12448
  const button = elements.stickyUserPromptButton;
11757
12449
  if (!button) return;
11758
12450
  const targets = userPromptTargets();
@@ -12902,6 +13594,7 @@ function pruneDisconnectedLiveToolCards() {
12902
13594
  }
12903
13595
 
12904
13596
  function renderAllMessages({ preserveScroll = false, forceRebuild = false } = {}) {
13597
+ if (deferUiRenderDuringPointerActivation("messages", () => renderAllMessages({ preserveScroll, forceRebuild }))) return;
12905
13598
  const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
12906
13599
  const previousScrollTop = elements.chat.scrollTop;
12907
13600
  const transcriptItems = orderedTranscriptItems();
@@ -13073,6 +13766,7 @@ function scheduleChatFollowScroll() {
13073
13766
  }
13074
13767
 
13075
13768
  function scrollChatToBottom({ force = false } = {}) {
13769
+ if (deferChatFollowScrollDuringPointerActivation({ force })) return;
13076
13770
  if (force) autoFollowChat = true;
13077
13771
  if (!autoFollowChat) {
13078
13772
  updateJumpToLatestButton();
@@ -13128,11 +13822,47 @@ function sendPromptFromModeButton(kind, button) {
13128
13822
  sendPrompt(kind);
13129
13823
  }
13130
13824
 
13825
+ async function sendBtwQuestion(question, { clearComposerDraft = false } = {}) {
13826
+ const cleanQuestion = String(question || "").trim();
13827
+ if (!cleanQuestion) return false;
13828
+ const message = /^\/btw(?:\s|$)/.test(cleanQuestion) ? cleanQuestion : `/btw ${cleanQuestion}`;
13829
+ const targetTabId = activeTabId;
13830
+ btwWidgetComposerOpen = true;
13831
+ btwWidgetDismissedId = "";
13832
+ try {
13833
+ await sendPrompt("prompt", message, { targetTabId, throwOnError: true });
13834
+ } catch {
13835
+ return false;
13836
+ }
13837
+ if (!targetTabId) return true;
13838
+ if (clearComposerDraft) {
13839
+ if (targetTabId === activeTabId) {
13840
+ elements.promptInput.value = "";
13841
+ resizePromptInput();
13842
+ hideCommandSuggestions();
13843
+ saveActiveDraft();
13844
+ } else {
13845
+ tabDrafts.set(targetTabId, "");
13846
+ }
13847
+ }
13848
+ return true;
13849
+ }
13850
+
13851
+ async function sendBtwPromptFromButton() {
13852
+ const question = String(elements.promptInput.value || "").trim();
13853
+ if (!question) {
13854
+ openBtwComposerWidget();
13855
+ return;
13856
+ }
13857
+ await sendBtwQuestion(question, { clearComposerDraft: true });
13858
+ }
13859
+
13131
13860
  function setPublishMenuOpen(open) {
13132
13861
  publishMenuOpen = !!open;
13133
13862
  elements.publishButton.setAttribute("aria-expanded", publishMenuOpen ? "true" : "false");
13134
13863
  elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
13135
13864
  elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
13865
+ scheduleMobileDropdownScrollBoundsUpdate();
13136
13866
  }
13137
13867
 
13138
13868
  function setNativeCommandMenuOpen(open) {
@@ -13140,6 +13870,7 @@ function setNativeCommandMenuOpen(open) {
13140
13870
  elements.nativeCommandMenuButton.setAttribute("aria-expanded", nativeCommandMenuOpen ? "true" : "false");
13141
13871
  elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
13142
13872
  elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
13873
+ scheduleMobileDropdownScrollBoundsUpdate();
13143
13874
  }
13144
13875
 
13145
13876
  function setAppRunnerMenuOpen(open) {
@@ -13147,6 +13878,7 @@ function setAppRunnerMenuOpen(open) {
13147
13878
  elements.appRunnerMenuButton?.setAttribute("aria-expanded", appRunnerMenuOpen ? "true" : "false");
13148
13879
  elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
13149
13880
  elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
13881
+ scheduleMobileDropdownScrollBoundsUpdate();
13150
13882
  }
13151
13883
 
13152
13884
  function setOptionsMenuOpen(open) {
@@ -13154,6 +13886,7 @@ function setOptionsMenuOpen(open) {
13154
13886
  elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
13155
13887
  elements.optionsMenuButton.classList.toggle("menu-open", optionsMenuOpen);
13156
13888
  elements.optionsMenuButton.parentElement?.classList.toggle("open", optionsMenuOpen);
13889
+ scheduleMobileDropdownScrollBoundsUpdate();
13157
13890
  }
13158
13891
 
13159
13892
  function optionalFeatureIdForCommand(name) {
@@ -13309,15 +14042,18 @@ function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force =
13309
14042
  }
13310
14043
 
13311
14044
  function updateOptionalFeatureAvailability() {
14045
+ optionalFeatureAvailability.btwCommand = hasAvailableCommand("btw") || optionalFeatureAvailability.btwCommand || statusEntries.has(BTW_WEBUI_STATUS_KEY) || widgets.has(BTW_OUTPUT_WIDGET_KEY);
13312
14046
  optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
13313
14047
  optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
13314
14048
  optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
14049
+ optionalFeatureAvailability.workflows = hasAvailableCommand("workflow") || hasAvailableCommand("workflow-clear") || optionalFeatureAvailability.workflows || widgets.has("workflow") || widgets.has("workflow:subprocess");
13315
14050
  optionalFeatureAvailability.safetyGuard = hasAvailableCommand("safety-guard") || optionalFeatureAvailability.safetyGuard || statusEntries.has("safety-guard");
13316
14051
  optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
13317
14052
  optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY);
13318
14053
  optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
13319
14054
  optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
13320
14055
  optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
14056
+ optionalFeatureAvailability.remoteWebui = hasAvailableCommand("remote") || optionalFeatureAvailability.remoteWebui || statusEntries.has("pi-remote-webui") || widgets.has("pi-remote-webui");
13321
14057
  optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
13322
14058
  requestGitFooterWebuiPayload();
13323
14059
  renderOptionalFeatureControls();
@@ -13339,12 +14075,19 @@ function optionalFeatureStatus(featureId) {
13339
14075
  }
13340
14076
 
13341
14077
  function optionalFeatureWidgetFeatureId(key) {
14078
+ if (key.startsWith("btw:")) return "btwCommand";
13342
14079
  if (key.startsWith("release-npm:")) return "releaseNpm";
13343
14080
  if (key.startsWith("release-aur:")) return "releaseAur";
14081
+ if (key === "workflow" || key.startsWith("workflow:")) return "workflows";
13344
14082
  if (key === "todo-progress") return "todoProgressWidget";
14083
+ if (key === "pi-remote-webui") return "remoteWebui";
13345
14084
  return null;
13346
14085
  }
13347
14086
 
14087
+ function optionalFeatureWidgetHasSpecializedRenderer(key) {
14088
+ return key.startsWith("btw:") || key.startsWith("release-npm:") || key.startsWith("release-aur:") || key === "workflow:subprocess";
14089
+ }
14090
+
13348
14091
  function renderOptionalFeaturePanel() {
13349
14092
  if (!elements.optionalFeaturesBox) return;
13350
14093
  elements.optionalFeaturesBox.replaceChildren();
@@ -13405,6 +14148,16 @@ function renderOptionalFeaturePanel() {
13405
14148
  }
13406
14149
 
13407
14150
  function renderOptionalFeatureControls() {
14151
+ const hasBtwCommand = isOptionalFeatureEnabled("btwCommand");
14152
+ if (elements.btwButton) {
14153
+ elements.btwButton.hidden = !hasBtwCommand;
14154
+ setOptionalControlState(
14155
+ elements.btwButton,
14156
+ hasBtwCommand,
14157
+ optionalFeatureUnavailableMessage("btwCommand"),
14158
+ );
14159
+ }
14160
+
13408
14161
  const hasGitWorkflow = isOptionalFeatureEnabled("gitWorkflow");
13409
14162
  elements.gitWorkflowButton.hidden = !hasGitWorkflow;
13410
14163
  setOptionalControlState(
@@ -13449,6 +14202,16 @@ function renderOptionalFeatureControls() {
13449
14202
  );
13450
14203
  }
13451
14204
 
14205
+ const hasRemoteWebuiCommand = isOptionalFeatureEnabled("remoteWebui") && hasAvailableCommand("remote");
14206
+ if (elements.optionsRemoteButton) {
14207
+ elements.optionsRemoteButton.hidden = !hasRemoteWebuiCommand;
14208
+ setOptionalControlState(
14209
+ elements.optionsRemoteButton,
14210
+ hasRemoteWebuiCommand,
14211
+ optionalFeatureUnavailableMessage("remoteWebui"),
14212
+ );
14213
+ }
14214
+
13452
14215
  renderOptionalFeaturePanel();
13453
14216
  }
13454
14217
 
@@ -14751,6 +15514,10 @@ async function refreshState(tabContext = activeTabContext()) {
14751
15514
  if (!isCurrentTabContext(tabContext)) return;
14752
15515
  const previousState = currentState;
14753
15516
  currentState = response.data || null;
15517
+ if (latestMessages.length) {
15518
+ latestMessagesSessionKey = resolveMessagesSessionKey(tabContext.tabId);
15519
+ cacheMessagesForTab(tabContext.tabId, latestMessages, latestMessagesSessionKey);
15520
+ }
14754
15521
  const shouldRefreshGitFooter = gitFooterRelevantStateChanged(previousState, currentState);
14755
15522
  syncActiveTabActivityFromState(currentState);
14756
15523
  syncRunIndicatorFromState(currentState);
@@ -14904,7 +15671,32 @@ async function refreshFooterData(tabContext = activeTabContext()) {
14904
15671
 
14905
15672
  // Session key of the last applied transcript fetch; deltas are only
14906
15673
  // attempted while the tab+session is unchanged.
14907
- let latestMessagesSessionKey = "";
15674
+ function resolveMessagesSessionKey(tabId = activeTabId) {
15675
+ if (!tabId) return "";
15676
+ const stateSessionId = tabId === activeTabId ? currentState?.sessionId : null;
15677
+ if (stateSessionId) return `${tabId}|${stateSessionId}`;
15678
+ if (latestMessagesSessionKey.startsWith(`${tabId}|`)) return latestMessagesSessionKey;
15679
+ const cached = tabMessagesCache.get(tabId);
15680
+ if (cached?.sessionKey?.startsWith(`${tabId}|`)) return cached.sessionKey;
15681
+ return `${tabId}|`;
15682
+ }
15683
+
15684
+ function cacheMessagesForTab(tabId = activeTabId, messages = latestMessages, sessionKey = latestMessagesSessionKey) {
15685
+ if (!tabId || !Array.isArray(messages)) return;
15686
+ const stateSessionKey = tabId === activeTabId && currentState?.sessionId ? `${tabId}|${currentState.sessionId}` : "";
15687
+ const resolvedSessionKey = stateSessionKey || (sessionKey?.startsWith(`${tabId}|`) ? sessionKey : resolveMessagesSessionKey(tabId));
15688
+ tabMessagesCache.set(tabId, { messages, sessionKey: resolvedSessionKey });
15689
+ }
15690
+
15691
+ function restoreCachedMessagesForActiveTab() {
15692
+ if (!activeTabId) return false;
15693
+ const cached = tabMessagesCache.get(activeTabId);
15694
+ if (!cached || !Array.isArray(cached.messages)) return false;
15695
+ latestMessages = cached.messages;
15696
+ latestMessagesSessionKey = cached.sessionKey || resolveMessagesSessionKey(activeTabId);
15697
+ renderMessages(latestMessages);
15698
+ return true;
15699
+ }
14908
15700
 
14909
15701
  function messagesLookEqual(a, b) {
14910
15702
  return !!a && !!b && a.role === b.role && String(a.timestamp || "") === String(b.timestamp || "")
@@ -14933,7 +15725,7 @@ function mergeMessagesDelta(previous, data) {
14933
15725
  async function refreshMessages(tabContext = activeTabContext()) {
14934
15726
  if (!tabContext.tabId) return;
14935
15727
  const previousMessages = latestMessages;
14936
- const sessionKey = `${tabContext.tabId}|${currentState?.sessionId || ""}`;
15728
+ const sessionKey = resolveMessagesSessionKey(tabContext.tabId);
14937
15729
  let nextMessages = null;
14938
15730
  if (previousMessages.length > 1 && sessionKey === latestMessagesSessionKey) {
14939
15731
  // Delta fetch with a one-message overlap: the last known message is
@@ -14949,6 +15741,7 @@ async function refreshMessages(tabContext = activeTabContext()) {
14949
15741
  }
14950
15742
  latestMessages = nextMessages;
14951
15743
  latestMessagesSessionKey = sessionKey;
15744
+ cacheMessagesForTab(tabContext.tabId, latestMessages, latestMessagesSessionKey);
14952
15745
  const preserveLiveStream = liveStreamRenderActive();
14953
15746
  if (!preserveLiveStream) resetStreamBubble();
14954
15747
  renderMessages(latestMessages);
@@ -14989,7 +15782,7 @@ async function refreshModels(tabContext = activeTabContext()) {
14989
15782
  syncModelSelectToState();
14990
15783
  renderFooter();
14991
15784
  renderFeedbackTray();
14992
- if (elements.commandPaletteDialog?.open) renderCommandPalette();
15785
+ if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
14993
15786
  }
14994
15787
 
14995
15788
  function syncModelSelectToState() {
@@ -15466,7 +16259,7 @@ async function refreshCommands(tabContext = activeTabContext()) {
15466
16259
  availableCommands = normalizeCommands(response.data?.commands || []);
15467
16260
  updateOptionalFeatureAvailability();
15468
16261
  renderCommands();
15469
- if (elements.commandPaletteDialog?.open) renderCommandPalette();
16262
+ if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
15470
16263
  }
15471
16264
 
15472
16265
  function paletteText(value) {
@@ -15563,12 +16356,14 @@ function setCommandPaletteIndex(index) {
15563
16356
  renderCommandPaletteList();
15564
16357
  }
15565
16358
 
15566
- function renderCommandPaletteList() {
16359
+ function renderCommandPaletteList({ preserveScroll = false } = {}) {
15567
16360
  const list = elements.commandPaletteList;
15568
16361
  if (!list) return;
16362
+ const scrollTop = preserveScroll ? list.scrollTop : 0;
15569
16363
  list.replaceChildren();
15570
16364
  if (!commandPaletteItems.length) {
15571
16365
  list.append(make("div", "command-palette-empty muted", "No matching actions."));
16366
+ if (preserveScroll) list.scrollTop = scrollTop;
15572
16367
  return;
15573
16368
  }
15574
16369
  commandPaletteItems.forEach((item, index) => {
@@ -15584,14 +16379,18 @@ function renderCommandPaletteList() {
15584
16379
  );
15585
16380
  list.append(button);
15586
16381
  });
16382
+ if (preserveScroll) {
16383
+ list.scrollTop = scrollTop;
16384
+ return;
16385
+ }
15587
16386
  const active = list.children[commandPaletteIndex];
15588
16387
  active?.scrollIntoView({ block: "nearest" });
15589
16388
  }
15590
16389
 
15591
- function renderCommandPalette() {
16390
+ function renderCommandPalette({ preserveScroll = false } = {}) {
15592
16391
  commandPaletteItems = filteredCommandPaletteItems();
15593
16392
  if (commandPaletteIndex >= commandPaletteItems.length) commandPaletteIndex = 0;
15594
- renderCommandPaletteList();
16393
+ renderCommandPaletteList({ preserveScroll });
15595
16394
  }
15596
16395
 
15597
16396
  function openCommandPalette(initialQuery = "") {
@@ -16068,7 +16867,7 @@ async function sendUserBashCommand(parsed, { usesPromptInput = false, targetTabI
16068
16867
  await runUserBashCommand(parsed, { usesPromptInput, targetTabId });
16069
16868
  }
16070
16869
 
16071
- async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = activeTabId, throwOnError = false } = {}) {
16870
+ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = activeTabId, throwOnError = false, streamingBehavior } = {}) {
16072
16871
  const usesPromptInput = explicitMessage === undefined;
16073
16872
  const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
16074
16873
  const originalMessage = String(rawMessage || "").trim();
@@ -16114,7 +16913,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
16114
16913
  response = await api("/api/follow-up", { method: "POST", body: bodyBase, tabId: targetTabId });
16115
16914
  } else {
16116
16915
  const body = { ...bodyBase };
16117
- if (targetWasStreaming) body.streamingBehavior = busyBehavior;
16916
+ if (targetWasStreaming) body.streamingBehavior = streamingBehavior || busyBehavior;
16118
16917
  response = await api("/api/prompt", { method: "POST", body, tabId: targetTabId });
16119
16918
  }
16120
16919
  applyResponseTab(response);
@@ -16167,7 +16966,7 @@ function hasQueuedDialogRequest(id) {
16167
16966
 
16168
16967
  function removeQueuedDialogRequests(ids = []) {
16169
16968
  const idSet = new Set(ids.map((id) => String(id)).filter(Boolean));
16170
- if (idSet.size === 0) return;
16969
+ if (idSet.size === 0) return false;
16171
16970
  for (let i = dialogQueue.length - 1; i >= 0; i -= 1) {
16172
16971
  if (idSet.has(String(dialogQueue[i]?.id || ""))) dialogQueue.splice(i, 1);
16173
16972
  }
@@ -16175,7 +16974,9 @@ function removeQueuedDialogRequests(ids = []) {
16175
16974
  if (elements.dialog.open) elements.dialog.close();
16176
16975
  activeDialog = null;
16177
16976
  showNextDialog();
16977
+ return true;
16178
16978
  }
16979
+ return false;
16179
16980
  }
16180
16981
 
16181
16982
  function handleExtensionUiRequest(request) {
@@ -16197,16 +16998,25 @@ function handleExtensionUiRequest(request) {
16197
16998
  statusEntries.delete(statusKey);
16198
16999
  }
16199
17000
  if (statusKey === STATS_WEBUI_STATUS_KEY) handleStatsWebuiStatus(request.statusText);
17001
+ if (statusKey === BTW_WEBUI_STATUS_KEY) handleBtwWebuiStatus(request.statusText);
16200
17002
  updateOptionalFeatureAvailability();
16201
17003
  renderStatus();
16202
17004
  return;
16203
17005
  }
16204
- case "setWidget":
16205
- if (Array.isArray(request.widgetLines)) widgets.set(request.widgetKey || request.id, request);
16206
- else widgets.delete(request.widgetKey || request.id);
17006
+ case "setWidget": {
17007
+ const widgetKey = request.widgetKey || request.id;
17008
+ if (widgetKey === "pi-remote-webui") {
17009
+ widgets.delete(widgetKey);
17010
+ if (Array.isArray(request.widgetLines)) mirrorRemoteWebuiWidgetToTranscript(widgetKey, request.widgetLines, request);
17011
+ } else if (Array.isArray(request.widgetLines)) {
17012
+ widgets.set(widgetKey, request);
17013
+ } else {
17014
+ widgets.delete(widgetKey);
17015
+ }
16207
17016
  updateOptionalFeatureAvailability();
16208
17017
  renderWidgets();
16209
17018
  return;
17019
+ }
16210
17020
  case "setTitle":
16211
17021
  if (request.title) document.title = request.title;
16212
17022
  return;
@@ -16239,6 +17049,7 @@ function handleExtensionUiRequest(request) {
16239
17049
  async function sendDialogResponse(payload) {
16240
17050
  const { tabId = activeTabId, ...body } = payload;
16241
17051
  const tabContext = activeTabContext(tabId);
17052
+ const responseId = String(body.id || "");
16242
17053
  try {
16243
17054
  const response = await api("/api/extension-ui-response", { method: "POST", body, tabId });
16244
17055
  if (!applyResponseTab(response) && decrementTabPendingBlockerCount(tabId)) renderTabs();
@@ -16246,6 +17057,7 @@ async function sendDialogResponse(payload) {
16246
17057
  if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
16247
17058
  } finally {
16248
17059
  if (!isCurrentTabContext(tabContext)) return;
17060
+ if (responseId && activeDialog && String(activeDialog.id || "") !== responseId) return;
16249
17061
  if (elements.dialog.open) elements.dialog.close();
16250
17062
  activeDialog = null;
16251
17063
  if (runIndicatorIsActive()) setRunIndicatorActivity("Continuing after your response…");
@@ -16336,6 +17148,7 @@ function handleInactiveTabEvent(event) {
16336
17148
 
16337
17149
  function handleEvent(event) {
16338
17150
  ingestEventTabActivity(event);
17151
+ trackAutoRetryStateFromEvent(event);
16339
17152
  trackSkillsFromEvent(event);
16340
17153
  if (!eventTargetsActiveTab(event)) {
16341
17154
  handleInactiveTabEvent(event);
@@ -16375,6 +17188,10 @@ function handleEvent(event) {
16375
17188
  clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
16376
17189
  statusEntries.clear();
16377
17190
  widgets.clear();
17191
+ latestBtwWidgetPayload = null;
17192
+ btwWidgetDismissedId = "";
17193
+ btwWidgetComposerOpen = false;
17194
+ btwWidgetInputDraft = "";
16378
17195
  resetOptionalFeatureAvailability();
16379
17196
  renderStatus();
16380
17197
  renderWidgets();
@@ -16390,6 +17207,14 @@ function handleEvent(event) {
16390
17207
  removeQueuedDialogRequests(event.ids || []);
16391
17208
  addEvent(`cancelled ${event.ids?.length || 0} pending extension UI request(s)`, "warn");
16392
17209
  break;
17210
+ case "webui_extension_ui_resolved": {
17211
+ const closedActiveDialog = removeQueuedDialogRequests([event.id]);
17212
+ if (closedActiveDialog) {
17213
+ addEvent("extension UI request resolved");
17214
+ if (runIndicatorIsActive() && !activeDialog) setRunIndicatorActivity("Continuing after extension UI response…");
17215
+ }
17216
+ break;
17217
+ }
16393
17218
  case "webui_app_runner_update":
16394
17219
  setAppRunnerData(event.tabId || activeTabId, { cwd: event.cwd, activeRun: event.activeRun });
16395
17220
  renderAppRunnerControls();
@@ -16719,6 +17544,7 @@ elements.busyPromptBehaviorMenu?.addEventListener("keydown", (event) => {
16719
17544
  });
16720
17545
  elements.steerButton.addEventListener("click", () => sendPromptFromModeButton("steer", elements.steerButton));
16721
17546
  elements.followUpButton.addEventListener("click", () => sendPromptFromModeButton("follow-up", elements.followUpButton));
17547
+ elements.btwButton?.addEventListener("click", () => sendBtwPromptFromButton());
16722
17548
  elements.terminalTabsToggleButton.addEventListener("click", () => {
16723
17549
  setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
16724
17550
  });
@@ -16894,6 +17720,7 @@ elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu(
16894
17720
  elements.optionsCommandPaletteButton.addEventListener("click", () => openCommandPalette());
16895
17721
  elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
16896
17722
  elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
17723
+ elements.optionsRemoteButton.addEventListener("click", () => runNativeCommandMenu("/remote"));
16897
17724
  elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
16898
17725
  elements.optionsCloneButton.addEventListener("click", () => runNativeCommandMenu("/clone"));
16899
17726
  elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandMenu("/settings"));
@@ -16954,6 +17781,7 @@ elements.commandPaletteDialog?.addEventListener("cancel", (event) => {
16954
17781
  event.preventDefault();
16955
17782
  closeCommandPalette();
16956
17783
  });
17784
+ elements.commandPaletteCloseButton?.addEventListener("click", closeCommandPalette);
16957
17785
  elements.commandPaletteInput?.addEventListener("input", () => {
16958
17786
  commandPaletteIndex = 0;
16959
17787
  renderCommandPalette();
@@ -16986,11 +17814,104 @@ elements.editRetryCancelButton?.addEventListener("click", closeEditRetryDialog);
16986
17814
  elements.editRetryForkButton?.addEventListener("click", () => submitEditRetry({ send: false }));
16987
17815
  elements.editRetrySendButton?.addEventListener("click", () => submitEditRetry({ send: true }));
16988
17816
 
16989
- function resetAbortLongPressAffordance() {
17817
+ function abortButtonHoldSeconds() {
17818
+ return String(Math.round(ABORT_LONG_PRESS_MS / 1000));
17819
+ }
17820
+
17821
+ function abortButtonReadyTitle() {
17822
+ return `Hold Esc or the Abort button for ${abortButtonHoldSeconds()} seconds to abort the active Pi run`;
17823
+ }
17824
+
17825
+ function clearAbortLongPressResetTimer() {
17826
+ clearTimeout(abortLongPressResetTimer);
17827
+ abortLongPressResetTimer = null;
17828
+ }
17829
+
17830
+ function clearAbortLongPressCompletionTimers() {
16990
17831
  clearTimeout(abortLongPressTimer);
17832
+ clearInterval(abortLongPressTickTimer);
16991
17833
  abortLongPressTimer = null;
17834
+ abortLongPressTickTimer = null;
17835
+ }
17836
+
17837
+ function isAbortLongPressActive() {
17838
+ return abortLongPressStartedAt > 0;
17839
+ }
17840
+
17841
+ function abortLongPressRemainingMs() {
17842
+ if (!abortLongPressStartedAt || !abortLongPressDeadlineAt) return ABORT_LONG_PRESS_MS;
17843
+ return Math.max(0, abortLongPressDeadlineAt - performance.now());
17844
+ }
17845
+
17846
+ function formatAbortLongPressRemaining(ms) {
17847
+ return (Math.ceil(Math.max(0, ms) / 100) / 10).toFixed(1);
17848
+ }
17849
+
17850
+ function abortLongPressLabel() {
17851
+ const remaining = formatAbortLongPressRemaining(abortLongPressRemainingMs());
17852
+ return abortLongPressSource === "escape" ? `Hold Esc ${remaining}s` : `Hold ${remaining}s`;
17853
+ }
17854
+
17855
+ function renderAbortLongPressAffordance() {
17856
+ const label = abortLongPressLabel();
17857
+ elements.abortButton.textContent = label;
17858
+ elements.abortButton.title = `${label} more to abort the active Pi run`;
17859
+ elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
17860
+ }
17861
+
17862
+ function completeAbortLongPress() {
17863
+ if (!isAbortLongPressActive()) return;
17864
+ if (abortLongPressReleasePending) return;
17865
+ const source = abortLongPressSource;
17866
+ clearAbortLongPressResetTimer();
17867
+ clearAbortLongPressCompletionTimers();
17868
+ abortLongPressHandled = true;
17869
+ if (isAbortAvailable()) abortActiveRun({ source });
17870
+ else {
17871
+ resetAbortLongPressAffordance();
17872
+ updateComposerModeButtons();
17873
+ }
17874
+ }
17875
+
17876
+ function tickAbortLongPressAffordance() {
17877
+ if (!isAbortLongPressActive()) return;
17878
+ renderAbortLongPressAffordance();
17879
+ if (abortLongPressRemainingMs() <= 0) completeAbortLongPress();
17880
+ }
17881
+
17882
+ function resumeAbortLongPressAffordance() {
17883
+ if (!isAbortLongPressActive()) return;
17884
+ clearAbortLongPressResetTimer();
17885
+ abortLongPressReleasePending = false;
17886
+ tickAbortLongPressAffordance();
17887
+ }
17888
+
17889
+ function scheduleAbortLongPressReleaseReset() {
17890
+ if (!isAbortLongPressActive()) return;
17891
+ abortLongPressReleasePending = true;
17892
+ clearAbortLongPressResetTimer();
17893
+ abortLongPressResetTimer = setTimeout(() => {
17894
+ abortLongPressResetTimer = null;
17895
+ if (!abortLongPressReleasePending) return;
17896
+ resetAbortLongPressAffordance();
17897
+ updateComposerModeButtons();
17898
+ }, ABORT_LONG_PRESS_RELEASE_GRACE_MS);
17899
+ }
17900
+
17901
+ function resetAbortLongPressAffordance() {
17902
+ clearAbortLongPressResetTimer();
17903
+ clearAbortLongPressCompletionTimers();
17904
+ abortLongPressStartedAt = 0;
17905
+ abortLongPressDeadlineAt = 0;
17906
+ abortLongPressSource = "long-press";
17907
+ abortLongPressReleasePending = false;
16992
17908
  elements.abortButton.classList.remove("long-pressing");
16993
- if (!abortRequestInFlight) elements.abortButton.textContent = "Abort";
17909
+ elements.abortButton.style.removeProperty("--abort-long-press-duration");
17910
+ if (!abortRequestInFlight) {
17911
+ elements.abortButton.textContent = "Abort";
17912
+ elements.abortButton.title = isAbortAvailable() ? abortButtonReadyTitle() : "Abort is available while Pi is running";
17913
+ elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
17914
+ }
16994
17915
  }
16995
17916
 
16996
17917
  async function abortActiveRun({ source = "button" } = {}) {
@@ -17026,31 +17947,41 @@ async function abortActiveRun({ source = "button" } = {}) {
17026
17947
  }
17027
17948
  }
17028
17949
 
17029
- function startAbortLongPress(event) {
17030
- if (!isAbortAvailable() || abortRequestInFlight) return;
17031
- if (event.button !== undefined && event.button !== 0) return;
17950
+ function startAbortLongPress(event, { source = "long-press" } = {}) {
17951
+ if (!isAbortAvailable() || abortRequestInFlight) return false;
17952
+ if (source !== "escape" && event?.button !== undefined && event.button !== 0) return false;
17953
+ if (isAbortLongPressActive()) {
17954
+ resumeAbortLongPressAffordance();
17955
+ return true;
17956
+ }
17032
17957
  resetAbortLongPressAffordance();
17033
17958
  abortLongPressHandled = false;
17959
+ abortLongPressReleasePending = false;
17960
+ abortLongPressSource = source;
17961
+ abortLongPressStartedAt = performance.now();
17962
+ abortLongPressDeadlineAt = abortLongPressStartedAt + ABORT_LONG_PRESS_MS;
17963
+ elements.abortButton.style.setProperty("--abort-long-press-duration", `${ABORT_LONG_PRESS_MS}ms`);
17034
17964
  elements.abortButton.classList.add("long-pressing");
17035
- elements.abortButton.textContent = "Hold…";
17036
- abortLongPressTimer = setTimeout(() => {
17037
- abortLongPressTimer = null;
17038
- abortLongPressHandled = true;
17039
- abortActiveRun({ source: "long-press" });
17040
- }, ABORT_LONG_PRESS_MS);
17965
+ renderAbortLongPressAffordance();
17966
+ abortLongPressTickTimer = setInterval(tickAbortLongPressAffordance, ABORT_LONG_PRESS_TICK_MS);
17967
+ abortLongPressTimer = setTimeout(tickAbortLongPressAffordance, ABORT_LONG_PRESS_MS + 10);
17968
+ return true;
17041
17969
  }
17042
17970
 
17043
17971
  elements.abortButton.addEventListener("pointerdown", startAbortLongPress);
17044
17972
  for (const eventName of ["pointerup", "pointerleave", "pointercancel", "blur"]) {
17045
17973
  elements.abortButton.addEventListener(eventName, resetAbortLongPressAffordance);
17046
17974
  }
17975
+ elements.abortButton.addEventListener("keydown", (event) => {
17976
+ if (event.key !== " " && event.key !== "Enter") return;
17977
+ if (startAbortLongPress(event)) event.preventDefault();
17978
+ });
17979
+ elements.abortButton.addEventListener("keyup", (event) => {
17980
+ if (event.key === " " || event.key === "Enter") resetAbortLongPressAffordance();
17981
+ });
17047
17982
  elements.abortButton.addEventListener("click", (event) => {
17048
- if (abortLongPressHandled) {
17049
- event.preventDefault();
17050
- abortLongPressHandled = false;
17051
- return;
17052
- }
17053
- abortActiveRun({ source: "button" });
17983
+ event.preventDefault();
17984
+ if (abortLongPressHandled) abortLongPressHandled = false;
17054
17985
  });
17055
17986
  elements.newSessionButton.addEventListener("click", async () => {
17056
17987
  setComposerActionsOpen(false);
@@ -17166,6 +18097,10 @@ elements.chat.addEventListener("scroll", () => {
17166
18097
  markTabOutputSeen();
17167
18098
  updateStickyUserPromptButton();
17168
18099
  }, { passive: true });
18100
+ document.addEventListener("pointerdown", beginPointerActivation, { capture: true, passive: true });
18101
+ document.addEventListener("pointerup", finishPointerActivation, { capture: true, passive: true });
18102
+ document.addEventListener("pointercancel", cancelPointerActivation, { capture: true, passive: true });
18103
+ window.addEventListener("blur", cancelPointerActivation, { passive: true });
17169
18104
  document.addEventListener("pointerdown", (event) => {
17170
18105
  if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
17171
18106
  clearOpenTerminalTabGroup(openTerminalTabGroupKey);
@@ -17386,12 +18321,14 @@ window.addEventListener("keydown", (event) => {
17386
18321
  window.addEventListener("keydown", handleNativeAppShortcut, { capture: true });
17387
18322
  document.addEventListener("visibilitychange", () => {
17388
18323
  if (document.visibilityState === "visible") scheduleForegroundReconcile("visibility resume", 0);
18324
+ else resetAbortLongPressAffordance();
17389
18325
  });
17390
18326
  window.addEventListener("pageshow", () => scheduleForegroundReconcile("page show", 0));
17391
18327
  window.addEventListener("focus", () => scheduleForegroundReconcile("window focus"));
17392
18328
  window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
17393
18329
  window.addEventListener("keydown", (event) => {
17394
18330
  if (event.key !== "Escape") return;
18331
+ if (event.defaultPrevented) return;
17395
18332
  if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open) return;
17396
18333
  if (publishMenuOpen) {
17397
18334
  setPublishMenuOpen(false);
@@ -17437,6 +18374,20 @@ window.addEventListener("keydown", (event) => {
17437
18374
  hideCommandSuggestions();
17438
18375
  return;
17439
18376
  }
18377
+ if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
18378
+ setSidePanelCollapsed(true);
18379
+ return;
18380
+ }
18381
+ if (isAbortAvailable()) {
18382
+ event.preventDefault();
18383
+ if (abortLongPressSource === "escape" && isAbortLongPressActive()) resumeAbortLongPressAffordance();
18384
+ else if (!event.repeat) startAbortLongPress(event, { source: "escape" });
18385
+ return;
18386
+ }
18387
+ if (event.repeat) {
18388
+ event.preventDefault();
18389
+ return;
18390
+ }
17440
18391
  if (document.activeElement === elements.promptInput && !elements.promptInput.value.trim() && doubleEscapeAction !== "none") {
17441
18392
  const now = Date.now();
17442
18393
  if (now - lastEmptyPromptEscapeTime < 500) {
@@ -17447,14 +18398,13 @@ window.addEventListener("keydown", (event) => {
17447
18398
  }
17448
18399
  lastEmptyPromptEscapeTime = now;
17449
18400
  }
17450
- if (isSidePanelOverlayView() && !document.body.classList.contains("side-panel-collapsed")) {
17451
- setSidePanelCollapsed(true);
17452
- return;
17453
- }
17454
- if (isAbortAvailable()) {
17455
- event.preventDefault();
17456
- abortActiveRun({ source: "escape" });
17457
- }
18401
+ });
18402
+ window.addEventListener("keyup", (event) => {
18403
+ if (event.key === "Escape" && abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
18404
+ }, { capture: true });
18405
+ window.addEventListener("blur", () => {
18406
+ if (abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
18407
+ else resetAbortLongPressAffordance();
17458
18408
  });
17459
18409
 
17460
18410
  elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);