@firstpick/pi-package-webui 0.2.7 → 0.2.9

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
@@ -47,6 +47,16 @@ const elements = {
47
47
  nativeCommandMenu: $("#nativeCommandMenu"),
48
48
  nativeSkillsButton: $("#nativeSkillsButton"),
49
49
  nativeToolsButton: $("#nativeToolsButton"),
50
+ optionsMenuButton: $("#optionsMenuButton"),
51
+ optionsMenu: $("#optionsMenu"),
52
+ optionsResumeButton: $("#optionsResumeButton"),
53
+ optionsReloadButton: $("#optionsReloadButton"),
54
+ optionsNameButton: $("#optionsNameButton"),
55
+ optionsCloneButton: $("#optionsCloneButton"),
56
+ optionsSettingsButton: $("#optionsSettingsButton"),
57
+ optionsExportButton: $("#optionsExportButton"),
58
+ optionsForkButton: $("#optionsForkButton"),
59
+ optionsTreeButton: $("#optionsTreeButton"),
50
60
  gitWorkflowPanel: $("#gitWorkflowPanel"),
51
61
  gitWorkflowTitle: $("#gitWorkflowTitle"),
52
62
  gitWorkflowHint: $("#gitWorkflowHint"),
@@ -81,6 +91,7 @@ const elements = {
81
91
  sidePanel: $("#sidePanel"),
82
92
  stateDetails: $("#stateDetails"),
83
93
  queueBox: $("#queueBox"),
94
+ commandSearchInput: $("#commandSearchInput"),
84
95
  commandsBox: $("#commandsBox"),
85
96
  eventLog: $("#eventLog"),
86
97
  dialog: $("#extensionDialog"),
@@ -148,6 +159,7 @@ let pathFastPicksLoadPromise = null;
148
159
  let mobileTabsExpanded = false;
149
160
  let openTerminalTabGroupKey = null;
150
161
  let nativeCommandMenuOpen = false;
162
+ let optionsMenuOpen = false;
151
163
  let availableCommands = [];
152
164
  let rawAvailableCommands = [];
153
165
  let commandSuggestions = [];
@@ -185,6 +197,11 @@ let blockedTabNotificationPermissionRequested = false;
185
197
  let blockedTabNotificationFallbackNoted = false;
186
198
  let agentDoneNotificationsEnabled = false;
187
199
  let thinkingOutputVisible = true;
200
+ let webuiSettings = {};
201
+ let autocompleteMaxVisible = 12;
202
+ let doubleEscapeAction = "none";
203
+ let treeFilterMode = "default";
204
+ let lastEmptyPromptEscapeTime = 0;
188
205
  let toolOutputGloballyExpanded = false;
189
206
  let agentDoneNotificationPermissionRequested = false;
190
207
  let agentDoneNotificationFallbackNoted = false;
@@ -205,6 +222,7 @@ let lastChatProgrammaticScrollAt = 0;
205
222
  let chatUserScrollIntentUntil = 0;
206
223
  let mobileFooterExpanded = false;
207
224
  let footerModelPickerOpen = false;
225
+ let footerThinkingPickerOpen = false;
208
226
  let publishMenuOpen = false;
209
227
  let maxVisualViewportHeight = 0;
210
228
  let abortRequestInFlight = false;
@@ -374,7 +392,25 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
374
392
  ["todo-progress-status", "todoProgressWidget"],
375
393
  ]);
376
394
  const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
377
- const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
395
+ const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "name", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
396
+ const SETTINGS_THINKING_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhigh"];
397
+ const SETTINGS_TRANSPORT_OPTIONS = ["sse", "websocket", "websocket-cached", "auto"];
398
+ const SETTINGS_HTTP_IDLE_TIMEOUT_OPTIONS = [
399
+ { value: "30000", label: "30 sec" },
400
+ { value: "60000", label: "1 min" },
401
+ { value: "120000", label: "2 min" },
402
+ { value: "300000", label: "5 min" },
403
+ { value: "0", label: "disabled" },
404
+ ];
405
+ const SETTINGS_DOUBLE_ESCAPE_OPTIONS = [
406
+ { value: "tree", label: "open /tree" },
407
+ { value: "fork", label: "open /fork" },
408
+ { value: "none", label: "do nothing" },
409
+ ];
410
+ const SETTINGS_TREE_FILTER_OPTIONS = ["default", "no-tools", "user-only", "labeled-only", "all"];
411
+ const SETTINGS_IMAGE_WIDTH_OPTIONS = ["60", "80", "120"];
412
+ const SETTINGS_EDITOR_PADDING_OPTIONS = ["0", "1", "2", "3"];
413
+ const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
378
414
  const optionalFeatureInstallInProgress = new Set();
379
415
  const gitFooterPayloadRefreshInFlightByTab = new Set();
380
416
 
@@ -696,11 +732,37 @@ function restoreThinkingVisibilitySetting() {
696
732
  renderThinkingVisibilityToggle();
697
733
  }
698
734
 
735
+ function clampAutocompleteMaxVisible(value) {
736
+ const number = Number(value);
737
+ if (!Number.isFinite(number)) return 12;
738
+ return Math.max(3, Math.min(20, Math.floor(number)));
739
+ }
740
+
741
+ function applyNativeSettingsForBrowser(settings = {}, { syncThinkingVisibility = false } = {}) {
742
+ if (!settings || typeof settings !== "object") return;
743
+ webuiSettings = { ...webuiSettings, ...settings, warnings: { ...(webuiSettings.warnings || {}), ...(settings.warnings || {}) } };
744
+ if (settings.autocompleteMaxVisible !== undefined) autocompleteMaxVisible = clampAutocompleteMaxVisible(settings.autocompleteMaxVisible);
745
+ if (SETTINGS_DOUBLE_ESCAPE_OPTIONS.some((option) => option.value === settings.doubleEscapeAction)) doubleEscapeAction = settings.doubleEscapeAction;
746
+ if (SETTINGS_TREE_FILTER_OPTIONS.includes(settings.treeFilterMode)) treeFilterMode = settings.treeFilterMode;
747
+ if (syncThinkingVisibility && typeof settings.hideThinkingBlock === "boolean") setThinkingOutputVisible(!settings.hideThinkingBlock);
748
+ }
749
+
750
+ async function refreshNativeSettings(tabContext = activeTabContext()) {
751
+ if (!tabContext.tabId) return;
752
+ const response = await api("/api/settings", { tabId: tabContext.tabId });
753
+ if (!isCurrentTabContext(tabContext)) return;
754
+ applyNativeSettingsForBrowser(response.data?.settings || {});
755
+ }
756
+
699
757
  function setComposerActionsOpen(open) {
700
758
  const shouldOpen = open && isMobileView();
701
759
  document.body.classList.toggle("composer-actions-open", shouldOpen);
702
760
  elements.composerActionsButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
703
- if (!shouldOpen) setPublishMenuOpen(false);
761
+ if (!shouldOpen) {
762
+ setPublishMenuOpen(false);
763
+ setNativeCommandMenuOpen(false);
764
+ setOptionsMenuOpen(false);
765
+ }
704
766
  }
705
767
 
