@firstpick/pi-package-webui 0.2.6 → 0.2.8

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";
@@ -7288,10 +7529,10 @@ function resetOptionalFeatureAvailability() {
7288
7529
  renderOptionalFeatureControls();
7289
7530
  }
7290
7531
 
7291
- function requestGitFooterWebuiPayload(tabContext = activeTabContext()) {
7532
+ function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
7292
7533
  if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
7293
7534
  if (currentState?.isStreaming || currentState?.isCompacting) return;
7294
- if (!hasAvailableCommand("git-footer-refresh") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY)) return;
7535
+ if (!hasAvailableCommand("git-footer-refresh") || (!force && statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY))) return;
7295
7536
  if (gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId)) return;
7296
7537
 
7297
7538
  gitFooterPayloadRefreshInFlightByTab.add(tabContext.tabId);
@@ -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,81 +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
- if (thinking.select.value !== state.thinkingLevel) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: thinking.select.value } }));
7734
- if (steering.select.value !== state.steeringMode) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: steering.select.value } }));
7735
- if (followUp.select.value !== state.followUpMode) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: followUp.select.value } }));
7736
- if (autoCompact.input.checked !== state.autoCompactionEnabled) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: autoCompact.input.checked } }));
7737
- elements.busyBehavior.value = busyBehavior.select.value;
7738
- if (thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(thinkingOutput.input.checked);
7739
- 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);
7740
8126
  await Promise.all(requests);
7741
- addTransientMessage({ role: "native", title: "/settings", content: "Settings updated.", level: "info" });
8127
+ const response = await nativeCommandApi("/api/settings", { method: "POST", body: { settings: collectNativeSettingsPayload(controls), reload } });
8128
+ applyResponseTab(response);
8129
+ applyNativeSettingsForBrowser(response.data?.settings || collectNativeSettingsPayload(controls));
8130
+ if (thinkingLevelChanged) requestGitFooterWebuiPayload(activeTabContext(), { force: true });
8131
+ addTransientMessage({ role: "native", title: "/settings", content: nativeSettingsChangedMessage(response, reload), level: response.data?.reloadRecommended?.length && !response.data?.reloaded ? "warn" : "info" });
7742
8132
  closeNativeCommandDialog();
7743
- await refreshState();
8133
+ await refreshAll();
7744
8134
  } catch (error) {
7745
8135
  setNativeCommandError(error.message || String(error));
7746
8136
  } finally {
7747
- setNativeActionBusy(save, false);
8137
+ setNativeActionBusy(button, false);
7748
8138
  }
7749
- }, "primary");
8139
+ };
8140
+
8141
+ const reloadButton = addNativeCommandAction("Apply & reload tab", () => applySettings(true, reloadButton));
8142
+ const save = addNativeCommandAction("Apply", () => applySettings(false, save), "primary");
7750
8143
  }
7751
8144
 
7752
8145
  async function openNativeForkSelector() {
@@ -7811,6 +8204,41 @@ function openNativeCloneDialog() {
7811
8204
  }, "primary");
7812
8205
  }
7813
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
+
7814
8242
  async function openNativeResumeSelector(scope = "current") {
7815
8243
  openNativeCommandDialog({ title: "/resume", message: "Resume another persisted Pi session.", searchPlaceholder: "Filter sessions…" });
7816
8244
  renderNativeLoading("Loading sessions…");
@@ -7852,21 +8280,39 @@ async function openNativeResumeSelector(scope = "current") {
7852
8280
  }
7853
8281
  }
