@firstpick/pi-package-webui 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -2
- package/bin/pi-webui.mjs +94 -8
- package/images/Webui_AppRunner_v0.4.8.png +0 -0
- package/images/Webui_BTW_v0.4.8.png +0 -0
- package/images/Webui_CWDpicker_v0.4.8.png +0 -0
- package/images/Webui_CodexUsage_v0.4.8.png +0 -0
- package/images/Webui_ControlPanel_v0.4.8.png +0 -0
- package/images/Webui_Effort_v0.4.8.png +0 -0
- package/images/Webui_GitBranches_v0.4.8.png +0 -0
- package/images/Webui_GitDiff_v0.4.8.png +0 -0
- package/images/Webui_GitWorkflow_v0.4.8.png +0 -0
- package/images/Webui_OptionalFeatures_v0.4.8.png +0 -0
- package/images/Webui_Pistats_v0.4.8.png +0 -0
- package/images/Webui_Queues_v0.4.8.png +0 -0
- package/images/Webui_ScopedModels_v0.4.8.png +0 -0
- package/images/Webui_SkillSetup_v0.4.8.png +0 -0
- package/images/Webui_ToolsSetup_v0.4.8.png +0 -0
- package/images/Webui_Workspace_v0.4.8.png +0 -0
- package/package.json +2 -2
- package/public/app.js +880 -71
- package/public/index.html +6 -2
- package/public/styles.css +458 -46
- package/tests/http-endpoints-harness.test.mjs +55 -0
- package/tests/mobile-static.test.mjs +27 -10
- package/tests/native-parity.test.mjs +5 -1
package/public/app.js
CHANGED
|
@@ -117,6 +117,9 @@ const elements = {
|
|
|
117
117
|
gitChangesRefreshButton: $("#gitChangesRefreshButton"),
|
|
118
118
|
gitChangesPullButton: $("#gitChangesPullButton"),
|
|
119
119
|
gitChangesCloseButton: $("#gitChangesCloseButton"),
|
|
120
|
+
modelControlLabel: $("#modelControlLabel"),
|
|
121
|
+
modelSearchInput: $("#modelSearchInput"),
|
|
122
|
+
modelSearchResults: $("#modelSearchResults"),
|
|
120
123
|
modelSelect: $("#modelSelect"),
|
|
121
124
|
setModelButton: $("#setModelButton"),
|
|
122
125
|
thinkingSelect: $("#thinkingSelect"),
|
|
@@ -125,6 +128,9 @@ const elements = {
|
|
|
125
128
|
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
126
129
|
terminalTabsLayoutSelect: $("#terminalTabsLayoutSelect"),
|
|
127
130
|
terminalTabsLayoutStatus: $("#terminalTabsLayoutStatus"),
|
|
131
|
+
themeControlLabel: $("#themeControlLabel"),
|
|
132
|
+
themeSearchInput: $("#themeSearchInput"),
|
|
133
|
+
themeSearchResults: $("#themeSearchResults"),
|
|
128
134
|
themeSelect: $("#themeSelect"),
|
|
129
135
|
backgroundInput: $("#backgroundInput"),
|
|
130
136
|
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
@@ -349,6 +355,7 @@ let thinkingOutputVisible = true;
|
|
|
349
355
|
let terminalTabsLayout = "top";
|
|
350
356
|
let webuiSettings = {};
|
|
351
357
|
let busyPromptBehavior = "followUp";
|
|
358
|
+
let composerModeRenderSignature = "";
|
|
352
359
|
let autocompleteMaxVisible = 12;
|
|
353
360
|
let doubleEscapeAction = "none";
|
|
354
361
|
let treeFilterMode = "default";
|
|
@@ -405,6 +412,8 @@ let abortLongPressDeadlineAt = 0;
|
|
|
405
412
|
let abortLongPressSource = "long-press";
|
|
406
413
|
let abortLongPressReleasePending = false;
|
|
407
414
|
let abortLongPressHandled = false;
|
|
415
|
+
let escapeAbortHoldSuppressesDoubleEscape = false;
|
|
416
|
+
let suppressEmptyPromptEscapeUntil = 0;
|
|
408
417
|
const dialogQueue = [];
|
|
409
418
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
410
419
|
const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed";
|
|
@@ -491,6 +500,7 @@ const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
|
491
500
|
const ABORT_LONG_PRESS_MS = 3000;
|
|
492
501
|
const ABORT_LONG_PRESS_TICK_MS = 100;
|
|
493
502
|
const ABORT_LONG_PRESS_RELEASE_GRACE_MS = 350;
|
|
503
|
+
const EMPTY_PROMPT_ESCAPE_AFTER_ABORT_GRACE_MS = 1000;
|
|
494
504
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
495
505
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
496
506
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
@@ -514,6 +524,7 @@ const widgets = new Map();
|
|
|
514
524
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
515
525
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
516
526
|
const appRunnerDataByTab = new Map();
|
|
527
|
+
const appRunnerInputDraftByRun = new Map();
|
|
517
528
|
const liveToolRuns = new Map();
|
|
518
529
|
const liveToolCards = new Map();
|
|
519
530
|
const liveToolRenderQueue = new Map();
|
|
@@ -933,6 +944,37 @@ function deferChatFollowScrollDuringPointerActivation({ force = false } = {}) {
|
|
|
933
944
|
return true;
|
|
934
945
|
}
|
|
935
946
|
|
|
947
|
+
function isInteractiveDropdownOpen() {
|
|
948
|
+
return Boolean(
|
|
949
|
+
document.body.classList.contains("composer-actions-open")
|
|
950
|
+
|| publishMenuOpen
|
|
951
|
+
|| nativeCommandMenuOpen
|
|
952
|
+
|| appRunnerMenuOpen
|
|
953
|
+
|| optionsMenuOpen
|
|
954
|
+
|| busyPromptBehaviorMenuOpen
|
|
955
|
+
|| newTabMenuOpen
|
|
956
|
+
|| isFooterPickerOpen()
|
|
957
|
+
|| elements.commandSuggest?.hidden === false
|
|
958
|
+
|| elements.modelSearchInput?.hidden === false
|
|
959
|
+
|| elements.themeSearchInput?.hidden === false,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function deferChatFollowScrollDuringInteractiveDropdown({ force = false } = {}) {
|
|
964
|
+
if (force || !isInteractiveDropdownOpen()) return false;
|
|
965
|
+
deferredChatFollowScroll = true;
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function scheduleDeferredUiFlushAfterDropdownClose() {
|
|
970
|
+
if (!deferredChatFollowScroll && deferredUiRenderCallbacks.size === 0) return;
|
|
971
|
+
const flush = () => {
|
|
972
|
+
if (!isInteractiveDropdownOpen()) flushDeferredUiRenders();
|
|
973
|
+
};
|
|
974
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
|
|
975
|
+
else setTimeout(flush, 0);
|
|
976
|
+
}
|
|
977
|
+
|
|
936
978
|
function flushDeferredUiRenders() {
|
|
937
979
|
const callbacks = [...deferredUiRenderCallbacks.values()];
|
|
938
980
|
deferredUiRenderCallbacks.clear();
|
|
@@ -1954,7 +1996,10 @@ function setBusyPromptBehaviorMenuOpen(open, { focusCurrent = false } = {}) {
|
|
|
1954
1996
|
elements.busyPromptBehaviorTag?.setAttribute("aria-expanded", busyPromptBehaviorMenuOpen ? "true" : "false");
|
|
1955
1997
|
elements.busyPromptBehaviorTag?.classList.toggle("menu-open", busyPromptBehaviorMenuOpen);
|
|
1956
1998
|
if (elements.busyPromptBehaviorMenu) elements.busyPromptBehaviorMenu.hidden = !busyPromptBehaviorMenuOpen;
|
|
1957
|
-
if (!busyPromptBehaviorMenuOpen)
|
|
1999
|
+
if (!busyPromptBehaviorMenuOpen) {
|
|
2000
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
1958
2003
|
renderBusyPromptBehaviorMenu();
|
|
1959
2004
|
if (focusCurrent) {
|
|
1960
2005
|
requestAnimationFrame(() => {
|
|
@@ -2025,6 +2070,7 @@ function setComposerActionsOpen(open) {
|
|
|
2025
2070
|
setBusyPromptBehaviorMenuOpen(false);
|
|
2026
2071
|
}
|
|
2027
2072
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
2073
|
+
if (!shouldOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
2028
2074
|
}
|
|
2029
2075
|
|
|
2030
2076
|
function isUserBashActive(tabId = activeTabId) {
|
|
@@ -2069,6 +2115,18 @@ function resizePromptInput() {
|
|
|
2069
2115
|
function updateComposerModeButtons() {
|
|
2070
2116
|
const runActive = isRunActive();
|
|
2071
2117
|
const abortAvailable = isAbortAvailable();
|
|
2118
|
+
const abortHoldSnapshot = isAbortLongPressActive();
|
|
2119
|
+
const nextSignature = [
|
|
2120
|
+
activeTabGeneration,
|
|
2121
|
+
runActive ? "run" : "idle",
|
|
2122
|
+
abortAvailable ? "abort" : "no-abort",
|
|
2123
|
+
abortHoldSnapshot ? `hold:${abortLongPressLabel()}` : "no-hold",
|
|
2124
|
+
abortRequestInFlight ? "aborting" : "ready",
|
|
2125
|
+
busyPromptBehavior,
|
|
2126
|
+
].join("|");
|
|
2127
|
+
if (nextSignature === composerModeRenderSignature) return;
|
|
2128
|
+
composerModeRenderSignature = nextSignature;
|
|
2129
|
+
|
|
2072
2130
|
const target = runActive ? elements.composerRow : elements.composerActionsPanel;
|
|
2073
2131
|
const before = runActive ? elements.abortButton : null;
|
|
2074
2132
|
for (const button of [elements.steerButton, elements.followUpButton]) {
|
|
@@ -2083,9 +2141,11 @@ function updateComposerModeButtons() {
|
|
|
2083
2141
|
if (abortHoldActive) {
|
|
2084
2142
|
renderAbortLongPressAffordance();
|
|
2085
2143
|
} else {
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
elements.abortButton.
|
|
2144
|
+
const abortText = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
2145
|
+
const abortTitle = abortAvailable ? abortButtonReadyTitle() : "Abort is available while Pi is running";
|
|
2146
|
+
if (elements.abortButton.textContent !== abortText) elements.abortButton.textContent = abortText;
|
|
2147
|
+
if (elements.abortButton.title !== abortTitle) elements.abortButton.title = abortTitle;
|
|
2148
|
+
if (elements.abortButton.getAttribute("aria-label") !== abortTitle) elements.abortButton.setAttribute("aria-label", abortTitle);
|
|
2089
2149
|
}
|
|
2090
2150
|
renderBusyPromptBehaviorTag();
|
|
2091
2151
|
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
@@ -2648,6 +2708,88 @@ function triggerNativeDownload(download) {
|
|
|
2648
2708
|
return true;
|
|
2649
2709
|
}
|
|
2650
2710
|
|
|
2711
|
+
function isStandalonePwaWindow() {
|
|
2712
|
+
return window.matchMedia?.("(display-mode: standalone)")?.matches === true || window.navigator.standalone === true;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function alternateLoopbackBrowserUrl(value) {
|
|
2716
|
+
const url = safeHttpUrl(value);
|
|
2717
|
+
if (!url) return "";
|
|
2718
|
+
try {
|
|
2719
|
+
const parsed = new URL(url);
|
|
2720
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
2721
|
+
if (hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1") parsed.hostname = "localhost";
|
|
2722
|
+
else if (hostname === "localhost") parsed.hostname = "127.0.0.1";
|
|
2723
|
+
else return "";
|
|
2724
|
+
return parsed.href;
|
|
2725
|
+
} catch {
|
|
2726
|
+
return "";
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
function nativeDownloadOpenUrl(download, { externalBrowser = false } = {}) {
|
|
2731
|
+
const rawUrl = download?.openUrl || download?.url;
|
|
2732
|
+
const url = safeHttpUrl(rawUrl);
|
|
2733
|
+
if (!url) return "";
|
|
2734
|
+
let openUrl = url;
|
|
2735
|
+
if (!download?.openUrl) {
|
|
2736
|
+
try {
|
|
2737
|
+
const inlineUrl = new URL(url);
|
|
2738
|
+
inlineUrl.searchParams.set("disposition", "inline");
|
|
2739
|
+
openUrl = inlineUrl.href;
|
|
2740
|
+
} catch {
|
|
2741
|
+
openUrl = url;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
return externalBrowser && isStandalonePwaWindow() ? alternateLoopbackBrowserUrl(openUrl) || openUrl : openUrl;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function openNativeDownloadInBrowser(download) {
|
|
2748
|
+
const url = nativeDownloadOpenUrl(download, { externalBrowser: true });
|
|
2749
|
+
if (!url) return false;
|
|
2750
|
+
const anchor = document.createElement("a");
|
|
2751
|
+
anchor.href = url;
|
|
2752
|
+
anchor.target = "_blank";
|
|
2753
|
+
anchor.rel = "noopener";
|
|
2754
|
+
anchor.hidden = true;
|
|
2755
|
+
document.body.append(anchor);
|
|
2756
|
+
anchor.click();
|
|
2757
|
+
anchor.remove();
|
|
2758
|
+
return true;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function openNativeExportDownloadPrompt(download) {
|
|
2762
|
+
const url = nativeDownloadOpenUrl(download, { externalBrowser: true });
|
|
2763
|
+
if (!url) return false;
|
|
2764
|
+
const fileName = String(download?.fileName || "session export");
|
|
2765
|
+
openNativeCommandDialog({ title: "/export", message: "Session export is ready." });
|
|
2766
|
+
elements.nativeCommandBody.append(
|
|
2767
|
+
make("p", "native-command-note", `File: ${fileName}`),
|
|
2768
|
+
make("p", "native-command-note muted", "Open it in your browser, or cancel and use the transcript download URL later."),
|
|
2769
|
+
);
|
|
2770
|
+
elements.nativeCommandActions.replaceChildren();
|
|
2771
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
2772
|
+
addNativeCommandAction("Copy URL", async () => {
|
|
2773
|
+
try {
|
|
2774
|
+
await copyText(url);
|
|
2775
|
+
addEvent("copied export browser URL", "info");
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
addEvent(`copy export URL failed: ${error.message || String(error)}`, "error");
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
addNativeCommandAction("Open in browser", () => {
|
|
2781
|
+
if (openNativeDownloadInBrowser(download)) addEvent(`opened export: ${fileName}`, "info");
|
|
2782
|
+
else addEvent("could not open export URL", "error");
|
|
2783
|
+
closeNativeCommandDialog();
|
|
2784
|
+
}, "primary");
|
|
2785
|
+
return true;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
function handleNativeDownloadResponse(download, command) {
|
|
2789
|
+
if (String(command || "").toLowerCase() === "export") return openNativeExportDownloadPrompt(download);
|
|
2790
|
+
return triggerNativeDownload(download);
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2651
2793
|
async function copyServerStartCommand() {
|
|
2652
2794
|
const command = serverStartCommandText();
|
|
2653
2795
|
try {
|
|
@@ -3786,6 +3928,23 @@ function storeDisabledOptionalFeatures() {
|
|
|
3786
3928
|
}
|
|
3787
3929
|
}
|
|
3788
3930
|
|
|
3931
|
+
function reconcileDisabledOptionalFeaturesFromStorage() {
|
|
3932
|
+
const nextDisabled = new Set(loadDisabledOptionalFeatures());
|
|
3933
|
+
let changed = nextDisabled.size !== disabledOptionalFeatures.size;
|
|
3934
|
+
if (!changed) {
|
|
3935
|
+
for (const featureId of nextDisabled) {
|
|
3936
|
+
if (!disabledOptionalFeatures.has(featureId)) {
|
|
3937
|
+
changed = true;
|
|
3938
|
+
break;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
if (!changed) return false;
|
|
3943
|
+
disabledOptionalFeatures = nextDisabled;
|
|
3944
|
+
renderOptionalFeatureDependentDisplays();
|
|
3945
|
+
return true;
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3789
3948
|
function isOptionalFeatureDetected(featureId) {
|
|
3790
3949
|
return optionalFeatureAvailability[featureId] === true;
|
|
3791
3950
|
}
|
|
@@ -3818,6 +3977,7 @@ function setOptionalFeatureDisabled(featureId, disabled) {
|
|
|
3818
3977
|
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
3819
3978
|
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
3820
3979
|
else disabledOptionalFeatures.delete(featureId);
|
|
3980
|
+
if (featureId === "remoteWebui") syncRemoteWebuiControlVisibility(false);
|
|
3821
3981
|
if (featureId === "gitFooterStatus") {
|
|
3822
3982
|
statusEntries.delete(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
3823
3983
|
clearGitFooterWebuiPayloadCache();
|
|
@@ -4045,6 +4205,79 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
4045
4205
|
if (announce) addEvent(`theme changed to ${theme.label || displayThemeName(theme.name) || theme.name}`);
|
|
4046
4206
|
}
|
|
4047
4207
|
|
|
4208
|
+
function themeDisplayLabel(theme) {
|
|
4209
|
+
return theme?.label || displayThemeName(theme?.name) || theme?.name || "";
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
function themeSearchText(theme) {
|
|
4213
|
+
return [themeDisplayLabel(theme), theme?.name, theme?.author].filter(Boolean).join(" ");
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
function renderThemeSearchResults(themes = []) {
|
|
4217
|
+
if (!elements.themeSearchResults) return;
|
|
4218
|
+
elements.themeSearchResults.replaceChildren();
|
|
4219
|
+
if (elements.themeSearchInput?.hidden) return;
|
|
4220
|
+
if (!themes.length) {
|
|
4221
|
+
elements.themeSearchResults.append(make("div", "model-search-empty", "No themes match the search"));
|
|
4222
|
+
elements.themeSearchResults.hidden = false;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
for (const theme of themes) {
|
|
4226
|
+
const selected = theme.name === currentThemeName;
|
|
4227
|
+
const button = make("button", `model-search-result theme-search-result${selected ? " active" : ""}`);
|
|
4228
|
+
button.type = "button";
|
|
4229
|
+
button.setAttribute("role", "option");
|
|
4230
|
+
button.setAttribute("aria-selected", String(selected));
|
|
4231
|
+
button.title = themeSearchText(theme);
|
|
4232
|
+
button.append(make("span", "model-search-result-main", themeDisplayLabel(theme)));
|
|
4233
|
+
button.addEventListener("click", () => {
|
|
4234
|
+
if (elements.themeSelect) elements.themeSelect.value = theme.name;
|
|
4235
|
+
setThemeByName(theme.name, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
4236
|
+
});
|
|
4237
|
+
elements.themeSearchResults.append(button);
|
|
4238
|
+
}
|
|
4239
|
+
elements.themeSearchResults.hidden = false;
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
function populateThemeSelect(themes = availableThemes, query = "") {
|
|
4243
|
+
if (!elements.themeSelect) return [];
|
|
4244
|
+
const normalizedQuery = String(query || "").trim().toLowerCase();
|
|
4245
|
+
const matchingThemes = themes.filter((theme) => !normalizedQuery || themeSearchText(theme).toLowerCase().includes(normalizedQuery));
|
|
4246
|
+
renderThemeSearchResults(matchingThemes);
|
|
4247
|
+
return matchingThemes;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
function showThemeSearchInput({ focus = true } = {}) {
|
|
4251
|
+
if (!elements.themeSearchInput) return;
|
|
4252
|
+
elements.themeSearchInput.hidden = false;
|
|
4253
|
+
elements.themeSearchResults.hidden = false;
|
|
4254
|
+
elements.themeSelect.classList.add("model-select-expanded");
|
|
4255
|
+
populateThemeSelect(availableThemes, elements.themeSearchInput.value);
|
|
4256
|
+
if (focus) {
|
|
4257
|
+
requestAnimationFrame(() => {
|
|
4258
|
+
elements.themeSearchInput.focus();
|
|
4259
|
+
elements.themeSearchInput.select();
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
function hideThemeSearchInput() {
|
|
4265
|
+
if (!elements.themeSearchInput) return;
|
|
4266
|
+
elements.themeSearchInput.hidden = true;
|
|
4267
|
+
elements.themeSearchInput.value = "";
|
|
4268
|
+
if (elements.themeSearchResults) {
|
|
4269
|
+
elements.themeSearchResults.hidden = true;
|
|
4270
|
+
elements.themeSearchResults.replaceChildren();
|
|
4271
|
+
}
|
|
4272
|
+
elements.themeSelect?.classList.remove("model-select-expanded");
|
|
4273
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
function toggleThemeSearchInput() {
|
|
4277
|
+
if (elements.themeSearchInput?.hidden) showThemeSearchInput();
|
|
4278
|
+
else hideThemeSearchInput();
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4048
4281
|
function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {}) {
|
|
4049
4282
|
if (!elements.themeSelect) return;
|
|
4050
4283
|
elements.themeSelect.replaceChildren();
|
|
@@ -4069,6 +4302,7 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
4069
4302
|
elements.themeSelect.append(option);
|
|
4070
4303
|
}
|
|
4071
4304
|
elements.themeSelect.value = currentThemeName;
|
|
4305
|
+
populateThemeSelect(availableThemes, elements.themeSearchInput?.value || "");
|
|
4072
4306
|
}
|
|
4073
4307
|
|
|
4074
4308
|
async function setThemeByName(name, options = {}) {
|
|
@@ -4077,6 +4311,7 @@ async function setThemeByName(name, options = {}) {
|
|
|
4077
4311
|
if (!theme) return;
|
|
4078
4312
|
currentThemeName = theme.name;
|
|
4079
4313
|
if (elements.themeSelect && elements.themeSelect.value !== theme.name) elements.themeSelect.value = theme.name;
|
|
4314
|
+
populateThemeSelect(availableThemes, elements.themeSearchInput?.value || "");
|
|
4080
4315
|
setCustomBackgroundRecord(null);
|
|
4081
4316
|
customBackgroundLoading = true;
|
|
4082
4317
|
applyTheme(theme, options);
|
|
@@ -4777,6 +5012,7 @@ function setNewTabMenuOpen(open) {
|
|
|
4777
5012
|
elements.newTabButton?.setAttribute("aria-expanded", newTabMenuOpen ? "true" : "false");
|
|
4778
5013
|
elements.newTabButton?.classList.toggle("menu-open", newTabMenuOpen);
|
|
4779
5014
|
elements.newTabMenu?.classList.toggle("open", newTabMenuOpen);
|
|
5015
|
+
if (!newTabMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
4780
5016
|
}
|
|
4781
5017
|
|
|
4782
5018
|
function openNewTabMenu() {
|
|
@@ -6294,29 +6530,90 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
6294
6530
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
6295
6531
|
}
|
|
6296
6532
|
|
|
6533
|
+
// Shape key for a footer chip with the frequently-changing fields removed, so
|
|
6534
|
+
// live streaming metrics (token counts, tok/s, cost, context %) can be updated
|
|
6535
|
+
// in place without tearing down the footer DOM (and any open dropdown inside it).
|
|
6536
|
+
function gitFooterChipShapeKey(chip) {
|
|
6537
|
+
const shape = {};
|
|
6538
|
+
for (const [key, value] of Object.entries(chip || {})) {
|
|
6539
|
+
if (key === "value") continue;
|
|
6540
|
+
if (key === "contextUsage") {
|
|
6541
|
+
shape.contextUsage = value ? true : false;
|
|
6542
|
+
continue;
|
|
6543
|
+
}
|
|
6544
|
+
shape[key] = value;
|
|
6545
|
+
}
|
|
6546
|
+
return JSON.stringify(shape);
|
|
6547
|
+
}
|
|
6548
|
+
|
|
6549
|
+
function gitFooterPickerStateKey() {
|
|
6550
|
+
return `${footerModelPickerOpen ? 1 : 0}|${footerThinkingPickerOpen ? 1 : 0}|${footerBranchPickerOpen ? 1 : 0}|${mobileFooterExpanded ? 1 : 0}`;
|
|
6551
|
+
}
|
|
6552
|
+
|
|
6553
|
+
function updateGitFooterChipNodeValue(node, chip, valueSelector) {
|
|
6554
|
+
if (!node) return;
|
|
6555
|
+
const valueNode = node.querySelector(valueSelector);
|
|
6556
|
+
if (valueNode && valueNode.textContent !== String(chip.value ?? "")) valueNode.textContent = String(chip.value ?? "");
|
|
6557
|
+
if (chip.contextUsage) applyFooterContextUsage(node, chip.contextUsage);
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6560
|
+
let gitFooterRenderCache = null;
|
|
6561
|
+
|
|
6562
|
+
function invalidateGitFooterRenderCache() {
|
|
6563
|
+
gitFooterRenderCache = null;
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6297
6566
|
function renderGitFooterPayload(payload) {
|
|
6298
6567
|
const tab = activeTab();
|
|
6568
|
+
const pickerKey = gitFooterPickerStateKey();
|
|
6569
|
+
const mainKeys = payload.main.map(gitFooterChipShapeKey);
|
|
6570
|
+
const metaKeys = payload.meta.map(gitFooterChipShapeKey);
|
|
6571
|
+
|
|
6572
|
+
// Fast path: only live metric values changed. Update text in place instead of
|
|
6573
|
+
// rebuilding the footer so buttons do not flicker and an open dropdown is not
|
|
6574
|
+
// destroyed/recreated/repositioned during streaming.
|
|
6575
|
+
const cache = gitFooterRenderCache;
|
|
6576
|
+
if (
|
|
6577
|
+
cache &&
|
|
6578
|
+
elements.statusBar.classList.contains("statusbar-git-footer") &&
|
|
6579
|
+
cache.pickerKey === pickerKey &&
|
|
6580
|
+
cache.mainKeys.length === mainKeys.length &&
|
|
6581
|
+
cache.metaKeys.length === metaKeys.length &&
|
|
6582
|
+
cache.mainKeys.every((key, index) => key === mainKeys[index]) &&
|
|
6583
|
+
cache.metaKeys.every((key, index) => key === metaKeys[index]) &&
|
|
6584
|
+
cache.mainNodes.every((node) => node.isConnected) &&
|
|
6585
|
+
cache.metaNodes.every((node) => node.isConnected)
|
|
6586
|
+
) {
|
|
6587
|
+
payload.main.forEach((chip, index) => updateGitFooterChipNodeValue(cache.mainNodes[index], chip, ".footer-metric-value"));
|
|
6588
|
+
payload.meta.forEach((chip, index) => updateGitFooterChipNodeValue(cache.metaNodes[index], chip, ".footer-meta-value"));
|
|
6589
|
+
if (isFooterPickerOpen()) updateFooterModelPickerPosition();
|
|
6590
|
+
return;
|
|
6591
|
+
}
|
|
6592
|
+
|
|
6299
6593
|
hideFooterTooltip();
|
|
6300
6594
|
elements.statusBar.replaceChildren();
|
|
6301
6595
|
elements.statusBar.classList.remove("statusbar-tui-footer");
|
|
6302
6596
|
elements.statusBar.classList.add("statusbar-git-footer");
|
|
6303
6597
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
6304
6598
|
|
|
6599
|
+
const mainNodes = payload.main.map(renderGitFooterPayloadMetric);
|
|
6305
6600
|
const row1 = make("div", "footer-line footer-line-main");
|
|
6306
|
-
row1.append(...
|
|
6601
|
+
row1.append(...mainNodes);
|
|
6307
6602
|
|
|
6308
6603
|
const footerToggle = make("button", "footer-details-toggle", mobileFooterExpanded ? "Less" : "Details");
|
|
6309
6604
|
footerToggle.type = "button";
|
|
6310
6605
|
footerToggle.setAttribute("aria-expanded", mobileFooterExpanded ? "true" : "false");
|
|
6311
6606
|
footerToggle.addEventListener("click", () => setMobileFooterExpanded(!mobileFooterExpanded));
|
|
6312
6607
|
|
|
6608
|
+
const metaNodes = payload.meta.map((chip) => renderGitFooterPayloadMeta(chip, tab));
|
|
6313
6609
|
const row2 = make("div", "footer-line footer-line-meta");
|
|
6314
|
-
row2.append(...
|
|
6610
|
+
row2.append(...metaNodes, footerToggle);
|
|
6315
6611
|
|
|
6316
6612
|
elements.statusBar.append(row1, row2);
|
|
6317
6613
|
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
6318
6614
|
if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
|
|
6319
6615
|
if (footerBranchPickerOpen) elements.statusBar.append(renderFooterBranchPicker());
|
|
6616
|
+
gitFooterRenderCache = { pickerKey, mainKeys, metaKeys, mainNodes, metaNodes };
|
|
6320
6617
|
setMobileFooterExpanded(mobileFooterExpanded);
|
|
6321
6618
|
updateFooterModelPickerPosition();
|
|
6322
6619
|
}
|
|
@@ -6653,9 +6950,36 @@ function renderGitUntrackedSection(untracked) {
|
|
|
6653
6950
|
return wrapper;
|
|
6654
6951
|
}
|
|
6655
6952
|
|
|
6953
|
+
function renderGitChangesFileList(parsedSections, untracked) {
|
|
6954
|
+
const items = [];
|
|
6955
|
+
for (const entry of parsedSections || []) {
|
|
6956
|
+
for (const file of entry.files || []) {
|
|
6957
|
+
items.push({ path: file.path || "diff", stats: file.statsText || `+${file.additions || 0} −${file.deletions || 0}`, section: entry.section?.label || "Diff" });
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
for (const entry of untracked || []) {
|
|
6961
|
+
items.push({ path: entry.path, stats: entry.binary ? "binary" : "new", section: "Untracked" });
|
|
6962
|
+
}
|
|
6963
|
+
if (!items.length) return null;
|
|
6964
|
+
const list = make("nav", "git-changes-file-list");
|
|
6965
|
+
list.setAttribute("aria-label", "Changed files");
|
|
6966
|
+
list.append(make("span", "git-changes-file-list-label", `${items.length} file${items.length === 1 ? "" : "s"}`));
|
|
6967
|
+
for (const item of items) {
|
|
6968
|
+
const button = make("button", "git-changes-file-jump");
|
|
6969
|
+
button.type = "button";
|
|
6970
|
+
button.dataset.gitChangesJumpFile = item.path;
|
|
6971
|
+
button.title = `${item.section}: ${item.path}`;
|
|
6972
|
+
button.append(make("span", "git-changes-file-jump-name", item.path), make("span", "git-changes-file-jump-meta", `${item.section} · ${item.stats}`));
|
|
6973
|
+
list.append(button);
|
|
6974
|
+
}
|
|
6975
|
+
return list;
|
|
6976
|
+
}
|
|
6977
|
+
|
|
6656
6978
|
function renderGitCurrentFileHeader() {
|
|
6657
|
-
const header = make("
|
|
6658
|
-
header.
|
|
6979
|
+
const header = make("button", "git-current-file-header");
|
|
6980
|
+
header.type = "button";
|
|
6981
|
+
header.title = "Collapse or expand the current file diff";
|
|
6982
|
+
header.append(make("span", "git-current-file-label", "Current file"), make("span", "git-current-file-name", "—"), make("span", "git-current-file-toggle", "Toggle"));
|
|
6659
6983
|
return header;
|
|
6660
6984
|
}
|
|
6661
6985
|
|
|
@@ -6682,7 +7006,10 @@ function updateGitChangesCurrentFileHeader() {
|
|
|
6682
7006
|
if (rect.top <= markerY) current = file;
|
|
6683
7007
|
else break;
|
|
6684
7008
|
}
|
|
6685
|
-
|
|
7009
|
+
const currentPath = current?.dataset.gitDiffFile || "—";
|
|
7010
|
+
name.textContent = currentPath;
|
|
7011
|
+
header.dataset.gitCurrentFile = currentPath;
|
|
7012
|
+
header.setAttribute("aria-expanded", String(current?.open !== false));
|
|
6686
7013
|
}
|
|
6687
7014
|
|
|
6688
7015
|
function gitChangesGeneratedLabel(data) {
|
|
@@ -6739,7 +7066,11 @@ function renderGitChangesDialog() {
|
|
|
6739
7066
|
.filter((entry) => entry.files.length > 0);
|
|
6740
7067
|
const untracked = gitUntrackedEntries(data.untracked);
|
|
6741
7068
|
const hasVisibleFiles = parsedSections.length > 0 || untracked.length > 0;
|
|
6742
|
-
if (hasVisibleFiles)
|
|
7069
|
+
if (hasVisibleFiles) {
|
|
7070
|
+
const fileList = renderGitChangesFileList(parsedSections, untracked);
|
|
7071
|
+
if (fileList) body.append(fileList);
|
|
7072
|
+
body.append(renderGitCurrentFileHeader());
|
|
7073
|
+
}
|
|
6743
7074
|
for (const entry of parsedSections) body.append(renderGitDiffSection(entry.section, entry.files));
|
|
6744
7075
|
if (untracked.length) body.append(renderGitUntrackedSection(untracked));
|
|
6745
7076
|
if (!hasVisibleFiles) {
|
|
@@ -6837,6 +7168,7 @@ function gitFooterFallbackMessage() {
|
|
|
6837
7168
|
}
|
|
6838
7169
|
|
|
6839
7170
|
function renderMinimalFooter() {
|
|
7171
|
+
invalidateGitFooterRenderCache();
|
|
6840
7172
|
hideFooterTooltip();
|
|
6841
7173
|
const tab = activeTab();
|
|
6842
7174
|
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "loading…");
|
|
@@ -6988,10 +7320,66 @@ function renderContextMeter() {
|
|
|
6988
7320
|
root.replaceChildren(summary, meter, actions);
|
|
6989
7321
|
}
|
|
6990
7322
|
|
|
6991
|
-
function
|
|
6992
|
-
const
|
|
6993
|
-
|
|
6994
|
-
|
|
7323
|
+
function compactDashboardText(value, maxLength = 34) {
|
|
7324
|
+
const text = String(value || "").trim();
|
|
7325
|
+
if (!text || text.length <= maxLength) return text;
|
|
7326
|
+
const available = Math.max(8, maxLength - 1);
|
|
7327
|
+
const headLength = Math.max(4, Math.ceil(available * 0.58));
|
|
7328
|
+
const tailLength = Math.max(4, available - headLength);
|
|
7329
|
+
return `${text.slice(0, headLength)}…${text.slice(-tailLength)}`;
|
|
7330
|
+
}
|
|
7331
|
+
|
|
7332
|
+
function compactDashboardPath(value, maxLength = 52) {
|
|
7333
|
+
const text = normalizeDisplayPath(value || "").trim();
|
|
7334
|
+
if (!text || text.length <= maxLength) return text;
|
|
7335
|
+
const segments = text.split("/").filter(Boolean);
|
|
7336
|
+
if (segments.length >= 3) {
|
|
7337
|
+
const tail = `…/${segments.slice(-3).join("/")}`;
|
|
7338
|
+
if (tail.length <= maxLength) return tail;
|
|
7339
|
+
}
|
|
7340
|
+
return compactDashboardText(text, maxLength);
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
function contextDashboardTone(snapshot) {
|
|
7344
|
+
if (!snapshot) return "muted";
|
|
7345
|
+
if (typeof snapshot.percent !== "number") return "neutral";
|
|
7346
|
+
if (snapshot.percent >= 85) return "danger";
|
|
7347
|
+
if (snapshot.percent >= 70) return "warning";
|
|
7348
|
+
return "ok";
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7351
|
+
function dashboardSessionSummary() {
|
|
7352
|
+
const rawSession = currentState?.sessionName || currentState?.sessionId || "";
|
|
7353
|
+
const rawFile = currentState?.sessionFile || "in-memory";
|
|
7354
|
+
const sessionLabel = rawSession ? compactDashboardText(rawSession, 28) : "loading…";
|
|
7355
|
+
const fileLabel = rawFile === "in-memory" ? rawFile : compactDashboardPath(rawFile, 58);
|
|
7356
|
+
return {
|
|
7357
|
+
value: sessionLabel,
|
|
7358
|
+
detail: fileLabel,
|
|
7359
|
+
title: [rawSession || "Session loading…", rawFile].filter(Boolean).join("\n"),
|
|
7360
|
+
};
|
|
7361
|
+
}
|
|
7362
|
+
|
|
7363
|
+
function dashboardMetric(label, value, detail = "", options = {}) {
|
|
7364
|
+
const tone = options.tone || "neutral";
|
|
7365
|
+
const item = make("div", `workspace-dashboard-metric tone-${tone}${options.meterSnapshot ? " with-meter" : ""}`);
|
|
7366
|
+
const valueText = value || "—";
|
|
7367
|
+
const title = options.title || [label, valueText, detail].filter(Boolean).join(" · ");
|
|
7368
|
+
item.title = title;
|
|
7369
|
+
item.setAttribute("aria-label", title);
|
|
7370
|
+
|
|
7371
|
+
const icon = make("span", "workspace-dashboard-metric-icon", options.icon || "•");
|
|
7372
|
+
icon.setAttribute("aria-hidden", "true");
|
|
7373
|
+
const copy = make("span", "workspace-dashboard-metric-copy");
|
|
7374
|
+
copy.append(make("span", "workspace-dashboard-metric-label", label), make("strong", undefined, valueText));
|
|
7375
|
+
if (detail) copy.append(make("span", "workspace-dashboard-metric-detail", detail));
|
|
7376
|
+
item.append(icon, copy);
|
|
7377
|
+
|
|
7378
|
+
if (options.meterSnapshot) {
|
|
7379
|
+
const meter = make("span", "workspace-dashboard-mini-meter");
|
|
7380
|
+
appendContextMeterFill(meter, options.meterSnapshot);
|
|
7381
|
+
item.append(meter);
|
|
7382
|
+
}
|
|
6995
7383
|
return item;
|
|
6996
7384
|
}
|
|
6997
7385
|
|
|
@@ -7002,6 +7390,8 @@ function dashboardAction(label, handler, className = "") {
|
|
|
7002
7390
|
return button;
|
|
7003
7391
|
}
|
|
7004
7392
|
|
|
7393
|
+
let workspaceDashboardSignature = null;
|
|
7394
|
+
|
|
7005
7395
|
function renderWorkspaceDashboard() {
|
|
7006
7396
|
if (deferUiRenderDuringPointerActivation("workspace-dashboard", renderWorkspaceDashboard)) return;
|
|
7007
7397
|
const root = elements.workspaceDashboard;
|
|
@@ -7010,11 +7400,51 @@ function renderWorkspaceDashboard() {
|
|
|
7010
7400
|
const snapshot = contextUsageSnapshot();
|
|
7011
7401
|
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "Choose or create a tab to start");
|
|
7012
7402
|
const queueCount = Number(currentState?.pendingMessageCount || 0) || 0;
|
|
7403
|
+
const tabIndicatorState = tab ? tabIndicator(tab) : null;
|
|
7404
|
+
const sessionSummary = dashboardSessionSummary();
|
|
7405
|
+
const modelLabel = currentState?.model ? shortModelLabel(currentState.model) : "loading…";
|
|
7406
|
+
const queueDetail = queueCount === 0 ? "idle" : queueCount === 1 ? "pending message" : "pending messages";
|
|
7407
|
+
|
|
7408
|
+
// Skip rebuilding the dashboard (and its buttons) when nothing it shows has
|
|
7409
|
+
// changed. Extension status pushes during streaming would otherwise rebuild
|
|
7410
|
+
// every workspace button on each token, causing flicker.
|
|
7411
|
+
const signature = JSON.stringify({
|
|
7412
|
+
title: tab?.title || "Pi Web UI",
|
|
7413
|
+
workspaceLabel,
|
|
7414
|
+
tabIndicatorState,
|
|
7415
|
+
tabsLength: tabs.length,
|
|
7416
|
+
queueCount,
|
|
7417
|
+
modelLabel,
|
|
7418
|
+
modelTitle: currentState?.model ? shortModelLabel(currentState.model) : "Model loading…",
|
|
7419
|
+
thinking: currentState?.thinkingLevel || "",
|
|
7420
|
+
context: { display: contextUsageDisplay(snapshot), detail: contextUsageDetail(snapshot), tone: contextDashboardTone(snapshot) },
|
|
7421
|
+
session: sessionSummary,
|
|
7422
|
+
activeTabId,
|
|
7423
|
+
tabs: tabs.slice(0, 8).map((item) => ({ id: item.id, title: item.title, state: tabIndicator(item).state, active: item.id === activeTabId, cwd: item.cwd ? normalizeDisplayPath(item.cwd) : "" })),
|
|
7424
|
+
overflow: tabs.length > 8 ? tabs.length - 8 : 0,
|
|
7425
|
+
});
|
|
7426
|
+
if (signature === workspaceDashboardSignature && root.childElementCount > 0) return;
|
|
7427
|
+
workspaceDashboardSignature = signature;
|
|
7013
7428
|
root.replaceChildren();
|
|
7014
7429
|
|
|
7015
7430
|
const header = make("div", "workspace-dashboard-header");
|
|
7016
7431
|
const title = make("div", "workspace-dashboard-title");
|
|
7017
|
-
|
|
7432
|
+
const heading = make("h2", undefined, tab?.title || "Pi Web UI");
|
|
7433
|
+
heading.title = tab?.title || "Pi Web UI";
|
|
7434
|
+
const cwd = make("p", "workspace-dashboard-cwd muted", workspaceLabel);
|
|
7435
|
+
cwd.title = workspaceLabel;
|
|
7436
|
+
const meta = make("div", "workspace-dashboard-title-meta");
|
|
7437
|
+
if (tabIndicatorState) {
|
|
7438
|
+
const statusChip = make("span", `workspace-dashboard-chip activity-${tabIndicatorState.state}`);
|
|
7439
|
+
statusChip.title = tabIndicatorState.label;
|
|
7440
|
+
statusChip.append(make("span", "workspace-dashboard-chip-dot", tabIndicatorState.glyph), make("span", undefined, tabIndicatorState.label));
|
|
7441
|
+
meta.append(statusChip);
|
|
7442
|
+
}
|
|
7443
|
+
meta.append(
|
|
7444
|
+
make("span", "workspace-dashboard-chip", `${tabs.length} tab${tabs.length === 1 ? "" : "s"}`),
|
|
7445
|
+
make("span", `workspace-dashboard-chip ${queueCount ? "attention" : ""}`.trim(), queueCount ? `${queueCount} queued` : "queue clear"),
|
|
7446
|
+
);
|
|
7447
|
+
title.append(make("span", "workspace-dashboard-kicker", "Workspace"), heading, cwd, meta);
|
|
7018
7448
|
const actions = make("div", "workspace-dashboard-actions");
|
|
7019
7449
|
actions.append(
|
|
7020
7450
|
dashboardAction("Command palette", () => openCommandPalette(), "primary"),
|
|
@@ -7027,24 +7457,45 @@ function renderWorkspaceDashboard() {
|
|
|
7027
7457
|
|
|
7028
7458
|
const metrics = make("div", "workspace-dashboard-metrics");
|
|
7029
7459
|
metrics.append(
|
|
7030
|
-
dashboardMetric("Model",
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7460
|
+
dashboardMetric("Model", modelLabel, currentState?.thinkingLevel ? `thinking ${currentState.thinkingLevel}` : "ready", {
|
|
7461
|
+
icon: "◌",
|
|
7462
|
+
tone: currentState?.model ? "neutral" : "muted",
|
|
7463
|
+
title: currentState?.model ? shortModelLabel(currentState.model) : "Model loading…",
|
|
7464
|
+
}),
|
|
7465
|
+
dashboardMetric("Context", contextUsageDisplay(snapshot), contextUsageDetail(snapshot), {
|
|
7466
|
+
icon: "◐",
|
|
7467
|
+
tone: contextDashboardTone(snapshot),
|
|
7468
|
+
meterSnapshot: snapshot,
|
|
7469
|
+
}),
|
|
7470
|
+
dashboardMetric("Session", sessionSummary.value, sessionSummary.detail, {
|
|
7471
|
+
icon: "#",
|
|
7472
|
+
tone: currentState?.sessionId || currentState?.sessionName ? "neutral" : "muted",
|
|
7473
|
+
title: sessionSummary.title,
|
|
7474
|
+
}),
|
|
7475
|
+
dashboardMetric("Queue", `${queueCount}`, queueDetail, {
|
|
7476
|
+
icon: queueCount ? "↳" : "✓",
|
|
7477
|
+
tone: queueCount ? "warning" : "ok",
|
|
7478
|
+
}),
|
|
7034
7479
|
);
|
|
7035
7480
|
|
|
7036
7481
|
const tabsPanel = make("div", "workspace-dashboard-tabs");
|
|
7037
|
-
|
|
7482
|
+
const tabsHeader = make("div", "workspace-dashboard-tabs-header");
|
|
7483
|
+
tabsHeader.append(
|
|
7484
|
+
make("span", "workspace-dashboard-tabs-title", `Open tabs (${tabs.length})`),
|
|
7485
|
+
make("span", "workspace-dashboard-tabs-hint", tabs.length ? "Click a tab to switch" : "No tabs yet"),
|
|
7486
|
+
);
|
|
7487
|
+
tabsPanel.append(tabsHeader);
|
|
7038
7488
|
const tabList = make("div", "workspace-dashboard-tab-list");
|
|
7039
7489
|
for (const item of tabs.slice(0, 8)) {
|
|
7040
7490
|
const indicator = tabIndicator(item);
|
|
7041
7491
|
const button = make("button", `workspace-dashboard-tab activity-${indicator.state}${item.id === activeTabId ? " active" : ""}`);
|
|
7042
7492
|
button.type = "button";
|
|
7043
|
-
button.title = `${item.title} · ${indicator.label}`;
|
|
7044
|
-
button.append(make("span", "workspace-dashboard-tab-dot", indicator.glyph), make("span",
|
|
7493
|
+
button.title = `${item.title} · ${indicator.label}${item.cwd ? ` · ${normalizeDisplayPath(item.cwd)}` : ""}`;
|
|
7494
|
+
button.append(make("span", "workspace-dashboard-tab-dot", indicator.glyph), make("span", "workspace-dashboard-tab-label", item.title));
|
|
7045
7495
|
button.addEventListener("click", () => switchTab(item.id));
|
|
7046
7496
|
tabList.append(button);
|
|
7047
7497
|
}
|
|
7498
|
+
if (!tabs.length) tabList.append(make("span", "workspace-dashboard-tab-empty", "Create a tab to start a workspace."));
|
|
7048
7499
|
if (tabs.length > 8) tabList.append(make("span", "workspace-dashboard-tab-more", `+${tabs.length - 8} more`));
|
|
7049
7500
|
tabsPanel.append(tabList);
|
|
7050
7501
|
|
|
@@ -7052,6 +7503,7 @@ function renderWorkspaceDashboard() {
|
|
|
7052
7503
|
}
|
|
7053
7504
|
|
|
7054
7505
|
function setFooterModelPickerOpen(open) {
|
|
7506
|
+
const wasOpen = footerModelPickerOpen;
|
|
7055
7507
|
footerModelPickerOpen = !!open;
|
|
7056
7508
|
if (footerModelPickerOpen) {
|
|
7057
7509
|
footerThinkingPickerOpen = false;
|
|
@@ -7067,9 +7519,11 @@ function setFooterModelPickerOpen(open) {
|
|
|
7067
7519
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
7068
7520
|
renderFooter();
|
|
7069
7521
|
updateFooterModelPickerPosition();
|
|
7522
|
+
if (wasOpen && !footerModelPickerOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
7070
7523
|
}
|
|
7071
7524
|
|
|
7072
7525
|
function setFooterThinkingPickerOpen(open) {
|
|
7526
|
+
const wasOpen = footerThinkingPickerOpen;
|
|
7073
7527
|
footerThinkingPickerOpen = !!open;
|
|
7074
7528
|
if (footerThinkingPickerOpen) {
|
|
7075
7529
|
footerModelPickerOpen = false;
|
|
@@ -7085,6 +7539,7 @@ function setFooterThinkingPickerOpen(open) {
|
|
|
7085
7539
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
7086
7540
|
renderFooter();
|
|
7087
7541
|
updateFooterModelPickerPosition();
|
|
7542
|
+
if (wasOpen && !footerThinkingPickerOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
7088
7543
|
}
|
|
7089
7544
|
|
|
7090
7545
|
function normalizeFooterGitBranches(data = {}) {
|
|
@@ -7154,6 +7609,7 @@ async function loadFooterBranchPicker(tabContext = activeTabContext()) {
|
|
|
7154
7609
|
}
|
|
7155
7610
|
|
|
7156
7611
|
function setFooterBranchPickerOpen(open) {
|
|
7612
|
+
const wasOpen = footerBranchPickerOpen;
|
|
7157
7613
|
footerBranchPickerOpen = !!open;
|
|
7158
7614
|
if (footerBranchPickerOpen) {
|
|
7159
7615
|
footerModelPickerOpen = false;
|
|
@@ -7171,6 +7627,7 @@ function setFooterBranchPickerOpen(open) {
|
|
|
7171
7627
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
7172
7628
|
renderFooter();
|
|
7173
7629
|
updateFooterModelPickerPosition();
|
|
7630
|
+
if (wasOpen && !footerBranchPickerOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
7174
7631
|
}
|
|
7175
7632
|
|
|
7176
7633
|
function pathLooksInside(parentPath, childPath) {
|
|
@@ -8691,9 +9148,67 @@ async function clearAppRunner() {
|
|
|
8691
9148
|
}
|
|
8692
9149
|
}
|
|
8693
9150
|
|
|
9151
|
+
async function sendAppRunnerInput(run, form, { closeStdin = false } = {}) {
|
|
9152
|
+
const tabContext = activeTabContext();
|
|
9153
|
+
const input = form?.querySelector?.(".app-runner-stdin-input");
|
|
9154
|
+
if (!tabContext.tabId || !input || !appRunnerIsRunning(run)) return;
|
|
9155
|
+
const draftKey = appRunnerInputDraftKey(run);
|
|
9156
|
+
const text = input.value || "";
|
|
9157
|
+
const buttons = [...form.querySelectorAll("button")];
|
|
9158
|
+
buttons.forEach((button) => { button.disabled = true; });
|
|
9159
|
+
try {
|
|
9160
|
+
const response = await api("/api/app-runner/input", { method: "POST", body: { text, newline: !closeStdin || Boolean(text), closeStdin }, tabId: tabContext.tabId });
|
|
9161
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
9162
|
+
appRunnerInputDraftByRun.set(draftKey, "");
|
|
9163
|
+
input.value = "";
|
|
9164
|
+
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
9165
|
+
renderAppRunnerControls();
|
|
9166
|
+
renderWidgets();
|
|
9167
|
+
addEvent(closeStdin ? "sent app runner EOF" : text ? "sent app runner input" : "sent app runner Enter", "info");
|
|
9168
|
+
} catch (error) {
|
|
9169
|
+
if (isCurrentTabContext(tabContext)) addEvent(`app runner input failed: ${error.message || String(error)}`, "error");
|
|
9170
|
+
} finally {
|
|
9171
|
+
buttons.forEach((button) => { button.disabled = false; });
|
|
9172
|
+
}
|
|
9173
|
+
}
|
|
9174
|
+
|
|
9175
|
+
function appRunnerOutputLines(run) {
|
|
9176
|
+
const lines = Array.isArray(run?.lines) ? [...run.lines] : [];
|
|
9177
|
+
if (appRunnerIsRunning(run) && run?.pendingLine) lines.push(run.pendingLine);
|
|
9178
|
+
return lines;
|
|
9179
|
+
}
|
|
9180
|
+
|
|
8694
9181
|
function appRunnerOutputText(run) {
|
|
8695
|
-
|
|
8696
|
-
|
|
9182
|
+
return appRunnerOutputLines(run).join("\n").trimEnd();
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
function appRunnerInputDraftKey(run) {
|
|
9186
|
+
return run?.id || run?.runnerId || "active";
|
|
9187
|
+
}
|
|
9188
|
+
|
|
9189
|
+
function captureAppRunnerInputFocus() {
|
|
9190
|
+
const input = document.activeElement;
|
|
9191
|
+
if (!input?.classList?.contains("app-runner-stdin-input")) return null;
|
|
9192
|
+
return {
|
|
9193
|
+
runId: input.dataset.runId || "",
|
|
9194
|
+
value: input.value || "",
|
|
9195
|
+
selectionStart: input.selectionStart ?? input.value.length,
|
|
9196
|
+
selectionEnd: input.selectionEnd ?? input.value.length,
|
|
9197
|
+
};
|
|
9198
|
+
}
|
|
9199
|
+
|
|
9200
|
+
function restoreAppRunnerInputFocus(state) {
|
|
9201
|
+
if (!state?.runId) return;
|
|
9202
|
+
const input = document.querySelector(".app-runner-stdin-input");
|
|
9203
|
+
if (!input || input.dataset.runId !== state.runId) return;
|
|
9204
|
+
appRunnerInputDraftByRun.set(state.runId, state.value);
|
|
9205
|
+
input.value = state.value;
|
|
9206
|
+
try {
|
|
9207
|
+
input.focus({ preventScroll: true });
|
|
9208
|
+
input.setSelectionRange(state.selectionStart, state.selectionEnd);
|
|
9209
|
+
} catch {
|
|
9210
|
+
input.focus();
|
|
9211
|
+
}
|
|
8697
9212
|
}
|
|
8698
9213
|
|
|
8699
9214
|
async function copyAppRunnerOutput(run) {
|
|
@@ -9070,6 +9585,7 @@ function renderAppRunnerInfoDialog() {
|
|
|
9070
9585
|
"Detection is scoped to the active terminal tab's current working directory.",
|
|
9071
9586
|
"Only commands/files that exist and runner binaries available on this system are shown.",
|
|
9072
9587
|
"Starting a runner keeps live output pinned above the chat/terminal area.",
|
|
9588
|
+
"While a runner is active, the widget can send line-oriented stdin to interactive scripts.",
|
|
9073
9589
|
"Only one app runner can be active per tab; Close/Stop terminates the process/server.",
|
|
9074
9590
|
]) howList.append(make("li", "", line));
|
|
9075
9591
|
how.append(howList);
|
|
@@ -9094,6 +9610,42 @@ function closeAppRunnerInfoDialog() {
|
|
|
9094
9610
|
if (elements.appRunnerInfoDialog?.open) elements.appRunnerInfoDialog.close();
|
|
9095
9611
|
}
|
|
9096
9612
|
|
|
9613
|
+
function renderAppRunnerInputForm(run) {
|
|
9614
|
+
if (!appRunnerIsRunning(run)) return null;
|
|
9615
|
+
const key = appRunnerInputDraftKey(run);
|
|
9616
|
+
const form = make("form", "app-runner-stdin-form");
|
|
9617
|
+
form.dataset.runId = key;
|
|
9618
|
+
const input = make("textarea", "app-runner-stdin-input");
|
|
9619
|
+
input.rows = 1;
|
|
9620
|
+
input.value = appRunnerInputDraftByRun.get(key) || "";
|
|
9621
|
+
input.placeholder = run.stdinClosed ? "stdin is closed" : "Send stdin… Enter sends, Shift+Enter inserts a newline";
|
|
9622
|
+
input.disabled = run.stdinClosed === true;
|
|
9623
|
+
input.dataset.runId = key;
|
|
9624
|
+
input.setAttribute("aria-label", "Send input to app runner stdin");
|
|
9625
|
+
input.addEventListener("input", () => { appRunnerInputDraftByRun.set(key, input.value); });
|
|
9626
|
+
input.addEventListener("keydown", (event) => {
|
|
9627
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
9628
|
+
event.preventDefault();
|
|
9629
|
+
form.requestSubmit();
|
|
9630
|
+
});
|
|
9631
|
+
const send = make("button", "release-npm-action app-runner-stdin-send", input.value ? "Send input" : "Send Enter");
|
|
9632
|
+
send.type = "submit";
|
|
9633
|
+
send.disabled = run.stdinClosed === true;
|
|
9634
|
+
send.title = run.stdinClosed ? (run.stdinError || "App runner stdin is closed") : "Send this text followed by Enter to the running app runner";
|
|
9635
|
+
const eof = make("button", "release-npm-action app-runner-stdin-eof", "EOF");
|
|
9636
|
+
eof.type = "button";
|
|
9637
|
+
eof.disabled = run.stdinClosed === true;
|
|
9638
|
+
eof.title = run.stdinClosed ? (run.stdinError || "App runner stdin is closed") : "Close stdin after optionally sending the current text";
|
|
9639
|
+
eof.addEventListener("click", () => sendAppRunnerInput(run, form, { closeStdin: true }));
|
|
9640
|
+
input.addEventListener("input", () => { send.textContent = input.value ? "Send input" : "Send Enter"; });
|
|
9641
|
+
form.addEventListener("submit", (event) => {
|
|
9642
|
+
event.preventDefault();
|
|
9643
|
+
sendAppRunnerInput(run, form);
|
|
9644
|
+
});
|
|
9645
|
+
form.append(input, send, eof);
|
|
9646
|
+
return form;
|
|
9647
|
+
}
|
|
9648
|
+
|
|
9097
9649
|
function renderAppRunnerWidget() {
|
|
9098
9650
|
const data = activeAppRunnerData();
|
|
9099
9651
|
const run = data.activeRun;
|
|
@@ -9110,14 +9662,15 @@ function renderAppRunnerWidget() {
|
|
|
9110
9662
|
const elapsed = appRunnerElapsedLabel(run);
|
|
9111
9663
|
header.append(titleWrap);
|
|
9112
9664
|
|
|
9113
|
-
const
|
|
9114
|
-
const
|
|
9665
|
+
const outputLines = appRunnerOutputLines(run);
|
|
9666
|
+
const lines = outputLines.length ? outputLines : [run.displayCommand ? `$ ${run.displayCommand}` : "Waiting for app runner output..."];
|
|
9667
|
+
const streamHeader = releaseNpmStreamHeader(running ? "Live app output" : "App output", Math.max(run.lineCount || 0, lines.length), { live: running });
|
|
9115
9668
|
const terminal = make("div", "release-npm-terminal");
|
|
9116
9669
|
terminal.setAttribute("role", "log");
|
|
9117
9670
|
terminal.setAttribute("aria-live", running ? "polite" : "off");
|
|
9118
9671
|
for (const line of lines) appendReleaseNpmTerminalLine(terminal, line);
|
|
9119
9672
|
|
|
9120
|
-
const controlParts = [run.displayCommand, run.cwd, run.truncated ? "output truncated" : ""].map(cleanStatusText).filter(Boolean);
|
|
9673
|
+
const controlParts = [run.displayCommand, run.cwd, run.truncated ? "output truncated" : "", run.stdinError ? `stdin: ${run.stdinError}` : ""].map(cleanStatusText).filter(Boolean);
|
|
9121
9674
|
const controls = make("div", "release-npm-controls app-runner-output-controls");
|
|
9122
9675
|
const actions = make("div", "app-runner-output-actions");
|
|
9123
9676
|
const closeButton = appRunnerActionButton("Close", running ? stopAppRunner : clearAppRunner, running ? "danger app-runner-close-action" : "app-runner-close-action");
|
|
@@ -9132,11 +9685,14 @@ function renderAppRunnerWidget() {
|
|
|
9132
9685
|
if (canRunAgain) actions.append(appRunnerActionButton("Run again", () => runAppRunner(run.runnerId)));
|
|
9133
9686
|
actions.append(appRunnerActionButton("Clear", clearAppRunner));
|
|
9134
9687
|
}
|
|
9688
|
+
const inputForm = running ? renderAppRunnerInputForm(run) : null;
|
|
9135
9689
|
const pills = make("div", "app-runner-output-pills");
|
|
9136
9690
|
if (run.kind) pills.append(make("span", "release-npm-pill", run.kind));
|
|
9137
9691
|
pills.append(make("span", `release-npm-pill app-runner-status ${run.status || "running"}`.trim(), status));
|
|
9138
9692
|
if (elapsed) pills.append(make("span", "release-npm-pill elapsed", elapsed));
|
|
9139
|
-
controls.append(actions
|
|
9693
|
+
controls.append(actions);
|
|
9694
|
+
if (inputForm) controls.append(inputForm);
|
|
9695
|
+
controls.append(pills, make("span", "app-runner-output-meta", controlParts.join(" · ")));
|
|
9140
9696
|
const outputDetails = renderReleaseNpmOutputDetails(`app-runner:${run.id || run.runnerId || "active"}`, streamHeader, terminal, controls);
|
|
9141
9697
|
node.append(header, outputDetails);
|
|
9142
9698
|
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
@@ -9893,6 +10449,7 @@ function mirrorRemoteWebuiWidgetToTranscript(widgetKey, lines = [], request = {}
|
|
|
9893
10449
|
|
|
9894
10450
|
function renderWidgets() {
|
|
9895
10451
|
if (deferUiRenderDuringPointerActivation("widgets", renderWidgets)) return;
|
|
10452
|
+
const appRunnerInputFocus = captureAppRunnerInputFocus();
|
|
9896
10453
|
elements.widgetArea.replaceChildren();
|
|
9897
10454
|
const releaseOutput = renderReleaseNpmOutputWidget();
|
|
9898
10455
|
if (releaseOutput) elements.widgetArea.append(releaseOutput);
|
|
@@ -9926,6 +10483,7 @@ function renderWidgets() {
|
|
|
9926
10483
|
node.textContent = `${key}\n${cleanLines.join("\n")}`;
|
|
9927
10484
|
elements.widgetArea.append(node);
|
|
9928
10485
|
}
|
|
10486
|
+
restoreAppRunnerInputFocus(appRunnerInputFocus);
|
|
9929
10487
|
}
|
|
9930
10488
|
|
|
9931
10489
|
function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
@@ -10218,7 +10776,7 @@ function renderGitInitStackInput() {
|
|
|
10218
10776
|
elements.gitWorkflowActions.append(row);
|
|
10219
10777
|
}
|
|
10220
10778
|
|
|
10221
|
-
function renderGitWorkflowManualCommitInput() {
|
|
10779
|
+
function renderGitWorkflowManualCommitInput({ appendCommitButton = true } = {}) {
|
|
10222
10780
|
const tabId = gitWorkflowActionTabId();
|
|
10223
10781
|
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
10224
10782
|
const defaultCommitMessage = String(workflow?.manualCommitMessageDefault || "").trim();
|
|
@@ -10263,8 +10821,10 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
10263
10821
|
loadGitWorkflowDefaultCommitMessage({ runId: workflow?.runId, tabId });
|
|
10264
10822
|
|
|
10265
10823
|
field.append(input);
|
|
10266
|
-
row.append(field
|
|
10824
|
+
row.append(field);
|
|
10825
|
+
if (appendCommitButton) row.append(commitButton);
|
|
10267
10826
|
elements.gitWorkflowActions.append(row);
|
|
10827
|
+
return commitButton;
|
|
10268
10828
|
}
|
|
10269
10829
|
|
|
10270
10830
|
function setGitPrDialogStatus(message = "", level = "muted") {
|
|
@@ -10603,9 +11163,10 @@ function renderGitWorkflow() {
|
|
|
10603
11163
|
if (gitWorkflow.step === "add") {
|
|
10604
11164
|
addGitWorkflowAction("Run git add .", () => runGitAdd(), "primary", false);
|
|
10605
11165
|
} else if (gitWorkflow.step === "generate") {
|
|
10606
|
-
renderGitWorkflowManualCommitInput();
|
|
11166
|
+
const commitInputButton = renderGitWorkflowManualCommitInput({ appendCommitButton: false });
|
|
10607
11167
|
addGitWorkflowAction("Run /git-staged-msg", () => runGitMessagePrompt(), "primary", false);
|
|
10608
11168
|
addGitWorkflowAction("Preview current message files", () => loadGitWorkflowMessage({ requireFresh: false }), "", false);
|
|
11169
|
+
elements.gitWorkflowActions.append(commitInputButton);
|
|
10609
11170
|
} else if (gitWorkflow.step === "generating") {
|
|
10610
11171
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
10611
11172
|
} else if (gitWorkflow.step === "message") {
|
|
@@ -10613,10 +11174,11 @@ function renderGitWorkflow() {
|
|
|
10613
11174
|
addGitWorkflowAction("Create PR", () => createGitPrBranch(), "primary", false, GIT_WORKFLOW_CREATE_PR_TOOLTIP);
|
|
10614
11175
|
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
10615
11176
|
}
|
|
10616
|
-
renderGitWorkflowManualCommitInput();
|
|
11177
|
+
const commitInputButton = renderGitWorkflowManualCommitInput({ appendCommitButton: false });
|
|
10617
11178
|
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), gitWorkflow.prMode ? "primary" : "", false);
|
|
10618
11179
|
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), gitWorkflow.prMode ? "primary" : "", false);
|
|
10619
11180
|
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
11181
|
+
elements.gitWorkflowActions.append(commitInputButton);
|
|
10620
11182
|
} else if (gitWorkflow.step === "branchNaming") {
|
|
10621
11183
|
addGitWorkflowAction("Refresh branch name", () => loadGitWorkflowBranchName({ requireFresh: true }), "", false);
|
|
10622
11184
|
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", !!gitWorkflow.busy, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
@@ -13822,12 +14384,17 @@ function renderRunIndicator({ scroll = false } = {}) {
|
|
|
13822
14384
|
}
|
|
13823
14385
|
|
|
13824
14386
|
function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}) {
|
|
14387
|
+
const wasLocallyActive = runIndicatorLocallyActive;
|
|
14388
|
+
const previousActivity = runIndicatorActivity;
|
|
14389
|
+
const hadRunIndicatorBubble = runIndicatorBubble?.parentElement === elements.chat;
|
|
13825
14390
|
if (active) {
|
|
13826
14391
|
runIndicatorLocallyActive = true;
|
|
13827
14392
|
if (!runIndicatorStartedAt) runIndicatorStartedAt = performance.now();
|
|
13828
14393
|
}
|
|
13829
14394
|
runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
|
|
13830
|
-
|
|
14395
|
+
const needsRender = scroll || !hadRunIndicatorBubble || wasLocallyActive !== runIndicatorLocallyActive || previousActivity !== runIndicatorActivity;
|
|
14396
|
+
if (needsRender) renderRunIndicator({ scroll });
|
|
14397
|
+
else if (runIndicatorIsActive()) startRunIndicatorTicker();
|
|
13831
14398
|
updateComposerModeButtons();
|
|
13832
14399
|
if (active) scheduleRunIndicatorGraceCheck();
|
|
13833
14400
|
}
|
|
@@ -14156,8 +14723,8 @@ function applyNativeSlashCommandEffects(response, message, tabContext = activeTa
|
|
|
14156
14723
|
});
|
|
14157
14724
|
}
|
|
14158
14725
|
|
|
14159
|
-
if (data.download &&
|
|
14160
|
-
addEvent(`download started: ${data.download.fileName || data.download.url}`, "info");
|
|
14726
|
+
if (data.download && handleNativeDownloadResponse(data.download, data.command)) {
|
|
14727
|
+
addEvent(data.command === "export" ? `export ready: ${data.download.fileName || data.download.url}` : `download started: ${data.download.fileName || data.download.url}`, "info");
|
|
14161
14728
|
}
|
|
14162
14729
|
|
|
14163
14730
|
const cards = Array.isArray(data.cards) && data.cards.length ? data.cards : null;
|
|
@@ -14265,6 +14832,7 @@ function scheduleChatFollowScroll() {
|
|
|
14265
14832
|
|
|
14266
14833
|
function scrollChatToBottom({ force = false } = {}) {
|
|
14267
14834
|
if (deferChatFollowScrollDuringPointerActivation({ force })) return;
|
|
14835
|
+
if (deferChatFollowScrollDuringInteractiveDropdown({ force })) return;
|
|
14268
14836
|
if (force) autoFollowChat = true;
|
|
14269
14837
|
if (!autoFollowChat) {
|
|
14270
14838
|
updateJumpToLatestButton();
|
|
@@ -14361,6 +14929,7 @@ function setPublishMenuOpen(open) {
|
|
|
14361
14929
|
elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
|
|
14362
14930
|
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
14363
14931
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14932
|
+
if (!publishMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14364
14933
|
}
|
|
14365
14934
|
|
|
14366
14935
|
function setNativeCommandMenuOpen(open) {
|
|
@@ -14369,6 +14938,7 @@ function setNativeCommandMenuOpen(open) {
|
|
|
14369
14938
|
elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
|
|
14370
14939
|
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
14371
14940
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14941
|
+
if (!nativeCommandMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14372
14942
|
}
|
|
14373
14943
|
|
|
14374
14944
|
function setAppRunnerMenuOpen(open) {
|
|
@@ -14377,6 +14947,7 @@ function setAppRunnerMenuOpen(open) {
|
|
|
14377
14947
|
elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
|
|
14378
14948
|
elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
|
|
14379
14949
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14950
|
+
if (!appRunnerMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14380
14951
|
}
|
|
14381
14952
|
|
|
14382
14953
|
function setOptionsMenuOpen(open) {
|
|
@@ -14385,6 +14956,7 @@ function setOptionsMenuOpen(open) {
|
|
|
14385
14956
|
elements.optionsMenuButton.classList.toggle("menu-open", optionsMenuOpen);
|
|
14386
14957
|
elements.optionsMenuButton.parentElement?.classList.toggle("open", optionsMenuOpen);
|
|
14387
14958
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14959
|
+
if (!optionsMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14388
14960
|
}
|
|
14389
14961
|
|
|
14390
14962
|
function optionalFeatureIdForCommand(name) {
|
|
@@ -14572,6 +15144,19 @@ function optionalFeatureStatus(featureId) {
|
|
|
14572
15144
|
return { label: "Install needed", className: "missing", detail: installMessage || "Package is not installed or not visible from the Web UI package root" };
|
|
14573
15145
|
}
|
|
14574
15146
|
|
|
15147
|
+
function optionalFeatureTooltip(feature, status) {
|
|
15148
|
+
return [
|
|
15149
|
+
feature.label,
|
|
15150
|
+
`Status: ${status.label}`,
|
|
15151
|
+
status.detail,
|
|
15152
|
+
"",
|
|
15153
|
+
feature.description,
|
|
15154
|
+
"",
|
|
15155
|
+
`Check: ${feature.capabilityLabel}`,
|
|
15156
|
+
`Package: ${feature.packageName}`,
|
|
15157
|
+
].join("\n");
|
|
15158
|
+
}
|
|
15159
|
+
|
|
14575
15160
|
function optionalFeatureWidgetFeatureId(key) {
|
|
14576
15161
|
if (key.startsWith("btw:")) return "btwCommand";
|
|
14577
15162
|
if (key.startsWith("release-npm:")) return "releaseNpm";
|
|
@@ -14598,26 +15183,17 @@ function renderOptionalFeaturePanel() {
|
|
|
14598
15183
|
const packageStatus = optionalFeaturePackageStatus(feature.id);
|
|
14599
15184
|
const status = optionalFeatureStatus(feature.id);
|
|
14600
15185
|
const row = make("div", `optional-feature-row ${status.className}`);
|
|
15186
|
+
const tooltip = optionalFeatureTooltip(feature, status);
|
|
15187
|
+
row.dataset.tooltip = tooltip;
|
|
15188
|
+
row.setAttribute("aria-label", tooltip.replace(/\s+/g, " "));
|
|
15189
|
+
row.tabIndex = 0;
|
|
14601
15190
|
|
|
14602
15191
|
const main = make("div", "optional-feature-main");
|
|
14603
15192
|
const title = make("div", "optional-feature-title");
|
|
14604
15193
|
title.append(make("strong", undefined, feature.label), make("span", `optional-feature-pill ${status.className}`, status.label));
|
|
14605
|
-
|
|
14606
|
-
const description = make("div", "optional-feature-description", feature.description);
|
|
14607
|
-
const packageLine = make("code", "optional-feature-package", feature.packageName);
|
|
14608
|
-
main.append(title, detail, description, packageLine);
|
|
15194
|
+
main.append(title);
|
|
14609
15195
|
|
|
14610
15196
|
const actions = make("div", "optional-feature-actions");
|
|
14611
|
-
if (feature.id === "gitFooterStatus") {
|
|
14612
|
-
const setup = make("button", "optional-feature-action setup", "git-footer-status-setup");
|
|
14613
|
-
setup.type = "button";
|
|
14614
|
-
setup.title = GIT_FOOTER_STATUS_SETUP_TOOLTIP;
|
|
14615
|
-
setup.dataset.tooltip = GIT_FOOTER_STATUS_SETUP_TOOLTIP;
|
|
14616
|
-
setup.disabled = installing;
|
|
14617
|
-
setup.addEventListener("click", () => configureGitFooterStatusSetup({ force: true }));
|
|
14618
|
-
actions.append(setup);
|
|
14619
|
-
}
|
|
14620
|
-
|
|
14621
15197
|
const action = make("button", "optional-feature-action");
|
|
14622
15198
|
action.type = "button";
|
|
14623
15199
|
action.disabled = installing;
|
|
@@ -14709,18 +15285,23 @@ function renderOptionalFeatureControls() {
|
|
|
14709
15285
|
optionalFeatureUnavailableMessage("remoteWebui"),
|
|
14710
15286
|
);
|
|
14711
15287
|
}
|
|
14712
|
-
|
|
14713
|
-
elements.networkControlField.hidden = !hasRemoteWebuiCommand;
|
|
14714
|
-
elements.networkControlField.classList.toggle("feature-unavailable", !hasRemoteWebuiCommand);
|
|
14715
|
-
const label = elements.networkControlField.querySelector("label");
|
|
14716
|
-
const payload = remoteWebuiControlsPayload();
|
|
14717
|
-
if (label) label.textContent = payload?.title || "Remote WebUI";
|
|
14718
|
-
elements.networkControlField.title = hasRemoteWebuiCommand ? payload?.description || "Remote WebUI controls are provided by @firstpick/pi-package-remote-webui." : optionalFeatureUnavailableMessage("remoteWebui");
|
|
14719
|
-
}
|
|
15288
|
+
syncRemoteWebuiControlVisibility(hasRemoteWebuiCommand);
|
|
14720
15289
|
|
|
14721
15290
|
renderOptionalFeaturePanel();
|
|
14722
15291
|
}
|
|
14723
15292
|
|
|
15293
|
+
function syncRemoteWebuiControlVisibility(hasRemoteWebuiCommand = isOptionalFeatureEnabled("remoteWebui") && hasAvailableCommand("remote")) {
|
|
15294
|
+
if (!elements.networkControlField) return;
|
|
15295
|
+
elements.networkControlField.hidden = !hasRemoteWebuiCommand;
|
|
15296
|
+
elements.networkControlField.classList.toggle("feature-unavailable", !hasRemoteWebuiCommand);
|
|
15297
|
+
const label = elements.networkControlField.querySelector("label");
|
|
15298
|
+
const payload = remoteWebuiControlsPayload();
|
|
15299
|
+
if (label) label.textContent = payload?.title || "Remote WebUI";
|
|
15300
|
+
elements.networkControlField.title = hasRemoteWebuiCommand
|
|
15301
|
+
? payload?.description || "Remote WebUI controls are provided by @firstpick/pi-package-remote-webui."
|
|
15302
|
+
: optionalFeatureUnavailableMessage("remoteWebui");
|
|
15303
|
+
}
|
|
15304
|
+
|
|
14724
15305
|
function commandUnavailableMessage(commandName) {
|
|
14725
15306
|
const featureId = optionalFeatureIdForCommand(commandName);
|
|
14726
15307
|
if (featureId) return optionalFeatureUnavailableMessage(featureId);
|
|
@@ -14875,7 +15456,7 @@ function nativeSelectorMatches(item, query) {
|
|
|
14875
15456
|
.some((value) => String(value).toLowerCase().includes(needle));
|
|
14876
15457
|
}
|
|
14877
15458
|
|
|
14878
|
-
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId } = {}) {
|
|
15459
|
+
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId, numbered = false } = {}) {
|
|
14879
15460
|
const query = elements.nativeCommandSearch.value.trim();
|
|
14880
15461
|
const filtered = items.filter((item) => nativeSelectorMatches(item, query));
|
|
14881
15462
|
elements.nativeCommandBody.replaceChildren();
|
|
@@ -14884,13 +15465,13 @@ function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect,
|
|
|
14884
15465
|
return;
|
|
14885
15466
|
}
|
|
14886
15467
|
const list = make("div", "native-selector-list");
|
|
14887
|
-
for (const item of filtered) {
|
|
15468
|
+
for (const [index, item] of filtered.entries()) {
|
|
14888
15469
|
const button = make("button", `native-selector-item${item.id === activeId ? " active" : ""}`);
|
|
14889
15470
|
button.type = "button";
|
|
14890
|
-
if (item.depth !== undefined) button.style.setProperty("--tree-depth", String(item.depth));
|
|
14891
15471
|
button.disabled = item.disabled === true;
|
|
14892
15472
|
button.addEventListener("click", () => onSelect?.(item));
|
|
14893
15473
|
const title = make("span", "native-selector-title");
|
|
15474
|
+
if (numbered) title.append(make("span", "native-selector-index", `${index + 1}.`));
|
|
14894
15475
|
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
14895
15476
|
if (item.badge) {
|
|
14896
15477
|
const badgeState = String(item.badge).toLowerCase();
|
|
@@ -15460,7 +16041,6 @@ async function openNativeTreeSelector() {
|
|
|
15460
16041
|
description: node.text || "",
|
|
15461
16042
|
meta: `${node.timestamp || ""}${node.childCount ? ` · ${node.childCount} child${node.childCount === 1 ? "" : "ren"}` : ""}`,
|
|
15462
16043
|
badge: node.currentLeaf ? "leaf" : "",
|
|
15463
|
-
depth: node.depth || 0,
|
|
15464
16044
|
node,
|
|
15465
16045
|
}));
|
|
15466
16046
|
const navigate = async (item) => {
|
|
@@ -15483,7 +16063,7 @@ async function openNativeTreeSelector() {
|
|
|
15483
16063
|
}
|
|
15484
16064
|
};
|
|
15485
16065
|
const render = () => {
|
|
15486
|
-
renderNativeSelectorItems(toItems(), { emptyText: "No session tree entries match this filter.", onSelect: navigate });
|
|
16066
|
+
renderNativeSelectorItems(toItems(), { emptyText: "No session tree entries match this filter.", onSelect: navigate, numbered: true });
|
|
15487
16067
|
elements.nativeCommandBody.prepend(options);
|
|
15488
16068
|
};
|
|
15489
16069
|
filterField.select.addEventListener("change", () => {
|
|
@@ -15973,7 +16553,6 @@ function handleMessageUpdate(event) {
|
|
|
15973
16553
|
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
15974
16554
|
if (streamThinking) streamThinking.textContent += delta;
|
|
15975
16555
|
}
|
|
15976
|
-
renderFooter();
|
|
15977
16556
|
scrollChatToBottom();
|
|
15978
16557
|
} else if (update.type === "thinking_end") {
|
|
15979
16558
|
const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event)) || thinkingDeltaText(update);
|
|
@@ -15989,7 +16568,8 @@ function handleMessageUpdate(event) {
|
|
|
15989
16568
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
15990
16569
|
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
15991
16570
|
else scheduleStreamingAssistantTextRender();
|
|
15992
|
-
|
|
16571
|
+
// Streaming output must stay transcript-local. Full footer/status
|
|
16572
|
+
// reconciliation happens on message/state refreshes, not per token.
|
|
15993
16573
|
scrollChatToBottom();
|
|
15994
16574
|
} else if (update.type === "toolcall_start") {
|
|
15995
16575
|
streamToolCallSeen = true;
|
|
@@ -16075,6 +16655,7 @@ function renderNetworkStatus() {
|
|
|
16075
16655
|
const rebinding = opening || closing;
|
|
16076
16656
|
const localUrl = network?.localUrl || `${window.location.origin}/`;
|
|
16077
16657
|
const networkUrls = Array.isArray(network?.networkUrls) ? network.networkUrls : [];
|
|
16658
|
+
syncRemoteWebuiControlVisibility();
|
|
16078
16659
|
elements.networkStatus.className = `network-status ${opening ? "opening" : closing ? "closing" : open ? "open" : "closed"}`;
|
|
16079
16660
|
elements.networkStatus.title = closing
|
|
16080
16661
|
? "Closing network access and returning to local-only"
|
|
@@ -16282,17 +16863,122 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
16282
16863
|
footerScopedModelPatterns = scopedModelPatterns;
|
|
16283
16864
|
footerScopedModelSource = scopedModelSource;
|
|
16284
16865
|
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
16285
|
-
elements.
|
|
16866
|
+
populateModelSelect(models, elements.modelSearchInput?.value || "");
|
|
16867
|
+
syncModelSelectToState();
|
|
16868
|
+
renderFooter();
|
|
16869
|
+
renderFeedbackTray();
|
|
16870
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
|
|
16871
|
+
}
|
|
16872
|
+
|
|
16873
|
+
function modelSelectOptionText(model) {
|
|
16874
|
+
return `${model.provider}/${model.id}${model.name ? ` · ${model.name}` : ""}`;
|
|
16875
|
+
}
|
|
16876
|
+
|
|
16877
|
+
function modelSelectValue(model) {
|
|
16878
|
+
return JSON.stringify({ provider: model.provider, modelId: model.id });
|
|
16879
|
+
}
|
|
16880
|
+
|
|
16881
|
+
function modelSearchDisplayParts(model) {
|
|
16882
|
+
const id = `${model.provider}/${model.id}`;
|
|
16883
|
+
const name = String(model.name || "").trim();
|
|
16884
|
+
if (!name) return { primary: id, secondary: "" };
|
|
16885
|
+
const match = name.match(/^(.*?)(\s+\([^)]+\))$/);
|
|
16886
|
+
return { primary: (match?.[1] || name).trim(), secondary: `${id}${match?.[2] || ""}` };
|
|
16887
|
+
}
|
|
16888
|
+
|
|
16889
|
+
let modelSearchResultsSignature = null;
|
|
16890
|
+
|
|
16891
|
+
function renderModelSearchResults(models = []) {
|
|
16892
|
+
if (!elements.modelSearchResults) return;
|
|
16893
|
+
// Skip redundant rebuilds (e.g. extension status pushes during streaming that
|
|
16894
|
+
// funnel through renderStatus -> syncModelSelectToState) so the Controls-panel
|
|
16895
|
+
// model list does not flicker or drop an in-progress interaction.
|
|
16896
|
+
const signature = JSON.stringify({
|
|
16897
|
+
hidden: !!elements.modelSearchInput?.hidden,
|
|
16898
|
+
selected: elements.modelSelect?.value || "",
|
|
16899
|
+
models: models.map((model) => `${modelSelectValue(model)}\u0000${modelSelectOptionText(model)}`),
|
|
16900
|
+
});
|
|
16901
|
+
if (signature === modelSearchResultsSignature) return;
|
|
16902
|
+
modelSearchResultsSignature = signature;
|
|
16903
|
+
elements.modelSearchResults.replaceChildren();
|
|
16904
|
+
if (elements.modelSearchInput?.hidden) return;
|
|
16905
|
+
if (!models.length) {
|
|
16906
|
+
elements.modelSearchResults.append(make("div", "model-search-empty", "No models match the search"));
|
|
16907
|
+
elements.modelSearchResults.hidden = false;
|
|
16908
|
+
return;
|
|
16909
|
+
}
|
|
16286
16910
|
for (const model of models) {
|
|
16911
|
+
const value = modelSelectValue(model);
|
|
16912
|
+
const selected = elements.modelSelect?.value === value;
|
|
16913
|
+
const button = make("button", `model-search-result${selected ? " active" : ""}`);
|
|
16914
|
+
button.type = "button";
|
|
16915
|
+
button.setAttribute("role", "option");
|
|
16916
|
+
button.setAttribute("aria-selected", String(selected));
|
|
16917
|
+
button.title = modelSelectOptionText(model);
|
|
16918
|
+
const display = modelSearchDisplayParts(model);
|
|
16919
|
+
button.append(
|
|
16920
|
+
make("span", "model-search-result-main", display.primary),
|
|
16921
|
+
make("span", "model-search-result-name", display.secondary),
|
|
16922
|
+
);
|
|
16923
|
+
button.addEventListener("click", () => {
|
|
16924
|
+
if (elements.modelSelect) elements.modelSelect.value = value;
|
|
16925
|
+
renderModelSearchResults(models);
|
|
16926
|
+
});
|
|
16927
|
+
button.addEventListener("dblclick", () => elements.setModelButton?.click());
|
|
16928
|
+
elements.modelSearchResults.append(button);
|
|
16929
|
+
}
|
|
16930
|
+
elements.modelSearchResults.hidden = false;
|
|
16931
|
+
}
|
|
16932
|
+
|
|
16933
|
+
function populateModelSelect(models = availableModels, query = "") {
|
|
16934
|
+
if (!elements.modelSelect) return;
|
|
16935
|
+
const previousValue = elements.modelSelect.value;
|
|
16936
|
+
const normalizedQuery = String(query || "").trim().toLowerCase();
|
|
16937
|
+
const matchingModels = models.filter((model) => !normalizedQuery || modelSelectOptionText(model).toLowerCase().includes(normalizedQuery));
|
|
16938
|
+
elements.modelSelect.replaceChildren();
|
|
16939
|
+
for (const model of matchingModels) {
|
|
16287
16940
|
const option = document.createElement("option");
|
|
16288
|
-
option.value =
|
|
16289
|
-
option.textContent =
|
|
16941
|
+
option.value = modelSelectValue(model);
|
|
16942
|
+
option.textContent = modelSelectOptionText(model);
|
|
16943
|
+
elements.modelSelect.append(option);
|
|
16944
|
+
}
|
|
16945
|
+
if (!matchingModels.length) {
|
|
16946
|
+
const option = document.createElement("option");
|
|
16947
|
+
option.value = "";
|
|
16948
|
+
option.textContent = "No models match the search";
|
|
16949
|
+
option.disabled = true;
|
|
16290
16950
|
elements.modelSelect.append(option);
|
|
16291
16951
|
}
|
|
16952
|
+
if (previousValue && [...elements.modelSelect.options].some((option) => option.value === previousValue)) elements.modelSelect.value = previousValue;
|
|
16953
|
+
renderModelSearchResults(matchingModels);
|
|
16954
|
+
}
|
|
16955
|
+
|
|
16956
|
+
function showModelSearchInput({ focus = true } = {}) {
|
|
16957
|
+
if (!elements.modelSearchInput) return;
|
|
16958
|
+
elements.modelSearchInput.hidden = false;
|
|
16959
|
+
elements.modelSearchResults.hidden = false;
|
|
16960
|
+
elements.modelSelect.classList.add("model-select-expanded");
|
|
16961
|
+
populateModelSelect(availableModels, elements.modelSearchInput.value);
|
|
16962
|
+
if (focus) {
|
|
16963
|
+
requestAnimationFrame(() => {
|
|
16964
|
+
elements.modelSearchInput.focus();
|
|
16965
|
+
elements.modelSearchInput.select();
|
|
16966
|
+
});
|
|
16967
|
+
}
|
|
16968
|
+
}
|
|
16969
|
+
|
|
16970
|
+
function hideModelSearchInput() {
|
|
16971
|
+
if (!elements.modelSearchInput) return;
|
|
16972
|
+
elements.modelSearchInput.hidden = true;
|
|
16973
|
+
elements.modelSearchInput.value = "";
|
|
16974
|
+
if (elements.modelSearchResults) {
|
|
16975
|
+
elements.modelSearchResults.hidden = true;
|
|
16976
|
+
elements.modelSearchResults.replaceChildren();
|
|
16977
|
+
}
|
|
16978
|
+
elements.modelSelect?.classList.remove("model-select-expanded");
|
|
16979
|
+
populateModelSelect(availableModels, "");
|
|
16292
16980
|
syncModelSelectToState();
|
|
16293
|
-
|
|
16294
|
-
renderFeedbackTray();
|
|
16295
|
-
if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
|
|
16981
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
16296
16982
|
}
|
|
16297
16983
|
|
|
16298
16984
|
function syncModelSelectToState() {
|
|
@@ -16301,6 +16987,7 @@ function syncModelSelectToState() {
|
|
|
16301
16987
|
for (const option of elements.modelSelect.options) {
|
|
16302
16988
|
if (option.value === value) {
|
|
16303
16989
|
elements.modelSelect.value = value;
|
|
16990
|
+
renderModelSearchResults(availableModels.filter((model) => !elements.modelSearchInput?.value.trim() || modelSelectOptionText(model).toLowerCase().includes(elements.modelSearchInput.value.trim().toLowerCase())));
|
|
16304
16991
|
break;
|
|
16305
16992
|
}
|
|
16306
16993
|
}
|
|
@@ -16490,6 +17177,7 @@ function hideCommandSuggestions() {
|
|
|
16490
17177
|
pathSuggestions = [];
|
|
16491
17178
|
suggestionMode = "none";
|
|
16492
17179
|
commandSuggestIndex = 0;
|
|
17180
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
16493
17181
|
}
|
|
16494
17182
|
|
|
16495
17183
|
function setActiveCommandSuggestion(index) {
|
|
@@ -17458,6 +18146,15 @@ function handleExtensionUiRequest(request) {
|
|
|
17458
18146
|
if (statusKey === STATS_WEBUI_STATUS_KEY) handleStatsWebuiStatus(request.statusText);
|
|
17459
18147
|
if (statusKey === BTW_WEBUI_STATUS_KEY) handleBtwWebuiStatus(request.statusText);
|
|
17460
18148
|
updateOptionalFeatureAvailability();
|
|
18149
|
+
if (statusKey === GIT_FOOTER_WEBUI_STATUS_KEY) {
|
|
18150
|
+
if (currentState?.isStreaming || runIndicatorLocallyActive) return;
|
|
18151
|
+
if (isInteractiveDropdownOpen()) {
|
|
18152
|
+
deferredUiRenderCallbacks.set("footer", renderFooter);
|
|
18153
|
+
return;
|
|
18154
|
+
}
|
|
18155
|
+
renderFooter();
|
|
18156
|
+
return;
|
|
18157
|
+
}
|
|
17461
18158
|
renderStatus();
|
|
17462
18159
|
return;
|
|
17463
18160
|
}
|
|
@@ -18280,6 +18977,25 @@ function abortButtonReadyTitle() {
|
|
|
18280
18977
|
return `Hold Esc or the Abort button for ${abortButtonHoldSeconds()} seconds to abort the active Pi run`;
|
|
18281
18978
|
}
|
|
18282
18979
|
|
|
18980
|
+
function suppressEmptyPromptEscapeAction({ untilKeyup = false, graceMs = EMPTY_PROMPT_ESCAPE_AFTER_ABORT_GRACE_MS } = {}) {
|
|
18981
|
+
lastEmptyPromptEscapeTime = 0;
|
|
18982
|
+
suppressEmptyPromptEscapeUntil = Math.max(suppressEmptyPromptEscapeUntil, Date.now() + graceMs);
|
|
18983
|
+
if (untilKeyup) escapeAbortHoldSuppressesDoubleEscape = true;
|
|
18984
|
+
}
|
|
18985
|
+
|
|
18986
|
+
function finishEscapeAbortHoldSuppression() {
|
|
18987
|
+
if (!escapeAbortHoldSuppressesDoubleEscape) return;
|
|
18988
|
+
escapeAbortHoldSuppressesDoubleEscape = false;
|
|
18989
|
+
suppressEmptyPromptEscapeAction();
|
|
18990
|
+
}
|
|
18991
|
+
|
|
18992
|
+
function shouldSuppressEmptyPromptEscapeAction() {
|
|
18993
|
+
if (escapeAbortHoldSuppressesDoubleEscape) return true;
|
|
18994
|
+
if (suppressEmptyPromptEscapeUntil > Date.now()) return true;
|
|
18995
|
+
suppressEmptyPromptEscapeUntil = 0;
|
|
18996
|
+
return false;
|
|
18997
|
+
}
|
|
18998
|
+
|
|
18283
18999
|
function clearAbortLongPressResetTimer() {
|
|
18284
19000
|
clearTimeout(abortLongPressResetTimer);
|
|
18285
19001
|
abortLongPressResetTimer = null;
|
|
@@ -18321,6 +19037,7 @@ function completeAbortLongPress() {
|
|
|
18321
19037
|
if (!isAbortLongPressActive()) return;
|
|
18322
19038
|
if (abortLongPressReleasePending) return;
|
|
18323
19039
|
const source = abortLongPressSource;
|
|
19040
|
+
if (source === "escape") suppressEmptyPromptEscapeAction({ untilKeyup: true });
|
|
18324
19041
|
clearAbortLongPressResetTimer();
|
|
18325
19042
|
clearAbortLongPressCompletionTimers();
|
|
18326
19043
|
abortLongPressHandled = true;
|
|
@@ -18408,6 +19125,7 @@ async function abortActiveRun({ source = "button" } = {}) {
|
|
|
18408
19125
|
function startAbortLongPress(event, { source = "long-press" } = {}) {
|
|
18409
19126
|
if (!isAbortAvailable() || abortRequestInFlight) return false;
|
|
18410
19127
|
if (source !== "escape" && event?.button !== undefined && event.button !== 0) return false;
|
|
19128
|
+
if (source === "escape") suppressEmptyPromptEscapeAction({ untilKeyup: true, graceMs: ABORT_LONG_PRESS_MS + EMPTY_PROMPT_ESCAPE_AFTER_ABORT_GRACE_MS });
|
|
18411
19129
|
if (isAbortLongPressActive()) {
|
|
18412
19130
|
resumeAbortLongPressAffordance();
|
|
18413
19131
|
return true;
|
|
@@ -18461,6 +19179,41 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
18461
19179
|
setComposerActionsOpen(false);
|
|
18462
19180
|
await requestManualCompaction({ triggerButton: elements.compactButton });
|
|
18463
19181
|
});
|
|
19182
|
+
function toggleModelSearchInput() {
|
|
19183
|
+
if (elements.modelSearchInput?.hidden) showModelSearchInput();
|
|
19184
|
+
else hideModelSearchInput();
|
|
19185
|
+
}
|
|
19186
|
+
|
|
19187
|
+
elements.modelControlLabel?.addEventListener("click", (event) => {
|
|
19188
|
+
event.preventDefault();
|
|
19189
|
+
toggleModelSearchInput();
|
|
19190
|
+
});
|
|
19191
|
+
elements.modelControlLabel?.addEventListener("keydown", (event) => {
|
|
19192
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
19193
|
+
event.preventDefault();
|
|
19194
|
+
toggleModelSearchInput();
|
|
19195
|
+
});
|
|
19196
|
+
elements.modelSelect?.addEventListener("pointerdown", (event) => {
|
|
19197
|
+
if (!elements.modelSearchInput || !elements.modelSearchInput.hidden) return;
|
|
19198
|
+
event.preventDefault();
|
|
19199
|
+
showModelSearchInput();
|
|
19200
|
+
});
|
|
19201
|
+
elements.modelSelect?.addEventListener("focus", () => showModelSearchInput());
|
|
19202
|
+
elements.modelSearchInput?.addEventListener("input", () => {
|
|
19203
|
+
populateModelSelect(availableModels, elements.modelSearchInput.value);
|
|
19204
|
+
syncModelSelectToState();
|
|
19205
|
+
});
|
|
19206
|
+
elements.modelSearchInput?.addEventListener("keydown", (event) => {
|
|
19207
|
+
if (event.key === "Enter") {
|
|
19208
|
+
event.preventDefault();
|
|
19209
|
+
elements.setModelButton?.click();
|
|
19210
|
+
} else if (event.key === "Escape") {
|
|
19211
|
+
elements.modelSearchInput.value = "";
|
|
19212
|
+
populateModelSelect(availableModels, "");
|
|
19213
|
+
syncModelSelectToState();
|
|
19214
|
+
elements.modelSearchInput.focus();
|
|
19215
|
+
}
|
|
19216
|
+
});
|
|
18464
19217
|
elements.setModelButton.addEventListener("click", async () => {
|
|
18465
19218
|
if (!elements.modelSelect.value) return;
|
|
18466
19219
|
const tabContext = activeTabContext();
|
|
@@ -18494,6 +19247,33 @@ elements.setThinkingButton.addEventListener("click", async () => {
|
|
|
18494
19247
|
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
18495
19248
|
}
|
|
18496
19249
|
});
|
|
19250
|
+
elements.themeControlLabel?.addEventListener("click", (event) => {
|
|
19251
|
+
event.preventDefault();
|
|
19252
|
+
toggleThemeSearchInput();
|
|
19253
|
+
});
|
|
19254
|
+
elements.themeControlLabel?.addEventListener("keydown", (event) => {
|
|
19255
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
19256
|
+
event.preventDefault();
|
|
19257
|
+
toggleThemeSearchInput();
|
|
19258
|
+
});
|
|
19259
|
+
elements.themeSelect?.addEventListener("pointerdown", (event) => {
|
|
19260
|
+
if (!elements.themeSearchInput || !elements.themeSearchInput.hidden) return;
|
|
19261
|
+
event.preventDefault();
|
|
19262
|
+
showThemeSearchInput();
|
|
19263
|
+
});
|
|
19264
|
+
elements.themeSelect?.addEventListener("focus", () => showThemeSearchInput());
|
|
19265
|
+
elements.themeSearchInput?.addEventListener("input", () => populateThemeSelect(availableThemes, elements.themeSearchInput.value));
|
|
19266
|
+
elements.themeSearchInput?.addEventListener("keydown", (event) => {
|
|
19267
|
+
if (event.key === "Enter") {
|
|
19268
|
+
event.preventDefault();
|
|
19269
|
+
const [theme] = populateThemeSelect(availableThemes, elements.themeSearchInput.value);
|
|
19270
|
+
if (theme) setThemeByName(theme.name, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
19271
|
+
} else if (event.key === "Escape") {
|
|
19272
|
+
elements.themeSearchInput.value = "";
|
|
19273
|
+
populateThemeSelect(availableThemes, "");
|
|
19274
|
+
elements.themeSearchInput.focus();
|
|
19275
|
+
}
|
|
19276
|
+
});
|
|
18497
19277
|
elements.themeSelect.addEventListener("change", () => {
|
|
18498
19278
|
setThemeByName(elements.themeSelect.value, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
18499
19279
|
});
|
|
@@ -18785,6 +19565,9 @@ document.addEventListener("visibilitychange", () => {
|
|
|
18785
19565
|
window.addEventListener("pageshow", () => scheduleForegroundReconcile("page show", 0));
|
|
18786
19566
|
window.addEventListener("focus", () => scheduleForegroundReconcile("window focus"));
|
|
18787
19567
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
19568
|
+
window.addEventListener("storage", (event) => {
|
|
19569
|
+
if (event.key === OPTIONAL_FEATURES_STORAGE_KEY) reconcileDisabledOptionalFeaturesFromStorage();
|
|
19570
|
+
});
|
|
18788
19571
|
window.addEventListener("keydown", (event) => {
|
|
18789
19572
|
if (event.key !== "Escape") return;
|
|
18790
19573
|
if (event.defaultPrevented) return;
|
|
@@ -18843,6 +19626,10 @@ window.addEventListener("keydown", (event) => {
|
|
|
18843
19626
|
else if (!event.repeat) startAbortLongPress(event, { source: "escape" });
|
|
18844
19627
|
return;
|
|
18845
19628
|
}
|
|
19629
|
+
if (shouldSuppressEmptyPromptEscapeAction()) {
|
|
19630
|
+
event.preventDefault();
|
|
19631
|
+
return;
|
|
19632
|
+
}
|
|
18846
19633
|
if (event.repeat) {
|
|
18847
19634
|
event.preventDefault();
|
|
18848
19635
|
return;
|
|
@@ -18859,17 +19646,39 @@ window.addEventListener("keydown", (event) => {
|
|
|
18859
19646
|
}
|
|
18860
19647
|
});
|
|
18861
19648
|
window.addEventListener("keyup", (event) => {
|
|
18862
|
-
if (event.key
|
|
19649
|
+
if (event.key !== "Escape") return;
|
|
19650
|
+
if (abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
|
|
19651
|
+
finishEscapeAbortHoldSuppression();
|
|
18863
19652
|
}, { capture: true });
|
|
18864
19653
|
window.addEventListener("blur", () => {
|
|
18865
19654
|
if (abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
|
|
18866
19655
|
else resetAbortLongPressAffordance();
|
|
19656
|
+
finishEscapeAbortHoldSuppression();
|
|
18867
19657
|
});
|
|
18868
19658
|
|
|
18869
19659
|
elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);
|
|
18870
19660
|
elements.gitChangesPullButton?.addEventListener("click", () => pullGitChangesDialog().catch((error) => addEvent(error.message || String(error), "error")));
|
|
18871
19661
|
elements.gitChangesCloseButton?.addEventListener("click", closeGitChangesDialog);
|
|
18872
19662
|
elements.gitChangesBody?.addEventListener("scroll", updateGitChangesCurrentFileHeader, { passive: true });
|
|
19663
|
+
elements.gitChangesBody?.addEventListener("click", (event) => {
|
|
19664
|
+
const currentHeader = event.target.closest(".git-current-file-header[data-git-current-file]");
|
|
19665
|
+
if (currentHeader) {
|
|
19666
|
+
const path = currentHeader.dataset.gitCurrentFile || "";
|
|
19667
|
+
const file = elements.gitChangesBody?.querySelector(`.git-diff-file[data-git-diff-file=\"${CSS.escape(path)}\"]`);
|
|
19668
|
+
if (!file) return;
|
|
19669
|
+
file.open = !file.open;
|
|
19670
|
+
updateGitChangesCurrentFileHeader();
|
|
19671
|
+
return;
|
|
19672
|
+
}
|
|
19673
|
+
const button = event.target.closest("[data-git-changes-jump-file]");
|
|
19674
|
+
if (!button) return;
|
|
19675
|
+
const path = button.dataset.gitChangesJumpFile || "";
|
|
19676
|
+
const file = elements.gitChangesBody?.querySelector(`.git-diff-file[data-git-diff-file=\"${CSS.escape(path)}\"]`);
|
|
19677
|
+
if (!file) return;
|
|
19678
|
+
file.open = true;
|
|
19679
|
+
file.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
19680
|
+
requestAnimationFrame(updateGitChangesCurrentFileHeader);
|
|
19681
|
+
});
|
|
18873
19682
|
elements.gitChangesDialog?.addEventListener("cancel", (event) => {
|
|
18874
19683
|
event.preventDefault();
|
|
18875
19684
|
closeGitChangesDialog();
|