@firstpick/pi-package-webui 0.4.0 → 0.4.2
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 +24 -9
- package/bin/pi-webui.mjs +415 -29
- package/index.ts +16 -1
- package/lib/trust-boundaries.mjs +1 -0
- package/package.json +1 -1
- package/public/app.js +766 -47
- package/public/index.html +44 -1
- package/public/styles.css +516 -4
- package/tests/http-endpoints-harness.test.mjs +97 -1
- package/tests/mobile-static.test.mjs +21 -10
- package/tests/session-auth-harness.test.mjs +4 -0
package/public/app.js
CHANGED
|
@@ -11,7 +11,11 @@ const elements = {
|
|
|
11
11
|
newTabCurrentDirectoryButton: $("#newTabCurrentDirectoryButton"),
|
|
12
12
|
newTabChooseDirectoryButton: $("#newTabChooseDirectoryButton"),
|
|
13
13
|
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
14
|
+
commandPaletteButton: $("#commandPaletteButton"),
|
|
15
|
+
workspaceDashboardToggleButton: $("#workspaceDashboardToggleButton"),
|
|
16
|
+
workspaceDashboard: $("#workspaceDashboard"),
|
|
14
17
|
statusBar: $("#statusBar"),
|
|
18
|
+
contextMeterBar: $("#contextMeterBar"),
|
|
15
19
|
serverOfflinePanel: $("#serverOfflinePanel"),
|
|
16
20
|
serverRestartPanel: $("#serverRestartPanel"),
|
|
17
21
|
serverRestartMessage: $("#serverRestartMessage"),
|
|
@@ -78,6 +82,7 @@ const elements = {
|
|
|
78
82
|
appRunnerMenuPanel: $("#appRunnerMenuPanel"),
|
|
79
83
|
optionsMenuButton: $("#optionsMenuButton"),
|
|
80
84
|
optionsMenu: $("#optionsMenu"),
|
|
85
|
+
optionsCommandPaletteButton: $("#optionsCommandPaletteButton"),
|
|
81
86
|
optionsResumeButton: $("#optionsResumeButton"),
|
|
82
87
|
optionsReloadButton: $("#optionsReloadButton"),
|
|
83
88
|
optionsNameButton: $("#optionsNameButton"),
|
|
@@ -122,6 +127,8 @@ const elements = {
|
|
|
122
127
|
backgroundClearButton: $("#backgroundClearButton"),
|
|
123
128
|
backgroundStatus: $("#backgroundStatus"),
|
|
124
129
|
networkStatus: $("#networkStatus"),
|
|
130
|
+
remoteAuthToggle: $("#remoteAuthToggle"),
|
|
131
|
+
remoteAuthStatus: $("#remoteAuthStatus"),
|
|
125
132
|
openNetworkButton: $("#openNetworkButton"),
|
|
126
133
|
serverActionSelect: $("#serverActionSelect"),
|
|
127
134
|
runServerActionButton: $("#runServerActionButton"),
|
|
@@ -186,6 +193,17 @@ const elements = {
|
|
|
186
193
|
pathPickerError: $("#pathPickerError"),
|
|
187
194
|
pathPickerCancelButton: $("#pathPickerCancelButton"),
|
|
188
195
|
pathPickerChooseButton: $("#pathPickerChooseButton"),
|
|
196
|
+
commandPaletteDialog: $("#commandPaletteDialog"),
|
|
197
|
+
commandPaletteInput: $("#commandPaletteInput"),
|
|
198
|
+
commandPaletteList: $("#commandPaletteList"),
|
|
199
|
+
commandPaletteHint: $("#commandPaletteHint"),
|
|
200
|
+
editRetryDialog: $("#editRetryDialog"),
|
|
201
|
+
editRetryMessage: $("#editRetryMessage"),
|
|
202
|
+
editRetryText: $("#editRetryText"),
|
|
203
|
+
editRetryStatus: $("#editRetryStatus"),
|
|
204
|
+
editRetryCancelButton: $("#editRetryCancelButton"),
|
|
205
|
+
editRetryForkButton: $("#editRetryForkButton"),
|
|
206
|
+
editRetrySendButton: $("#editRetrySendButton"),
|
|
189
207
|
nativeCommandDialog: $("#nativeCommandDialog"),
|
|
190
208
|
nativeCommandTitle: $("#nativeCommandTitle"),
|
|
191
209
|
nativeCommandMessage: $("#nativeCommandMessage"),
|
|
@@ -352,6 +370,10 @@ let userBashQueuesByTab = new Map();
|
|
|
352
370
|
let latestQueuedMessagesByTab = new Map();
|
|
353
371
|
let loadedPromptList = null;
|
|
354
372
|
let promptListRunning = false;
|
|
373
|
+
let workspaceDashboardCollapsed = false;
|
|
374
|
+
let commandPaletteIndex = 0;
|
|
375
|
+
let commandPaletteItems = [];
|
|
376
|
+
let activeEditRetry = null;
|
|
355
377
|
let abortLongPressTimer = null;
|
|
356
378
|
let abortLongPressHandled = false;
|
|
357
379
|
const dialogQueue = [];
|
|
@@ -389,6 +411,7 @@ const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
|
|
|
389
411
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
390
412
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
391
413
|
const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
|
|
414
|
+
const WORKSPACE_DASHBOARD_STORAGE_KEY = "pi-webui-workspace-dashboard-collapsed";
|
|
392
415
|
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
393
416
|
const ATTACHMENT_MAX_FILES = 12;
|
|
394
417
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
@@ -571,6 +594,8 @@ const SETTINGS_IMAGE_WIDTH_OPTIONS = ["60", "80", "120"];
|
|
|
571
594
|
const SETTINGS_EDITOR_PADDING_OPTIONS = ["0", "1", "2", "3"];
|
|
572
595
|
const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
|
|
573
596
|
const optionalFeatureInstallInProgress = new Set();
|
|
597
|
+
const optionalFeaturePackageStatuses = new Map();
|
|
598
|
+
const optionalFeatureInstallMessages = new Map();
|
|
574
599
|
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
575
600
|
|
|
576
601
|
function createGitWorkflowActionsDone(patch = {}) {
|
|
@@ -597,6 +622,21 @@ function gitWorkflowActionDonePatch(workflow, process) {
|
|
|
597
622
|
return { actionsDone: createGitWorkflowActionsDone({ ...workflow?.actionsDone, [process]: true }) };
|
|
598
623
|
}
|
|
599
624
|
|
|
625
|
+
function resetGitWorkflowManualCommitDefaultPatch() {
|
|
626
|
+
return {
|
|
627
|
+
manualCommitMessageDefault: "",
|
|
628
|
+
manualCommitMessageDefaultReason: "",
|
|
629
|
+
manualCommitMessageDefaultPath: "",
|
|
630
|
+
manualCommitMessageDefaultAction: "",
|
|
631
|
+
manualCommitMessageDefaultRequestedAt: 0,
|
|
632
|
+
manualCommitMessageDefaultLoading: false,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function gitWorkflowManualCommitInputMessage(workflow) {
|
|
637
|
+
return String(workflow?.manualCommitMessage || "").trim() || String(workflow?.manualCommitMessageDefault || "").trim();
|
|
638
|
+
}
|
|
639
|
+
|
|
600
640
|
function createGitWorkflowState() {
|
|
601
641
|
return {
|
|
602
642
|
active: false,
|
|
@@ -616,6 +656,7 @@ function createGitWorkflowState() {
|
|
|
616
656
|
initFilesStatus: null,
|
|
617
657
|
message: null,
|
|
618
658
|
manualCommitMessage: "",
|
|
659
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
619
660
|
messageRequestedAt: 0,
|
|
620
661
|
branchName: "",
|
|
621
662
|
branchNameRequestedAt: 0,
|
|
@@ -1722,6 +1763,41 @@ function restoreSidePanelState() {
|
|
|
1722
1763
|
setSidePanelCollapsed(stored ?? false, { persist: stored !== null });
|
|
1723
1764
|
}
|
|
1724
1765
|
|
|
1766
|
+
function readStoredWorkspaceDashboardCollapsed() {
|
|
1767
|
+
try {
|
|
1768
|
+
const stored = localStorage.getItem(WORKSPACE_DASHBOARD_STORAGE_KEY);
|
|
1769
|
+
return stored === null ? true : stored === "1";
|
|
1770
|
+
} catch {
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function persistWorkspaceDashboardCollapsed(collapsed) {
|
|
1776
|
+
try {
|
|
1777
|
+
localStorage.setItem(WORKSPACE_DASHBOARD_STORAGE_KEY, collapsed ? "1" : "0");
|
|
1778
|
+
} catch {
|
|
1779
|
+
// Ignore storage failures; this is only a browser preference.
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function setWorkspaceDashboardCollapsed(collapsed, { persist = true } = {}) {
|
|
1784
|
+
workspaceDashboardCollapsed = !!collapsed;
|
|
1785
|
+
if (elements.workspaceDashboard) elements.workspaceDashboard.hidden = workspaceDashboardCollapsed;
|
|
1786
|
+
if (elements.workspaceDashboardToggleButton) {
|
|
1787
|
+
elements.workspaceDashboardToggleButton.setAttribute("aria-expanded", workspaceDashboardCollapsed ? "false" : "true");
|
|
1788
|
+
const tooltip = workspaceDashboardCollapsed ? "Show workspace overview" : "Hide workspace overview";
|
|
1789
|
+
const tooltipDetail = `${tooltip}:\n• Shows current tab, cwd, model, context, session, and queue.\n• Opens common workspace/session actions from one place.`;
|
|
1790
|
+
elements.workspaceDashboardToggleButton.title = tooltip;
|
|
1791
|
+
elements.workspaceDashboardToggleButton.setAttribute("aria-label", tooltip);
|
|
1792
|
+
elements.workspaceDashboardToggleButton.setAttribute("data-tooltip", tooltipDetail);
|
|
1793
|
+
}
|
|
1794
|
+
if (persist) persistWorkspaceDashboardCollapsed(workspaceDashboardCollapsed);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function restoreWorkspaceDashboardState() {
|
|
1798
|
+
setWorkspaceDashboardCollapsed(readStoredWorkspaceDashboardCollapsed(), { persist: false });
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1725
1801
|
function bindMobileViewChanges() {
|
|
1726
1802
|
if (!mobileViewMedia) return;
|
|
1727
1803
|
const syncForViewport = (event) => {
|
|
@@ -1971,6 +2047,142 @@ function attachMessageCopyButton(bubble, message, body) {
|
|
|
1971
2047
|
return button;
|
|
1972
2048
|
}
|
|
1973
2049
|
|
|
2050
|
+
function userMessageEditText(message) {
|
|
2051
|
+
return textFromContent(message?.content).trim();
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function messageEntryId(message) {
|
|
2055
|
+
for (const key of ["entryId", "id", "sessionEntryId", "messageId"]) {
|
|
2056
|
+
const value = message?.[key];
|
|
2057
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2058
|
+
}
|
|
2059
|
+
return "";
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function userMessageOrdinalAtIndex(messageIndex) {
|
|
2063
|
+
if (!Number.isInteger(messageIndex) || messageIndex < 0) return -1;
|
|
2064
|
+
let ordinal = -1;
|
|
2065
|
+
for (let index = 0; index <= messageIndex && index < latestMessages.length; index += 1) {
|
|
2066
|
+
if (latestMessages[index]?.role === "user") ordinal += 1;
|
|
2067
|
+
}
|
|
2068
|
+
return ordinal;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
async function resolveForkMessageForEdit(message, messageIndex, tabId = activeTabId) {
|
|
2072
|
+
const directEntryId = messageEntryId(message);
|
|
2073
|
+
const text = userMessageEditText(message);
|
|
2074
|
+
if (directEntryId) return { entryId: directEntryId, text };
|
|
2075
|
+
const response = await api("/api/fork-messages", { tabId });
|
|
2076
|
+
const forkMessages = Array.isArray(response.data?.messages) ? response.data.messages : [];
|
|
2077
|
+
const ordinal = userMessageOrdinalAtIndex(messageIndex);
|
|
2078
|
+
const ordinalMatch = ordinal >= 0 ? forkMessages[ordinal] : null;
|
|
2079
|
+
if (ordinalMatch?.entryId && userMessageEditText({ content: ordinalMatch.text }) === text) return ordinalMatch;
|
|
2080
|
+
const exactMatches = forkMessages.filter((item) => item?.entryId && String(item.text || "").trim() === text);
|
|
2081
|
+
if (exactMatches.length === 1) return exactMatches[0];
|
|
2082
|
+
if (exactMatches.length > 1 && ordinalMatch?.entryId) return ordinalMatch;
|
|
2083
|
+
throw new Error("Could not map this transcript message to a fork point. Use /fork for the full selector.");
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function setEditRetryStatus(message = "", level = "info") {
|
|
2087
|
+
if (!elements.editRetryStatus) return;
|
|
2088
|
+
elements.editRetryStatus.textContent = message;
|
|
2089
|
+
elements.editRetryStatus.hidden = !message;
|
|
2090
|
+
elements.editRetryStatus.className = `edit-retry-status ${level} ${message ? "" : "muted"}`.trim();
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function setEditRetryBusy(busy, label = "Working…") {
|
|
2094
|
+
for (const button of [elements.editRetryForkButton, elements.editRetrySendButton, elements.editRetryCancelButton].filter(Boolean)) button.disabled = !!busy;
|
|
2095
|
+
if (elements.editRetrySendButton) elements.editRetrySendButton.textContent = busy ? label : "Fork & run";
|
|
2096
|
+
if (elements.editRetryForkButton) elements.editRetryForkButton.textContent = busy ? "Forking…" : "Fork only";
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function openEditRetryDialog(message, messageIndex = -1) {
|
|
2100
|
+
const text = userMessageEditText(message);
|
|
2101
|
+
if (!text) {
|
|
2102
|
+
addEvent("user message has no editable text", "warn");
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
activeEditRetry = { message, messageIndex, tabId: activeTabId };
|
|
2106
|
+
if (elements.editRetryMessage) elements.editRetryMessage.textContent = `Fork from user message #${messageIndex >= 0 ? messageIndex + 1 : "?"}, edit it, then run or leave the edited prompt in the composer.`;
|
|
2107
|
+
if (elements.editRetryText) {
|
|
2108
|
+
elements.editRetryText.value = text;
|
|
2109
|
+
elements.editRetryText.style.height = "auto";
|
|
2110
|
+
}
|
|
2111
|
+
setEditRetryStatus();
|
|
2112
|
+
setEditRetryBusy(false);
|
|
2113
|
+
if (!elements.editRetryDialog.open) elements.editRetryDialog.showModal();
|
|
2114
|
+
queueMicrotask(() => {
|
|
2115
|
+
elements.editRetryText?.focus();
|
|
2116
|
+
elements.editRetryText?.select();
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function closeEditRetryDialog() {
|
|
2121
|
+
activeEditRetry = null;
|
|
2122
|
+
if (elements.editRetryDialog?.open) elements.editRetryDialog.close();
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
async function submitEditRetry({ send = false } = {}) {
|
|
2126
|
+
if (!activeEditRetry) return;
|
|
2127
|
+
const editedText = String(elements.editRetryText?.value || "").trim();
|
|
2128
|
+
if (!editedText) {
|
|
2129
|
+
setEditRetryStatus("Prompt cannot be empty.", "error");
|
|
2130
|
+
elements.editRetryText?.focus();
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const { message, messageIndex, tabId } = activeEditRetry;
|
|
2134
|
+
const tabContext = activeTabContext(tabId || activeTabId);
|
|
2135
|
+
setEditRetryBusy(true, "Forking…");
|
|
2136
|
+
setEditRetryStatus("Resolving fork point…");
|
|
2137
|
+
try {
|
|
2138
|
+
const forkMessage = await resolveForkMessageForEdit(message, messageIndex, tabContext.tabId);
|
|
2139
|
+
setEditRetryStatus("Forking session…");
|
|
2140
|
+
const result = await api("/api/fork", { method: "POST", body: { entryId: forkMessage.entryId }, tabId: tabContext.tabId });
|
|
2141
|
+
applyResponseTab(result);
|
|
2142
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2143
|
+
closeEditRetryDialog();
|
|
2144
|
+
await refreshAll(tabContext);
|
|
2145
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2146
|
+
if (send) {
|
|
2147
|
+
addEvent("forked session; sending edited prompt", "info");
|
|
2148
|
+
await sendPrompt("prompt", editedText, { targetTabId: tabContext.tabId, throwOnError: true });
|
|
2149
|
+
} else {
|
|
2150
|
+
elements.promptInput.value = editedText;
|
|
2151
|
+
resizePromptInput();
|
|
2152
|
+
focusPromptInput({ defer: true });
|
|
2153
|
+
addEvent("forked session; edited prompt restored in composer", "info");
|
|
2154
|
+
}
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
setEditRetryStatus(error.message || String(error), "error");
|
|
2157
|
+
if (send) {
|
|
2158
|
+
elements.promptInput.value = editedText;
|
|
2159
|
+
resizePromptInput();
|
|
2160
|
+
}
|
|
2161
|
+
} finally {
|
|
2162
|
+
setEditRetryBusy(false);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function attachMessageEditRetryButton(bubble, message, messageIndex, { streaming = false, transient = false } = {}) {
|
|
2167
|
+
if (!bubble || streaming || transient || message?.role !== "user") return null;
|
|
2168
|
+
const text = userMessageEditText(message);
|
|
2169
|
+
if (!text) return null;
|
|
2170
|
+
const existing = bubble.querySelector(":scope > .message-edit-retry-button");
|
|
2171
|
+
if (existing) return existing;
|
|
2172
|
+
const button = make("button", "message-edit-retry-button", "↺");
|
|
2173
|
+
button.type = "button";
|
|
2174
|
+
button.title = "Edit this prompt and retry from here";
|
|
2175
|
+
button.setAttribute("aria-label", button.title);
|
|
2176
|
+
button.addEventListener("click", (event) => {
|
|
2177
|
+
event.preventDefault();
|
|
2178
|
+
event.stopPropagation();
|
|
2179
|
+
openEditRetryDialog(message, messageIndex);
|
|
2180
|
+
});
|
|
2181
|
+
bubble.classList.add("has-edit-retry-action");
|
|
2182
|
+
bubble.append(button);
|
|
2183
|
+
return button;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
1974
2186
|
function safeHttpUrl(value, base = window.location.href) {
|
|
1975
2187
|
const text = String(value || "").trim();
|
|
1976
2188
|
if (!text) return "";
|
|
@@ -2057,6 +2269,10 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
|
|
|
2057
2269
|
setBackendOffline(false);
|
|
2058
2270
|
const data = await response.json().catch(() => ({}));
|
|
2059
2271
|
if (!response.ok) {
|
|
2272
|
+
if (response.status === 401 && data.remoteAuthRequired) {
|
|
2273
|
+
const returnPath = `${window.location.pathname}${window.location.search || ""}` || "/";
|
|
2274
|
+
window.location.assign(`/remote-auth?return=${encodeURIComponent(returnPath)}`);
|
|
2275
|
+
}
|
|
2060
2276
|
const error = new Error(data.error || data.message || JSON.stringify(data));
|
|
2061
2277
|
error.statusCode = response.status;
|
|
2062
2278
|
error.data = data;
|
|
@@ -3722,7 +3938,7 @@ function restoreActiveDraft() {
|
|
|
3722
3938
|
|
|
3723
3939
|
function focusPromptInput({ defer = false } = {}) {
|
|
3724
3940
|
const focus = () => {
|
|
3725
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
|
|
3941
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
|
|
3726
3942
|
try {
|
|
3727
3943
|
elements.promptInput.focus({ preventScroll: true });
|
|
3728
3944
|
} catch {
|
|
@@ -4062,6 +4278,9 @@ function renderTabs() {
|
|
|
4062
4278
|
updateTerminalTabGroupOpenState();
|
|
4063
4279
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
4064
4280
|
updateDocumentTitle();
|
|
4281
|
+
renderWorkspaceDashboard();
|
|
4282
|
+
renderContextMeter();
|
|
4283
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
4065
4284
|
syncTabPolling();
|
|
4066
4285
|
}
|
|
4067
4286
|
|
|
@@ -5879,6 +6098,194 @@ function renderMinimalFooter() {
|
|
|
5879
6098
|
updateFooterModelPickerPosition();
|
|
5880
6099
|
}
|
|
5881
6100
|
|
|
6101
|
+
function contextUsageSnapshot() {
|
|
6102
|
+
const usage = latestStats?.contextUsage || currentState?.contextUsage || null;
|
|
6103
|
+
const contextWindow = contextWindowFromSources(usage, currentState?.model?.contextWindow);
|
|
6104
|
+
if (!contextWindow) return null;
|
|
6105
|
+
const rawPercent = Number(usage?.percent);
|
|
6106
|
+
const unknown = contextUsageUnknownAfterCompaction() || !Number.isFinite(rawPercent);
|
|
6107
|
+
const rawTokens = Number(usage?.tokens);
|
|
6108
|
+
return {
|
|
6109
|
+
tokens: Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : null,
|
|
6110
|
+
contextWindow,
|
|
6111
|
+
percent: unknown ? null : Math.max(0, Math.min(100, rawPercent)),
|
|
6112
|
+
unknown,
|
|
6113
|
+
autoCompactionEnabled: footerAutoCompactionEnabled(),
|
|
6114
|
+
};
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6117
|
+
function contextUsageDisplay(snapshot = contextUsageSnapshot()) {
|
|
6118
|
+
if (!snapshot) return "Context unknown";
|
|
6119
|
+
const windowText = formatFooterTokenCount(snapshot.contextWindow);
|
|
6120
|
+
if (typeof snapshot.percent === "number") return `${snapshot.percent.toFixed(1)}% of ${windowText}`;
|
|
6121
|
+
return `?/${windowText}`;
|
|
6122
|
+
}
|
|
6123
|
+
|
|
6124
|
+
function contextUsageDetail(snapshot = contextUsageSnapshot()) {
|
|
6125
|
+
if (!snapshot) return "Waiting for model context-window data.";
|
|
6126
|
+
const tokenText = snapshot.tokens === null ? "tokens unknown" : `${formatFooterTokenCount(snapshot.tokens)} tokens`;
|
|
6127
|
+
const autoText = snapshot.autoCompactionEnabled ? "auto-compaction on" : "auto-compaction off";
|
|
6128
|
+
return `${tokenText} · ${formatFooterTokenCount(snapshot.contextWindow)} window · ${autoText}`;
|
|
6129
|
+
}
|
|
6130
|
+
|
|
6131
|
+
function appendContextMeterFill(meter, snapshot) {
|
|
6132
|
+
const fill = make("span", "context-meter-fill");
|
|
6133
|
+
const percent = typeof snapshot?.percent === "number" ? snapshot.percent : 0;
|
|
6134
|
+
fill.style.width = `${Math.max(0, Math.min(100, percent)).toFixed(1)}%`;
|
|
6135
|
+
if (typeof snapshot?.percent === "number") {
|
|
6136
|
+
const activeColor = contextUsageActiveColor(snapshot.percent);
|
|
6137
|
+
fill.style.setProperty("--context-active-color", activeColor.color);
|
|
6138
|
+
fill.style.setProperty("--context-active-glow", activeColor.glow);
|
|
6139
|
+
}
|
|
6140
|
+
meter.append(fill);
|
|
6141
|
+
}
|
|
6142
|
+
|
|
6143
|
+
async function requestManualCompaction({ triggerButton = null } = {}) {
|
|
6144
|
+
const tabContext = activeTabContext();
|
|
6145
|
+
if (!tabContext.tabId) return;
|
|
6146
|
+
const buttons = [...new Set([elements.compactButton, triggerButton].filter(Boolean))];
|
|
6147
|
+
try {
|
|
6148
|
+
for (const button of buttons) {
|
|
6149
|
+
button.disabled = true;
|
|
6150
|
+
button.textContent = "Compacting…";
|
|
6151
|
+
}
|
|
6152
|
+
setRunIndicatorActivity("Requesting context compaction…");
|
|
6153
|
+
scrollChatToBottom({ force: true });
|
|
6154
|
+
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
6155
|
+
renderFooter();
|
|
6156
|
+
renderContextMeter();
|
|
6157
|
+
renderWorkspaceDashboard();
|
|
6158
|
+
addEvent("manual compaction requested");
|
|
6159
|
+
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
6160
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6161
|
+
scheduleRefreshState(120, tabContext);
|
|
6162
|
+
scheduleRefreshMessages(600, tabContext);
|
|
6163
|
+
scheduleRefreshFooter(600, tabContext);
|
|
6164
|
+
} catch (error) {
|
|
6165
|
+
if (isCurrentTabContext(tabContext)) {
|
|
6166
|
+
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
6167
|
+
clearRunIndicatorActivity();
|
|
6168
|
+
renderFooter();
|
|
6169
|
+
renderContextMeter();
|
|
6170
|
+
renderWorkspaceDashboard();
|
|
6171
|
+
addEvent(error.message, "error");
|
|
6172
|
+
}
|
|
6173
|
+
} finally {
|
|
6174
|
+
if (isCurrentTabContext(tabContext)) {
|
|
6175
|
+
for (const button of buttons) {
|
|
6176
|
+
button.disabled = !!currentState?.isCompacting;
|
|
6177
|
+
button.textContent = button === elements.compactButton && currentState?.isCompacting ? "Compacting…" : button === elements.compactButton ? "Compact" : "Compact now";
|
|
6178
|
+
}
|
|
6179
|
+
renderContextMeter();
|
|
6180
|
+
renderWorkspaceDashboard();
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
function renderContextMeter() {
|
|
6186
|
+
const root = elements.contextMeterBar;
|
|
6187
|
+
if (!root) return;
|
|
6188
|
+
const tab = activeTab();
|
|
6189
|
+
if (!tab) {
|
|
6190
|
+
root.hidden = true;
|
|
6191
|
+
root.replaceChildren();
|
|
6192
|
+
return;
|
|
6193
|
+
}
|
|
6194
|
+
root.hidden = false;
|
|
6195
|
+
const snapshot = contextUsageSnapshot();
|
|
6196
|
+
if (!snapshot || typeof snapshot.percent !== "number" || snapshot.percent <= 50) {
|
|
6197
|
+
root.hidden = true;
|
|
6198
|
+
root.replaceChildren();
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
const meter = make("div", `context-meter${snapshot?.unknown ? " unknown" : ""}`);
|
|
6202
|
+
appendContextMeterFill(meter, snapshot);
|
|
6203
|
+
|
|
6204
|
+
const summary = make("div", "context-meter-summary");
|
|
6205
|
+
summary.append(
|
|
6206
|
+
make("strong", undefined, contextUsageDisplay(snapshot)),
|
|
6207
|
+
make("span", "muted", contextUsageDetail(snapshot)),
|
|
6208
|
+
);
|
|
6209
|
+
|
|
6210
|
+
const actions = make("div", "context-meter-actions");
|
|
6211
|
+
const compact = make("button", "context-meter-compact", currentState?.isCompacting ? "Compacting…" : "Compact now");
|
|
6212
|
+
compact.type = "button";
|
|
6213
|
+
compact.disabled = !!currentState?.isCompacting;
|
|
6214
|
+
compact.title = "Manually compact this tab's conversation context";
|
|
6215
|
+
compact.addEventListener("click", () => requestManualCompaction({ triggerButton: compact }));
|
|
6216
|
+
const auto = make("button", "context-meter-auto", footerAutoCompactionEnabled() ? "Auto on" : "Auto off");
|
|
6217
|
+
auto.type = "button";
|
|
6218
|
+
auto.setAttribute("aria-pressed", footerAutoCompactionEnabled() ? "true" : "false");
|
|
6219
|
+
auto.disabled = footerAutoCompactionToggleInFlight;
|
|
6220
|
+
auto.title = footerAutoCompactionToggleInFlight ? "Updating auto-compaction…" : footerAutoCompactionToggleAction();
|
|
6221
|
+
auto.addEventListener("click", () => toggleFooterAutoCompaction());
|
|
6222
|
+
actions.append(compact, auto);
|
|
6223
|
+
|
|
6224
|
+
root.replaceChildren(summary, meter, actions);
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
function dashboardMetric(label, value, detail = "") {
|
|
6228
|
+
const item = make("div", "workspace-dashboard-metric");
|
|
6229
|
+
item.append(make("span", "workspace-dashboard-metric-label", label), make("strong", undefined, value || "—"));
|
|
6230
|
+
if (detail) item.append(make("span", "workspace-dashboard-metric-detail", detail));
|
|
6231
|
+
return item;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
function dashboardAction(label, handler, className = "") {
|
|
6235
|
+
const button = make("button", `workspace-dashboard-action ${className}`.trim(), label);
|
|
6236
|
+
button.type = "button";
|
|
6237
|
+
button.addEventListener("click", handler);
|
|
6238
|
+
return button;
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
function renderWorkspaceDashboard() {
|
|
6242
|
+
const root = elements.workspaceDashboard;
|
|
6243
|
+
if (!root) return;
|
|
6244
|
+
const tab = activeTab();
|
|
6245
|
+
const snapshot = contextUsageSnapshot();
|
|
6246
|
+
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "Choose or create a tab to start");
|
|
6247
|
+
const queueCount = Number(currentState?.pendingMessageCount || 0) || 0;
|
|
6248
|
+
root.replaceChildren();
|
|
6249
|
+
|
|
6250
|
+
const header = make("div", "workspace-dashboard-header");
|
|
6251
|
+
const title = make("div", "workspace-dashboard-title");
|
|
6252
|
+
title.append(make("span", "workspace-dashboard-kicker", "Workspace"), make("h2", undefined, tab?.title || "Pi Web UI"), make("p", "muted", workspaceLabel));
|
|
6253
|
+
const actions = make("div", "workspace-dashboard-actions");
|
|
6254
|
+
actions.append(
|
|
6255
|
+
dashboardAction("Command palette", () => openCommandPalette(), "primary"),
|
|
6256
|
+
dashboardAction("New tab", () => createTerminalTab()),
|
|
6257
|
+
dashboardAction("Resume", () => runNativeCommandMenu("/resume")),
|
|
6258
|
+
dashboardAction("Model", () => runNativeCommandMenu("/model")),
|
|
6259
|
+
dashboardAction("Settings", () => runNativeCommandMenu("/settings")),
|
|
6260
|
+
);
|
|
6261
|
+
header.append(title, actions);
|
|
6262
|
+
|
|
6263
|
+
const metrics = make("div", "workspace-dashboard-metrics");
|
|
6264
|
+
metrics.append(
|
|
6265
|
+
dashboardMetric("Model", currentState?.model ? shortModelLabel(currentState.model) : "loading…", currentState?.thinkingLevel ? `thinking ${currentState.thinkingLevel}` : ""),
|
|
6266
|
+
dashboardMetric("Context", contextUsageDisplay(snapshot), contextUsageDetail(snapshot)),
|
|
6267
|
+
dashboardMetric("Session", currentState?.sessionName || currentState?.sessionId || "loading…", currentState?.sessionFile || "in-memory"),
|
|
6268
|
+
dashboardMetric("Queue", `${queueCount}`, queueCount === 1 ? "pending message" : "pending messages"),
|
|
6269
|
+
);
|
|
6270
|
+
|
|
6271
|
+
const tabsPanel = make("div", "workspace-dashboard-tabs");
|
|
6272
|
+
tabsPanel.append(make("span", "workspace-dashboard-tabs-title", `Open tabs (${tabs.length})`));
|
|
6273
|
+
const tabList = make("div", "workspace-dashboard-tab-list");
|
|
6274
|
+
for (const item of tabs.slice(0, 8)) {
|
|
6275
|
+
const indicator = tabIndicator(item);
|
|
6276
|
+
const button = make("button", `workspace-dashboard-tab activity-${indicator.state}${item.id === activeTabId ? " active" : ""}`);
|
|
6277
|
+
button.type = "button";
|
|
6278
|
+
button.title = `${item.title} · ${indicator.label}`;
|
|
6279
|
+
button.append(make("span", "workspace-dashboard-tab-dot", indicator.glyph), make("span", undefined, item.title));
|
|
6280
|
+
button.addEventListener("click", () => switchTab(item.id));
|
|
6281
|
+
tabList.append(button);
|
|
6282
|
+
}
|
|
6283
|
+
if (tabs.length > 8) tabList.append(make("span", "workspace-dashboard-tab-more", `+${tabs.length - 8} more`));
|
|
6284
|
+
tabsPanel.append(tabList);
|
|
6285
|
+
|
|
6286
|
+
root.append(header, metrics, tabsPanel);
|
|
6287
|
+
}
|
|
6288
|
+
|
|
5882
6289
|
function setFooterModelPickerOpen(open) {
|
|
5883
6290
|
footerModelPickerOpen = !!open;
|
|
5884
6291
|
if (footerModelPickerOpen) {
|
|
@@ -6892,6 +7299,8 @@ function renderStatus() {
|
|
|
6892
7299
|
elements.compactButton.textContent = state?.isCompacting ? "Compacting…" : "Compact";
|
|
6893
7300
|
syncModelSelectToState();
|
|
6894
7301
|
renderFooter();
|
|
7302
|
+
renderContextMeter();
|
|
7303
|
+
renderWorkspaceDashboard();
|
|
6895
7304
|
renderFeedbackTray();
|
|
6896
7305
|
}
|
|
6897
7306
|
|
|
@@ -8625,6 +9034,7 @@ function renderGitInitStackInput() {
|
|
|
8625
9034
|
function renderGitWorkflowManualCommitInput() {
|
|
8626
9035
|
const tabId = gitWorkflowActionTabId();
|
|
8627
9036
|
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
9037
|
+
const defaultCommitMessage = String(workflow?.manualCommitMessageDefault || "").trim();
|
|
8628
9038
|
const row = make("div", "git-workflow-message-input-row");
|
|
8629
9039
|
const field = make("label", "git-workflow-message-input-field");
|
|
8630
9040
|
field.setAttribute("for", "gitWorkflowManualCommitMessage");
|
|
@@ -8634,7 +9044,7 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
8634
9044
|
input.id = "gitWorkflowManualCommitMessage";
|
|
8635
9045
|
input.type = "text";
|
|
8636
9046
|
input.value = workflow?.manualCommitMessage || "";
|
|
8637
|
-
input.placeholder = "Type a commit message to use instead of short/long";
|
|
9047
|
+
input.placeholder = defaultCommitMessage || "Type a commit message to use instead of short/long";
|
|
8638
9048
|
input.autocomplete = "off";
|
|
8639
9049
|
input.spellcheck = true;
|
|
8640
9050
|
|
|
@@ -8642,7 +9052,10 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
8642
9052
|
commitButton.type = "button";
|
|
8643
9053
|
const updateCommitState = () => {
|
|
8644
9054
|
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
8645
|
-
|
|
9055
|
+
const message = String(input.value || "").trim() || String(currentWorkflow?.manualCommitMessageDefault || "").trim();
|
|
9056
|
+
commitButton.disabled = !currentWorkflow || !!currentWorkflow.busy || !message;
|
|
9057
|
+
if (message && !String(input.value || "").trim()) commitButton.title = `Use default commit message: ${message}`;
|
|
9058
|
+
else commitButton.removeAttribute("title");
|
|
8646
9059
|
};
|
|
8647
9060
|
input.addEventListener("input", () => {
|
|
8648
9061
|
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
@@ -8660,6 +9073,7 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
8660
9073
|
commitGitWorkflow("input", tabId);
|
|
8661
9074
|
});
|
|
8662
9075
|
updateCommitState();
|
|
9076
|
+
loadGitWorkflowDefaultCommitMessage({ runId: workflow?.runId, tabId });
|
|
8663
9077
|
|
|
8664
9078
|
field.append(input);
|
|
8665
9079
|
row.append(field, commitButton);
|
|
@@ -8765,6 +9179,7 @@ function selectGitInitWorkflowProcess(processValue, tabId, workflow) {
|
|
|
8765
9179
|
initFilesStatus: null,
|
|
8766
9180
|
message: null,
|
|
8767
9181
|
manualCommitMessage: "",
|
|
9182
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
8768
9183
|
messageRequestedAt: 0,
|
|
8769
9184
|
branchName: "",
|
|
8770
9185
|
branchNameRequestedAt: 0,
|
|
@@ -8811,7 +9226,7 @@ function selectGitWorkflowProcess(processValue, tabId = gitWorkflowActionTabId()
|
|
|
8811
9226
|
const process = GIT_WORKFLOW_PROCESS_VALUES.has(processValue) ? processValue : "stage";
|
|
8812
9227
|
workflow.runId += 1;
|
|
8813
9228
|
const runId = workflow.runId;
|
|
8814
|
-
const base = { mode: "standard", active: true, process, busy: false, error: "", githubUsername: "", repoName: "", remoteUrl: "", stack: "", readmeRequestedAt: 0, gitignoreRequestedAt: 0, initFilesStatus: null, manualCommitMessage: "", messageRequestedAt: 0, branchName: "", branchNameRequestedAt: 0, prMode: false, prBranch: "", pr: null, prRequestedAt: 0 };
|
|
9229
|
+
const base = { mode: "standard", active: true, process, busy: false, error: "", githubUsername: "", repoName: "", remoteUrl: "", stack: "", readmeRequestedAt: 0, gitignoreRequestedAt: 0, initFilesStatus: null, manualCommitMessage: "", ...resetGitWorkflowManualCommitDefaultPatch(), messageRequestedAt: 0, branchName: "", branchNameRequestedAt: 0, prMode: false, prBranch: "", pr: null, prRequestedAt: 0 };
|
|
8815
9230
|
|
|
8816
9231
|
if (process === "stage") {
|
|
8817
9232
|
setGitWorkflow({ ...base, step: "add", message: null, output: "Ready to stage all changes with git add ." }, { tabId });
|
|
@@ -9101,6 +9516,7 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
9101
9516
|
initFilesStatus: null,
|
|
9102
9517
|
message: null,
|
|
9103
9518
|
manualCommitMessage: "",
|
|
9519
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
9104
9520
|
messageRequestedAt: 0,
|
|
9105
9521
|
branchName: "",
|
|
9106
9522
|
branchNameRequestedAt: 0,
|
|
@@ -9140,6 +9556,7 @@ function startGitInitWorkflow(tabId = activeTabId) {
|
|
|
9140
9556
|
initFilesStatus: null,
|
|
9141
9557
|
message: null,
|
|
9142
9558
|
manualCommitMessage: "",
|
|
9559
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
9143
9560
|
messageRequestedAt: 0,
|
|
9144
9561
|
branchName: "",
|
|
9145
9562
|
branchNameRequestedAt: 0,
|
|
@@ -9430,17 +9847,47 @@ async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
|
9430
9847
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9431
9848
|
if (!workflow) return;
|
|
9432
9849
|
const runId = workflow.runId;
|
|
9433
|
-
setGitWorkflow({ step: "add", busy: true, error: "", output: "Running git add ." }, { tabId });
|
|
9850
|
+
setGitWorkflow({ step: "add", busy: true, error: "", ...resetGitWorkflowManualCommitDefaultPatch(), output: "Running git add ." }, { tabId });
|
|
9434
9851
|
try {
|
|
9435
9852
|
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
9436
9853
|
if (!result) return;
|
|
9437
|
-
setGitWorkflow({ step: "generate", busy: false, ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
9854
|
+
setGitWorkflow({ step: "generate", busy: false, ...resetGitWorkflowManualCommitDefaultPatch(), ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
9438
9855
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9439
9856
|
} catch (error) {
|
|
9440
9857
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
9441
9858
|
}
|
|
9442
9859
|
}
|
|
9443
9860
|
|
|
9861
|
+
async function loadGitWorkflowDefaultCommitMessage({ runId, tabId = activeTabId } = {}) {
|
|
9862
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9863
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
9864
|
+
if (!workflow || workflow.manualCommitMessageDefaultLoading || workflow.manualCommitMessageDefaultRequestedAt) return;
|
|
9865
|
+
workflow.manualCommitMessageDefaultLoading = true;
|
|
9866
|
+
workflow.manualCommitMessageDefaultRequestedAt = Date.now();
|
|
9867
|
+
try {
|
|
9868
|
+
const data = await gitWorkflowRequest("/api/git-workflow/default-commit-message", { method: "GET", runId: expectedRunId, tabId });
|
|
9869
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9870
|
+
if (!data || !currentWorkflow || !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
9871
|
+
setGitWorkflow({
|
|
9872
|
+
manualCommitMessageDefault: String(data.message || "").trim(),
|
|
9873
|
+
manualCommitMessageDefaultReason: String(data.reason || ""),
|
|
9874
|
+
manualCommitMessageDefaultPath: String(data.path || ""),
|
|
9875
|
+
manualCommitMessageDefaultAction: String(data.action || ""),
|
|
9876
|
+
manualCommitMessageDefaultLoading: false,
|
|
9877
|
+
}, { tabId });
|
|
9878
|
+
} catch (error) {
|
|
9879
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9880
|
+
if (!currentWorkflow || !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
9881
|
+
setGitWorkflow({
|
|
9882
|
+
manualCommitMessageDefault: "",
|
|
9883
|
+
manualCommitMessageDefaultReason: error?.message || String(error),
|
|
9884
|
+
manualCommitMessageDefaultPath: "",
|
|
9885
|
+
manualCommitMessageDefaultAction: "",
|
|
9886
|
+
manualCommitMessageDefaultLoading: false,
|
|
9887
|
+
}, { tabId });
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
|
|
9444
9891
|
async function runGitMessagePrompt(tabId = gitWorkflowActionTabId()) {
|
|
9445
9892
|
const tabContext = activeTabContext(tabId);
|
|
9446
9893
|
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
@@ -9642,9 +10089,9 @@ async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
|
9642
10089
|
if (!workflow) return;
|
|
9643
10090
|
const runId = workflow.runId;
|
|
9644
10091
|
const failureStep = variant === "input" && workflow.step === "generate" ? "generate" : "message";
|
|
9645
|
-
const inputMessage = variant === "input" ?
|
|
10092
|
+
const inputMessage = variant === "input" ? gitWorkflowManualCommitInputMessage(workflow) : "";
|
|
9646
10093
|
if (variant === "input" && !inputMessage) {
|
|
9647
|
-
failGitWorkflow(new Error("Type a commit message
|
|
10094
|
+
failGitWorkflow(new Error("Type a commit message, or stage exactly one created/updated/deleted file to use the default."), failureStep, { tabId });
|
|
9648
10095
|
return;
|
|
9649
10096
|
}
|
|
9650
10097
|
const preview = variant === "input" ? formatInputCommitMessagePreview(inputMessage) : formatCommitMessagePreview(workflow.message);
|
|
@@ -12010,6 +12457,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
12010
12457
|
bubble.append(header, body);
|
|
12011
12458
|
}
|
|
12012
12459
|
attachMessageCopyButton(bubble, message, body);
|
|
12460
|
+
attachMessageEditRetryButton(bubble, message, messageIndex, { streaming, transient });
|
|
12013
12461
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
12014
12462
|
appendChatMessageBubble(bubble);
|
|
12015
12463
|
return { bubble, body };
|
|
@@ -12815,6 +13263,30 @@ function resetOptionalFeatureAvailability() {
|
|
|
12815
13263
|
renderOptionalFeatureControls();
|
|
12816
13264
|
}
|
|
12817
13265
|
|
|
13266
|
+
function optionalFeaturePackageStatus(featureId) {
|
|
13267
|
+
return optionalFeaturePackageStatuses.get(featureId) || null;
|
|
13268
|
+
}
|
|
13269
|
+
|
|
13270
|
+
function optionalFeaturePackageVersionLabel(status) {
|
|
13271
|
+
if (!status?.installedVersion) return "";
|
|
13272
|
+
return status.declaredSpec ? `${status.installedVersion} (expects ${status.declaredSpec})` : status.installedVersion;
|
|
13273
|
+
}
|
|
13274
|
+
|
|
13275
|
+
async function refreshOptionalFeaturePackageStatuses({ announce = false } = {}) {
|
|
13276
|
+
try {
|
|
13277
|
+
const response = await api("/api/optional-features", { scoped: false });
|
|
13278
|
+
optionalFeaturePackageStatuses.clear();
|
|
13279
|
+
for (const status of response.data?.features || []) {
|
|
13280
|
+
if (status?.featureId) optionalFeaturePackageStatuses.set(status.featureId, status);
|
|
13281
|
+
}
|
|
13282
|
+
renderOptionalFeatureControls();
|
|
13283
|
+
return true;
|
|
13284
|
+
} catch (error) {
|
|
13285
|
+
if (announce) addEvent(`optional feature package status check failed: ${error.message || String(error)}`, "warn");
|
|
13286
|
+
return false;
|
|
13287
|
+
}
|
|
13288
|
+
}
|
|
13289
|
+
|
|
12818
13290
|
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
12819
13291
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
12820
13292
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
@@ -12854,9 +13326,16 @@ function updateOptionalFeatureAvailability() {
|
|
|
12854
13326
|
function optionalFeatureStatus(featureId) {
|
|
12855
13327
|
const detected = isOptionalFeatureDetected(featureId);
|
|
12856
13328
|
const disabled = isOptionalFeatureDisabled(featureId);
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
13329
|
+
const packageStatus = optionalFeaturePackageStatus(featureId);
|
|
13330
|
+
const installMessage = optionalFeatureInstallMessages.get(featureId);
|
|
13331
|
+
const versionLabel = optionalFeaturePackageVersionLabel(packageStatus);
|
|
13332
|
+
const versionSuffix = versionLabel ? ` · package ${versionLabel}` : "";
|
|
13333
|
+
if (optionalFeatureInstallInProgress.has(featureId)) return { label: "Installing", className: "updating", detail: installMessage || "npm install is running; waiting for the package manager to finish" };
|
|
13334
|
+
if (packageStatus?.updateAvailable) return { label: "Update available", className: "updating", detail: packageStatus.updateReason || `Installed package is older than the Web UI expects${versionSuffix}` };
|
|
13335
|
+
if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: `Detected and enabled in Web UI${versionSuffix}` };
|
|
13336
|
+
if (detected && disabled) return { label: "Disabled", className: "disabled", detail: `Detected, but disabled in Web UI${versionSuffix}` };
|
|
13337
|
+
if (packageStatus?.installed) return { label: "Installed", className: "installed", detail: `Package is installed but not loaded in the active Pi tab${versionSuffix}` };
|
|
13338
|
+
return { label: "Install needed", className: "missing", detail: installMessage || "Package is not installed or not visible from the Web UI package root" };
|
|
12860
13339
|
}
|
|
12861
13340
|
|
|
12862
13341
|
function optionalFeatureWidgetFeatureId(key) {
|
|
@@ -12875,6 +13354,7 @@ function renderOptionalFeaturePanel() {
|
|
|
12875
13354
|
const detected = isOptionalFeatureDetected(feature.id);
|
|
12876
13355
|
const enabled = isOptionalFeatureEnabled(feature.id);
|
|
12877
13356
|
const installing = optionalFeatureInstallInProgress.has(feature.id);
|
|
13357
|
+
const packageStatus = optionalFeaturePackageStatus(feature.id);
|
|
12878
13358
|
const status = optionalFeatureStatus(feature.id);
|
|
12879
13359
|
const row = make("div", `optional-feature-row ${status.className}`);
|
|
12880
13360
|
|
|
@@ -12902,9 +13382,16 @@ function renderOptionalFeaturePanel() {
|
|
|
12902
13382
|
action.disabled = installing;
|
|
12903
13383
|
if (installing) {
|
|
12904
13384
|
action.textContent = "Installing…";
|
|
13385
|
+
} else if (packageStatus?.updateAvailable) {
|
|
13386
|
+
action.textContent = "Update…";
|
|
13387
|
+
action.classList.add("update");
|
|
13388
|
+
action.addEventListener("click", () => installOptionalFeature(feature.id, { update: true }));
|
|
12905
13389
|
} else if (detected) {
|
|
12906
13390
|
action.textContent = enabled ? "Disable" : "Enable";
|
|
12907
13391
|
action.addEventListener("click", () => setOptionalFeatureDisabled(feature.id, enabled));
|
|
13392
|
+
} else if (packageStatus?.installed) {
|
|
13393
|
+
action.textContent = "Reload";
|
|
13394
|
+
action.addEventListener("click", () => sendPrompt("prompt", "/reload"));
|
|
12908
13395
|
} else {
|
|
12909
13396
|
action.textContent = "Install…";
|
|
12910
13397
|
action.classList.add("install");
|
|
@@ -12971,15 +13458,17 @@ function commandUnavailableMessage(commandName) {
|
|
|
12971
13458
|
return `Command unavailable: /${commandName} is not loaded in the active Pi tab.`;
|
|
12972
13459
|
}
|
|
12973
13460
|
|
|
12974
|
-
async function installOptionalFeature(featureId) {
|
|
13461
|
+
async function installOptionalFeature(featureId, { update = false } = {}) {
|
|
12975
13462
|
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
12976
13463
|
if (!feature || optionalFeatureInstallInProgress.has(featureId)) return;
|
|
12977
13464
|
|
|
13465
|
+
const actionLabel = update ? "Update" : "Install";
|
|
12978
13466
|
const warning = [
|
|
12979
|
-
|
|
13467
|
+
`${actionLabel} optional feature: ${feature.label}?`,
|
|
12980
13468
|
"",
|
|
12981
13469
|
`This will run npm install for ${feature.packageName} in the Web UI package install root.`,
|
|
12982
13470
|
"It can download code from npm and modify the local Pi/Web UI npm installation.",
|
|
13471
|
+
"Progress and failures will be shown in the optional-features row and activity log.",
|
|
12983
13472
|
"If this feature is already installed but disabled in Pi settings, cancel and enable it there instead.",
|
|
12984
13473
|
"",
|
|
12985
13474
|
"Continue?",
|
|
@@ -12987,14 +13476,20 @@ async function installOptionalFeature(featureId) {
|
|
|
12987
13476
|
if (!confirm(warning)) return;
|
|
12988
13477
|
|
|
12989
13478
|
optionalFeatureInstallInProgress.add(featureId);
|
|
13479
|
+
optionalFeatureInstallMessages.set(featureId, `${actionLabel} running via npm; waiting for package-manager output…`);
|
|
12990
13480
|
renderOptionalFeatureControls();
|
|
12991
|
-
addEvent(
|
|
13481
|
+
addEvent(`${update ? "updating" : "installing"} optional feature ${feature.label} (${feature.packageName})…`, "warn");
|
|
12992
13482
|
try {
|
|
12993
13483
|
const response = await api("/api/optional-feature-install", { method: "POST", body: { featureId }, scoped: false });
|
|
12994
13484
|
disabledOptionalFeatures.delete(featureId);
|
|
12995
13485
|
storeDisabledOptionalFeatures();
|
|
12996
|
-
|
|
12997
|
-
|
|
13486
|
+
const command = response.data?.command ? ` · ${response.data.command}` : "";
|
|
13487
|
+
optionalFeatureInstallMessages.set(featureId, `${response.data?.message || `${actionLabel} finished`}${command}`);
|
|
13488
|
+
addEvent(response.data?.message || `${update ? "updated" : "installed"} ${feature.packageName}`, "info");
|
|
13489
|
+
const output = [response.data?.stderr, response.data?.stdout].filter(Boolean).join("\n").trim();
|
|
13490
|
+
if (output) addEvent(`npm output for ${feature.packageName}:\n${output.slice(-4000)}`, "info");
|
|
13491
|
+
await refreshOptionalFeaturePackageStatuses({ announce: true });
|
|
13492
|
+
if (confirm(`${feature.label} ${actionLabel.toLowerCase()} finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
12998
13493
|
sendPrompt("prompt", "/reload");
|
|
12999
13494
|
} else {
|
|
13000
13495
|
const tabContext = activeTabContext();
|
|
@@ -13002,6 +13497,7 @@ async function installOptionalFeature(featureId) {
|
|
|
13002
13497
|
if (isCurrentTabContext(tabContext)) renderOptionalFeatureControls();
|
|
13003
13498
|
}
|
|
13004
13499
|
} catch (error) {
|
|
13500
|
+
optionalFeatureInstallMessages.set(featureId, `${actionLabel} failed: ${error.message || String(error)}`);
|
|
13005
13501
|
addEvent(error.message || String(error), "error");
|
|
13006
13502
|
} finally {
|
|
13007
13503
|
optionalFeatureInstallInProgress.delete(featureId);
|
|
@@ -14268,6 +14764,8 @@ async function refreshStats(tabContext = activeTabContext()) {
|
|
|
14268
14764
|
if (!isCurrentTabContext(tabContext)) return;
|
|
14269
14765
|
latestStats = response.data || null;
|
|
14270
14766
|
renderFooter();
|
|
14767
|
+
renderContextMeter();
|
|
14768
|
+
renderWorkspaceDashboard();
|
|
14271
14769
|
}
|
|
14272
14770
|
|
|
14273
14771
|
async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
@@ -14293,6 +14791,7 @@ async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
|
14293
14791
|
latestWorkspace = nextWorkspace;
|
|
14294
14792
|
rememberServerStartCwd(nextWorkspace?.cwd);
|
|
14295
14793
|
renderFooter();
|
|
14794
|
+
renderWorkspaceDashboard();
|
|
14296
14795
|
}
|
|
14297
14796
|
|
|
14298
14797
|
function renderNetworkStatus() {
|
|
@@ -14345,7 +14844,22 @@ function renderNetworkStatus() {
|
|
|
14345
14844
|
if (networkUrls.length === 0) list.append(make("div", "network-status-empty", "No LAN address detected."));
|
|
14346
14845
|
}
|
|
14347
14846
|
|
|
14348
|
-
|
|
14847
|
+
const auth = network?.auth || {};
|
|
14848
|
+
const authText = auth.enabled
|
|
14849
|
+
? auth.pin
|
|
14850
|
+
? `Remote PIN auth on · PIN ${auth.pin}`
|
|
14851
|
+
: "Remote PIN auth on"
|
|
14852
|
+
: "Remote PIN auth off";
|
|
14853
|
+
const authDetail = make("div", "network-status-detail", authText);
|
|
14854
|
+
|
|
14855
|
+
elements.networkStatus.replaceChildren(heading, detail, list, authDetail);
|
|
14856
|
+
elements.remoteAuthToggle.checked = !!auth.enabled;
|
|
14857
|
+
elements.remoteAuthToggle.disabled = rebinding;
|
|
14858
|
+
elements.remoteAuthStatus.textContent = auth.enabled
|
|
14859
|
+
? auth.pin
|
|
14860
|
+
? `PIN ${auth.pin}`
|
|
14861
|
+
: "On"
|
|
14862
|
+
: "Off";
|
|
14349
14863
|
elements.openNetworkButton.disabled = rebinding;
|
|
14350
14864
|
elements.openNetworkButton.textContent = opening ? "Opening…" : closing ? "Closing…" : open ? "Close for network" : "Open to network";
|
|
14351
14865
|
}
|
|
@@ -14361,6 +14875,28 @@ async function refreshNetworkStatus() {
|
|
|
14361
14875
|
renderNetworkStatus();
|
|
14362
14876
|
}
|
|
14363
14877
|
|
|
14878
|
+
async function toggleRemoteAuth() {
|
|
14879
|
+
const enable = !latestNetwork?.auth?.enabled;
|
|
14880
|
+
const message = enable
|
|
14881
|
+
? "Enable remote PIN authentication?\n\nA random 4-digit PIN will be required for non-local browser clients. The PIN is shown in Controls."
|
|
14882
|
+
: "Disable remote PIN authentication?\n\nNon-local browser clients will no longer need a PIN while the network listener is open.";
|
|
14883
|
+
if (!confirm(message)) {
|
|
14884
|
+
renderNetworkStatus();
|
|
14885
|
+
return;
|
|
14886
|
+
}
|
|
14887
|
+
|
|
14888
|
+
elements.remoteAuthToggle.disabled = true;
|
|
14889
|
+
try {
|
|
14890
|
+
const response = await api("/api/remote-auth/settings", { method: "POST", body: { enabled: enable }, scoped: false });
|
|
14891
|
+
latestNetwork = response.data?.network || { ...(latestNetwork || {}), auth: response.data?.auth };
|
|
14892
|
+
addEvent(enable ? "remote PIN auth enabled" : "remote PIN auth disabled", enable ? "warn" : "info");
|
|
14893
|
+
} catch (error) {
|
|
14894
|
+
addEvent(error.message || String(error), "error");
|
|
14895
|
+
} finally {
|
|
14896
|
+
renderNetworkStatus();
|
|
14897
|
+
}
|
|
14898
|
+
}
|
|
14899
|
+
|
|
14364
14900
|
async function refreshFooterData(tabContext = activeTabContext()) {
|
|
14365
14901
|
if (!tabContext.tabId) return;
|
|
14366
14902
|
await Promise.allSettled([refreshStats(tabContext), refreshWorkspace(tabContext)]);
|
|
@@ -14453,6 +14989,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
14453
14989
|
syncModelSelectToState();
|
|
14454
14990
|
renderFooter();
|
|
14455
14991
|
renderFeedbackTray();
|
|
14992
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
14456
14993
|
}
|
|
14457
14994
|
|
|
14458
14995
|
function syncModelSelectToState() {
|
|
@@ -14929,6 +15466,162 @@ async function refreshCommands(tabContext = activeTabContext()) {
|
|
|
14929
15466
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
14930
15467
|
updateOptionalFeatureAvailability();
|
|
14931
15468
|
renderCommands();
|
|
15469
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
15470
|
+
}
|
|
15471
|
+
|
|
15472
|
+
function paletteText(value) {
|
|
15473
|
+
return String(value || "").toLowerCase();
|
|
15474
|
+
}
|
|
15475
|
+
|
|
15476
|
+
function paletteItemMatches(item, query) {
|
|
15477
|
+
const text = [item.label, item.description, item.kind, item.keywords].map(paletteText).join(" ");
|
|
15478
|
+
return query.split(/\s+/).filter(Boolean).every((token) => text.includes(token));
|
|
15479
|
+
}
|
|
15480
|
+
|
|
15481
|
+
function commandPaletteCoreItems() {
|
|
15482
|
+
const items = [
|
|
15483
|
+
{ kind: "Action", label: "New tab", description: "Start an isolated Pi terminal in the current directory", keywords: "workspace session", run: () => createTerminalTab() },
|
|
15484
|
+
{ kind: "Action", label: "Choose directory for new tab", description: "Pick a cwd before starting a tab", keywords: "cwd folder workspace", run: () => createTerminalTabFromChosenDirectory({ triggerButton: elements.commandPaletteButton }) },
|
|
15485
|
+
{ kind: "Action", label: "New session", description: "Start a fresh session in the active tab", keywords: "/new clear", run: () => elements.newSessionButton.click() },
|
|
15486
|
+
{ kind: "Action", label: "Compact context", description: contextUsageDetail(), keywords: "/compact context window tokens", run: () => requestManualCompaction() },
|
|
15487
|
+
{ kind: "Action", label: footerAutoCompactionEnabled() ? "Disable auto-compaction" : "Enable auto-compaction", description: footerAutoCompactionToggleAction(), keywords: "context automatic", run: () => toggleFooterAutoCompaction() },
|
|
15488
|
+
{ kind: "Action", label: workspaceDashboardCollapsed ? "Show workspace dashboard" : "Hide workspace dashboard", description: "Toggle the launch/workspace overview", keywords: "home overview", run: () => setWorkspaceDashboardCollapsed(!workspaceDashboardCollapsed) },
|
|
15489
|
+
{ kind: "Action", label: document.body.classList.contains("side-panel-collapsed") ? "Show side panel" : "Hide side panel", description: "Toggle the Control Deck", keywords: "controls settings", run: () => setSidePanelCollapsed(!document.body.classList.contains("side-panel-collapsed"), { focusPanel: true }) },
|
|
15490
|
+
{ kind: "Action", label: "Change working directory", description: "Restart active tab in another cwd", keywords: "cwd folder workspace", run: () => changeActiveTabCwd() },
|
|
15491
|
+
{ kind: "Action", label: "Search transcript", description: "Open transcript search", keywords: "find", run: () => openChatSearch() },
|
|
15492
|
+
{ kind: "Pi", label: "/model", description: "Select the active model", keywords: "provider llm", run: () => runNativeCommandMenu("/model") },
|
|
15493
|
+
{ kind: "Pi", label: "/resume", description: "Resume a previous session", keywords: "sessions history", run: () => runNativeCommandMenu("/resume") },
|
|
15494
|
+
{ kind: "Pi", label: "/fork", description: "Fork from a previous user message", keywords: "branch edit retry", run: () => runNativeCommandMenu("/fork") },
|
|
15495
|
+
{ kind: "Pi", label: "/tree", description: "Navigate the session tree", keywords: "branch history", run: () => runNativeCommandMenu("/tree") },
|
|
15496
|
+
{ kind: "Pi", label: "/settings", description: "Open settings", keywords: "configuration", run: () => runNativeCommandMenu("/settings") },
|
|
15497
|
+
{ kind: "Pi", label: "/scoped-models", description: "Manage model cycling scope", keywords: "models cycle ctrl p", run: () => runNativeCommandMenu("/scoped-models") },
|
|
15498
|
+
{ kind: "Pi", label: "/tools", description: "Manage active tools", keywords: "capabilities", run: () => runNativeCommandMenu("/tools") },
|
|
15499
|
+
{ kind: "Pi", label: "/skills", description: "Manage active skills", keywords: "system prompt", run: () => runNativeCommandMenu("/skills") },
|
|
15500
|
+
];
|
|
15501
|
+
if (isOptionalFeatureEnabled("statsCommand")) items.push({ kind: "Pi", label: "/stats-webui", description: "Open usage dashboard", keywords: "tokens cost budget", run: () => openStatsOverlay({ refresh: true }) });
|
|
15502
|
+
return items;
|
|
15503
|
+
}
|
|
15504
|
+
|
|
15505
|
+
function commandPaletteTabItems() {
|
|
15506
|
+
return tabs.map((tab) => {
|
|
15507
|
+
const indicator = tabIndicator(tab);
|
|
15508
|
+
return {
|
|
15509
|
+
kind: "Tab",
|
|
15510
|
+
label: tab.id === activeTabId ? `Current tab: ${tab.title}` : `Switch to tab: ${tab.title}`,
|
|
15511
|
+
description: `${indicator.label} · ${normalizeDisplayPath(tab.cwd || "")}`,
|
|
15512
|
+
keywords: `${tab.id} ${tab.cwd || ""}`,
|
|
15513
|
+
run: () => switchTab(tab.id),
|
|
15514
|
+
};
|
|
15515
|
+
});
|
|
15516
|
+
}
|
|
15517
|
+
|
|
15518
|
+
function commandPaletteModelItems() {
|
|
15519
|
+
return availableModels.map((model) => ({
|
|
15520
|
+
kind: "Model",
|
|
15521
|
+
label: `${model.provider}/${model.id}`,
|
|
15522
|
+
description: model.name || (model.contextWindow ? `context ${formatFooterTokenCount(model.contextWindow)}` : "Set active model"),
|
|
15523
|
+
keywords: `${model.provider} ${model.id} ${model.name || ""}`,
|
|
15524
|
+
run: async () => {
|
|
15525
|
+
const tabContext = activeTabContext();
|
|
15526
|
+
const response = await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
|
|
15527
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
15528
|
+
applyOptimisticModelSelection(response.data || model, tabContext);
|
|
15529
|
+
await refreshState(tabContext);
|
|
15530
|
+
await refreshModels(tabContext);
|
|
15531
|
+
},
|
|
15532
|
+
}));
|
|
15533
|
+
}
|
|
15534
|
+
|
|
15535
|
+
function commandPaletteSlashItems() {
|
|
15536
|
+
return visibleCommands().slice(0, 140).map((command) => ({
|
|
15537
|
+
kind: command.source || "Command",
|
|
15538
|
+
label: `/${command.name}`,
|
|
15539
|
+
description: command.description || "Run slash command",
|
|
15540
|
+
keywords: `${command.location || ""} ${command.path || ""}`,
|
|
15541
|
+
run: () => sendPrompt("prompt", `/${command.name}`),
|
|
15542
|
+
}));
|
|
15543
|
+
}
|
|
15544
|
+
|
|
15545
|
+
function buildCommandPaletteItems() {
|
|
15546
|
+
return [
|
|
15547
|
+
...commandPaletteCoreItems(),
|
|
15548
|
+
...commandPaletteTabItems(),
|
|
15549
|
+
...commandPaletteModelItems(),
|
|
15550
|
+
...commandPaletteSlashItems(),
|
|
15551
|
+
];
|
|
15552
|
+
}
|
|
15553
|
+
|
|
15554
|
+
function filteredCommandPaletteItems() {
|
|
15555
|
+
const query = paletteText(elements.commandPaletteInput?.value || "").trim();
|
|
15556
|
+
const items = buildCommandPaletteItems();
|
|
15557
|
+
return (query ? items.filter((item) => paletteItemMatches(item, query)) : items).slice(0, 80);
|
|
15558
|
+
}
|
|
15559
|
+
|
|
15560
|
+
function setCommandPaletteIndex(index) {
|
|
15561
|
+
const count = commandPaletteItems.length;
|
|
15562
|
+
commandPaletteIndex = count ? (index + count) % count : 0;
|
|
15563
|
+
renderCommandPaletteList();
|
|
15564
|
+
}
|
|
15565
|
+
|
|
15566
|
+
function renderCommandPaletteList() {
|
|
15567
|
+
const list = elements.commandPaletteList;
|
|
15568
|
+
if (!list) return;
|
|
15569
|
+
list.replaceChildren();
|
|
15570
|
+
if (!commandPaletteItems.length) {
|
|
15571
|
+
list.append(make("div", "command-palette-empty muted", "No matching actions."));
|
|
15572
|
+
return;
|
|
15573
|
+
}
|
|
15574
|
+
commandPaletteItems.forEach((item, index) => {
|
|
15575
|
+
const button = make("button", `command-palette-item${index === commandPaletteIndex ? " active" : ""}`);
|
|
15576
|
+
button.type = "button";
|
|
15577
|
+
button.setAttribute("role", "option");
|
|
15578
|
+
button.setAttribute("aria-selected", index === commandPaletteIndex ? "true" : "false");
|
|
15579
|
+
button.addEventListener("click", () => executeCommandPaletteItem(item));
|
|
15580
|
+
button.append(
|
|
15581
|
+
make("span", "command-palette-item-kind", item.kind || "Action"),
|
|
15582
|
+
make("span", "command-palette-item-label", item.label || "Untitled action"),
|
|
15583
|
+
make("span", "command-palette-item-description", item.description || ""),
|
|
15584
|
+
);
|
|
15585
|
+
list.append(button);
|
|
15586
|
+
});
|
|
15587
|
+
const active = list.children[commandPaletteIndex];
|
|
15588
|
+
active?.scrollIntoView({ block: "nearest" });
|
|
15589
|
+
}
|
|
15590
|
+
|
|
15591
|
+
function renderCommandPalette() {
|
|
15592
|
+
commandPaletteItems = filteredCommandPaletteItems();
|
|
15593
|
+
if (commandPaletteIndex >= commandPaletteItems.length) commandPaletteIndex = 0;
|
|
15594
|
+
renderCommandPaletteList();
|
|
15595
|
+
}
|
|
15596
|
+
|
|
15597
|
+
function openCommandPalette(initialQuery = "") {
|
|
15598
|
+
setComposerActionsOpen(false);
|
|
15599
|
+
setPublishMenuOpen(false);
|
|
15600
|
+
setNativeCommandMenuOpen(false);
|
|
15601
|
+
setAppRunnerMenuOpen(false);
|
|
15602
|
+
setOptionsMenuOpen(false);
|
|
15603
|
+
if (elements.commandPaletteInput) elements.commandPaletteInput.value = initialQuery;
|
|
15604
|
+
commandPaletteIndex = 0;
|
|
15605
|
+
renderCommandPalette();
|
|
15606
|
+
if (!elements.commandPaletteDialog.open) elements.commandPaletteDialog.showModal();
|
|
15607
|
+
queueMicrotask(() => {
|
|
15608
|
+
elements.commandPaletteInput?.focus();
|
|
15609
|
+
elements.commandPaletteInput?.select();
|
|
15610
|
+
});
|
|
15611
|
+
}
|
|
15612
|
+
|
|
15613
|
+
function closeCommandPalette() {
|
|
15614
|
+
if (elements.commandPaletteDialog?.open) elements.commandPaletteDialog.close();
|
|
15615
|
+
}
|
|
15616
|
+
|
|
15617
|
+
async function executeCommandPaletteItem(item = commandPaletteItems[commandPaletteIndex]) {
|
|
15618
|
+
if (!item) return;
|
|
15619
|
+
closeCommandPalette();
|
|
15620
|
+
try {
|
|
15621
|
+
await item.run?.();
|
|
15622
|
+
} catch (error) {
|
|
15623
|
+
addEvent(error.message || String(error), "error");
|
|
15624
|
+
}
|
|
14932
15625
|
}
|
|
14933
15626
|
|
|
14934
15627
|
async function refreshAll(tabContext = activeTabContext()) {
|
|
@@ -14986,7 +15679,7 @@ async function openToNetwork() {
|
|
|
14986
15679
|
await closeNetworkAccess();
|
|
14987
15680
|
return;
|
|
14988
15681
|
}
|
|
14989
|
-
if (!confirm(
|
|
15682
|
+
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;
|
|
14990
15683
|
|
|
14991
15684
|
elements.openNetworkButton.disabled = true;
|
|
14992
15685
|
elements.openNetworkButton.textContent = "Opening…";
|
|
@@ -15724,6 +16417,11 @@ function handleEvent(event) {
|
|
|
15724
16417
|
renderNetworkStatus();
|
|
15725
16418
|
break;
|
|
15726
16419
|
}
|
|
16420
|
+
case "webui_remote_auth_changed":
|
|
16421
|
+
latestNetwork = { ...(latestNetwork || {}), auth: event.auth || {} };
|
|
16422
|
+
addEvent(`remote PIN auth ${event.auth?.enabled ? "enabled" : "disabled"}`, event.auth?.enabled ? "warn" : "info");
|
|
16423
|
+
renderNetworkStatus();
|
|
16424
|
+
break;
|
|
15727
16425
|
case "pi_process_exit":
|
|
15728
16426
|
addEvent(`pi rpc exited (${event.code ?? event.signal ?? "unknown"})`, "error");
|
|
15729
16427
|
clearRunIndicatorActivity();
|
|
@@ -16066,6 +16764,8 @@ elements.newTabMenu?.addEventListener("focusout", () => {
|
|
|
16066
16764
|
elements.newTabCurrentDirectoryButton?.addEventListener("click", () => createTerminalTab(currentDirectoryForNewTab(), { triggerButton: elements.newTabCurrentDirectoryButton }));
|
|
16067
16765
|
elements.newTabChooseDirectoryButton?.addEventListener("click", () => createTerminalTabFromChosenDirectory({ triggerButton: elements.newTabChooseDirectoryButton }));
|
|
16068
16766
|
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
16767
|
+
elements.commandPaletteButton?.addEventListener("click", () => openCommandPalette());
|
|
16768
|
+
elements.workspaceDashboardToggleButton?.addEventListener("click", () => setWorkspaceDashboardCollapsed(!workspaceDashboardCollapsed));
|
|
16069
16769
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
16070
16770
|
setComposerActionsOpen(false);
|
|
16071
16771
|
startGitWorkflow();
|
|
@@ -16191,6 +16891,7 @@ elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/r
|
|
|
16191
16891
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
16192
16892
|
elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
|
|
16193
16893
|
elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu("/tools"));
|
|
16894
|
+
elements.optionsCommandPaletteButton.addEventListener("click", () => openCommandPalette());
|
|
16194
16895
|
elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
|
|
16195
16896
|
elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
|
|
16196
16897
|
elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
|
|
@@ -16248,6 +16949,42 @@ elements.nativeCommandDialog.addEventListener("close", () => {
|
|
|
16248
16949
|
elements.nativeCommandSearch.oninput = null;
|
|
16249
16950
|
nativeCommandTabId = null;
|
|
16250
16951
|
});
|
|
16952
|
+
elements.commandPaletteDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
16953
|
+
elements.commandPaletteDialog?.addEventListener("cancel", (event) => {
|
|
16954
|
+
event.preventDefault();
|
|
16955
|
+
closeCommandPalette();
|
|
16956
|
+
});
|
|
16957
|
+
elements.commandPaletteInput?.addEventListener("input", () => {
|
|
16958
|
+
commandPaletteIndex = 0;
|
|
16959
|
+
renderCommandPalette();
|
|
16960
|
+
});
|
|
16961
|
+
elements.commandPaletteInput?.addEventListener("keydown", (event) => {
|
|
16962
|
+
if (event.key === "ArrowDown") {
|
|
16963
|
+
event.preventDefault();
|
|
16964
|
+
setCommandPaletteIndex(commandPaletteIndex + 1);
|
|
16965
|
+
} else if (event.key === "ArrowUp") {
|
|
16966
|
+
event.preventDefault();
|
|
16967
|
+
setCommandPaletteIndex(commandPaletteIndex - 1);
|
|
16968
|
+
} else if (event.key === "Enter") {
|
|
16969
|
+
event.preventDefault();
|
|
16970
|
+
executeCommandPaletteItem();
|
|
16971
|
+
} else if (event.key === "Escape") {
|
|
16972
|
+
event.preventDefault();
|
|
16973
|
+
closeCommandPalette();
|
|
16974
|
+
}
|
|
16975
|
+
});
|
|
16976
|
+
elements.editRetryDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
16977
|
+
elements.editRetryDialog?.addEventListener("cancel", (event) => {
|
|
16978
|
+
event.preventDefault();
|
|
16979
|
+
closeEditRetryDialog();
|
|
16980
|
+
});
|
|
16981
|
+
elements.editRetryDialog?.addEventListener("close", () => {
|
|
16982
|
+
activeEditRetry = null;
|
|
16983
|
+
setEditRetryBusy(false);
|
|
16984
|
+
});
|
|
16985
|
+
elements.editRetryCancelButton?.addEventListener("click", closeEditRetryDialog);
|
|
16986
|
+
elements.editRetryForkButton?.addEventListener("click", () => submitEditRetry({ send: false }));
|
|
16987
|
+
elements.editRetrySendButton?.addEventListener("click", () => submitEditRetry({ send: true }));
|
|
16251
16988
|
|
|
16252
16989
|
function resetAbortLongPressAffordance() {
|
|
16253
16990
|
clearTimeout(abortLongPressTimer);
|
|
@@ -16333,33 +17070,7 @@ elements.newSessionButton.addEventListener("click", async () => {
|
|
|
16333
17070
|
});
|
|
16334
17071
|
elements.compactButton.addEventListener("click", async () => {
|
|
16335
17072
|
setComposerActionsOpen(false);
|
|
16336
|
-
|
|
16337
|
-
try {
|
|
16338
|
-
elements.compactButton.disabled = true;
|
|
16339
|
-
elements.compactButton.textContent = "Compacting…";
|
|
16340
|
-
setRunIndicatorActivity("Requesting context compaction…");
|
|
16341
|
-
scrollChatToBottom({ force: true });
|
|
16342
|
-
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
16343
|
-
renderFooter();
|
|
16344
|
-
addEvent("manual compaction requested");
|
|
16345
|
-
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
16346
|
-
if (!isCurrentTabContext(tabContext)) return;
|
|
16347
|
-
scheduleRefreshState(120, tabContext);
|
|
16348
|
-
scheduleRefreshMessages(600, tabContext);
|
|
16349
|
-
scheduleRefreshFooter(600, tabContext);
|
|
16350
|
-
} catch (error) {
|
|
16351
|
-
if (isCurrentTabContext(tabContext)) {
|
|
16352
|
-
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
16353
|
-
clearRunIndicatorActivity();
|
|
16354
|
-
renderFooter();
|
|
16355
|
-
addEvent(error.message, "error");
|
|
16356
|
-
}
|
|
16357
|
-
} finally {
|
|
16358
|
-
if (isCurrentTabContext(tabContext)) {
|
|
16359
|
-
elements.compactButton.disabled = !!currentState?.isCompacting;
|
|
16360
|
-
elements.compactButton.textContent = currentState?.isCompacting ? "Compacting…" : "Compact";
|
|
16361
|
-
}
|
|
16362
|
-
}
|
|
17073
|
+
await requestManualCompaction({ triggerButton: elements.compactButton });
|
|
16363
17074
|
});
|
|
16364
17075
|
elements.setModelButton.addEventListener("click", async () => {
|
|
16365
17076
|
if (!elements.modelSelect.value) return;
|
|
@@ -16408,6 +17119,7 @@ if (elements.backgroundChooseButton && elements.backgroundInput) {
|
|
|
16408
17119
|
if (elements.backgroundClearButton) {
|
|
16409
17120
|
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
16410
17121
|
}
|
|
17122
|
+
elements.remoteAuthToggle.addEventListener("change", () => toggleRemoteAuth().catch((error) => addEvent(error.message || String(error), "error")));
|
|
16411
17123
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
16412
17124
|
elements.serverActionSelect.addEventListener("change", updateServerActionButton);
|
|
16413
17125
|
elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
|
|
@@ -16504,7 +17216,7 @@ function isTextEntryTarget(target) {
|
|
|
16504
17216
|
|
|
16505
17217
|
function shouldHandleNativeAppShortcut(event) {
|
|
16506
17218
|
if (event.defaultPrevented) return false;
|
|
16507
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
17219
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
16508
17220
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
16509
17221
|
}
|
|
16510
17222
|
|
|
@@ -16514,6 +17226,11 @@ function handleNativeAppShortcut(event) {
|
|
|
16514
17226
|
const lowerKey = String(key || "").toLowerCase();
|
|
16515
17227
|
const ctrlOrMeta = event.ctrlKey || event.metaKey;
|
|
16516
17228
|
|
|
17229
|
+
if (ctrlOrMeta && !event.altKey && !event.shiftKey && lowerKey === "k") {
|
|
17230
|
+
event.preventDefault();
|
|
17231
|
+
openCommandPalette();
|
|
17232
|
+
return;
|
|
17233
|
+
}
|
|
16517
17234
|
if (ctrlOrMeta && !event.altKey && lowerKey === "l") {
|
|
16518
17235
|
event.preventDefault();
|
|
16519
17236
|
openNativeModelSelector();
|
|
@@ -16675,7 +17392,7 @@ window.addEventListener("focus", () => scheduleForegroundReconcile("window focus
|
|
|
16675
17392
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
16676
17393
|
window.addEventListener("keydown", (event) => {
|
|
16677
17394
|
if (event.key !== "Escape") return;
|
|
16678
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open) return;
|
|
17395
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open) return;
|
|
16679
17396
|
if (publishMenuOpen) {
|
|
16680
17397
|
setPublishMenuOpen(false);
|
|
16681
17398
|
return;
|
|
@@ -16866,6 +17583,7 @@ restoreStoredSkillUsage();
|
|
|
16866
17583
|
restoreBusyPromptBehaviorSetting();
|
|
16867
17584
|
updateComposerModeButtons();
|
|
16868
17585
|
updateOptionalFeatureAvailability();
|
|
17586
|
+
refreshOptionalFeaturePackageStatuses({ announce: true });
|
|
16869
17587
|
renderAppRunnerControls();
|
|
16870
17588
|
renderLoadedPromptListPreview();
|
|
16871
17589
|
loadLastUserPromptCache();
|
|
@@ -16882,6 +17600,7 @@ restoreAgentDoneNotificationsSetting();
|
|
|
16882
17600
|
restoreThinkingVisibilitySetting();
|
|
16883
17601
|
restoreTerminalTabsLayoutSetting();
|
|
16884
17602
|
restoreToolOutputExpansionSetting();
|
|
17603
|
+
restoreWorkspaceDashboardState();
|
|
16885
17604
|
restoreSidePanelSectionState();
|
|
16886
17605
|
bindSidePanelSectionToggles();
|
|
16887
17606
|
restoreSidePanelState();
|