@firstpick/pi-package-webui 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/bin/pi-webui.mjs +148 -52
- package/package.json +2 -2
- package/public/app.js +998 -152
- package/public/index.html +11 -5
- package/public/styles.css +458 -46
- package/tests/http-endpoints-harness.test.mjs +112 -0
- package/tests/mobile-static.test.mjs +40 -18
- package/tests/native-parity.test.mjs +5 -1
package/public/app.js
CHANGED
|
@@ -24,6 +24,7 @@ const elements = {
|
|
|
24
24
|
updateNotificationMessage: $("#updateNotificationMessage"),
|
|
25
25
|
updateNotificationDetail: $("#updateNotificationDetail"),
|
|
26
26
|
updateNotificationUpdateButton: $("#updateNotificationUpdateButton"),
|
|
27
|
+
updateNotificationUpdateAllButton: $("#updateNotificationUpdateAllButton"),
|
|
27
28
|
updateNotificationDismissButton: $("#updateNotificationDismissButton"),
|
|
28
29
|
serverOfflineCommand: $("#serverOfflineCommand"),
|
|
29
30
|
serverOfflineSlashCommand: $("#serverOfflineSlashCommand"),
|
|
@@ -116,6 +117,9 @@ const elements = {
|
|
|
116
117
|
gitChangesRefreshButton: $("#gitChangesRefreshButton"),
|
|
117
118
|
gitChangesPullButton: $("#gitChangesPullButton"),
|
|
118
119
|
gitChangesCloseButton: $("#gitChangesCloseButton"),
|
|
120
|
+
modelControlLabel: $("#modelControlLabel"),
|
|
121
|
+
modelSearchInput: $("#modelSearchInput"),
|
|
122
|
+
modelSearchResults: $("#modelSearchResults"),
|
|
119
123
|
modelSelect: $("#modelSelect"),
|
|
120
124
|
setModelButton: $("#setModelButton"),
|
|
121
125
|
thinkingSelect: $("#thinkingSelect"),
|
|
@@ -124,11 +128,15 @@ const elements = {
|
|
|
124
128
|
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
125
129
|
terminalTabsLayoutSelect: $("#terminalTabsLayoutSelect"),
|
|
126
130
|
terminalTabsLayoutStatus: $("#terminalTabsLayoutStatus"),
|
|
131
|
+
themeControlLabel: $("#themeControlLabel"),
|
|
132
|
+
themeSearchInput: $("#themeSearchInput"),
|
|
133
|
+
themeSearchResults: $("#themeSearchResults"),
|
|
127
134
|
themeSelect: $("#themeSelect"),
|
|
128
135
|
backgroundInput: $("#backgroundInput"),
|
|
129
136
|
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
130
137
|
backgroundClearButton: $("#backgroundClearButton"),
|
|
131
138
|
backgroundStatus: $("#backgroundStatus"),
|
|
139
|
+
networkControlField: $("#networkControlField"),
|
|
132
140
|
networkStatus: $("#networkStatus"),
|
|
133
141
|
remoteAuthToggle: $("#remoteAuthToggle"),
|
|
134
142
|
remoteAuthStatus: $("#remoteAuthStatus"),
|
|
@@ -347,6 +355,7 @@ let thinkingOutputVisible = true;
|
|
|
347
355
|
let terminalTabsLayout = "top";
|
|
348
356
|
let webuiSettings = {};
|
|
349
357
|
let busyPromptBehavior = "followUp";
|
|
358
|
+
let composerModeRenderSignature = "";
|
|
350
359
|
let autocompleteMaxVisible = 12;
|
|
351
360
|
let doubleEscapeAction = "none";
|
|
352
361
|
let treeFilterMode = "default";
|
|
@@ -403,6 +412,8 @@ let abortLongPressDeadlineAt = 0;
|
|
|
403
412
|
let abortLongPressSource = "long-press";
|
|
404
413
|
let abortLongPressReleasePending = false;
|
|
405
414
|
let abortLongPressHandled = false;
|
|
415
|
+
let escapeAbortHoldSuppressesDoubleEscape = false;
|
|
416
|
+
let suppressEmptyPromptEscapeUntil = 0;
|
|
406
417
|
const dialogQueue = [];
|
|
407
418
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
408
419
|
const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed";
|
|
@@ -436,6 +447,9 @@ const GIT_INIT_STACK_STORAGE_KEY = "pi-webui-git-init-stack";
|
|
|
436
447
|
const STATS_WEBUI_STATUS_KEY = "stats-webui";
|
|
437
448
|
const STATS_WEBUI_PAYLOAD_TYPE = "firstpick.pi-extension-stats.overlay";
|
|
438
449
|
const STATS_WEBUI_PAYLOAD_VERSION = 1;
|
|
450
|
+
const REMOTE_WEBUI_CONTROLS_STATUS_KEY = "pi-remote-webui:controls";
|
|
451
|
+
const REMOTE_WEBUI_CONTROLS_PAYLOAD_TYPE = "firstpick.pi-package-remote-webui.controls";
|
|
452
|
+
const REMOTE_WEBUI_CONTROLS_PAYLOAD_VERSION = 1;
|
|
439
453
|
const BTW_WEBUI_STATUS_KEY = "btw-webui";
|
|
440
454
|
const BTW_OUTPUT_WIDGET_KEY = "btw:output";
|
|
441
455
|
const BTW_FOOTER_WIDGET_KEY = "btw:footer";
|
|
@@ -486,6 +500,7 @@ const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
|
486
500
|
const ABORT_LONG_PRESS_MS = 3000;
|
|
487
501
|
const ABORT_LONG_PRESS_TICK_MS = 100;
|
|
488
502
|
const ABORT_LONG_PRESS_RELEASE_GRACE_MS = 350;
|
|
503
|
+
const EMPTY_PROMPT_ESCAPE_AFTER_ABORT_GRACE_MS = 1000;
|
|
489
504
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
490
505
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
491
506
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
@@ -509,6 +524,7 @@ const widgets = new Map();
|
|
|
509
524
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
510
525
|
const releaseNpmOutputExpandedByTab = new Map();
|
|
511
526
|
const appRunnerDataByTab = new Map();
|
|
527
|
+
const appRunnerInputDraftByRun = new Map();
|
|
512
528
|
const liveToolRuns = new Map();
|
|
513
529
|
const liveToolCards = new Map();
|
|
514
530
|
const liveToolRenderQueue = new Map();
|
|
@@ -928,6 +944,37 @@ function deferChatFollowScrollDuringPointerActivation({ force = false } = {}) {
|
|
|
928
944
|
return true;
|
|
929
945
|
}
|
|
930
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
|
+
|
|
931
978
|
function flushDeferredUiRenders() {
|
|
932
979
|
const callbacks = [...deferredUiRenderCallbacks.values()];
|
|
933
980
|
deferredUiRenderCallbacks.clear();
|
|
@@ -1949,7 +1996,10 @@ function setBusyPromptBehaviorMenuOpen(open, { focusCurrent = false } = {}) {
|
|
|
1949
1996
|
elements.busyPromptBehaviorTag?.setAttribute("aria-expanded", busyPromptBehaviorMenuOpen ? "true" : "false");
|
|
1950
1997
|
elements.busyPromptBehaviorTag?.classList.toggle("menu-open", busyPromptBehaviorMenuOpen);
|
|
1951
1998
|
if (elements.busyPromptBehaviorMenu) elements.busyPromptBehaviorMenu.hidden = !busyPromptBehaviorMenuOpen;
|
|
1952
|
-
if (!busyPromptBehaviorMenuOpen)
|
|
1999
|
+
if (!busyPromptBehaviorMenuOpen) {
|
|
2000
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
1953
2003
|
renderBusyPromptBehaviorMenu();
|
|
1954
2004
|
if (focusCurrent) {
|
|
1955
2005
|
requestAnimationFrame(() => {
|
|
@@ -2020,6 +2070,7 @@ function setComposerActionsOpen(open) {
|
|
|
2020
2070
|
setBusyPromptBehaviorMenuOpen(false);
|
|
2021
2071
|
}
|
|
2022
2072
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
2073
|
+
if (!shouldOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
2023
2074
|
}
|
|
2024
2075
|
|
|
2025
2076
|
function isUserBashActive(tabId = activeTabId) {
|
|
@@ -2064,6 +2115,18 @@ function resizePromptInput() {
|
|
|
2064
2115
|
function updateComposerModeButtons() {
|
|
2065
2116
|
const runActive = isRunActive();
|
|
2066
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
|
+
|
|
2067
2130
|
const target = runActive ? elements.composerRow : elements.composerActionsPanel;
|
|
2068
2131
|
const before = runActive ? elements.abortButton : null;
|
|
2069
2132
|
for (const button of [elements.steerButton, elements.followUpButton]) {
|
|
@@ -2078,9 +2141,11 @@ function updateComposerModeButtons() {
|
|
|
2078
2141
|
if (abortHoldActive) {
|
|
2079
2142
|
renderAbortLongPressAffordance();
|
|
2080
2143
|
} else {
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
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);
|
|
2084
2149
|
}
|
|
2085
2150
|
renderBusyPromptBehaviorTag();
|
|
2086
2151
|
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
@@ -2643,6 +2708,88 @@ function triggerNativeDownload(download) {
|
|
|
2643
2708
|
return true;
|
|
2644
2709
|
}
|
|
2645
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
|
+
|
|
2646
2793
|
async function copyServerStartCommand() {
|
|
2647
2794
|
const command = serverStartCommandText();
|
|
2648
2795
|
try {
|
|
@@ -2825,22 +2972,34 @@ function renderUpdateNotification(status = latestUpdateStatus, { force = false }
|
|
|
2825
2972
|
}
|
|
2826
2973
|
|
|
2827
2974
|
const canRunUpdate = latestUpdateStatus.canRunUpdate !== false;
|
|
2975
|
+
const hasPiUpdate = !!latestUpdateStatus.pi?.updateAvailable;
|
|
2976
|
+
const hasPackageUpdate = !!latestUpdateStatus.webui?.updateAvailable;
|
|
2828
2977
|
if (elements.updateNotificationTitle) elements.updateNotificationTitle.textContent = items.length === 1 ? `${items[0]} available` : "Pi updates available";
|
|
2829
2978
|
if (elements.updateNotificationMessage) {
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2979
|
+
let message = "Updates are available. Direct Web UI updates are only enabled from localhost on the host machine.";
|
|
2980
|
+
if (canRunUpdate) {
|
|
2981
|
+
if (hasPiUpdate && hasPackageUpdate) message = "Run pi update for Pi only, or pi update --all to include Web UI/package updates, then restart this Web UI server automatically.";
|
|
2982
|
+
else if (hasPackageUpdate) message = "Run pi update --all to update Web UI/package entries, then restart this Web UI server automatically.";
|
|
2983
|
+
else message = "Run pi update for Pi only, then restart this Web UI server automatically.";
|
|
2984
|
+
}
|
|
2985
|
+
elements.updateNotificationMessage.textContent = message;
|
|
2833
2986
|
}
|
|
2834
2987
|
const details = [
|
|
2835
2988
|
items.join(" · "),
|
|
2836
|
-
latestUpdateStatus.webuiDev && latestUpdateStatus.webui?.updateAvailable ? "The current Web UI is a dev checkout; update
|
|
2989
|
+
latestUpdateStatus.webuiDev && latestUpdateStatus.webui?.updateAvailable ? "The current Web UI is a dev checkout; pi update --all refreshes configured package dependencies when possible." : "",
|
|
2837
2990
|
latestUpdateStatus.packages?.note || "",
|
|
2838
2991
|
].filter(Boolean).join(" ");
|
|
2839
2992
|
if (elements.updateNotificationDetail) elements.updateNotificationDetail.textContent = details;
|
|
2840
2993
|
if (elements.updateNotificationUpdateButton) {
|
|
2841
|
-
elements.updateNotificationUpdateButton.hidden = !canRunUpdate;
|
|
2994
|
+
elements.updateNotificationUpdateButton.hidden = !canRunUpdate || !hasPiUpdate;
|
|
2842
2995
|
elements.updateNotificationUpdateButton.disabled = updateRequestInProgress || latestUpdateStatus.updateInProgress;
|
|
2843
|
-
elements.updateNotificationUpdateButton.textContent = latestUpdateStatus.updateInProgress ? "Updating…" : "Update & restart";
|
|
2996
|
+
elements.updateNotificationUpdateButton.textContent = latestUpdateStatus.updateInProgress ? "Updating…" : "Update Pi & restart";
|
|
2997
|
+
}
|
|
2998
|
+
if (elements.updateNotificationUpdateAllButton) {
|
|
2999
|
+
elements.updateNotificationUpdateAllButton.hidden = !canRunUpdate || !hasPackageUpdate;
|
|
3000
|
+
elements.updateNotificationUpdateAllButton.disabled = updateRequestInProgress || latestUpdateStatus.updateInProgress;
|
|
3001
|
+
elements.updateNotificationUpdateAllButton.classList.toggle("primary", !hasPiUpdate);
|
|
3002
|
+
elements.updateNotificationUpdateAllButton.textContent = latestUpdateStatus.updateInProgress ? "Updating…" : "Update all & restart";
|
|
2844
3003
|
}
|
|
2845
3004
|
clearTimeout(updateNotificationHideTimer);
|
|
2846
3005
|
panel.hidden = false;
|
|
@@ -2871,30 +3030,33 @@ function initializeUpdateNotifications() {
|
|
|
2871
3030
|
}, UPDATE_STATUS_INITIAL_DELAY_MS);
|
|
2872
3031
|
}
|
|
2873
3032
|
|
|
2874
|
-
function piUpdateConfirmationText() {
|
|
3033
|
+
function piUpdateConfirmationText({ all = false } = {}) {
|
|
2875
3034
|
const items = updateNotificationItems();
|
|
2876
3035
|
const workingWarning = hasWorkingTab() ? "\n\nOne or more Pi tabs look busy or blocked. Finish or abort in-flight work before updating if you need to preserve it." : "";
|
|
2877
3036
|
const versionText = items.length ? `\n\nDetected update: ${items.join(" · ")}.` : "";
|
|
2878
|
-
|
|
3037
|
+
const command = all ? "pi update --all" : "pi update";
|
|
3038
|
+
const scope = all ? "Pi and configured package updates" : "Pi only";
|
|
3039
|
+
return `Run ${scope} now?${versionText}\n\nThis will run \"${command}\" on the Web UI host. After it finishes, Pi Web UI will restart itself. Browser clients will briefly disconnect, and managed Pi tabs/RPC processes will be restarted from saved session state when possible.${workingWarning}`;
|
|
2879
3040
|
}
|
|
2880
3041
|
|
|
2881
|
-
async function runPiUpdateAndRestart() {
|
|
3042
|
+
async function runPiUpdateAndRestart({ all = false } = {}) {
|
|
2882
3043
|
if (updateRequestInProgress) return;
|
|
2883
3044
|
if (latestUpdateStatus?.canRunUpdate === false) {
|
|
2884
|
-
addEvent("Pi
|
|
3045
|
+
addEvent("Pi updates can only be started from localhost on the Web UI host", "warn");
|
|
2885
3046
|
renderUpdateNotification(latestUpdateStatus, { force: true });
|
|
2886
3047
|
return;
|
|
2887
3048
|
}
|
|
2888
|
-
if (!confirm(piUpdateConfirmationText())) return;
|
|
3049
|
+
if (!confirm(piUpdateConfirmationText({ all }))) return;
|
|
2889
3050
|
|
|
3051
|
+
const updateLabel = all ? "Pi and package updates" : "Pi update";
|
|
2890
3052
|
updateRequestInProgress = true;
|
|
2891
3053
|
hideUpdateNotification();
|
|
2892
3054
|
setServerActionBusy("Updating…");
|
|
2893
|
-
setServerActionStatus(
|
|
2894
|
-
setServerRestartOverlay(true,
|
|
3055
|
+
setServerActionStatus(`Running ${updateLabel}. The server will restart after the update completes…`, "warn");
|
|
3056
|
+
setServerRestartOverlay(true, `Running ${updateLabel}. The server will restart after the update completes…`);
|
|
2895
3057
|
try {
|
|
2896
|
-
await api("/api/update", { method: "POST", scoped: false });
|
|
2897
|
-
addEvent(
|
|
3058
|
+
await api(all ? "/api/update?all=1" : "/api/update", { method: "POST", scoped: false });
|
|
3059
|
+
addEvent(`${updateLabel} completed; Pi Web UI server restart requested`, "warn");
|
|
2898
3060
|
} catch (error) {
|
|
2899
3061
|
if (!error?.backendOffline) {
|
|
2900
3062
|
updateRequestInProgress = false;
|
|
@@ -3766,6 +3928,23 @@ function storeDisabledOptionalFeatures() {
|
|
|
3766
3928
|
}
|
|
3767
3929
|
}
|
|
3768
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
|
+
|
|
3769
3948
|
function isOptionalFeatureDetected(featureId) {
|
|
3770
3949
|
return optionalFeatureAvailability[featureId] === true;
|
|
3771
3950
|
}
|
|
@@ -3798,6 +3977,7 @@ function setOptionalFeatureDisabled(featureId, disabled) {
|
|
|
3798
3977
|
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
3799
3978
|
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
3800
3979
|
else disabledOptionalFeatures.delete(featureId);
|
|
3980
|
+
if (featureId === "remoteWebui") syncRemoteWebuiControlVisibility(false);
|
|
3801
3981
|
if (featureId === "gitFooterStatus") {
|
|
3802
3982
|
statusEntries.delete(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
3803
3983
|
clearGitFooterWebuiPayloadCache();
|
|
@@ -3811,6 +3991,11 @@ function setOptionalFeatureDisabled(featureId, disabled) {
|
|
|
3811
3991
|
btwWidgetComposerOpen = false;
|
|
3812
3992
|
btwWidgetInputDraft = "";
|
|
3813
3993
|
}
|
|
3994
|
+
if (featureId === "remoteWebui") {
|
|
3995
|
+
statusEntries.delete(REMOTE_WEBUI_CONTROLS_STATUS_KEY);
|
|
3996
|
+
statusEntries.delete("pi-remote-webui");
|
|
3997
|
+
widgets.delete("pi-remote-webui");
|
|
3998
|
+
}
|
|
3814
3999
|
storeDisabledOptionalFeatures();
|
|
3815
4000
|
renderOptionalFeatureDependentDisplays();
|
|
3816
4001
|
const tabContext = activeTabContext();
|
|
@@ -4020,6 +4205,79 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
4020
4205
|
if (announce) addEvent(`theme changed to ${theme.label || displayThemeName(theme.name) || theme.name}`);
|
|
4021
4206
|
}
|
|
4022
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
|
+
|
|
4023
4281
|
function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {}) {
|
|
4024
4282
|
if (!elements.themeSelect) return;
|
|
4025
4283
|
elements.themeSelect.replaceChildren();
|
|
@@ -4044,6 +4302,7 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
4044
4302
|
elements.themeSelect.append(option);
|
|
4045
4303
|
}
|
|
4046
4304
|
elements.themeSelect.value = currentThemeName;
|
|
4305
|
+
populateThemeSelect(availableThemes, elements.themeSearchInput?.value || "");
|
|
4047
4306
|
}
|
|
4048
4307
|
|
|
4049
4308
|
async function setThemeByName(name, options = {}) {
|
|
@@ -4052,6 +4311,7 @@ async function setThemeByName(name, options = {}) {
|
|
|
4052
4311
|
if (!theme) return;
|
|
4053
4312
|
currentThemeName = theme.name;
|
|
4054
4313
|
if (elements.themeSelect && elements.themeSelect.value !== theme.name) elements.themeSelect.value = theme.name;
|
|
4314
|
+
populateThemeSelect(availableThemes, elements.themeSearchInput?.value || "");
|
|
4055
4315
|
setCustomBackgroundRecord(null);
|
|
4056
4316
|
customBackgroundLoading = true;
|
|
4057
4317
|
applyTheme(theme, options);
|
|
@@ -4752,6 +5012,7 @@ function setNewTabMenuOpen(open) {
|
|
|
4752
5012
|
elements.newTabButton?.setAttribute("aria-expanded", newTabMenuOpen ? "true" : "false");
|
|
4753
5013
|
elements.newTabButton?.classList.toggle("menu-open", newTabMenuOpen);
|
|
4754
5014
|
elements.newTabMenu?.classList.toggle("open", newTabMenuOpen);
|
|
5015
|
+
if (!newTabMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
4755
5016
|
}
|
|
4756
5017
|
|
|
4757
5018
|
function openNewTabMenu() {
|
|
@@ -5978,6 +6239,57 @@ function parseGitFooterWebuiPayloadRaw(raw) {
|
|
|
5978
6239
|
}
|
|
5979
6240
|
}
|
|
5980
6241
|
|
|
6242
|
+
function parseRemoteWebuiControlsPayloadRaw(raw) {
|
|
6243
|
+
if (!raw) return null;
|
|
6244
|
+
try {
|
|
6245
|
+
const parsed = JSON.parse(raw);
|
|
6246
|
+
if (!parsed || parsed.type !== REMOTE_WEBUI_CONTROLS_PAYLOAD_TYPE || parsed.version !== REMOTE_WEBUI_CONTROLS_PAYLOAD_VERSION) return null;
|
|
6247
|
+
if (parsed.featureId !== "remoteWebui") return null;
|
|
6248
|
+
const commands = parsed.commands && typeof parsed.commands === "object" ? parsed.commands : {};
|
|
6249
|
+
return {
|
|
6250
|
+
title: cleanFooterPayloadText(parsed.title, "Remote WebUI", 80),
|
|
6251
|
+
description: cleanFooterPayloadText(parsed.description, "Trusted-LAN browser access controlled by the Remote WebUI package.", 240),
|
|
6252
|
+
commands: {
|
|
6253
|
+
open: typeof commands.open === "string" ? commands.open : "/remote",
|
|
6254
|
+
close: typeof commands.close === "string" ? commands.close : "/remote close",
|
|
6255
|
+
refresh: typeof commands.refresh === "string" ? commands.refresh : "/remote refresh",
|
|
6256
|
+
status: typeof commands.status === "string" ? commands.status : "/remote status",
|
|
6257
|
+
authOn: typeof commands.authOn === "string" ? commands.authOn : "/remote auth on",
|
|
6258
|
+
authOff: typeof commands.authOff === "string" ? commands.authOff : "/remote auth off",
|
|
6259
|
+
},
|
|
6260
|
+
};
|
|
6261
|
+
} catch {
|
|
6262
|
+
return null;
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
function remoteWebuiControlsPayload() {
|
|
6267
|
+
if (isOptionalFeatureDisabled("remoteWebui")) return null;
|
|
6268
|
+
return parseRemoteWebuiControlsPayloadRaw(statusEntries.get(REMOTE_WEBUI_CONTROLS_STATUS_KEY));
|
|
6269
|
+
}
|
|
6270
|
+
|
|
6271
|
+
function remoteWebuiDefaultPortArg() {
|
|
6272
|
+
const port = Number.parseInt(String(latestNetwork?.port || DEFAULT_WEBUI_PORT), 10);
|
|
6273
|
+
return Number.isFinite(port) && port > 0 && port <= 65535 && String(port) !== DEFAULT_WEBUI_PORT ? ` --port ${port}` : "";
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
function remoteWebuiFallbackCommand(name, fallback) {
|
|
6277
|
+
const portArg = remoteWebuiDefaultPortArg();
|
|
6278
|
+
const commands = {
|
|
6279
|
+
open: `/remote${portArg}`,
|
|
6280
|
+
close: `/remote close${portArg}`,
|
|
6281
|
+
refresh: `/remote refresh${portArg}`,
|
|
6282
|
+
status: `/remote status${portArg}`,
|
|
6283
|
+
authOn: `/remote auth on${portArg}`,
|
|
6284
|
+
authOff: `/remote auth off${portArg}`,
|
|
6285
|
+
};
|
|
6286
|
+
return commands[name] || fallback;
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
function remoteWebuiCommand(name, fallback) {
|
|
6290
|
+
return remoteWebuiControlsPayload()?.commands?.[name] || remoteWebuiFallbackCommand(name, fallback);
|
|
6291
|
+
}
|
|
6292
|
+
|
|
5981
6293
|
function readCachedGitFooterWebuiPayloadRaw() {
|
|
5982
6294
|
try {
|
|
5983
6295
|
const cached = JSON.parse(localStorage.getItem(GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY) || "null");
|
|
@@ -6218,29 +6530,90 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
6218
6530
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
6219
6531
|
}
|
|
6220
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
|
+
|
|
6221
6566
|
function renderGitFooterPayload(payload) {
|
|
6222
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
|
+
|
|
6223
6593
|
hideFooterTooltip();
|
|
6224
6594
|
elements.statusBar.replaceChildren();
|
|
6225
6595
|
elements.statusBar.classList.remove("statusbar-tui-footer");
|
|
6226
6596
|
elements.statusBar.classList.add("statusbar-git-footer");
|
|
6227
6597
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
6228
6598
|
|
|
6599
|
+
const mainNodes = payload.main.map(renderGitFooterPayloadMetric);
|
|
6229
6600
|
const row1 = make("div", "footer-line footer-line-main");
|
|
6230
|
-
row1.append(...
|
|
6601
|
+
row1.append(...mainNodes);
|
|
6231
6602
|
|
|
6232
6603
|
const footerToggle = make("button", "footer-details-toggle", mobileFooterExpanded ? "Less" : "Details");
|
|
6233
6604
|
footerToggle.type = "button";
|
|
6234
6605
|
footerToggle.setAttribute("aria-expanded", mobileFooterExpanded ? "true" : "false");
|
|
6235
6606
|
footerToggle.addEventListener("click", () => setMobileFooterExpanded(!mobileFooterExpanded));
|
|
6236
6607
|
|
|
6608
|
+
const metaNodes = payload.meta.map((chip) => renderGitFooterPayloadMeta(chip, tab));
|
|
6237
6609
|
const row2 = make("div", "footer-line footer-line-meta");
|
|
6238
|
-
row2.append(...
|
|
6610
|
+
row2.append(...metaNodes, footerToggle);
|
|
6239
6611
|
|
|
6240
6612
|
elements.statusBar.append(row1, row2);
|
|
6241
6613
|
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
6242
6614
|
if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
|
|
6243
6615
|
if (footerBranchPickerOpen) elements.statusBar.append(renderFooterBranchPicker());
|
|
6616
|
+
gitFooterRenderCache = { pickerKey, mainKeys, metaKeys, mainNodes, metaNodes };
|
|
6244
6617
|
setMobileFooterExpanded(mobileFooterExpanded);
|
|
6245
6618
|
updateFooterModelPickerPosition();
|
|
6246
6619
|
}
|
|
@@ -6577,9 +6950,36 @@ function renderGitUntrackedSection(untracked) {
|
|
|
6577
6950
|
return wrapper;
|
|
6578
6951
|
}
|
|
6579
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
|
+
|
|
6580
6978
|
function renderGitCurrentFileHeader() {
|
|
6581
|
-
const header = make("
|
|
6582
|
-
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"));
|
|
6583
6983
|
return header;
|
|
6584
6984
|
}
|
|
6585
6985
|
|
|
@@ -6606,7 +7006,10 @@ function updateGitChangesCurrentFileHeader() {
|
|
|
6606
7006
|
if (rect.top <= markerY) current = file;
|
|
6607
7007
|
else break;
|
|
6608
7008
|
}
|
|
6609
|
-
|
|
7009
|
+
const currentPath = current?.dataset.gitDiffFile || "—";
|
|
7010
|
+
name.textContent = currentPath;
|
|
7011
|
+
header.dataset.gitCurrentFile = currentPath;
|
|
7012
|
+
header.setAttribute("aria-expanded", String(current?.open !== false));
|
|
6610
7013
|
}
|
|
6611
7014
|
|
|
6612
7015
|
function gitChangesGeneratedLabel(data) {
|
|
@@ -6663,7 +7066,11 @@ function renderGitChangesDialog() {
|
|
|
6663
7066
|
.filter((entry) => entry.files.length > 0);
|
|
6664
7067
|
const untracked = gitUntrackedEntries(data.untracked);
|
|
6665
7068
|
const hasVisibleFiles = parsedSections.length > 0 || untracked.length > 0;
|
|
6666
|
-
if (hasVisibleFiles)
|
|
7069
|
+
if (hasVisibleFiles) {
|
|
7070
|
+
const fileList = renderGitChangesFileList(parsedSections, untracked);
|
|
7071
|
+
if (fileList) body.append(fileList);
|
|
7072
|
+
body.append(renderGitCurrentFileHeader());
|
|
7073
|
+
}
|
|
6667
7074
|
for (const entry of parsedSections) body.append(renderGitDiffSection(entry.section, entry.files));
|
|
6668
7075
|
if (untracked.length) body.append(renderGitUntrackedSection(untracked));
|
|
6669
7076
|
if (!hasVisibleFiles) {
|
|
@@ -6761,6 +7168,7 @@ function gitFooterFallbackMessage() {
|
|
|
6761
7168
|
}
|
|
6762
7169
|
|
|
6763
7170
|
function renderMinimalFooter() {
|
|
7171
|
+
invalidateGitFooterRenderCache();
|
|
6764
7172
|
hideFooterTooltip();
|
|
6765
7173
|
const tab = activeTab();
|
|
6766
7174
|
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "loading…");
|
|
@@ -6912,10 +7320,66 @@ function renderContextMeter() {
|
|
|
6912
7320
|
root.replaceChildren(summary, meter, actions);
|
|
6913
7321
|
}
|
|
6914
7322
|
|
|
6915
|
-
function
|
|
6916
|
-
const
|
|
6917
|
-
|
|
6918
|
-
|
|
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
|
+
}
|
|
6919
7383
|
return item;
|
|
6920
7384
|
}
|
|
6921
7385
|
|
|
@@ -6926,6 +7390,8 @@ function dashboardAction(label, handler, className = "") {
|
|
|
6926
7390
|
return button;
|
|
6927
7391
|
}
|
|
6928
7392
|
|
|
7393
|
+
let workspaceDashboardSignature = null;
|
|
7394
|
+
|
|
6929
7395
|
function renderWorkspaceDashboard() {
|
|
6930
7396
|
if (deferUiRenderDuringPointerActivation("workspace-dashboard", renderWorkspaceDashboard)) return;
|
|
6931
7397
|
const root = elements.workspaceDashboard;
|
|
@@ -6934,11 +7400,51 @@ function renderWorkspaceDashboard() {
|
|
|
6934
7400
|
const snapshot = contextUsageSnapshot();
|
|
6935
7401
|
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "Choose or create a tab to start");
|
|
6936
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;
|
|
6937
7428
|
root.replaceChildren();
|
|
6938
7429
|
|
|
6939
7430
|
const header = make("div", "workspace-dashboard-header");
|
|
6940
7431
|
const title = make("div", "workspace-dashboard-title");
|
|
6941
|
-
|
|
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);
|
|
6942
7448
|
const actions = make("div", "workspace-dashboard-actions");
|
|
6943
7449
|
actions.append(
|
|
6944
7450
|
dashboardAction("Command palette", () => openCommandPalette(), "primary"),
|
|
@@ -6951,24 +7457,45 @@ function renderWorkspaceDashboard() {
|
|
|
6951
7457
|
|
|
6952
7458
|
const metrics = make("div", "workspace-dashboard-metrics");
|
|
6953
7459
|
metrics.append(
|
|
6954
|
-
dashboardMetric("Model",
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
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
|
+
}),
|
|
6958
7479
|
);
|
|
6959
7480
|
|
|
6960
7481
|
const tabsPanel = make("div", "workspace-dashboard-tabs");
|
|
6961
|
-
|
|
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);
|
|
6962
7488
|
const tabList = make("div", "workspace-dashboard-tab-list");
|
|
6963
7489
|
for (const item of tabs.slice(0, 8)) {
|
|
6964
7490
|
const indicator = tabIndicator(item);
|
|
6965
7491
|
const button = make("button", `workspace-dashboard-tab activity-${indicator.state}${item.id === activeTabId ? " active" : ""}`);
|
|
6966
7492
|
button.type = "button";
|
|
6967
|
-
button.title = `${item.title} · ${indicator.label}`;
|
|
6968
|
-
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));
|
|
6969
7495
|
button.addEventListener("click", () => switchTab(item.id));
|
|
6970
7496
|
tabList.append(button);
|
|
6971
7497
|
}
|
|
7498
|
+
if (!tabs.length) tabList.append(make("span", "workspace-dashboard-tab-empty", "Create a tab to start a workspace."));
|
|
6972
7499
|
if (tabs.length > 8) tabList.append(make("span", "workspace-dashboard-tab-more", `+${tabs.length - 8} more`));
|
|
6973
7500
|
tabsPanel.append(tabList);
|
|
6974
7501
|
|
|
@@ -6976,6 +7503,7 @@ function renderWorkspaceDashboard() {
|
|
|
6976
7503
|
}
|
|
6977
7504
|
|
|
6978
7505
|
function setFooterModelPickerOpen(open) {
|
|
7506
|
+
const wasOpen = footerModelPickerOpen;
|
|
6979
7507
|
footerModelPickerOpen = !!open;
|
|
6980
7508
|
if (footerModelPickerOpen) {
|
|
6981
7509
|
footerThinkingPickerOpen = false;
|
|
@@ -6991,9 +7519,11 @@ function setFooterModelPickerOpen(open) {
|
|
|
6991
7519
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
6992
7520
|
renderFooter();
|
|
6993
7521
|
updateFooterModelPickerPosition();
|
|
7522
|
+
if (wasOpen && !footerModelPickerOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
6994
7523
|
}
|
|
6995
7524
|
|
|
6996
7525
|
function setFooterThinkingPickerOpen(open) {
|
|
7526
|
+
const wasOpen = footerThinkingPickerOpen;
|
|
6997
7527
|
footerThinkingPickerOpen = !!open;
|
|
6998
7528
|
if (footerThinkingPickerOpen) {
|
|
6999
7529
|
footerModelPickerOpen = false;
|
|
@@ -7009,6 +7539,7 @@ function setFooterThinkingPickerOpen(open) {
|
|
|
7009
7539
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
7010
7540
|
renderFooter();
|
|
7011
7541
|
updateFooterModelPickerPosition();
|
|
7542
|
+
if (wasOpen && !footerThinkingPickerOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
7012
7543
|
}
|
|
7013
7544
|
|
|
7014
7545
|
function normalizeFooterGitBranches(data = {}) {
|
|
@@ -7078,6 +7609,7 @@ async function loadFooterBranchPicker(tabContext = activeTabContext()) {
|
|
|
7078
7609
|
}
|
|
7079
7610
|
|
|
7080
7611
|
function setFooterBranchPickerOpen(open) {
|
|
7612
|
+
const wasOpen = footerBranchPickerOpen;
|
|
7081
7613
|
footerBranchPickerOpen = !!open;
|
|
7082
7614
|
if (footerBranchPickerOpen) {
|
|
7083
7615
|
footerModelPickerOpen = false;
|
|
@@ -7095,6 +7627,7 @@ function setFooterBranchPickerOpen(open) {
|
|
|
7095
7627
|
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
7096
7628
|
renderFooter();
|
|
7097
7629
|
updateFooterModelPickerPosition();
|
|
7630
|
+
if (wasOpen && !footerBranchPickerOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
7098
7631
|
}
|
|
7099
7632
|
|
|
7100
7633
|
function pathLooksInside(parentPath, childPath) {
|
|
@@ -8615,9 +9148,67 @@ async function clearAppRunner() {
|
|
|
8615
9148
|
}
|
|
8616
9149
|
}
|
|
8617
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
|
+
|
|
8618
9181
|
function appRunnerOutputText(run) {
|
|
8619
|
-
|
|
8620
|
-
|
|
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
|
+
}
|
|
8621
9212
|
}
|
|
8622
9213
|
|
|
8623
9214
|
async function copyAppRunnerOutput(run) {
|
|
@@ -8994,6 +9585,7 @@ function renderAppRunnerInfoDialog() {
|
|
|
8994
9585
|
"Detection is scoped to the active terminal tab's current working directory.",
|
|
8995
9586
|
"Only commands/files that exist and runner binaries available on this system are shown.",
|
|
8996
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.",
|
|
8997
9589
|
"Only one app runner can be active per tab; Close/Stop terminates the process/server.",
|
|
8998
9590
|
]) howList.append(make("li", "", line));
|
|
8999
9591
|
how.append(howList);
|
|
@@ -9018,6 +9610,42 @@ function closeAppRunnerInfoDialog() {
|
|
|
9018
9610
|
if (elements.appRunnerInfoDialog?.open) elements.appRunnerInfoDialog.close();
|
|
9019
9611
|
}
|
|
9020
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
|
+
|
|
9021
9649
|
function renderAppRunnerWidget() {
|
|
9022
9650
|
const data = activeAppRunnerData();
|
|
9023
9651
|
const run = data.activeRun;
|
|
@@ -9034,14 +9662,15 @@ function renderAppRunnerWidget() {
|
|
|
9034
9662
|
const elapsed = appRunnerElapsedLabel(run);
|
|
9035
9663
|
header.append(titleWrap);
|
|
9036
9664
|
|
|
9037
|
-
const
|
|
9038
|
-
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 });
|
|
9039
9668
|
const terminal = make("div", "release-npm-terminal");
|
|
9040
9669
|
terminal.setAttribute("role", "log");
|
|
9041
9670
|
terminal.setAttribute("aria-live", running ? "polite" : "off");
|
|
9042
9671
|
for (const line of lines) appendReleaseNpmTerminalLine(terminal, line);
|
|
9043
9672
|
|
|
9044
|
-
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);
|
|
9045
9674
|
const controls = make("div", "release-npm-controls app-runner-output-controls");
|
|
9046
9675
|
const actions = make("div", "app-runner-output-actions");
|
|
9047
9676
|
const closeButton = appRunnerActionButton("Close", running ? stopAppRunner : clearAppRunner, running ? "danger app-runner-close-action" : "app-runner-close-action");
|
|
@@ -9056,11 +9685,14 @@ function renderAppRunnerWidget() {
|
|
|
9056
9685
|
if (canRunAgain) actions.append(appRunnerActionButton("Run again", () => runAppRunner(run.runnerId)));
|
|
9057
9686
|
actions.append(appRunnerActionButton("Clear", clearAppRunner));
|
|
9058
9687
|
}
|
|
9688
|
+
const inputForm = running ? renderAppRunnerInputForm(run) : null;
|
|
9059
9689
|
const pills = make("div", "app-runner-output-pills");
|
|
9060
9690
|
if (run.kind) pills.append(make("span", "release-npm-pill", run.kind));
|
|
9061
9691
|
pills.append(make("span", `release-npm-pill app-runner-status ${run.status || "running"}`.trim(), status));
|
|
9062
9692
|
if (elapsed) pills.append(make("span", "release-npm-pill elapsed", elapsed));
|
|
9063
|
-
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(" · ")));
|
|
9064
9696
|
const outputDetails = renderReleaseNpmOutputDetails(`app-runner:${run.id || run.runnerId || "active"}`, streamHeader, terminal, controls);
|
|
9065
9697
|
node.append(header, outputDetails);
|
|
9066
9698
|
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
@@ -9817,6 +10449,7 @@ function mirrorRemoteWebuiWidgetToTranscript(widgetKey, lines = [], request = {}
|
|
|
9817
10449
|
|
|
9818
10450
|
function renderWidgets() {
|
|
9819
10451
|
if (deferUiRenderDuringPointerActivation("widgets", renderWidgets)) return;
|
|
10452
|
+
const appRunnerInputFocus = captureAppRunnerInputFocus();
|
|
9820
10453
|
elements.widgetArea.replaceChildren();
|
|
9821
10454
|
const releaseOutput = renderReleaseNpmOutputWidget();
|
|
9822
10455
|
if (releaseOutput) elements.widgetArea.append(releaseOutput);
|
|
@@ -9850,6 +10483,7 @@ function renderWidgets() {
|
|
|
9850
10483
|
node.textContent = `${key}\n${cleanLines.join("\n")}`;
|
|
9851
10484
|
elements.widgetArea.append(node);
|
|
9852
10485
|
}
|
|
10486
|
+
restoreAppRunnerInputFocus(appRunnerInputFocus);
|
|
9853
10487
|
}
|
|
9854
10488
|
|
|
9855
10489
|
function setGitWorkflow(patch, { tabId = activeTabId } = {}) {
|
|
@@ -10142,7 +10776,7 @@ function renderGitInitStackInput() {
|
|
|
10142
10776
|
elements.gitWorkflowActions.append(row);
|
|
10143
10777
|
}
|
|
10144
10778
|
|
|
10145
|
-
function renderGitWorkflowManualCommitInput() {
|
|
10779
|
+
function renderGitWorkflowManualCommitInput({ appendCommitButton = true } = {}) {
|
|
10146
10780
|
const tabId = gitWorkflowActionTabId();
|
|
10147
10781
|
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
10148
10782
|
const defaultCommitMessage = String(workflow?.manualCommitMessageDefault || "").trim();
|
|
@@ -10187,8 +10821,10 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
10187
10821
|
loadGitWorkflowDefaultCommitMessage({ runId: workflow?.runId, tabId });
|
|
10188
10822
|
|
|
10189
10823
|
field.append(input);
|
|
10190
|
-
row.append(field
|
|
10824
|
+
row.append(field);
|
|
10825
|
+
if (appendCommitButton) row.append(commitButton);
|
|
10191
10826
|
elements.gitWorkflowActions.append(row);
|
|
10827
|
+
return commitButton;
|
|
10192
10828
|
}
|
|
10193
10829
|
|
|
10194
10830
|
function setGitPrDialogStatus(message = "", level = "muted") {
|
|
@@ -10527,9 +11163,10 @@ function renderGitWorkflow() {
|
|
|
10527
11163
|
if (gitWorkflow.step === "add") {
|
|
10528
11164
|
addGitWorkflowAction("Run git add .", () => runGitAdd(), "primary", false);
|
|
10529
11165
|
} else if (gitWorkflow.step === "generate") {
|
|
10530
|
-
renderGitWorkflowManualCommitInput();
|
|
11166
|
+
const commitInputButton = renderGitWorkflowManualCommitInput({ appendCommitButton: false });
|
|
10531
11167
|
addGitWorkflowAction("Run /git-staged-msg", () => runGitMessagePrompt(), "primary", false);
|
|
10532
11168
|
addGitWorkflowAction("Preview current message files", () => loadGitWorkflowMessage({ requireFresh: false }), "", false);
|
|
11169
|
+
elements.gitWorkflowActions.append(commitInputButton);
|
|
10533
11170
|
} else if (gitWorkflow.step === "generating") {
|
|
10534
11171
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
10535
11172
|
} else if (gitWorkflow.step === "message") {
|
|
@@ -10537,10 +11174,11 @@ function renderGitWorkflow() {
|
|
|
10537
11174
|
addGitWorkflowAction("Create PR", () => createGitPrBranch(), "primary", false, GIT_WORKFLOW_CREATE_PR_TOOLTIP);
|
|
10538
11175
|
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
10539
11176
|
}
|
|
10540
|
-
renderGitWorkflowManualCommitInput();
|
|
11177
|
+
const commitInputButton = renderGitWorkflowManualCommitInput({ appendCommitButton: false });
|
|
10541
11178
|
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), gitWorkflow.prMode ? "primary" : "", false);
|
|
10542
11179
|
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), gitWorkflow.prMode ? "primary" : "", false);
|
|
10543
11180
|
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
11181
|
+
elements.gitWorkflowActions.append(commitInputButton);
|
|
10544
11182
|
} else if (gitWorkflow.step === "branchNaming") {
|
|
10545
11183
|
addGitWorkflowAction("Refresh branch name", () => loadGitWorkflowBranchName({ requireFresh: true }), "", false);
|
|
10546
11184
|
addGitWorkflowAction("Manual branch", () => createGitPrBranchManually(), "", !!gitWorkflow.busy, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP);
|
|
@@ -13746,12 +14384,17 @@ function renderRunIndicator({ scroll = false } = {}) {
|
|
|
13746
14384
|
}
|
|
13747
14385
|
|
|
13748
14386
|
function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}) {
|
|
14387
|
+
const wasLocallyActive = runIndicatorLocallyActive;
|
|
14388
|
+
const previousActivity = runIndicatorActivity;
|
|
14389
|
+
const hadRunIndicatorBubble = runIndicatorBubble?.parentElement === elements.chat;
|
|
13749
14390
|
if (active) {
|
|
13750
14391
|
runIndicatorLocallyActive = true;
|
|
13751
14392
|
if (!runIndicatorStartedAt) runIndicatorStartedAt = performance.now();
|
|
13752
14393
|
}
|
|
13753
14394
|
runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
|
|
13754
|
-
|
|
14395
|
+
const needsRender = scroll || !hadRunIndicatorBubble || wasLocallyActive !== runIndicatorLocallyActive || previousActivity !== runIndicatorActivity;
|
|
14396
|
+
if (needsRender) renderRunIndicator({ scroll });
|
|
14397
|
+
else if (runIndicatorIsActive()) startRunIndicatorTicker();
|
|
13755
14398
|
updateComposerModeButtons();
|
|
13756
14399
|
if (active) scheduleRunIndicatorGraceCheck();
|
|
13757
14400
|
}
|
|
@@ -14080,8 +14723,8 @@ function applyNativeSlashCommandEffects(response, message, tabContext = activeTa
|
|
|
14080
14723
|
});
|
|
14081
14724
|
}
|
|
14082
14725
|
|
|
14083
|
-
if (data.download &&
|
|
14084
|
-
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");
|
|
14085
14728
|
}
|
|
14086
14729
|
|
|
14087
14730
|
const cards = Array.isArray(data.cards) && data.cards.length ? data.cards : null;
|
|
@@ -14189,6 +14832,7 @@ function scheduleChatFollowScroll() {
|
|
|
14189
14832
|
|
|
14190
14833
|
function scrollChatToBottom({ force = false } = {}) {
|
|
14191
14834
|
if (deferChatFollowScrollDuringPointerActivation({ force })) return;
|
|
14835
|
+
if (deferChatFollowScrollDuringInteractiveDropdown({ force })) return;
|
|
14192
14836
|
if (force) autoFollowChat = true;
|
|
14193
14837
|
if (!autoFollowChat) {
|
|
14194
14838
|
updateJumpToLatestButton();
|
|
@@ -14285,6 +14929,7 @@ function setPublishMenuOpen(open) {
|
|
|
14285
14929
|
elements.publishButton.classList.toggle("menu-open", publishMenuOpen);
|
|
14286
14930
|
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
14287
14931
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14932
|
+
if (!publishMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14288
14933
|
}
|
|
14289
14934
|
|
|
14290
14935
|
function setNativeCommandMenuOpen(open) {
|
|
@@ -14293,6 +14938,7 @@ function setNativeCommandMenuOpen(open) {
|
|
|
14293
14938
|
elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
|
|
14294
14939
|
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
14295
14940
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14941
|
+
if (!nativeCommandMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14296
14942
|
}
|
|
14297
14943
|
|
|
14298
14944
|
function setAppRunnerMenuOpen(open) {
|
|
@@ -14301,6 +14947,7 @@ function setAppRunnerMenuOpen(open) {
|
|
|
14301
14947
|
elements.appRunnerMenuButton?.classList.toggle("menu-open", appRunnerMenuOpen);
|
|
14302
14948
|
elements.appRunnerMenuButton?.parentElement?.classList.toggle("open", appRunnerMenuOpen);
|
|
14303
14949
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14950
|
+
if (!appRunnerMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14304
14951
|
}
|
|
14305
14952
|
|
|
14306
14953
|
function setOptionsMenuOpen(open) {
|
|
@@ -14309,6 +14956,7 @@ function setOptionsMenuOpen(open) {
|
|
|
14309
14956
|
elements.optionsMenuButton.classList.toggle("menu-open", optionsMenuOpen);
|
|
14310
14957
|
elements.optionsMenuButton.parentElement?.classList.toggle("open", optionsMenuOpen);
|
|
14311
14958
|
scheduleMobileDropdownScrollBoundsUpdate();
|
|
14959
|
+
if (!optionsMenuOpen) scheduleDeferredUiFlushAfterDropdownClose();
|
|
14312
14960
|
}
|
|
14313
14961
|
|
|
14314
14962
|
function optionalFeatureIdForCommand(name) {
|
|
@@ -14475,7 +15123,7 @@ function updateOptionalFeatureAvailability() {
|
|
|
14475
15123
|
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
14476
15124
|
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
14477
15125
|
optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
|
|
14478
|
-
optionalFeatureAvailability.remoteWebui = hasAvailableCommand("remote") || optionalFeatureAvailability.remoteWebui || statusEntries.has("pi-remote-webui") || widgets.has("pi-remote-webui");
|
|
15126
|
+
optionalFeatureAvailability.remoteWebui = hasAvailableCommand("remote") || optionalFeatureAvailability.remoteWebui || statusEntries.has("pi-remote-webui") || statusEntries.has(REMOTE_WEBUI_CONTROLS_STATUS_KEY) || widgets.has("pi-remote-webui");
|
|
14479
15127
|
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
14480
15128
|
requestGitFooterWebuiPayload();
|
|
14481
15129
|
renderOptionalFeatureControls();
|
|
@@ -14496,6 +15144,19 @@ function optionalFeatureStatus(featureId) {
|
|
|
14496
15144
|
return { label: "Install needed", className: "missing", detail: installMessage || "Package is not installed or not visible from the Web UI package root" };
|
|
14497
15145
|
}
|
|
14498
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
|
+
|
|
14499
15160
|
function optionalFeatureWidgetFeatureId(key) {
|
|
14500
15161
|
if (key.startsWith("btw:")) return "btwCommand";
|
|
14501
15162
|
if (key.startsWith("release-npm:")) return "releaseNpm";
|
|
@@ -14522,26 +15183,17 @@ function renderOptionalFeaturePanel() {
|
|
|
14522
15183
|
const packageStatus = optionalFeaturePackageStatus(feature.id);
|
|
14523
15184
|
const status = optionalFeatureStatus(feature.id);
|
|
14524
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;
|
|
14525
15190
|
|
|
14526
15191
|
const main = make("div", "optional-feature-main");
|
|
14527
15192
|
const title = make("div", "optional-feature-title");
|
|
14528
15193
|
title.append(make("strong", undefined, feature.label), make("span", `optional-feature-pill ${status.className}`, status.label));
|
|
14529
|
-
|
|
14530
|
-
const description = make("div", "optional-feature-description", feature.description);
|
|
14531
|
-
const packageLine = make("code", "optional-feature-package", feature.packageName);
|
|
14532
|
-
main.append(title, detail, description, packageLine);
|
|
15194
|
+
main.append(title);
|
|
14533
15195
|
|
|
14534
15196
|
const actions = make("div", "optional-feature-actions");
|
|
14535
|
-
if (feature.id === "gitFooterStatus") {
|
|
14536
|
-
const setup = make("button", "optional-feature-action setup", "git-footer-status-setup");
|
|
14537
|
-
setup.type = "button";
|
|
14538
|
-
setup.title = GIT_FOOTER_STATUS_SETUP_TOOLTIP;
|
|
14539
|
-
setup.dataset.tooltip = GIT_FOOTER_STATUS_SETUP_TOOLTIP;
|
|
14540
|
-
setup.disabled = installing;
|
|
14541
|
-
setup.addEventListener("click", () => configureGitFooterStatusSetup({ force: true }));
|
|
14542
|
-
actions.append(setup);
|
|
14543
|
-
}
|
|
14544
|
-
|
|
14545
15197
|
const action = make("button", "optional-feature-action");
|
|
14546
15198
|
action.type = "button";
|
|
14547
15199
|
action.disabled = installing;
|
|
@@ -14633,10 +15285,23 @@ function renderOptionalFeatureControls() {
|
|
|
14633
15285
|
optionalFeatureUnavailableMessage("remoteWebui"),
|
|
14634
15286
|
);
|
|
14635
15287
|
}
|
|
15288
|
+
syncRemoteWebuiControlVisibility(hasRemoteWebuiCommand);
|
|
14636
15289
|
|
|
14637
15290
|
renderOptionalFeaturePanel();
|
|
14638
15291
|
}
|
|
14639
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
|
+
|
|
14640
15305
|
function commandUnavailableMessage(commandName) {
|
|
14641
15306
|
const featureId = optionalFeatureIdForCommand(commandName);
|
|
14642
15307
|
if (featureId) return optionalFeatureUnavailableMessage(featureId);
|
|
@@ -14791,7 +15456,7 @@ function nativeSelectorMatches(item, query) {
|
|
|
14791
15456
|
.some((value) => String(value).toLowerCase().includes(needle));
|
|
14792
15457
|
}
|
|
14793
15458
|
|
|
14794
|
-
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId } = {}) {
|
|
15459
|
+
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId, numbered = false } = {}) {
|
|
14795
15460
|
const query = elements.nativeCommandSearch.value.trim();
|
|
14796
15461
|
const filtered = items.filter((item) => nativeSelectorMatches(item, query));
|
|
14797
15462
|
elements.nativeCommandBody.replaceChildren();
|
|
@@ -14800,13 +15465,13 @@ function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect,
|
|
|
14800
15465
|
return;
|
|
14801
15466
|
}
|
|
14802
15467
|
const list = make("div", "native-selector-list");
|
|
14803
|
-
for (const item of filtered) {
|
|
15468
|
+
for (const [index, item] of filtered.entries()) {
|
|
14804
15469
|
const button = make("button", `native-selector-item${item.id === activeId ? " active" : ""}`);
|
|
14805
15470
|
button.type = "button";
|
|
14806
|
-
if (item.depth !== undefined) button.style.setProperty("--tree-depth", String(item.depth));
|
|
14807
15471
|
button.disabled = item.disabled === true;
|
|
14808
15472
|
button.addEventListener("click", () => onSelect?.(item));
|
|
14809
15473
|
const title = make("span", "native-selector-title");
|
|
15474
|
+
if (numbered) title.append(make("span", "native-selector-index", `${index + 1}.`));
|
|
14810
15475
|
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
14811
15476
|
if (item.badge) {
|
|
14812
15477
|
const badgeState = String(item.badge).toLowerCase();
|
|
@@ -15376,7 +16041,6 @@ async function openNativeTreeSelector() {
|
|
|
15376
16041
|
description: node.text || "",
|
|
15377
16042
|
meta: `${node.timestamp || ""}${node.childCount ? ` · ${node.childCount} child${node.childCount === 1 ? "" : "ren"}` : ""}`,
|
|
15378
16043
|
badge: node.currentLeaf ? "leaf" : "",
|
|
15379
|
-
depth: node.depth || 0,
|
|
15380
16044
|
node,
|
|
15381
16045
|
}));
|
|
15382
16046
|
const navigate = async (item) => {
|
|
@@ -15399,7 +16063,7 @@ async function openNativeTreeSelector() {
|
|
|
15399
16063
|
}
|
|
15400
16064
|
};
|
|
15401
16065
|
const render = () => {
|
|
15402
|
-
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 });
|
|
15403
16067
|
elements.nativeCommandBody.prepend(options);
|
|
15404
16068
|
};
|
|
15405
16069
|
filterField.select.addEventListener("change", () => {
|
|
@@ -15889,7 +16553,6 @@ function handleMessageUpdate(event) {
|
|
|
15889
16553
|
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
15890
16554
|
if (streamThinking) streamThinking.textContent += delta;
|
|
15891
16555
|
}
|
|
15892
|
-
renderFooter();
|
|
15893
16556
|
scrollChatToBottom();
|
|
15894
16557
|
} else if (update.type === "thinking_end") {
|
|
15895
16558
|
const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event)) || thinkingDeltaText(update);
|
|
@@ -15905,7 +16568,8 @@ function handleMessageUpdate(event) {
|
|
|
15905
16568
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
15906
16569
|
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
15907
16570
|
else scheduleStreamingAssistantTextRender();
|
|
15908
|
-
|
|
16571
|
+
// Streaming output must stay transcript-local. Full footer/status
|
|
16572
|
+
// reconciliation happens on message/state refreshes, not per token.
|
|
15909
16573
|
scrollChatToBottom();
|
|
15910
16574
|
} else if (update.type === "toolcall_start") {
|
|
15911
16575
|
streamToolCallSeen = true;
|
|
@@ -15991,6 +16655,7 @@ function renderNetworkStatus() {
|
|
|
15991
16655
|
const rebinding = opening || closing;
|
|
15992
16656
|
const localUrl = network?.localUrl || `${window.location.origin}/`;
|
|
15993
16657
|
const networkUrls = Array.isArray(network?.networkUrls) ? network.networkUrls : [];
|
|
16658
|
+
syncRemoteWebuiControlVisibility();
|
|
15994
16659
|
elements.networkStatus.className = `network-status ${opening ? "opening" : closing ? "closing" : open ? "open" : "closed"}`;
|
|
15995
16660
|
elements.networkStatus.title = closing
|
|
15996
16661
|
? "Closing network access and returning to local-only"
|
|
@@ -16064,21 +16729,25 @@ async function refreshNetworkStatus() {
|
|
|
16064
16729
|
renderNetworkStatus();
|
|
16065
16730
|
}
|
|
16066
16731
|
|
|
16067
|
-
async function
|
|
16068
|
-
const
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
return;
|
|
16732
|
+
async function runRemoteWebuiCommand(command) {
|
|
16733
|
+
const commandName = String(command || "").replace(/^\//, "").split(/\s+/, 1)[0] || "remote";
|
|
16734
|
+
if (!isOptionalFeatureEnabled("remoteWebui") || !hasAvailableCommand(commandName)) {
|
|
16735
|
+
const message = commandUnavailableMessage(commandName);
|
|
16736
|
+
addEvent(message, "warn");
|
|
16737
|
+
refreshCommands(activeTabContext()).catch((error) => addEvent(error.message || String(error), "error"));
|
|
16738
|
+
return false;
|
|
16075
16739
|
}
|
|
16740
|
+
await runNativeCommandMenu(command);
|
|
16741
|
+
return true;
|
|
16742
|
+
}
|
|
16076
16743
|
|
|
16744
|
+
async function toggleRemoteAuth() {
|
|
16745
|
+
const enable = !latestNetwork?.auth?.enabled;
|
|
16077
16746
|
elements.remoteAuthToggle.disabled = true;
|
|
16078
16747
|
try {
|
|
16079
|
-
|
|
16080
|
-
|
|
16081
|
-
|
|
16748
|
+
await runRemoteWebuiCommand(remoteWebuiCommand(enable ? "authOn" : "authOff", enable ? "/remote auth on" : "/remote auth off"));
|
|
16749
|
+
await delay(250);
|
|
16750
|
+
await refreshNetworkStatus();
|
|
16082
16751
|
} catch (error) {
|
|
16083
16752
|
addEvent(error.message || String(error), "error");
|
|
16084
16753
|
} finally {
|
|
@@ -16194,17 +16863,122 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
16194
16863
|
footerScopedModelPatterns = scopedModelPatterns;
|
|
16195
16864
|
footerScopedModelSource = scopedModelSource;
|
|
16196
16865
|
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
16197
|
-
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
|
+
}
|
|
16198
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) {
|
|
16199
16940
|
const option = document.createElement("option");
|
|
16200
|
-
option.value =
|
|
16201
|
-
option.textContent =
|
|
16941
|
+
option.value = modelSelectValue(model);
|
|
16942
|
+
option.textContent = modelSelectOptionText(model);
|
|
16202
16943
|
elements.modelSelect.append(option);
|
|
16203
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;
|
|
16950
|
+
elements.modelSelect.append(option);
|
|
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, "");
|
|
16204
16980
|
syncModelSelectToState();
|
|
16205
|
-
|
|
16206
|
-
renderFeedbackTray();
|
|
16207
|
-
if (elements.commandPaletteDialog?.open) renderCommandPalette({ preserveScroll: true });
|
|
16981
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
16208
16982
|
}
|
|
16209
16983
|
|
|
16210
16984
|
function syncModelSelectToState() {
|
|
@@ -16213,6 +16987,7 @@ function syncModelSelectToState() {
|
|
|
16213
16987
|
for (const option of elements.modelSelect.options) {
|
|
16214
16988
|
if (option.value === value) {
|
|
16215
16989
|
elements.modelSelect.value = value;
|
|
16990
|
+
renderModelSearchResults(availableModels.filter((model) => !elements.modelSearchInput?.value.trim() || modelSelectOptionText(model).toLowerCase().includes(elements.modelSearchInput.value.trim().toLowerCase())));
|
|
16216
16991
|
break;
|
|
16217
16992
|
}
|
|
16218
16993
|
}
|
|
@@ -16402,6 +17177,7 @@ function hideCommandSuggestions() {
|
|
|
16402
17177
|
pathSuggestions = [];
|
|
16403
17178
|
suggestionMode = "none";
|
|
16404
17179
|
commandSuggestIndex = 0;
|
|
17180
|
+
scheduleDeferredUiFlushAfterDropdownClose();
|
|
16405
17181
|
}
|
|
16406
17182
|
|
|
16407
17183
|
function setActiveCommandSuggestion(index) {
|
|
@@ -16896,35 +17672,15 @@ function scheduleForegroundReconcile(reason = "resume", delay = FOREGROUND_RECON
|
|
|
16896
17672
|
}
|
|
16897
17673
|
|
|
16898
17674
|
async function openToNetwork() {
|
|
16899
|
-
|
|
16900
|
-
await closeNetworkAccess();
|
|
16901
|
-
return;
|
|
16902
|
-
}
|
|
16903
|
-
if (!confirm(`Open Pi Web UI to your local network?\n\nRemote PIN auth is ${latestNetwork?.auth?.enabled ? "ON" : "OFF"}. The Web UI can control Pi/tools, so only do this on a trusted LAN.`)) return;
|
|
16904
|
-
|
|
17675
|
+
const open = !!latestNetwork?.open;
|
|
16905
17676
|
elements.openNetworkButton.disabled = true;
|
|
16906
|
-
elements.openNetworkButton.textContent = "Opening…";
|
|
17677
|
+
elements.openNetworkButton.textContent = open ? "Closing…" : "Opening…";
|
|
16907
17678
|
try {
|
|
16908
|
-
await
|
|
16909
|
-
|
|
16910
|
-
renderNetworkStatus();
|
|
16911
|
-
addEvent("opening webui to local network", "warn");
|
|
16912
|
-
for (let attempt = 0; attempt < 20; attempt++) {
|
|
16913
|
-
await delay(350);
|
|
16914
|
-
try {
|
|
16915
|
-
await refreshNetworkStatus();
|
|
16916
|
-
if (latestNetwork?.open && !latestNetwork?.opening) {
|
|
16917
|
-
const url = latestNetwork.networkUrls?.[0];
|
|
16918
|
-
addEvent(`webui open to local network${url ? `: ${url}` : ""}`, "warn");
|
|
16919
|
-
return;
|
|
16920
|
-
}
|
|
16921
|
-
} catch {
|
|
16922
|
-
// The listener briefly drops while rebinding; retry.
|
|
16923
|
-
}
|
|
16924
|
-
}
|
|
17679
|
+
await runRemoteWebuiCommand(remoteWebuiCommand(open ? "close" : "open", open ? "/remote close" : "/remote"));
|
|
17680
|
+
await delay(350);
|
|
16925
17681
|
await refreshNetworkStatus();
|
|
16926
17682
|
} catch (error) {
|
|
16927
|
-
addEvent(error.message, "error");
|
|
17683
|
+
addEvent(error.message || String(error), "error");
|
|
16928
17684
|
} finally {
|
|
16929
17685
|
renderNetworkStatus();
|
|
16930
17686
|
}
|
|
@@ -16932,41 +17688,7 @@ async function openToNetwork() {
|
|
|
16932
17688
|
|
|
16933
17689
|
async function closeNetworkAccess() {
|
|
16934
17690
|
if (!latestNetwork?.open) return;
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
elements.openNetworkButton.disabled = true;
|
|
16938
|
-
elements.openNetworkButton.textContent = "Closing…";
|
|
16939
|
-
try {
|
|
16940
|
-
await api("/api/network/close", { method: "POST", scoped: false });
|
|
16941
|
-
latestNetwork = { ...(latestNetwork || {}), opening: false, closing: true };
|
|
16942
|
-
renderNetworkStatus();
|
|
16943
|
-
addEvent("closing webui network access", "warn");
|
|
16944
|
-
let refreshFailed = false;
|
|
16945
|
-
for (let attempt = 0; attempt < 20; attempt++) {
|
|
16946
|
-
await delay(350);
|
|
16947
|
-
try {
|
|
16948
|
-
await refreshNetworkStatus();
|
|
16949
|
-
if (!latestNetwork?.open && !latestNetwork?.closing) {
|
|
16950
|
-
addEvent("webui closed to local-only access", "warn");
|
|
16951
|
-
return;
|
|
16952
|
-
}
|
|
16953
|
-
} catch {
|
|
16954
|
-
refreshFailed = true;
|
|
16955
|
-
// Remote tabs will lose access after the listener returns to localhost.
|
|
16956
|
-
}
|
|
16957
|
-
}
|
|
16958
|
-
if (refreshFailed) {
|
|
16959
|
-
latestNetwork = { ...(latestNetwork || {}), open: false, opening: false, closing: false, networkUrls: [] };
|
|
16960
|
-
renderNetworkStatus();
|
|
16961
|
-
addEvent("webui network access closed; reconnect from this machine if this tab loses access", "warn");
|
|
16962
|
-
return;
|
|
16963
|
-
}
|
|
16964
|
-
addEvent("network close requested, but the server still reports network access open", "warn");
|
|
16965
|
-
} catch (error) {
|
|
16966
|
-
addEvent(error.message, "error");
|
|
16967
|
-
} finally {
|
|
16968
|
-
renderNetworkStatus();
|
|
16969
|
-
}
|
|
17691
|
+
await openToNetwork();
|
|
16970
17692
|
}
|
|
16971
17693
|
|
|
16972
17694
|
function setServerActionStatus(message = "", level = "info") {
|
|
@@ -16982,10 +17704,11 @@ function updateServerActionButton() {
|
|
|
16982
17704
|
const button = elements.runServerActionButton;
|
|
16983
17705
|
if (!button) return;
|
|
16984
17706
|
button.disabled = !action;
|
|
16985
|
-
button.textContent = action === "restart" ? "Restart" : action === "update" ? "Update" : action === "stop" ? "Stop" : "Run";
|
|
17707
|
+
button.textContent = action === "restart" ? "Restart" : action === "update" || action === "update-all" ? "Update" : action === "stop" ? "Stop" : "Run";
|
|
16986
17708
|
button.classList.toggle("danger", action === "stop");
|
|
16987
17709
|
if (action === "restart") setServerActionStatus("Ready to restart the Web UI server.", "info");
|
|
16988
|
-
else if (action === "update") setServerActionStatus("Ready to run pi update, then restart the Web UI server.", "info");
|
|
17710
|
+
else if (action === "update") setServerActionStatus("Ready to run pi update for Pi only, then restart the Web UI server.", "info");
|
|
17711
|
+
else if (action === "update-all") setServerActionStatus("Ready to run pi update --all for Pi and configured packages, then restart the Web UI server.", "info");
|
|
16989
17712
|
else if (action === "stop") setServerActionStatus("Ready to stop the Web UI server.", "info");
|
|
16990
17713
|
else setServerActionStatus();
|
|
16991
17714
|
}
|
|
@@ -17093,6 +17816,7 @@ async function runSelectedServerAction() {
|
|
|
17093
17816
|
const action = elements.serverActionSelect?.value || "";
|
|
17094
17817
|
if (action === "restart") await restartServer();
|
|
17095
17818
|
else if (action === "update") await runPiUpdateAndRestart();
|
|
17819
|
+
else if (action === "update-all") await runPiUpdateAndRestart({ all: true });
|
|
17096
17820
|
else if (action === "stop") await stopServer();
|
|
17097
17821
|
}
|
|
17098
17822
|
|
|
@@ -17422,6 +18146,15 @@ function handleExtensionUiRequest(request) {
|
|
|
17422
18146
|
if (statusKey === STATS_WEBUI_STATUS_KEY) handleStatsWebuiStatus(request.statusText);
|
|
17423
18147
|
if (statusKey === BTW_WEBUI_STATUS_KEY) handleBtwWebuiStatus(request.statusText);
|
|
17424
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
|
+
}
|
|
17425
18158
|
renderStatus();
|
|
17426
18159
|
return;
|
|
17427
18160
|
}
|
|
@@ -18244,6 +18977,25 @@ function abortButtonReadyTitle() {
|
|
|
18244
18977
|
return `Hold Esc or the Abort button for ${abortButtonHoldSeconds()} seconds to abort the active Pi run`;
|
|
18245
18978
|
}
|
|
18246
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
|
+
|
|
18247
18999
|
function clearAbortLongPressResetTimer() {
|
|
18248
19000
|
clearTimeout(abortLongPressResetTimer);
|
|
18249
19001
|
abortLongPressResetTimer = null;
|
|
@@ -18285,6 +19037,7 @@ function completeAbortLongPress() {
|
|
|
18285
19037
|
if (!isAbortLongPressActive()) return;
|
|
18286
19038
|
if (abortLongPressReleasePending) return;
|
|
18287
19039
|
const source = abortLongPressSource;
|
|
19040
|
+
if (source === "escape") suppressEmptyPromptEscapeAction({ untilKeyup: true });
|
|
18288
19041
|
clearAbortLongPressResetTimer();
|
|
18289
19042
|
clearAbortLongPressCompletionTimers();
|
|
18290
19043
|
abortLongPressHandled = true;
|
|
@@ -18372,6 +19125,7 @@ async function abortActiveRun({ source = "button" } = {}) {
|
|
|
18372
19125
|
function startAbortLongPress(event, { source = "long-press" } = {}) {
|
|
18373
19126
|
if (!isAbortAvailable() || abortRequestInFlight) return false;
|
|
18374
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 });
|
|
18375
19129
|
if (isAbortLongPressActive()) {
|
|
18376
19130
|
resumeAbortLongPressAffordance();
|
|
18377
19131
|
return true;
|
|
@@ -18425,6 +19179,41 @@ elements.compactButton.addEventListener("click", async () => {
|
|
|
18425
19179
|
setComposerActionsOpen(false);
|
|
18426
19180
|
await requestManualCompaction({ triggerButton: elements.compactButton });
|
|
18427
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
|
+
});
|
|
18428
19217
|
elements.setModelButton.addEventListener("click", async () => {
|
|
18429
19218
|
if (!elements.modelSelect.value) return;
|
|
18430
19219
|
const tabContext = activeTabContext();
|
|
@@ -18458,6 +19247,33 @@ elements.setThinkingButton.addEventListener("click", async () => {
|
|
|
18458
19247
|
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
18459
19248
|
}
|
|
18460
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
|
+
});
|
|
18461
19277
|
elements.themeSelect.addEventListener("change", () => {
|
|
18462
19278
|
setThemeByName(elements.themeSelect.value, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
18463
19279
|
});
|
|
@@ -18477,6 +19293,7 @@ elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
|
18477
19293
|
elements.serverActionSelect.addEventListener("change", updateServerActionButton);
|
|
18478
19294
|
elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
|
|
18479
19295
|
elements.updateNotificationUpdateButton?.addEventListener("click", () => runPiUpdateAndRestart().catch((error) => addEvent(error.message || String(error), "error")));
|
|
19296
|
+
elements.updateNotificationUpdateAllButton?.addEventListener("click", () => runPiUpdateAndRestart({ all: true }).catch((error) => addEvent(error.message || String(error), "error")));
|
|
18480
19297
|
elements.updateNotificationDismissButton?.addEventListener("click", () => hideUpdateNotification({ remember: true }));
|
|
18481
19298
|
updateServerActionButton();
|
|
18482
19299
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
@@ -18748,6 +19565,9 @@ document.addEventListener("visibilitychange", () => {
|
|
|
18748
19565
|
window.addEventListener("pageshow", () => scheduleForegroundReconcile("page show", 0));
|
|
18749
19566
|
window.addEventListener("focus", () => scheduleForegroundReconcile("window focus"));
|
|
18750
19567
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
19568
|
+
window.addEventListener("storage", (event) => {
|
|
19569
|
+
if (event.key === OPTIONAL_FEATURES_STORAGE_KEY) reconcileDisabledOptionalFeaturesFromStorage();
|
|
19570
|
+
});
|
|
18751
19571
|
window.addEventListener("keydown", (event) => {
|
|
18752
19572
|
if (event.key !== "Escape") return;
|
|
18753
19573
|
if (event.defaultPrevented) return;
|
|
@@ -18806,6 +19626,10 @@ window.addEventListener("keydown", (event) => {
|
|
|
18806
19626
|
else if (!event.repeat) startAbortLongPress(event, { source: "escape" });
|
|
18807
19627
|
return;
|
|
18808
19628
|
}
|
|
19629
|
+
if (shouldSuppressEmptyPromptEscapeAction()) {
|
|
19630
|
+
event.preventDefault();
|
|
19631
|
+
return;
|
|
19632
|
+
}
|
|
18809
19633
|
if (event.repeat) {
|
|
18810
19634
|
event.preventDefault();
|
|
18811
19635
|
return;
|
|
@@ -18822,17 +19646,39 @@ window.addEventListener("keydown", (event) => {
|
|
|
18822
19646
|
}
|
|
18823
19647
|
});
|
|
18824
19648
|
window.addEventListener("keyup", (event) => {
|
|
18825
|
-
if (event.key
|
|
19649
|
+
if (event.key !== "Escape") return;
|
|
19650
|
+
if (abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
|
|
19651
|
+
finishEscapeAbortHoldSuppression();
|
|
18826
19652
|
}, { capture: true });
|
|
18827
19653
|
window.addEventListener("blur", () => {
|
|
18828
19654
|
if (abortLongPressSource === "escape") scheduleAbortLongPressReleaseReset();
|
|
18829
19655
|
else resetAbortLongPressAffordance();
|
|
19656
|
+
finishEscapeAbortHoldSuppression();
|
|
18830
19657
|
});
|
|
18831
19658
|
|
|
18832
19659
|
elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);
|
|
18833
19660
|
elements.gitChangesPullButton?.addEventListener("click", () => pullGitChangesDialog().catch((error) => addEvent(error.message || String(error), "error")));
|
|
18834
19661
|
elements.gitChangesCloseButton?.addEventListener("click", closeGitChangesDialog);
|
|
18835
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
|
+
});
|
|
18836
19682
|
elements.gitChangesDialog?.addEventListener("cancel", (event) => {
|
|
18837
19683
|
event.preventDefault();
|
|
18838
19684
|
closeGitChangesDialog();
|