7854
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
+
7855
8299
  async function openNativeTreeSelector() {
7856
8300
  openNativeCommandDialog({ title: "/tree", message: "Navigate the current session tree. Choosing a user message restores it into the editor.", searchPlaceholder: "Filter tree…" });
7857
8301
  renderNativeLoading("Loading session tree…");
7858
8302
  try {
7859
8303
  const response = await nativeCommandApi("/api/session-tree");
7860
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.");
7861
8307
  const summarize = nativeSettingToggle("Summarize abandoned branch", false, "Optional; may call the active model before switching branches.");
7862
8308
  const labelField = make("label", "native-settings-field");
7863
- labelField.append(make("span", "native-settings-label", "Optional label"));
8309
+ labelField.append(nativeSettingsLabelRow("Optional label"));
7864
8310
  const labelInput = make("input", "dialog-input");
7865
8311
  labelInput.placeholder = "checkpoint label";
7866
8312
  labelField.append(labelInput);
7867
8313
  const options = make("div", "native-tree-options");
7868
- options.append(summarize.field, labelField);
7869
- const items = nodes.map((node) => ({
8314
+ options.append(filterField.field, summarize.field, labelField);
8315
+ const toItems = () => nodes.filter((node) => nativeTreeFilterMatches(node, selectedFilter)).map((node) => ({
7870
8316
  id: node.id,
7871
8317
  label: `${node.title}${node.label ? ` · ${node.label}` : ""}`,
7872
8318
  description: node.text || "",
@@ -7895,9 +8341,13 @@ async function openNativeTreeSelector() {
7895
8341
  }
7896
8342
  };
7897
8343
  const render = () => {
7898
- 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 });
7899
8345
  elements.nativeCommandBody.prepend(options);
7900
8346
  };
8347
+ filterField.select.addEventListener("change", () => {
8348
+ selectedFilter = filterField.select.value;
8349
+ render();
8350
+ });
7901
8351
  elements.nativeCommandSearch.oninput = render;
7902
8352
  render();
7903
8353
  } catch (error) {
@@ -8094,7 +8544,7 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
8094
8544
  await openNativeModelSelector();
8095
8545
  return true;
8096
8546
  case "settings":
8097
- openNativeSettingsDialog();
8547
+ await openNativeSettingsDialog();
8098
8548
  return true;
8099
8549
  case "theme":
8100
8550
  openNativeThemeSelector();
@@ -8105,6 +8555,9 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
8105
8555
  case "clone":
8106
8556
  openNativeCloneDialog();
8107
8557
  return true;
8558
+ case "name":
8559
+ openNativeNameDialog();
8560
+ return true;
8108
8561
  case "resume":
8109
8562
  await openNativeResumeSelector();
8110
8563
  return true;
@@ -8335,15 +8788,26 @@ function handleMessageUpdate(event) {
8335
8788
  }
8336
8789
  }
8337
8790
 
8791
+ function modelStateKey(model) {
8792
+ return model ? `${model.provider || ""}/${model.id || ""}` : "";
8793
+ }
8794
+
8795
+ function gitFooterRelevantStateChanged(previousState, nextState) {
8796
+ if (!previousState || !nextState) return false;
8797
+ return previousState.thinkingLevel !== nextState.thinkingLevel || modelStateKey(previousState.model) !== modelStateKey(nextState.model);
8798
+ }
8799
+
8338
8800
  async function refreshState(tabContext = activeTabContext()) {
8339
8801
  if (!tabContext.tabId) return;
8340
8802
  const response = await api("/api/state", { tabId: tabContext.tabId });
8341
8803
  if (!isCurrentTabContext(tabContext)) return;
8804
+ const previousState = currentState;
8342
8805
  currentState = response.data || null;
8806
+ const shouldRefreshGitFooter = gitFooterRelevantStateChanged(previousState, currentState);
8343
8807
  syncActiveTabActivityFromState(currentState);
8344
8808
  syncRunIndicatorFromState(currentState);
8345
8809
  renderStatus();
8346
- requestGitFooterWebuiPayload(tabContext);
8810
+ requestGitFooterWebuiPayload(tabContext, { force: shouldRefreshGitFooter });
8347
8811
  }
8348
8812
 
8349
8813
  async function refreshStats(tabContext = activeTabContext()) {
@@ -8602,10 +9066,23 @@ function getCommandMatches(query) {
8602
9066
  .map((command) => ({ command, score: scoreCommandSuggestion(command, query) }))
8603
9067
  .filter((item) => Number.isFinite(item.score))
8604
9068
  .sort((a, b) => a.score - b.score || a.command.name.localeCompare(b.command.name))
8605
- .slice(0, 12)
9069
+ .slice(0, clampAutocompleteMaxVisible(autocompleteMaxVisible))
8606
9070
  .map((item) => item.command);
8607
9071
  }
8608
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
+
8609
9086
  function activeSuggestionCount() {
8610
9087
  return suggestionMode === "path" ? pathSuggestions.length : commandSuggestions.length;
8611
9088
  }
@@ -8877,8 +9354,16 @@ function renderCommands() {
8877
9354
  hideCommandSuggestions();
8878
9355
  return;
8879
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
+ }
8880
9365
  elements.commandsBox.classList.remove("muted");
8881
- for (const command of commandsToShow.slice(0, 80)) {
9366
+ for (const command of filteredCommands.slice(0, 80)) {
8882
9367
  const item = make("button", "command-item");
8883
9368
  item.type = "button";
8884
9369
  item.title = `Send /${command.name}`;
@@ -8912,6 +9397,7 @@ async function refreshAll(tabContext = activeTabContext()) {
8912
9397
  refreshCommands(tabContext),
8913
9398
  refreshStats(tabContext),
8914
9399
  refreshWorkspace(tabContext),
9400
+ refreshNativeSettings(tabContext),
8915
9401
  refreshNetworkStatus(),
8916
9402
  refreshWebuiVersion(),
8917
9403
  ]);
@@ -9162,6 +9648,7 @@ async function cycleModelFromShortcut(direction = "forward") {
9162
9648
  const model = response.data?.model;
9163
9649
  const scope = response.data?.scoped ? `scoped (${response.data.scopeSource})` : "all models";
9164
9650
  if (isCurrentTabContext(tabContext)) {
9651
+ applyOptimisticModelSelection(model, tabContext);
9165
9652
  addTransientMessage({ role: "native", title: "model cycle", content: `Model set to ${appShortcutModelLabel(model)} via ${direction} cycle over ${scope}.`, level: "info" });
9166
9653
  await Promise.allSettled([refreshState(tabContext), refreshModels(tabContext), refreshStats(tabContext)]);
9167
9654
  }
@@ -9178,9 +9665,10 @@ async function cycleThinkingFromShortcut() {
9178
9665
  if (!tabContext.tabId) return;
9179
9666
  try {
9180
9667
  const response = await api("/api/thinking-cycle", { method: "POST", body: {}, tabId: tabContext.tabId });
9181
- if (response.data?.level && currentState) currentState = { ...currentState, thinkingLevel: response.data.level };
9182
9668
  if (isCurrentTabContext(tabContext)) {
9669
+ applyOptimisticThinkingSelection(response.data, tabContext);
9183
9670
  addTransientMessage({ role: "native", title: "thinking", content: response.data?.level ? `Thinking level: ${response.data.level}` : "Thinking level did not change.", level: "info" });
9671
+ if (response.data?.level) requestGitFooterWebuiPayload(tabContext, { force: true });
9184
9672
  await Promise.allSettled([refreshState(tabContext), refreshStats(tabContext)]);
9185
9673
  }
9186
9674
  } catch (error) {
@@ -9802,7 +10290,13 @@ function handleEvent(event) {
9802
10290
  syncRunIndicatorFromState(currentState);
9803
10291
  renderStatus();
9804
10292
  } else if (["set_model", "cycle_model", "set_thinking_level", "cycle_thinking_level", "new_session", "compact"].includes(event.command)) {
9805
- 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") {
9806
10300
  const tabId = event.tabId || activeTabId;
9807
10301
  forgetLastUserPrompt(tabId);
9808
10302
  resetGitWorkflowForTab(tabId);
@@ -9840,6 +10334,7 @@ function connectEvents(tabContext = activeTabContext()) {
9840
10334
 
9841
10335
  elements.copyServerCommandButton?.addEventListener("click", copyServerStartCommand);
9842
10336
  elements.retryServerConnectionButton?.addEventListener("click", retryServerConnection);
10337
+ elements.commandSearchInput?.addEventListener("input", renderCommands);
9843
10338
  elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
9844
10339
  elements.composer.addEventListener("submit", (event) => {
9845
10340
  event.preventDefault();
@@ -9862,15 +10357,18 @@ elements.gitWorkflowButton.addEventListener("click", () => {
9862
10357
  const publishMenuContainer = elements.publishButton.parentElement;
9863
10358
  elements.publishButton.addEventListener("click", () => {
9864
10359
  setNativeCommandMenuOpen(false);
10360
+ setOptionsMenuOpen(false);
9865
10361
  setPublishMenuOpen(true);
9866
10362
  });
9867
10363
  publishMenuContainer?.addEventListener("pointerenter", () => {
9868
10364
  setNativeCommandMenuOpen(false);
10365
+ setOptionsMenuOpen(false);
9869
10366
  setPublishMenuOpen(true);
9870
10367
  });
9871
10368
  publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
9872
10369
  publishMenuContainer?.addEventListener("focusin", () => {
9873
10370
  setNativeCommandMenuOpen(false);
10371
+ setOptionsMenuOpen(false);
9874
10372
  setPublishMenuOpen(true);
9875
10373
  });
9876
10374
  publishMenuContainer?.addEventListener("focusout", () => {
@@ -9881,15 +10379,18 @@ publishMenuContainer?.addEventListener("focusout", () => {
9881
10379
  const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
9882
10380
  elements.nativeCommandMenuButton.addEventListener("click", () => {
9883
10381
  setPublishMenuOpen(false);
10382
+ setOptionsMenuOpen(false);
9884
10383
  setNativeCommandMenuOpen(true);
9885
10384
  });
9886
10385
  nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
9887
10386
  setPublishMenuOpen(false);
10387
+ setOptionsMenuOpen(false);
9888
10388
  setNativeCommandMenuOpen(true);
9889
10389
  });
9890
10390
  nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
9891
10391
  nativeCommandMenuContainer?.addEventListener("focusin", () => {
9892
10392
  setPublishMenuOpen(false);
10393
+ setOptionsMenuOpen(false);
9893
10394
  setNativeCommandMenuOpen(true);
9894
10395
  });
9895
10396
  nativeCommandMenuContainer?.addEventListener("focusout", () => {
@@ -9897,10 +10398,40 @@ nativeCommandMenuContainer?.addEventListener("focusout", () => {
9897
10398
  if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
9898
10399
  }, 0);
9899
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
+ });
9900
10423
  elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
9901
10424
  elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
9902
10425
  elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
9903
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"));
9904
10435
  elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
9905
10436
  elements.nativeCommandDialog.addEventListener("close", () => {
9906
10437
  elements.nativeCommandSearch.oninput = null;
@@ -10020,8 +10551,11 @@ elements.setModelButton.addEventListener("click", async () => {
10020
10551
  const tabContext = activeTabContext();
10021
10552
  try {
10022
10553
  const selected = JSON.parse(elements.modelSelect.value);
10023
- await api("/api/model", { method: "POST", body: selected, tabId: tabContext.tabId });
10024
- 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
+ }
10025
10559
  } catch (error) {
10026
10560
  if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
10027
10561
  }
@@ -10031,7 +10565,14 @@ elements.setThinkingButton.addEventListener("click", async () => {
10031
10565
  try {
10032
10566
  const response = await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
10033
10567
  if (isCurrentTabContext(tabContext)) {
10034
- if (response.data?.pending) addEvent(response.data.message || `Thinking level ${response.data.level} will apply to the next prompt.`, "info");
10568
+ applyOptimisticThinkingSelection(response.data, tabContext);
10569
+ if (response.data?.pending) {
10570
+ addEvent(response.data.message || `Thinking level ${response.data.level} will apply to the next prompt.`, "info");
10571
+ } else if (response.data?.level) {
10572
+ const requested = response.data.requestedLevel;
10573
+ const effective = response.data.level;
10574
+ addEvent(requested && requested !== effective ? `Thinking level set to ${effective} (requested ${requested}).` : `Thinking level set to ${effective}.`, "info");
10575
+ }
10035
10576
  await refreshState(tabContext);
10036
10577
  }
10037
10578
  } catch (error) {
@@ -10104,11 +10645,15 @@ document.addEventListener("pointerdown", (event) => {
10104
10645
  if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
10105
10646
  setNativeCommandMenuOpen(false);
10106
10647
  }
10648
+ if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
10649
+ setOptionsMenuOpen(false);
10650
+ }
10107
10651
  if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
10108
10652
  setMobileTabsExpanded(false);
10109
10653
  }
10110
- if (footerModelPickerOpen && !elements.statusBar.contains(event.target)) {
10654
+ if (isFooterPickerOpen() && !elements.statusBar.contains(event.target)) {
10111
10655
  setFooterModelPickerOpen(false);
10656
+ setFooterThinkingPickerOpen(false);
10112
10657
  }
10113
10658
  }, { passive: true });
10114
10659
  document.addEventListener("pointermove", (event) => {
@@ -10194,6 +10739,10 @@ window.addEventListener("keydown", (event) => {
10194
10739
  setNativeCommandMenuOpen(false);
10195
10740
  return;
10196
10741
  }
10742
+ if (optionsMenuOpen) {
10743
+ setOptionsMenuOpen(false);
10744
+ return;
10745
+ }
10197
10746
  if (document.body.classList.contains("composer-actions-open")) {
10198
10747
  setComposerActionsOpen(false);
10199
10748
  return;
@@ -10202,14 +10751,25 @@ window.addEventListener("keydown", (event) => {
10202
10751
  setMobileTabsExpanded(false);
10203
10752
  return;
10204
10753
  }
10205
- if (footerModelPickerOpen) {
10754
+ if (isFooterPickerOpen()) {
10206
10755
  setFooterModelPickerOpen(false);
10756
+ setFooterThinkingPickerOpen(false);
10207
10757
  return;
10208
10758
  }
10209
10759
  if (!elements.commandSuggest.hidden) {
10210
10760
  hideCommandSuggestions();
10211
10761
  return;
10212
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
+ }
10213
10773
  if (isMobileView() && !document.body.classList.contains("side-panel-collapsed")) {
10214
10774
  setSidePanelCollapsed(true);
10215
10775
  return;