706
768
  function isUserBashActive(tabId = activeTabId) {
@@ -760,23 +822,63 @@ function updateComposerModeButtons() {
760
822
  document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
761
823
  }
762
824
 
825
+ function isFooterPickerOpen() {
826
+ return footerModelPickerOpen || footerThinkingPickerOpen;
827
+ }
828
+
829
+ function footerActivePickerTarget() {
830
+ if (footerThinkingPickerOpen) return elements.statusBar.querySelector(".footer-thinking.footer-meta-action");
831
+ if (footerModelPickerOpen) return elements.statusBar.querySelector(".footer-model.footer-meta-action, .footer-tui-model");
832
+ return null;
833
+ }
834
+
835
+ function clearFooterPickerPosition() {
836
+ document.documentElement.style.removeProperty("--footer-model-picker-bottom");
837
+ document.documentElement.style.removeProperty("--footer-model-picker-left");
838
+ document.documentElement.style.removeProperty("--footer-model-picker-right");
839
+ }
840
+
763
841
  function updateFooterModelPickerPosition() {
764
- if (!footerModelPickerOpen || !isMobileView()) {
765
- document.documentElement.style.removeProperty("--footer-model-picker-bottom");
842
+ if (!isFooterPickerOpen()) {
843
+ clearFooterPickerPosition();
766
844
  return;
767
845
  }
768
- const viewportHeight = window.innerHeight || window.visualViewport?.height || document.documentElement.clientHeight;
769
- const statusTop = elements.statusBar.getBoundingClientRect().top;
770
- const bottom = Math.max(8, Math.round(viewportHeight - statusTop + 6));
771
- document.documentElement.style.setProperty("--footer-model-picker-bottom", `${bottom}px`);
846
+ if (isMobileView()) {
847
+ document.documentElement.style.removeProperty("--footer-model-picker-left");
848
+ document.documentElement.style.removeProperty("--footer-model-picker-right");
849
+ const viewportHeight = window.innerHeight || window.visualViewport?.height || document.documentElement.clientHeight;
850
+ const statusTop = elements.statusBar.getBoundingClientRect().top;
851
+ const bottom = Math.max(8, Math.round(viewportHeight - statusTop + 6));
852
+ document.documentElement.style.setProperty("--footer-model-picker-bottom", `${bottom}px`);
853
+ return;
854
+ }
855
+ document.documentElement.style.removeProperty("--footer-model-picker-bottom");
856
+ const picker = elements.statusBar.querySelector(".footer-model-picker");
857
+ const target = footerActivePickerTarget();
858
+ if (!picker || !target) {
859
+ document.documentElement.style.removeProperty("--footer-model-picker-left");
860
+ document.documentElement.style.removeProperty("--footer-model-picker-right");
861
+ return;
862
+ }
863
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
864
+ const statusRect = elements.statusBar.getBoundingClientRect();
865
+ const targetRect = target.getBoundingClientRect();
866
+ const pickerWidth = picker.offsetWidth || Math.min(544, viewportWidth - 16);
867
+ const minLeft = 8 - statusRect.left;
868
+ const maxLeft = Math.max(minLeft, viewportWidth - pickerWidth - 8 - statusRect.left);
869
+ const targetCenterLeft = targetRect.left - statusRect.left + (targetRect.width / 2) - (pickerWidth / 2);
870
+ const left = Math.min(maxLeft, Math.max(minLeft, targetCenterLeft));
871
+ document.documentElement.style.setProperty("--footer-model-picker-left", `${Math.round(left)}px`);
872
+ document.documentElement.style.setProperty("--footer-model-picker-right", "auto");
772
873
  }
773
874
 
774
875
  function setMobileFooterExpanded(expanded) {
775
876
  mobileFooterExpanded = expanded && isMobileView();
776
- if (mobileFooterExpanded && footerModelPickerOpen) {
877
+ if (mobileFooterExpanded && isFooterPickerOpen()) {
777
878
  footerModelPickerOpen = false;
879
+ footerThinkingPickerOpen = false;
778
880
  document.body.classList.remove("footer-model-picker-open");
779
- elements.statusBar.querySelector(".footer-model-picker")?.remove();
881
+ elements.statusBar.querySelectorAll(".footer-model-picker").forEach((node) => node.remove());
780
882
  }
781
883
  document.body.classList.toggle("footer-details-expanded", mobileFooterExpanded);
782
884
  const button = elements.statusBar.querySelector(".footer-details-toggle");
@@ -2792,6 +2894,7 @@ async function switchTab(tabId) {
2792
2894
  clearOpenTerminalTabGroup(null, { force: true });
2793
2895
  setMobileTabsExpanded(false);
2794
2896
  footerModelPickerOpen = false;
2897
+ footerThinkingPickerOpen = false;
2795
2898
  saveActiveDraft();
2796
2899
  const tabContext = setActiveTabId(tabId, { remember: true });
2797
2900
  resetActiveTabUi();
@@ -3340,6 +3443,16 @@ function shortModelLabel(model) {
3340
3443
  return `(${model.provider}) ${model.id}`;
3341
3444
  }
3342
3445
 
3446
+ function footerThinkingLevelLabel(level) {
3447
+ return String(level || "unknown").trim() || "unknown";
3448
+ }
3449
+
3450
+ function footerThinkingDisplay(state = currentState) {
3451
+ const current = footerThinkingLevelLabel(state?.thinkingLevel);
3452
+ const pending = state?.pendingThinkingLevel ? footerThinkingLevelLabel(state.pendingThinkingLevel) : "";
3453
+ return pending && pending !== current ? `${current} → ${pending} next` : current;
3454
+ }
3455
+
3343
3456
  function footerModelLine(model = currentState?.model, thinkingLevel = currentState?.thinkingLevel) {
3344
3457
  const label = shortModelLabel(model);
3345
3458
  if (!model?.reasoning) return label;
@@ -3347,6 +3460,39 @@ function footerModelLine(model = currentState?.model, thinkingLevel = currentSta
3347
3460
  return `${label} • ${thinking}`;
3348
3461
  }
3349
3462
 
3463
+ function normalizeSelectedModel(model) {
3464
+ const provider = String(model?.provider || "").trim();
3465
+ const id = String(model?.id || model?.modelId || "").trim();
3466
+ if (!provider || !id) return null;
3467
+ const known = [...availableModels, ...footerScopedModels].find((item) => item?.provider === provider && item?.id === id);
3468
+ return { ...(known || {}), ...model, provider, id };
3469
+ }
3470
+
3471
+ function applyOptimisticModelSelection(model, tabContext = activeTabContext()) {
3472
+ const nextModel = normalizeSelectedModel(model);
3473
+ if (!nextModel || !currentState || !isCurrentTabContext(tabContext)) return nextModel;
3474
+ const changed = modelStateKey(currentState.model) !== modelStateKey(nextModel);
3475
+ currentState = { ...currentState, model: nextModel };
3476
+ renderStatus();
3477
+ if (changed) requestGitFooterWebuiPayload(tabContext, { force: true });
3478
+ return nextModel;
3479
+ }
3480
+
3481
+ function applyOptimisticThinkingSelection(data, tabContext = activeTabContext()) {
3482
+ const level = String(data?.level || data?.requestedLevel || "").trim();
3483
+ if (!level || !currentState || !isCurrentTabContext(tabContext)) return level;
3484
+ if (data?.pending) currentState = { ...currentState, pendingThinkingLevel: level };
3485
+ else currentState = { ...currentState, thinkingLevel: level, pendingThinkingLevel: undefined };
3486
+ renderStatus();
3487
+ if (!data?.pending) requestGitFooterWebuiPayload(tabContext, { force: true });
3488
+ return level;
3489
+ }
3490
+
3491
+ function footerThinkingLevels() {
3492
+ const levels = Array.from(elements.thinkingSelect?.options || []).map((option) => option.value).filter(Boolean);
3493
+ return levels.length ? levels : ["off", "minimal", "low", "medium", "high", "xhigh"];
3494
+ }
3495
+
3350
3496
  function formatFooterTokenCount(value) {
3351
3497
  const n = Math.max(0, Number(value) || 0);
3352
3498
  if (n < 1000) return `${Math.round(n)}`;
@@ -3478,6 +3624,7 @@ const FOOTER_META_CLASS_BY_KEY = new Map([
3478
3624
  ["git-extra", "footer-git-extra"],
3479
3625
  ["context", "footer-context"],
3480
3626
  ["model", "footer-model"],
3627
+ ["thinking", "footer-thinking"],
3481
3628
  ]);
3482
3629
 
3483
3630
  function cleanFooterPayloadText(value, fallback = "") {
@@ -3573,6 +3720,24 @@ function parseGitFooterWebuiPayload() {
3573
3720
  return parseGitFooterWebuiPayloadRaw(readCachedGitFooterWebuiPayloadRaw());
3574
3721
  }
3575
3722
 
3723
+ function footerPayloadWithLiveModel(payload) {
3724
+ if (!payload || !currentState?.model) return payload;
3725
+ const model = shortModelLabel(currentState.model);
3726
+ const effort = footerThinkingDisplay();
3727
+ const hasThinkingChip = [...payload.main, ...payload.meta].some((chip) => chip?.key === "thinking");
3728
+ const effortChip = (chip) => ({ ...chip, key: "thinking", label: "effort", value: effort, title: `effort: ${effort}`, tone: "mauve" });
3729
+ const splitChip = (chip) => {
3730
+ if (chip?.key === "thinking") return [effortChip(chip)];
3731
+ if (chip?.key !== "model") return [chip];
3732
+ const modelChip = { ...chip, value: model, title: `model: ${model}` };
3733
+ return hasThinkingChip ? [modelChip] : [modelChip, effortChip(chip)];
3734
+ };
3735
+ return {
3736
+ main: payload.main.flatMap(splitChip),
3737
+ meta: payload.meta.flatMap(splitChip),
3738
+ };
3739
+ }
3740
+
3576
3741
  function footerMetaClassForPayload(chip) {
3577
3742
  const base = FOOTER_META_CLASS_BY_KEY.get(chip.key) || "footer-extension-meta";
3578
3743
  const toneClass = chip.tone ? ` tone-${chip.tone}` : "";
@@ -3623,6 +3788,9 @@ function renderGitFooterPayloadMeta(chip, tab) {
3623
3788
  } else if (chip.key === "model") {
3624
3789
  options.onClick = () => setFooterModelPickerOpen(!footerModelPickerOpen);
3625
3790
  options.title = chip.title || `Change scoped model: ${chip.value}`;
3791
+ } else if (chip.key === "thinking") {
3792
+ options.onClick = () => setFooterThinkingPickerOpen(!footerThinkingPickerOpen);
3793
+ options.title = chip.title || `Change thinking effort: ${chip.value}`;
3626
3794
  }
3627
3795
  const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
3628
3796
  return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
@@ -3633,7 +3801,7 @@ function renderGitFooterPayload(payload) {
3633
3801
  elements.statusBar.replaceChildren();
3634
3802
  elements.statusBar.classList.remove("statusbar-tui-footer");
3635
3803
  elements.statusBar.classList.add("statusbar-git-footer");
3636
- document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
3804
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
3637
3805
 
3638
3806
  const row1 = make("div", "footer-line footer-line-main");
3639
3807
  row1.append(...payload.main.map(renderGitFooterPayloadMetric));
@@ -3648,6 +3816,7 @@ function renderGitFooterPayload(payload) {
3648
3816
 
3649
3817
  elements.statusBar.append(row1, row2);
3650
3818
  if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
3819
+ if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
3651
3820
  setMobileFooterExpanded(mobileFooterExpanded);
3652
3821
  updateFooterModelPickerPosition();
3653
3822
  }
@@ -3672,7 +3841,7 @@ function renderMinimalFooter() {
3672
3841
  elements.statusBar.replaceChildren();
3673
3842
  elements.statusBar.classList.remove("statusbar-git-footer");
3674
3843
  elements.statusBar.classList.add("statusbar-tui-footer");
3675
- document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
3844
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
3676
3845
  elements.statusBar.append(renderTuiFooterLine({
3677
3846
  cwd: workspaceLabel,
3678
3847
  cwdTitle: tab ? `Change cwd for ${tab.title}: ${workspaceLabel}` : undefined,
@@ -3681,19 +3850,35 @@ function renderMinimalFooter() {
3681
3850
  model: modelLine,
3682
3851
  }));
3683
3852
  if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
3853
+ if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
3684
3854
  setMobileFooterExpanded(false);
3685
3855
  updateFooterModelPickerPosition();
3686
3856
  }
3687
3857
 
3688
3858
  function setFooterModelPickerOpen(open) {
3689
3859
  footerModelPickerOpen = !!open;
3860
+ if (footerModelPickerOpen) footerThinkingPickerOpen = false;
3690
3861
  if (footerModelPickerOpen && isMobileView()) {
3691
3862
  mobileFooterExpanded = false;
3692
3863
  document.body.classList.remove("footer-details-expanded");
3693
3864
  setComposerActionsOpen(false);
3694
3865
  setMobileTabsExpanded(false);
3695
3866
  }
3696
- document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
3867
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
3868
+ renderFooter();
3869
+ updateFooterModelPickerPosition();
3870
+ }
3871
+
3872
+ function setFooterThinkingPickerOpen(open) {
3873
+ footerThinkingPickerOpen = !!open;
3874
+ if (footerThinkingPickerOpen) footerModelPickerOpen = false;
3875
+ if (footerThinkingPickerOpen && isMobileView()) {
3876
+ mobileFooterExpanded = false;
3877
+ document.body.classList.remove("footer-details-expanded");
3878
+ setComposerActionsOpen(false);
3879
+ setMobileTabsExpanded(false);
3880
+ }
3881
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
3697
3882
  renderFooter();
3698
3883
  updateFooterModelPickerPosition();
3699
3884
  }
@@ -3703,15 +3888,16 @@ async function applyFooterModel(model) {
3703
3888
  const tabContext = activeTabContext();
3704
3889
  try {
3705
3890
  footerModelPickerOpen = false;
3706
- await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
3891
+ const response = await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
3707
3892
  if (!isCurrentTabContext(tabContext)) return;
3893
+ applyOptimisticModelSelection(response.data || model, tabContext);
3708
3894
  await refreshState(tabContext);
3709
3895
  await refreshModels(tabContext);
3710
3896
  } catch (error) {
3711
3897
  if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
3712
3898
  } finally {
3713
3899
  if (isCurrentTabContext(tabContext)) {
3714
- document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
3900
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
3715
3901
  renderFooter();
3716
3902
  }
3717
3903
  }
@@ -3750,6 +3936,54 @@ function renderFooterModelPicker() {
3750
3936
  return picker;
3751
3937
  }
3752
3938
 
3939
+ async function applyFooterThinking(level) {
3940
+ const nextLevel = String(level || "").trim();
3941
+ if (!nextLevel) return;
3942
+ const tabContext = activeTabContext();
3943
+ try {
3944
+ footerThinkingPickerOpen = false;
3945
+ const response = await api("/api/thinking", { method: "POST", body: { level: nextLevel }, tabId: tabContext.tabId });
3946
+ if (!isCurrentTabContext(tabContext)) return;
3947
+ applyOptimisticThinkingSelection(response.data || { level: nextLevel }, tabContext);
3948
+ if (response.data?.pending) {
3949
+ addEvent(response.data.message || `Thinking effort ${response.data.level || nextLevel} will apply to the next prompt.`, "info");
3950
+ } else if (response.data?.level) {
3951
+ const requested = response.data.requestedLevel;
3952
+ const effective = response.data.level;
3953
+ addEvent(requested && requested !== effective ? `Thinking effort set to ${effective} (requested ${requested}).` : `Thinking effort set to ${effective}.`, "info");
3954
+ }
3955
+ await refreshState(tabContext);
3956
+ } catch (error) {
3957
+ if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
3958
+ } finally {
3959
+ if (isCurrentTabContext(tabContext)) {
3960
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
3961
+ renderFooter();
3962
+ }
3963
+ }
3964
+ }
3965
+
3966
+ function renderFooterThinkingPicker() {
3967
+ const picker = make("div", "footer-model-picker footer-thinking-picker");
3968
+ picker.setAttribute("role", "listbox");
3969
+ picker.setAttribute("aria-label", "Thinking effort");
3970
+ picker.append(make("div", "footer-model-picker-title", "Thinking effort"));
3971
+ picker.append(make("div", "footer-model-picker-source", "Applies to this Pi tab."));
3972
+ const current = currentState?.pendingThinkingLevel || currentState?.thinkingLevel || "off";
3973
+ for (const level of footerThinkingLevels()) {
3974
+ const selected = current === level;
3975
+ const button = make("button", `footer-model-option${selected ? " active" : ""}`);
3976
+ button.type = "button";
3977
+ button.setAttribute("role", "option");
3978
+ button.setAttribute("aria-selected", selected ? "true" : "false");
3979
+ button.title = `Set thinking effort to ${level}`;
3980
+ button.append(make("span", "footer-model-option-main", footerThinkingLevelLabel(level)));
3981
+ button.addEventListener("click", () => applyFooterThinking(level));
3982
+ picker.append(button);
3983
+ }
3984
+ return picker;
3985
+ }
3986
+
3753
3987
  function pathPickerButton(label, title, onClick, className = "") {
3754
3988
  const button = make("button", className, label);
3755
3989
  button.type = "button";
@@ -4017,7 +4251,7 @@ async function changeActiveTabCwd() {
4017
4251
  function renderFooter() {
4018
4252
  const gitFooterPayload = parseGitFooterWebuiPayload();
4019
4253
  if (gitFooterPayload) {
4020
- renderGitFooterPayload(gitFooterPayload);
4254
+ renderGitFooterPayload(footerPayloadWithLiveModel(gitFooterPayload));
4021
4255
  return;
4022
4256
  }
4023
4257
  renderMinimalFooter();
@@ -7221,6 +7455,13 @@ function setNativeCommandMenuOpen(open) {
7221
7455
  elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
7222
7456
  }
7223
7457
 
7458
+ function setOptionsMenuOpen(open) {
7459
+ optionsMenuOpen = !!open;
7460
+ elements.optionsMenuButton.setAttribute("aria-expanded", optionsMenuOpen ? "true" : "false");
7461
+ elements.optionsMenuButton.classList.toggle("menu-open", optionsMenuOpen);
7462
+ elements.optionsMenuButton.parentElement?.classList.toggle("open", optionsMenuOpen);
7463
+ }
7464
+
7224
7465
  function optionalFeatureIdForCommand(name) {
7225
7466
  if (OPTIONAL_COMMAND_FEATURES.has(name)) return OPTIONAL_COMMAND_FEATURES.get(name);
7226
7467
  if (name === "release-toggle" || name === "release-abort" || name === "release-npm-logs") return "releaseNpm";
@@ -7377,9 +7618,11 @@ function renderOptionalFeaturePanel() {
7377
7618
  }
7378
7619
 
7379
7620
  function renderOptionalFeatureControls() {
7621
+ const hasGitWorkflow = isOptionalFeatureEnabled("gitWorkflow");
7622
+ elements.gitWorkflowButton.hidden = !hasGitWorkflow;
7380
7623
  setOptionalControlState(
7381
7624
  elements.gitWorkflowButton,
7382
- isOptionalFeatureEnabled("gitWorkflow"),
7625
+ hasGitWorkflow,
7383
7626
  optionalFeatureUnavailableMessage("gitWorkflow"),
7384
7627
  );
7385
7628
 
@@ -7388,6 +7631,7 @@ function renderOptionalFeatureControls() {
7388
7631
  const hasPublishWorkflow = isOptionalFeatureEnabled("releaseNpm") || isOptionalFeatureEnabled("releaseAur");
7389
7632
  const publishContainer = elements.publishButton.parentElement;
7390
7633
  if (publishContainer) publishContainer.hidden = !hasPublishWorkflow;
7634
+ elements.publishButton.hidden = !hasPublishWorkflow;
7391
7635
  setOptionalControlState(
7392
7636
  elements.publishButton,
7393
7637
  hasPublishWorkflow,
@@ -7400,6 +7644,7 @@ function renderOptionalFeatureControls() {
7400
7644
  elements.nativeToolsButton.hidden = !isOptionalFeatureEnabled("tuiToolsCommand");
7401
7645
  const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
7402
7646
  if (nativeCommandMenuContainer) nativeCommandMenuContainer.hidden = !hasNativeCommandMenu;
7647
+ elements.nativeCommandMenuButton.hidden = !hasNativeCommandMenu;
7403
7648
  setOptionalControlState(
7404
7649
  elements.nativeCommandMenuButton,
7405
7650
  hasNativeCommandMenu,
@@ -7457,6 +7702,7 @@ async function installOptionalFeature(featureId) {
7457
7702
  function runPublishWorkflow(command) {
7458
7703
  setComposerActionsOpen(false);
7459
7704
  setPublishMenuOpen(false);
7705
+ setOptionsMenuOpen(false);
7460
7706
  const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
7461
7707
  const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
7462
7708
  if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
@@ -7474,6 +7720,7 @@ async function runNativeCommandMenu(command) {
7474
7720
  setComposerActionsOpen(false);
7475
7721
  setPublishMenuOpen(false);
7476
7722
  setNativeCommandMenuOpen(false);
7723
+ setOptionsMenuOpen(false);
7477
7724
  const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
7478
7725
  const featureId = optionalFeatureIdForCommand(commandName);
7479
7726
  if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
@@ -7484,7 +7731,8 @@ async function runNativeCommandMenu(command) {
7484
7731
  });
7485
7732
  return;
7486
7733
  }
7487
- await handleNativeSlashSelectorCommand(command);
7734
+ if (await handleNativeSlashSelectorCommand(command)) return;
7735
+ await sendPrompt("prompt", command);
7488
7736
  }
7489
7737
 
7490
7738
  function slashCommandName(message) {
@@ -7619,11 +7867,13 @@ async function openNativeModelSelector() {
7619
7867
  activeId,
7620
7868
  onSelect: async (item) => {
7621
7869
  setNativeCommandError("");
7870
+ const tabContext = activeTabContext(nativeCommandTabId || activeTabId);
7622
7871
  try {
7623
- await nativeCommandApi("/api/model", { method: "POST", body: { provider: item.model.provider, modelId: item.model.id } });
7872
+ const response = await nativeCommandApi("/api/model", { method: "POST", body: { provider: item.model.provider, modelId: item.model.id } });
7873
+ applyOptimisticModelSelection(response.data || item.model, tabContext);
7624
7874
  addTransientMessage({ role: "native", title: "/model", content: `Model set to ${item.label}.`, level: "info" });
7625
7875
  closeNativeCommandDialog();
7626
- await refreshState();
7876
+ await refreshState(tabContext);
7627
7877
  } catch (error) {
7628
7878
  setNativeCommandError(error.message || String(error));
7629
7879
  }
@@ -7672,83 +7922,224 @@ function openNativeThemeSelector() {
7672
7922
  });
7673
7923
  }
7674
7924
 
7675
- function nativeSettingSelect(label, value, options) {
7925
+ function nativeSettingsBadge(label, tone = "") {
7926
+ return make("span", `native-settings-badge${tone ? ` native-settings-badge-${tone}` : ""}`, label);
7927
+ }
7928
+
7929
+ function normalizedSettingsBadge(badge) {
7930
+ if (!badge) return null;
7931
+ if (typeof badge === "string") return { label: badge, tone: "" };
7932
+ return badge;
7933
+ }
7934
+
7935
+ function nativeSettingsLabelRow(label, badge) {
7936
+ const row = make("span", "native-settings-label-row");
7937
+ row.append(make("span", "native-settings-label", label));
7938
+ const normalized = normalizedSettingsBadge(badge);
7939
+ if (normalized?.label) row.append(nativeSettingsBadge(normalized.label, normalized.tone));
7940
+ return row;
7941
+ }
7942
+
7943
+ function normalizedSettingOptions(options) {
7944
+ return options.map((option) => {
7945
+ if (typeof option === "string" || typeof option === "number") return { value: String(option), label: String(option) };
7946
+ return { value: String(option.value), label: option.label || String(option.value) };
7947
+ });
7948
+ }
7949
+
7950
+ function nativeSettingSelect(label, value, options, hint, badge) {
7676
7951
  const field = make("label", "native-settings-field");
7677
- field.append(make("span", "native-settings-label", label));
7952
+ field.append(nativeSettingsLabelRow(label, badge));
7678
7953
  const select = make("select");
7679
- for (const option of options) {
7680
- const element = make("option", undefined, option.label || option.value);
7954
+ for (const option of normalizedSettingOptions(options)) {
7955
+ const element = make("option", undefined, option.label);
7681
7956
  element.value = option.value;
7682
7957
  select.append(element);
7683
7958
  }
7684
- select.value = value;
7959
+ select.value = String(value);
7685
7960
  field.append(select);
7961
+ if (hint) field.append(make("span", "native-settings-hint", hint));
7686
7962
  return { field, select };
7687
7963
  }
7688
7964
 
7689
- function nativeSettingToggle(label, checked, hint) {
7965
+ function nativeSettingToggle(label, checked, hint, badge) {
7690
7966
  const field = make("label", "native-settings-toggle");
7691
7967
  const input = make("input");
7692
7968
  input.type = "checkbox";
7693
7969
  input.checked = !!checked;
7694
7970
  const text = make("span");
7695
- text.append(make("strong", undefined, label));
7971
+ text.append(nativeSettingsLabelRow(label, badge));
7696
7972
  if (hint) text.append(make("span", "native-settings-hint", hint));
7697
7973
  field.append(input, text);
7698
7974
  return { field, input };
7699
7975
  }
7700
7976
 
7701
- function openNativeSettingsDialog() {
7702
- openNativeCommandDialog({ title: "/settings", message: "Quick Web UI settings for the active Pi tab." });
7703
- elements.nativeCommandBody.replaceChildren();
7977
+ function nativeSettingsSection(title, description, controls, { open = true, badge } = {}) {
7978
+ const section = make("details", "native-settings-section");
7979
+ section.open = !!open;
7980
+ const summary = make("summary", "native-settings-section-summary");
7981
+ const titleRow = make("span", "native-settings-section-title");
7982
+ titleRow.append(make("strong", undefined, title));
7983
+ const normalized = normalizedSettingsBadge(badge);
7984
+ if (normalized?.label) titleRow.append(nativeSettingsBadge(normalized.label, normalized.tone));
7985
+ summary.append(titleRow);
7986
+ if (description) summary.append(make("span", "native-settings-section-description", description));
7987
+ const grid = make("div", "native-settings-grid");
7988
+ grid.append(...controls.map((control) => control.field || control));
7989
+ section.append(summary, grid);
7990
+ return section;
7991
+ }
7992
+
7993
+ function nativeSettingsNote(title, text) {
7994
+ const note = make("div", "native-settings-note");
7995
+ note.append(make("strong", undefined, title));
7996
+ note.append(make("span", undefined, text));
7997
+ return note;
7998
+ }
7999
+
8000
+ function currentHttpIdleTimeoutValue(settings) {
8001
+ return String(settings.httpIdleTimeoutMs ?? "300000");
8002
+ }
8003
+
8004
+ function httpIdleTimeoutOptions(settings) {
8005
+ const current = currentHttpIdleTimeoutValue(settings);
8006
+ if (SETTINGS_HTTP_IDLE_TIMEOUT_OPTIONS.some((option) => option.value === current)) return SETTINGS_HTTP_IDLE_TIMEOUT_OPTIONS;
8007
+ const label = Number(current) === 0 ? "disabled (current)" : `${Number(current) / 1000} sec (current)`;
8008
+ return [{ value: current, label }, ...SETTINGS_HTTP_IDLE_TIMEOUT_OPTIONS];
8009
+ }
8010
+
8011
+ function collectNativeSettingsPayload(controls) {
8012
+ return {
8013
+ transport: controls.transport.select.value,
8014
+ httpIdleTimeoutMs: Number(controls.httpIdleTimeout.select.value),
8015
+ autoResizeImages: controls.autoResizeImages.input.checked,
8016
+ blockImages: controls.blockImages.input.checked,
8017
+ enableSkillCommands: controls.skillCommands.input.checked,
8018
+ hideThinkingBlock: !controls.thinkingOutput.input.checked,
8019
+ showImages: controls.showImages.input.checked,
8020
+ imageWidthCells: Number(controls.imageWidth.select.value),
8021
+ collapseChangelog: controls.collapseChangelog.input.checked,
8022
+ quietStartup: controls.quietStartup.input.checked,
8023
+ enableInstallTelemetry: controls.installTelemetry.input.checked,
8024
+ doubleEscapeAction: controls.doubleEscape.select.value,
8025
+ treeFilterMode: controls.treeFilter.select.value,
8026
+ showHardwareCursor: controls.hardwareCursor.input.checked,
8027
+ editorPaddingX: Number(controls.editorPadding.select.value),
8028
+ autocompleteMaxVisible: Number(controls.autocompleteMax.select.value),
8029
+ clearOnShrink: controls.clearOnShrink.input.checked,
8030
+ showTerminalProgress: controls.terminalProgress.input.checked,
8031
+ warnings: { anthropicExtraUsage: controls.anthropicWarning.input.checked },
8032
+ };
8033
+ }
8034
+
8035
+ function nativeSettingsChangedMessage(response, reloadRequested) {
8036
+ const changed = response.data?.changed || [];
8037
+ const reloadRecommended = response.data?.reloadRecommended || [];
8038
+ if (response.data?.reloaded) return `Settings updated and tab reloaded (${changed.length || 0} changed).`;
8039
+ if (reloadRecommended.length) return `Settings updated. Reload tab to apply: ${reloadRecommended.join(", ")}.`;
8040
+ if (reloadRequested) return `Settings updated. No reload-needed changes were detected.`;
8041
+ return `Settings updated${changed.length ? ` (${changed.length} changed)` : ""}.`;
8042
+ }
8043
+
8044
+ async function openNativeSettingsDialog() {
8045
+ openNativeCommandDialog({ title: "/settings", message: "Pi settings for this Web UI tab. Badges show whether changes apply now, in the browser, or after reloading the tab." });
8046
+ renderNativeLoading("Loading settings…");
8047
+ let settingsData;
8048
+ try {
8049
+ const response = await nativeCommandApi("/api/settings");
8050
+ settingsData = response.data || {};
8051
+ } catch (error) {
8052
+ setNativeCommandError(error.message || String(error));
8053
+ elements.nativeCommandBody.replaceChildren();
8054
+ return;
8055
+ }
8056
+
7704
8057
  const state = currentState || {};
7705
- const body = make("div", "native-settings-grid");
7706
- const thinking = nativeSettingSelect("Thinking level", state.thinkingLevel || "off", ["off", "minimal", "low", "medium", "high", "xhigh"].map((value) => ({ value })));
7707
- const steering = nativeSettingSelect("Steering queue", state.steeringMode || "one-at-a-time", [
7708
- { value: "one-at-a-time", label: "one at a time" },
7709
- { value: "all", label: "all queued" },
7710
- ]);
7711
- const followUp = nativeSettingSelect("Follow-up queue", state.followUpMode || "one-at-a-time", [
7712
- { value: "one-at-a-time", label: "one at a time" },
7713
- { value: "all", label: "all queued" },
7714
- ]);
7715
- const autoCompact = nativeSettingToggle("Auto compaction", state.autoCompactionEnabled !== false, "Let Pi compact when context is nearly full.");
7716
- const thinkingOutput = nativeSettingToggle("Show thinking output", thinkingOutputVisible, "Local browser transcript visibility.");
7717
- const doneNotifications = nativeSettingToggle("Agent done notifications", agentDoneNotificationsEnabled, "Browser notification after background tab work completes.");
7718
- const busyBehavior = nativeSettingSelect("Busy prompt behavior", elements.busyBehavior.value || "followUp", [
7719
- { value: "followUp", label: "follow-up" },
7720
- { value: "steer", label: "steer" },
7721
- ]);
7722
- body.append(thinking.field, steering.field, followUp.field, busyBehavior.field, autoCompact.field, thinkingOutput.field, doneNotifications.field);
7723
- elements.nativeCommandBody.append(body);
8058
+ const settings = settingsData.settings || {};
8059
+ applyNativeSettingsForBrowser(settings);
8060
+
8061
+ const controls = {
8062
+ thinking: nativeSettingSelect("Thinking level", state.thinkingLevel || "off", SETTINGS_THINKING_OPTIONS, "Reasoning depth for thinking-capable models.", { label: "now", tone: "now" }),
8063
+ autoCompact: nativeSettingToggle("Auto-compact", state.autoCompactionEnabled !== false, "Let Pi compact when context is nearly full.", { label: "now", tone: "now" }),
8064
+ steering: nativeSettingSelect("Steering mode", state.steeringMode || "one-at-a-time", [
8065
+ { value: "one-at-a-time", label: "one at a time" },
8066
+ { value: "all", label: "all queued" },
8067
+ ], "How Enter messages are delivered while the agent is streaming.", { label: "now", tone: "now" }),
8068
+ followUp: nativeSettingSelect("Follow-up mode", state.followUpMode || "one-at-a-time", [
8069
+ { value: "one-at-a-time", label: "one at a time" },
8070
+ { value: "all", label: "all queued" },
8071
+ ], "How queued follow-ups are delivered after the current response.", { label: "now", tone: "now" }),
8072
+ transport: nativeSettingSelect("Transport", settings.transport || "auto", SETTINGS_TRANSPORT_OPTIONS, "Preferred provider transport when multiple transports are supported.", { label: "reload", tone: "reload" }),
8073
+ httpIdleTimeout: nativeSettingSelect("HTTP idle timeout", currentHttpIdleTimeoutValue(settings), httpIdleTimeoutOptions(settings), "Maximum idle gap while waiting for provider HTTP data.", { label: "reload", tone: "reload" }),
8074
+ busyBehavior: nativeSettingSelect("Busy prompt behavior", elements.busyBehavior.value || "followUp", [
8075
+ { value: "followUp", label: "follow-up" },
8076
+ { value: "steer", label: "steer" },
8077
+ ], "When you submit a normal prompt while a tab is already busy.", { label: "browser", tone: "browser" }),
8078
+ thinkingOutput: nativeSettingToggle("Show thinking output", settings.hideThinkingBlock !== true, "Browser transcript visibility; also writes Pi's hide-thinking setting.", { label: "browser", tone: "browser" }),
8079
+ doneNotifications: nativeSettingToggle("Agent done notifications", agentDoneNotificationsEnabled, "Browser notification after background tab work completes.", { label: "browser", tone: "browser" }),
8080
+ autocompleteMax: nativeSettingSelect("Autocomplete max items", settings.autocompleteMaxVisible ?? autocompleteMaxVisible, SETTINGS_AUTOCOMPLETE_OPTIONS, "Maximum visible slash/path suggestions.", { label: "browser", tone: "browser" }),
8081
+ doubleEscape: nativeSettingSelect("Double-escape action", settings.doubleEscapeAction || doubleEscapeAction, SETTINGS_DOUBLE_ESCAPE_OPTIONS, "Action when pressing Escape twice with an empty composer.", { label: "browser", tone: "browser" }),
8082
+ treeFilter: nativeSettingSelect("Tree filter mode", settings.treeFilterMode || treeFilterMode, SETTINGS_TREE_FILTER_OPTIONS, "Default filter when opening /tree.", { label: "browser", tone: "browser" }),
8083
+ autoResizeImages: nativeSettingToggle("Auto-resize images", settings.autoResizeImages !== false, "Resize large images to 2000x2000 max for better model compatibility.", { label: "reload", tone: "reload" }),
8084
+ blockImages: nativeSettingToggle("Block images", settings.blockImages === true, "Prevent images from being sent to LLM providers.", { label: "reload", tone: "reload" }),
8085
+ showImages: nativeSettingToggle("Show terminal images", settings.showImages !== false, "Native TUI inline image rendering preference.", { label: "TUI", tone: "tui" }),
8086
+ imageWidth: nativeSettingSelect("Terminal image width", settings.imageWidthCells || 60, SETTINGS_IMAGE_WIDTH_OPTIONS, "Native TUI inline image width in terminal cells.", { label: "TUI", tone: "tui" }),
8087
+ skillCommands: nativeSettingToggle("Skill commands", settings.enableSkillCommands !== false, "Register skills as /skill:name commands.", { label: "reload", tone: "reload" }),
8088
+ anthropicWarning: nativeSettingToggle("Anthropic extra usage warning", settings.warnings?.anthropicExtraUsage !== false, "Warn when Anthropic subscription auth may use paid extra usage.", { label: "safety", tone: "safety" }),
8089
+ collapseChangelog: nativeSettingToggle("Collapse changelog", settings.collapseChangelog === true, "Show condensed changelog after updates.", { label: "startup", tone: "startup" }),
8090
+ quietStartup: nativeSettingToggle("Quiet startup", settings.quietStartup === true, "Disable verbose printing at startup.", { label: "startup", tone: "startup" }),
8091
+ installTelemetry: nativeSettingToggle("Install telemetry", settings.enableInstallTelemetry !== false, "Send anonymous version/update ping after changelog-detected updates.", { label: "startup", tone: "startup" }),
8092
+ hardwareCursor: nativeSettingToggle("Show hardware cursor", settings.showHardwareCursor === true, "Native TUI cursor display for IME support.", { label: "TUI", tone: "tui" }),
8093
+ editorPadding: nativeSettingSelect("Editor padding", settings.editorPaddingX ?? 0, SETTINGS_EDITOR_PADDING_OPTIONS, "Native TUI horizontal input padding.", { label: "TUI", tone: "tui" }),
8094
+ clearOnShrink: nativeSettingToggle("Clear on shrink", settings.clearOnShrink === true, "Native TUI row clearing when content shrinks; may flicker.", { label: "TUI", tone: "tui" }),
8095
+ terminalProgress: nativeSettingToggle("Terminal progress", settings.showTerminalProgress === true, "Native TUI OSC 9;4 terminal progress indicators.", { label: "TUI", tone: "tui" }),
8096
+ };
8097
+
8098
+ const body = make("div", "native-settings-panel");
8099
+ body.append(
8100
+ nativeSettingsNote("Scopes", "Runtime settings apply to the active tab. Reload-badged settings are saved globally and need a tab reload for the running Pi process."),
8101
+ nativeSettingsSection("Runtime", "Model behavior and request transport.", [controls.thinking, controls.autoCompact, controls.steering, controls.followUp, controls.transport, controls.httpIdleTimeout], { open: true }),
8102
+ nativeSettingsSection("Browser workflow", "Local Web UI behavior plus shared composer defaults.", [controls.busyBehavior, controls.thinkingOutput, controls.doneNotifications, controls.autocompleteMax, controls.doubleEscape, controls.treeFilter], { open: true }),
8103
+ nativeSettingsSection("Images", "Provider image policy and native terminal image display.", [controls.autoResizeImages, controls.blockImages, controls.showImages, controls.imageWidth], { open: true }),
8104
+ nativeSettingsSection("Startup & safety", "Command registration, warnings, update/startup behavior.", [controls.skillCommands, controls.anthropicWarning, controls.collapseChangelog, controls.quietStartup, controls.installTelemetry], { open: false }),
8105
+ nativeSettingsSection("Native TUI advanced", "Saved for the terminal UI; mostly informational in the browser.", [controls.hardwareCursor, controls.editorPadding, controls.clearOnShrink, controls.terminalProgress], { open: false })
8106
+ );
8107
+ elements.nativeCommandBody.replaceChildren(body);
7724
8108
  elements.nativeCommandActions.replaceChildren();
7725
8109
  addNativeCommandAction("Model…", () => openNativeModelSelector());
7726
8110
  addNativeCommandAction("Theme…", () => openNativeThemeSelector());
7727
8111
  addNativeCommandAction("Cancel", closeNativeCommandDialog);
7728
- const save = addNativeCommandAction("Apply", async () => {
7729
- setNativeActionBusy(save, true, "Applying…");
8112
+
8113
+ const applySettings = async (reload, button) => {
8114
+ setNativeActionBusy(button, true, reload ? "Applying & reloading…" : "Applying…");
7730
8115
  setNativeCommandError("");
7731
8116
  try {
7732
8117
  const requests = [];
7733
- const thinkingLevelChanged = thinking.select.value !== state.thinkingLevel;
7734
- if (thinkingLevelChanged) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: thinking.select.value } }));
7735
- if (steering.select.value !== state.steeringMode) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: steering.select.value } }));
7736
- if (followUp.select.value !== state.followUpMode) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: followUp.select.value } }));
7737
- if (autoCompact.input.checked !== state.autoCompactionEnabled) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: autoCompact.input.checked } }));
7738
- elements.busyBehavior.value = busyBehavior.select.value;
7739
- if (thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(thinkingOutput.input.checked);
7740
- if (doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(doneNotifications.input.checked);
8118
+ const thinkingLevelChanged = controls.thinking.select.value !== (state.thinkingLevel || "off");
8119
+ if (thinkingLevelChanged) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: controls.thinking.select.value } }));
8120
+ if (controls.steering.select.value !== (state.steeringMode || "one-at-a-time")) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: controls.steering.select.value } }));
8121
+ if (controls.followUp.select.value !== (state.followUpMode || "one-at-a-time")) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: controls.followUp.select.value } }));
8122
+ if (controls.autoCompact.input.checked !== (state.autoCompactionEnabled !== false)) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: controls.autoCompact.input.checked } }));
8123
+ elements.busyBehavior.value = controls.busyBehavior.select.value;
8124
+ if (controls.thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(controls.thinkingOutput.input.checked);
8125
+ if (controls.doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(controls.doneNotifications.input.checked);
7741
8126
  await Promise.all(requests);
8127
+ const response = await nativeCommandApi("/api/settings", { method: "POST", body: { settings: collectNativeSettingsPayload(controls), reload } });
8128
+ applyResponseTab(response);
8129
+ applyNativeSettingsForBrowser(response.data?.settings || collectNativeSettingsPayload(controls));
7742
8130
  if (thinkingLevelChanged) requestGitFooterWebuiPayload(activeTabContext(), { force: true });
7743
- addTransientMessage({ role: "native", title: "/settings", content: "Settings updated.", level: "info" });
8131
+ addTransientMessage({ role: "native", title: "/settings", content: nativeSettingsChangedMessage(response, reload), level: response.data?.reloadRecommended?.length && !response.data?.reloaded ? "warn" : "info" });
7744
8132
  closeNativeCommandDialog();
7745
- await refreshState();
8133
+ await refreshAll();
7746
8134
  } catch (error) {
7747
8135
  setNativeCommandError(error.message || String(error));
7748
8136
  } finally {
7749
- setNativeActionBusy(save, false);
8137
+ setNativeActionBusy(button, false);
7750
8138
  }
7751
- }, "primary");
8139
+ };
8140
+
8141
+ const reloadButton = addNativeCommandAction("Apply & reload tab", () => applySettings(true, reloadButton));
8142
+ const save = addNativeCommandAction("Apply", () => applySettings(false, save), "primary");
7752
8143
  }
7753
8144
 
7754
8145
  async function openNativeForkSelector() {
@@ -7813,6 +8204,41 @@ function openNativeCloneDialog() {
7813
8204
  }, "primary");
7814
8205
  }
7815
8206
 
8207
+ function openNativeNameDialog() {
8208
+ openNativeCommandDialog({ title: "/name", message: "Set the session and browser tab name." });
8209
+ const field = make("label", "native-settings-field");
8210
+ field.append(make("span", "native-settings-label", "Session name"));
8211
+ const input = make("input", "dialog-input");
8212
+ input.type = "text";
8213
+ input.autocomplete = "off";
8214
+ input.placeholder = "New session name";
8215
+ input.value = activeTab()?.title || "";
8216
+ field.append(input);
8217
+ elements.nativeCommandBody.append(field);
8218
+ elements.nativeCommandActions.replaceChildren();
8219
+ addNativeCommandAction("Cancel", closeNativeCommandDialog);
8220
+ const save = addNativeCommandAction("Name session", async () => {
8221
+ const name = input.value.trim();
8222
+ if (!name) {
8223
+ setNativeCommandError("Enter a session name.");
8224
+ input.focus();
8225
+ return;
8226
+ }
8227
+ setNativeActionBusy(save, true, "Saving…");
8228
+ closeNativeCommandDialog();
8229
+ await sendPrompt("prompt", `/name ${name}`);
8230
+ }, "primary");
8231
+ input.addEventListener("keydown", (event) => {
8232
+ if (event.key !== "Enter") return;
8233
+ event.preventDefault();
8234
+ save.click();
8235
+ });
8236
+ queueMicrotask(() => {
8237
+ input.focus();
8238
+ input.select();
8239
+ });
8240
+ }
8241
+
7816
8242
  async function openNativeResumeSelector(scope = "current") {
7817
8243
  openNativeCommandDialog({ title: "/resume", message: "Resume another persisted Pi session.", searchPlaceholder: "Filter sessions…" });
7818
8244
  renderNativeLoading("Loading sessions…");
@@ -7854,21 +8280,39 @@ async function openNativeResumeSelector(scope = "current") {
7854
8280
  }
7855
8281
  }
7856
8282
 
8283
+ function nativeTreeFilterMatches(node, filter) {
8284
+ const settingsTypes = new Set(["label", "custom", "custom_message", "model_change", "thinking_level_change", "session_info"]);
8285
+ switch (filter) {
8286
+ case "user-only":
8287
+ return node.type === "message" && node.role === "user";
8288
+ case "no-tools":
8289
+ return !settingsTypes.has(node.type) && !(node.type === "message" && node.role === "toolResult");
8290
+ case "labeled-only":
8291
+ return node.label !== undefined;
8292
+ case "all":
8293
+ return true;
8294
+ default:
8295
+ return !settingsTypes.has(node.type);
8296
+ }
8297
+ }
8298
+
7857
8299
  async function openNativeTreeSelector() {
7858
8300
  openNativeCommandDialog({ title: "/tree", message: "Navigate the current session tree. Choosing a user message restores it into the editor.", searchPlaceholder: "Filter tree…" });
7859
8301
  renderNativeLoading("Loading session tree…");
7860
8302
  try {
7861
8303
  const response = await nativeCommandApi("/api/session-tree");
7862
8304
  const nodes = response.data?.nodes || [];
8305
+ let selectedFilter = treeFilterMode || "default";
8306
+ const filterField = nativeSettingSelect("Filter", selectedFilter, SETTINGS_TREE_FILTER_OPTIONS, "Temporary filter for this tree view.");
7863
8307
  const summarize = nativeSettingToggle("Summarize abandoned branch", false, "Optional; may call the active model before switching branches.");
7864
8308
  const labelField = make("label", "native-settings-field");
7865
- labelField.append(make("span", "native-settings-label", "Optional label"));
8309
+ labelField.append(nativeSettingsLabelRow("Optional label"));
7866
8310
  const labelInput = make("input", "dialog-input");
7867
8311
  labelInput.placeholder = "checkpoint label";
7868
8312
  labelField.append(labelInput);
7869
8313
  const options = make("div", "native-tree-options");
7870
- options.append(summarize.field, labelField);
7871
- const items = nodes.map((node) => ({
8314
+ options.append(filterField.field, summarize.field, labelField);
8315
+ const toItems = () => nodes.filter((node) => nativeTreeFilterMatches(node, selectedFilter)).map((node) => ({
7872
8316
  id: node.id,
7873
8317
  label: `${node.title}${node.label ? ` · ${node.label}` : ""}`,
7874
8318
  description: node.text || "",
@@ -7897,9 +8341,13 @@ async function openNativeTreeSelector() {
7897
8341
  }
7898
8342
  };
7899
8343
  const render = () => {
7900
- renderNativeSelectorItems(items, { emptyText: "No session tree entries match this filter.", onSelect: navigate });
8344
+ renderNativeSelectorItems(toItems(), { emptyText: "No session tree entries match this filter.", onSelect: navigate });
7901
8345
  elements.nativeCommandBody.prepend(options);
7902
8346
  };
8347
+ filterField.select.addEventListener("change", () => {
8348
+ selectedFilter = filterField.select.value;
8349
+ render();
8350
+ });
7903
8351
  elements.nativeCommandSearch.oninput = render;
7904
8352
  render();
7905
8353
  } catch (error) {
@@ -8096,7 +8544,7 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
8096
8544
  await openNativeModelSelector();
8097
8545
  return true;
8098
8546
  case "settings":
8099
- openNativeSettingsDialog();
8547
+ await openNativeSettingsDialog();
8100
8548
  return true;
8101
8549
  case "theme":
8102
8550
  openNativeThemeSelector();
@@ -8107,6 +8555,9 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
8107
8555
  case "clone":
8108
8556
  openNativeCloneDialog();
8109
8557
  return true;
8558
+ case "name":
8559
+ openNativeNameDialog();
8560
+ return true;
8110
8561
  case "resume":
8111
8562
  await openNativeResumeSelector();
8112
8563
  return true;
@@ -8615,10 +9066,23 @@ function getCommandMatches(query) {
8615
9066
  .map((command) => ({ command, score: scoreCommandSuggestion(command, query) }))
8616
9067
  .filter((item) => Number.isFinite(item.score))
8617
9068
  .sort((a, b) => a.score - b.score || a.command.name.localeCompare(b.command.name))
8618
- .slice(0, 12)
9069
+ .slice(0, clampAutocompleteMaxVisible(autocompleteMaxVisible))
8619
9070
  .map((item) => item.command);
8620
9071
  }
8621
9072
 
9073
+ function commandSearchQuery() {
9074
+ return String(elements.commandSearchInput?.value || "").trim().replace(/^\/+/, "").toLowerCase();
9075
+ }
9076
+
9077
+ function commandMatchesSearch(command, query) {
9078
+ if (!query) return true;
9079
+ return [command.name, command.description, command.source, command.location]
9080
+ .filter(Boolean)
9081
+ .join(" ")
9082
+ .toLowerCase()
9083
+ .includes(query);
9084
+ }
9085
+
8622
9086
  function activeSuggestionCount() {
8623
9087
  return suggestionMode === "path" ? pathSuggestions.length : commandSuggestions.length;
8624
9088
  }
@@ -8890,8 +9354,16 @@ function renderCommands() {
8890
9354
  hideCommandSuggestions();
8891
9355
  return;
8892
9356
  }
9357
+ const searchQuery = commandSearchQuery();
9358
+ const filteredCommands = commandsToShow.filter((command) => commandMatchesSearch(command, searchQuery));
9359
+ if (!filteredCommands.length) {
9360
+ elements.commandsBox.textContent = `No commands match “${elements.commandSearchInput?.value.trim() || searchQuery}”.`;
9361
+ elements.commandsBox.classList.add("muted");
9362
+ renderCommandSuggestions();
9363
+ return;
9364
+ }
8893
9365
  elements.commandsBox.classList.remove("muted");
8894
- for (const command of commandsToShow.slice(0, 80)) {
9366
+ for (const command of filteredCommands.slice(0, 80)) {
8895
9367
  const item = make("button", "command-item");
8896
9368
  item.type = "button";
8897
9369
  item.title = `Send /${command.name}`;
@@ -8925,6 +9397,7 @@ async function refreshAll(tabContext = activeTabContext()) {
8925
9397
  refreshCommands(tabContext),
8926
9398
  refreshStats(tabContext),
8927
9399
  refreshWorkspace(tabContext),
9400
+ refreshNativeSettings(tabContext),
8928
9401
  refreshNetworkStatus(),
8929
9402
  refreshWebuiVersion(),
8930
9403
  ]);
@@ -9175,6 +9648,7 @@ async function cycleModelFromShortcut(direction = "forward") {
9175
9648
  const model = response.data?.model;
9176
9649
  const scope = response.data?.scoped ? `scoped (${response.data.scopeSource})` : "all models";
9177
9650
  if (isCurrentTabContext(tabContext)) {
9651
+ applyOptimisticModelSelection(model, tabContext);
9178
9652
  addTransientMessage({ role: "native", title: "model cycle", content: `Model set to ${appShortcutModelLabel(model)} via ${direction} cycle over ${scope}.`, level: "info" });
9179
9653
  await Promise.allSettled([refreshState(tabContext), refreshModels(tabContext), refreshStats(tabContext)]);
9180
9654
  }
@@ -9191,8 +9665,8 @@ async function cycleThinkingFromShortcut() {
9191
9665
  if (!tabContext.tabId) return;
9192
9666
  try {
9193
9667
  const response = await api("/api/thinking-cycle", { method: "POST", body: {}, tabId: tabContext.tabId });
9194
- if (response.data?.level && currentState) currentState = { ...currentState, thinkingLevel: response.data.level };
9195
9668
  if (isCurrentTabContext(tabContext)) {
9669
+ applyOptimisticThinkingSelection(response.data, tabContext);
9196
9670
  addTransientMessage({ role: "native", title: "thinking", content: response.data?.level ? `Thinking level: ${response.data.level}` : "Thinking level did not change.", level: "info" });
9197
9671
  if (response.data?.level) requestGitFooterWebuiPayload(tabContext, { force: true });
9198
9672
  await Promise.allSettled([refreshState(tabContext), refreshStats(tabContext)]);
@@ -9816,7 +10290,13 @@ function handleEvent(event) {
9816
10290
  syncRunIndicatorFromState(currentState);
9817
10291
  renderStatus();
9818
10292
  } else if (["set_model", "cycle_model", "set_thinking_level", "cycle_thinking_level", "new_session", "compact"].includes(event.command)) {
9819
- if (event.command === "new_session") {
10293
+ if (event.command === "set_model") {
10294
+ applyOptimisticModelSelection(event.data, tabContext);
10295
+ } else if (event.command === "cycle_model") {
10296
+ applyOptimisticModelSelection(event.data?.model, tabContext);
10297
+ } else if (event.command === "set_thinking_level" || event.command === "cycle_thinking_level") {
10298
+ applyOptimisticThinkingSelection(event.data, tabContext);
10299
+ } else if (event.command === "new_session") {
9820
10300
  const tabId = event.tabId || activeTabId;
9821
10301
  forgetLastUserPrompt(tabId);
9822
10302
  resetGitWorkflowForTab(tabId);
@@ -9854,6 +10334,7 @@ function connectEvents(tabContext = activeTabContext()) {
9854
10334
 
9855
10335
  elements.copyServerCommandButton?.addEventListener("click", copyServerStartCommand);
9856
10336
  elements.retryServerConnectionButton?.addEventListener("click", retryServerConnection);
10337
+ elements.commandSearchInput?.addEventListener("input", renderCommands);
9857
10338
  elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
9858
10339
  elements.composer.addEventListener("submit", (event) => {
9859
10340
  event.preventDefault();
@@ -9876,15 +10357,18 @@ elements.gitWorkflowButton.addEventListener("click", () => {
9876
10357
  const publishMenuContainer = elements.publishButton.parentElement;
9877
10358
  elements.publishButton.addEventListener("click", () => {
9878
10359
  setNativeCommandMenuOpen(false);
10360
+ setOptionsMenuOpen(false);
9879
10361
  setPublishMenuOpen(true);
9880
10362
  });
9881
10363
  publishMenuContainer?.addEventListener("pointerenter", () => {
9882
10364
  setNativeCommandMenuOpen(false);
10365
+ setOptionsMenuOpen(false);
9883
10366
  setPublishMenuOpen(true);
9884
10367
  });
9885
10368
  publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
9886
10369
  publishMenuContainer?.addEventListener("focusin", () => {
9887
10370
  setNativeCommandMenuOpen(false);
10371
+ setOptionsMenuOpen(false);
9888
10372
  setPublishMenuOpen(true);
9889
10373
  });
9890
10374
  publishMenuContainer?.addEventListener("focusout", () => {
@@ -9895,15 +10379,18 @@ publishMenuContainer?.addEventListener("focusout", () => {
9895
10379
  const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
9896
10380
  elements.nativeCommandMenuButton.addEventListener("click", () => {
9897
10381
  setPublishMenuOpen(false);
10382
+ setOptionsMenuOpen(false);
9898
10383
  setNativeCommandMenuOpen(true);
9899
10384
  });
9900
10385
  nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
9901
10386
  setPublishMenuOpen(false);
10387
+ setOptionsMenuOpen(false);
9902
10388
  setNativeCommandMenuOpen(true);
9903
10389
  });
9904
10390
  nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
9905
10391
  nativeCommandMenuContainer?.addEventListener("focusin", () => {
9906
10392
  setPublishMenuOpen(false);
10393
+ setOptionsMenuOpen(false);
9907
10394
  setNativeCommandMenuOpen(true);
9908
10395
  });
9909
10396
  nativeCommandMenuContainer?.addEventListener("focusout", () => {
@@ -9911,10 +10398,40 @@ nativeCommandMenuContainer?.addEventListener("focusout", () => {
9911
10398
  if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
9912
10399
  }, 0);
9913
10400
  });
10401
+ const optionsMenuContainer = elements.optionsMenuButton.parentElement;
10402
+ elements.optionsMenuButton.addEventListener("click", () => {
10403
+ setPublishMenuOpen(false);
10404
+ setNativeCommandMenuOpen(false);
10405
+ setOptionsMenuOpen(true);
10406
+ });
10407
+ optionsMenuContainer?.addEventListener("pointerenter", () => {
10408
+ setPublishMenuOpen(false);
10409
+ setNativeCommandMenuOpen(false);
10410
+ setOptionsMenuOpen(true);
10411
+ });
10412
+ optionsMenuContainer?.addEventListener("pointerleave", () => setOptionsMenuOpen(false));
10413
+ optionsMenuContainer?.addEventListener("focusin", () => {
10414
+ setPublishMenuOpen(false);
10415
+ setNativeCommandMenuOpen(false);
10416
+ setOptionsMenuOpen(true);
10417
+ });
10418
+ optionsMenuContainer?.addEventListener("focusout", () => {
10419
+ setTimeout(() => {
10420
+ if (!optionsMenuContainer?.contains(document.activeElement)) setOptionsMenuOpen(false);
10421
+ }, 0);
10422
+ });
9914
10423
  elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
9915
10424
  elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
9916
10425
  elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
9917
10426
  elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu("/tools"));
10427
+ elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
10428
+ elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
10429
+ elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
10430
+ elements.optionsCloneButton.addEventListener("click", () => runNativeCommandMenu("/clone"));
10431
+ elements.optionsSettingsButton.addEventListener("click", () => runNativeCommandMenu("/settings"));
10432
+ elements.optionsExportButton.addEventListener("click", () => runNativeCommandMenu("/export"));
10433
+ elements.optionsForkButton.addEventListener("click", () => runNativeCommandMenu("/fork"));
10434
+ elements.optionsTreeButton.addEventListener("click", () => runNativeCommandMenu("/tree"));
9918
10435
  elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
9919
10436
  elements.nativeCommandDialog.addEventListener("close", () => {
9920
10437
  elements.nativeCommandSearch.oninput = null;
@@ -10034,8 +10551,11 @@ elements.setModelButton.addEventListener("click", async () => {
10034
10551
  const tabContext = activeTabContext();
10035
10552
  try {
10036
10553
  const selected = JSON.parse(elements.modelSelect.value);
10037
- await api("/api/model", { method: "POST", body: selected, tabId: tabContext.tabId });
10038
- if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
10554
+ const response = await api("/api/model", { method: "POST", body: selected, tabId: tabContext.tabId });
10555
+ if (isCurrentTabContext(tabContext)) {
10556
+ applyOptimisticModelSelection(response.data || selected, tabContext);
10557
+ await refreshState(tabContext);
10558
+ }
10039
10559
  } catch (error) {
10040
10560
  if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
10041
10561
  }
@@ -10045,13 +10565,13 @@ elements.setThinkingButton.addEventListener("click", async () => {
10045
10565
  try {
10046
10566
  const response = await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
10047
10567
  if (isCurrentTabContext(tabContext)) {
10568
+ applyOptimisticThinkingSelection(response.data, tabContext);
10048
10569
  if (response.data?.pending) {
10049
10570
  addEvent(response.data.message || `Thinking level ${response.data.level} will apply to the next prompt.`, "info");
10050
10571
  } else if (response.data?.level) {
10051
10572
  const requested = response.data.requestedLevel;
10052
10573
  const effective = response.data.level;
10053
10574
  addEvent(requested && requested !== effective ? `Thinking level set to ${effective} (requested ${requested}).` : `Thinking level set to ${effective}.`, "info");
10054
- requestGitFooterWebuiPayload(tabContext, { force: true });
10055
10575
  }
10056
10576
  await refreshState(tabContext);
10057
10577
  }
@@ -10125,11 +10645,15 @@ document.addEventListener("pointerdown", (event) => {
10125
10645
  if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
10126
10646
  setNativeCommandMenuOpen(false);
10127
10647
  }
10648
+ if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
10649
+ setOptionsMenuOpen(false);
10650
+ }
10128
10651
  if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
10129
10652
  setMobileTabsExpanded(false);
10130
10653
  }
10131
- if (footerModelPickerOpen && !elements.statusBar.contains(event.target)) {
10654
+ if (isFooterPickerOpen() && !elements.statusBar.contains(event.target)) {
10132
10655
  setFooterModelPickerOpen(false);
10656
+ setFooterThinkingPickerOpen(false);
10133
10657
  }
10134
10658
  }, { passive: true });
10135
10659
  document.addEventListener("pointermove", (event) => {
@@ -10215,6 +10739,10 @@ window.addEventListener("keydown", (event) => {
10215
10739
  setNativeCommandMenuOpen(false);
10216
10740
  return;
10217
10741
  }
10742
+ if (optionsMenuOpen) {
10743
+ setOptionsMenuOpen(false);
10744
+ return;
10745
+ }
10218
10746
  if (document.body.classList.contains("composer-actions-open")) {
10219
10747
  setComposerActionsOpen(false);
10220
10748
  return;
@@ -10223,14 +10751,25 @@ window.addEventListener("keydown", (event) => {
10223
10751
  setMobileTabsExpanded(false);
10224
10752
  return;
10225
10753
  }
10226
- if (footerModelPickerOpen) {
10754
+ if (isFooterPickerOpen()) {
10227
10755
  setFooterModelPickerOpen(false);
10756
+ setFooterThinkingPickerOpen(false);
10228
10757
  return;
10229
10758
  }
10230
10759
  if (!elements.commandSuggest.hidden) {
10231
10760
  hideCommandSuggestions();
10232
10761
  return;
10233
10762
  }
10763
+ if (document.activeElement === elements.promptInput && !elements.promptInput.value.trim() && doubleEscapeAction !== "none") {
10764
+ const now = Date.now();
10765
+ if (now - lastEmptyPromptEscapeTime < 500) {
10766
+ event.preventDefault();
10767
+ lastEmptyPromptEscapeTime = 0;
10768
+ runNativeCommandMenu(`/${doubleEscapeAction}`).catch((error) => addEvent(error.message || String(error), "error"));
10769
+ return;
10770
+ }
10771
+ lastEmptyPromptEscapeTime = now;
10772
+ }
10234
10773
  if (isMobileView() && !document.body.classList.contains("side-panel-collapsed")) {
10235
10774
  setSidePanelCollapsed(true);
10236
10775
  return;