@firstpick/pi-package-webui 0.4.0 → 0.4.1
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 +8 -5
- package/bin/pi-webui.mjs +161 -14
- package/package.json +1 -1
- package/public/app.js +715 -45
- package/public/index.html +37 -1
- package/public/styles.css +516 -4
- package/tests/http-endpoints-harness.test.mjs +42 -1
- package/tests/mobile-static.test.mjs +19 -10
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"),
|
|
@@ -186,6 +191,17 @@ const elements = {
|
|
|
186
191
|
pathPickerError: $("#pathPickerError"),
|
|
187
192
|
pathPickerCancelButton: $("#pathPickerCancelButton"),
|
|
188
193
|
pathPickerChooseButton: $("#pathPickerChooseButton"),
|
|
194
|
+
commandPaletteDialog: $("#commandPaletteDialog"),
|
|
195
|
+
commandPaletteInput: $("#commandPaletteInput"),
|
|
196
|
+
commandPaletteList: $("#commandPaletteList"),
|
|
197
|
+
commandPaletteHint: $("#commandPaletteHint"),
|
|
198
|
+
editRetryDialog: $("#editRetryDialog"),
|
|
199
|
+
editRetryMessage: $("#editRetryMessage"),
|
|
200
|
+
editRetryText: $("#editRetryText"),
|
|
201
|
+
editRetryStatus: $("#editRetryStatus"),
|
|
202
|
+
editRetryCancelButton: $("#editRetryCancelButton"),
|
|
203
|
+
editRetryForkButton: $("#editRetryForkButton"),
|
|
204
|
+
editRetrySendButton: $("#editRetrySendButton"),
|
|
189
205
|
nativeCommandDialog: $("#nativeCommandDialog"),
|
|
190
206
|
nativeCommandTitle: $("#nativeCommandTitle"),
|
|
191
207
|
nativeCommandMessage: $("#nativeCommandMessage"),
|
|
@@ -352,6 +368,10 @@ let userBashQueuesByTab = new Map();
|
|
|
352
368
|
let latestQueuedMessagesByTab = new Map();
|
|
353
369
|
let loadedPromptList = null;
|
|
354
370
|
let promptListRunning = false;
|
|
371
|
+
let workspaceDashboardCollapsed = false;
|
|
372
|
+
let commandPaletteIndex = 0;
|
|
373
|
+
let commandPaletteItems = [];
|
|
374
|
+
let activeEditRetry = null;
|
|
355
375
|
let abortLongPressTimer = null;
|
|
356
376
|
let abortLongPressHandled = false;
|
|
357
377
|
const dialogQueue = [];
|
|
@@ -389,6 +409,7 @@ const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
|
|
|
389
409
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
390
410
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
391
411
|
const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
|
|
412
|
+
const WORKSPACE_DASHBOARD_STORAGE_KEY = "pi-webui-workspace-dashboard-collapsed";
|
|
392
413
|
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
393
414
|
const ATTACHMENT_MAX_FILES = 12;
|
|
394
415
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
@@ -571,6 +592,8 @@ const SETTINGS_IMAGE_WIDTH_OPTIONS = ["60", "80", "120"];
|
|
|
571
592
|
const SETTINGS_EDITOR_PADDING_OPTIONS = ["0", "1", "2", "3"];
|
|
572
593
|
const SETTINGS_AUTOCOMPLETE_OPTIONS = ["3", "5", "7", "10", "15", "20"];
|
|
573
594
|
const optionalFeatureInstallInProgress = new Set();
|
|
595
|
+
const optionalFeaturePackageStatuses = new Map();
|
|
596
|
+
const optionalFeatureInstallMessages = new Map();
|
|
574
597
|
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
575
598
|
|
|
576
599
|
function createGitWorkflowActionsDone(patch = {}) {
|
|
@@ -597,6 +620,21 @@ function gitWorkflowActionDonePatch(workflow, process) {
|
|
|
597
620
|
return { actionsDone: createGitWorkflowActionsDone({ ...workflow?.actionsDone, [process]: true }) };
|
|
598
621
|
}
|
|
599
622
|
|
|
623
|
+
function resetGitWorkflowManualCommitDefaultPatch() {
|
|
624
|
+
return {
|
|
625
|
+
manualCommitMessageDefault: "",
|
|
626
|
+
manualCommitMessageDefaultReason: "",
|
|
627
|
+
manualCommitMessageDefaultPath: "",
|
|
628
|
+
manualCommitMessageDefaultAction: "",
|
|
629
|
+
manualCommitMessageDefaultRequestedAt: 0,
|
|
630
|
+
manualCommitMessageDefaultLoading: false,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function gitWorkflowManualCommitInputMessage(workflow) {
|
|
635
|
+
return String(workflow?.manualCommitMessage || "").trim() || String(workflow?.manualCommitMessageDefault || "").trim();
|
|
636
|
+
}
|
|
637
|
+
|
|
600
638
|
function createGitWorkflowState() {
|
|
601
639
|
return {
|
|
602
640
|
active: false,
|
|
@@ -616,6 +654,7 @@ function createGitWorkflowState() {
|
|
|
616
654
|
initFilesStatus: null,
|
|
617
655
|
message: null,
|
|
618
656
|
manualCommitMessage: "",
|
|
657
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
619
658
|
messageRequestedAt: 0,
|
|
620
659
|
branchName: "",
|
|
621
660
|
branchNameRequestedAt: 0,
|
|
@@ -1722,6 +1761,41 @@ function restoreSidePanelState() {
|
|
|
1722
1761
|
setSidePanelCollapsed(stored ?? false, { persist: stored !== null });
|
|
1723
1762
|
}
|
|
1724
1763
|
|
|
1764
|
+
function readStoredWorkspaceDashboardCollapsed() {
|
|
1765
|
+
try {
|
|
1766
|
+
const stored = localStorage.getItem(WORKSPACE_DASHBOARD_STORAGE_KEY);
|
|
1767
|
+
return stored === null ? true : stored === "1";
|
|
1768
|
+
} catch {
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function persistWorkspaceDashboardCollapsed(collapsed) {
|
|
1774
|
+
try {
|
|
1775
|
+
localStorage.setItem(WORKSPACE_DASHBOARD_STORAGE_KEY, collapsed ? "1" : "0");
|
|
1776
|
+
} catch {
|
|
1777
|
+
// Ignore storage failures; this is only a browser preference.
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function setWorkspaceDashboardCollapsed(collapsed, { persist = true } = {}) {
|
|
1782
|
+
workspaceDashboardCollapsed = !!collapsed;
|
|
1783
|
+
if (elements.workspaceDashboard) elements.workspaceDashboard.hidden = workspaceDashboardCollapsed;
|
|
1784
|
+
if (elements.workspaceDashboardToggleButton) {
|
|
1785
|
+
elements.workspaceDashboardToggleButton.setAttribute("aria-expanded", workspaceDashboardCollapsed ? "false" : "true");
|
|
1786
|
+
const tooltip = workspaceDashboardCollapsed ? "Show workspace overview" : "Hide workspace overview";
|
|
1787
|
+
const tooltipDetail = `${tooltip}:\n• Shows current tab, cwd, model, context, session, and queue.\n• Opens common workspace/session actions from one place.`;
|
|
1788
|
+
elements.workspaceDashboardToggleButton.title = tooltip;
|
|
1789
|
+
elements.workspaceDashboardToggleButton.setAttribute("aria-label", tooltip);
|
|
1790
|
+
elements.workspaceDashboardToggleButton.setAttribute("data-tooltip", tooltipDetail);
|
|
1791
|
+
}
|
|
1792
|
+
if (persist) persistWorkspaceDashboardCollapsed(workspaceDashboardCollapsed);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function restoreWorkspaceDashboardState() {
|
|
1796
|
+
setWorkspaceDashboardCollapsed(readStoredWorkspaceDashboardCollapsed(), { persist: false });
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1725
1799
|
function bindMobileViewChanges() {
|
|
1726
1800
|
if (!mobileViewMedia) return;
|
|
1727
1801
|
const syncForViewport = (event) => {
|
|
@@ -1971,6 +2045,142 @@ function attachMessageCopyButton(bubble, message, body) {
|
|
|
1971
2045
|
return button;
|
|
1972
2046
|
}
|
|
1973
2047
|
|
|
2048
|
+
function userMessageEditText(message) {
|
|
2049
|
+
return textFromContent(message?.content).trim();
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function messageEntryId(message) {
|
|
2053
|
+
for (const key of ["entryId", "id", "sessionEntryId", "messageId"]) {
|
|
2054
|
+
const value = message?.[key];
|
|
2055
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2056
|
+
}
|
|
2057
|
+
return "";
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function userMessageOrdinalAtIndex(messageIndex) {
|
|
2061
|
+
if (!Number.isInteger(messageIndex) || messageIndex < 0) return -1;
|
|
2062
|
+
let ordinal = -1;
|
|
2063
|
+
for (let index = 0; index <= messageIndex && index < latestMessages.length; index += 1) {
|
|
2064
|
+
if (latestMessages[index]?.role === "user") ordinal += 1;
|
|
2065
|
+
}
|
|
2066
|
+
return ordinal;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
async function resolveForkMessageForEdit(message, messageIndex, tabId = activeTabId) {
|
|
2070
|
+
const directEntryId = messageEntryId(message);
|
|
2071
|
+
const text = userMessageEditText(message);
|
|
2072
|
+
if (directEntryId) return { entryId: directEntryId, text };
|
|
2073
|
+
const response = await api("/api/fork-messages", { tabId });
|
|
2074
|
+
const forkMessages = Array.isArray(response.data?.messages) ? response.data.messages : [];
|
|
2075
|
+
const ordinal = userMessageOrdinalAtIndex(messageIndex);
|
|
2076
|
+
const ordinalMatch = ordinal >= 0 ? forkMessages[ordinal] : null;
|
|
2077
|
+
if (ordinalMatch?.entryId && userMessageEditText({ content: ordinalMatch.text }) === text) return ordinalMatch;
|
|
2078
|
+
const exactMatches = forkMessages.filter((item) => item?.entryId && String(item.text || "").trim() === text);
|
|
2079
|
+
if (exactMatches.length === 1) return exactMatches[0];
|
|
2080
|
+
if (exactMatches.length > 1 && ordinalMatch?.entryId) return ordinalMatch;
|
|
2081
|
+
throw new Error("Could not map this transcript message to a fork point. Use /fork for the full selector.");
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function setEditRetryStatus(message = "", level = "info") {
|
|
2085
|
+
if (!elements.editRetryStatus) return;
|
|
2086
|
+
elements.editRetryStatus.textContent = message;
|
|
2087
|
+
elements.editRetryStatus.hidden = !message;
|
|
2088
|
+
elements.editRetryStatus.className = `edit-retry-status ${level} ${message ? "" : "muted"}`.trim();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function setEditRetryBusy(busy, label = "Working…") {
|
|
2092
|
+
for (const button of [elements.editRetryForkButton, elements.editRetrySendButton, elements.editRetryCancelButton].filter(Boolean)) button.disabled = !!busy;
|
|
2093
|
+
if (elements.editRetrySendButton) elements.editRetrySendButton.textContent = busy ? label : "Fork & run";
|
|
2094
|
+
if (elements.editRetryForkButton) elements.editRetryForkButton.textContent = busy ? "Forking…" : "Fork only";
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function openEditRetryDialog(message, messageIndex = -1) {
|
|
2098
|
+
const text = userMessageEditText(message);
|
|
2099
|
+
if (!text) {
|
|
2100
|
+
addEvent("user message has no editable text", "warn");
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
activeEditRetry = { message, messageIndex, tabId: activeTabId };
|
|
2104
|
+
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.`;
|
|
2105
|
+
if (elements.editRetryText) {
|
|
2106
|
+
elements.editRetryText.value = text;
|
|
2107
|
+
elements.editRetryText.style.height = "auto";
|
|
2108
|
+
}
|
|
2109
|
+
setEditRetryStatus();
|
|
2110
|
+
setEditRetryBusy(false);
|
|
2111
|
+
if (!elements.editRetryDialog.open) elements.editRetryDialog.showModal();
|
|
2112
|
+
queueMicrotask(() => {
|
|
2113
|
+
elements.editRetryText?.focus();
|
|
2114
|
+
elements.editRetryText?.select();
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function closeEditRetryDialog() {
|
|
2119
|
+
activeEditRetry = null;
|
|
2120
|
+
if (elements.editRetryDialog?.open) elements.editRetryDialog.close();
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
async function submitEditRetry({ send = false } = {}) {
|
|
2124
|
+
if (!activeEditRetry) return;
|
|
2125
|
+
const editedText = String(elements.editRetryText?.value || "").trim();
|
|
2126
|
+
if (!editedText) {
|
|
2127
|
+
setEditRetryStatus("Prompt cannot be empty.", "error");
|
|
2128
|
+
elements.editRetryText?.focus();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const { message, messageIndex, tabId } = activeEditRetry;
|
|
2132
|
+
const tabContext = activeTabContext(tabId || activeTabId);
|
|
2133
|
+
setEditRetryBusy(true, "Forking…");
|
|
2134
|
+
setEditRetryStatus("Resolving fork point…");
|
|
2135
|
+
try {
|
|
2136
|
+
const forkMessage = await resolveForkMessageForEdit(message, messageIndex, tabContext.tabId);
|
|
2137
|
+
setEditRetryStatus("Forking session…");
|
|
2138
|
+
const result = await api("/api/fork", { method: "POST", body: { entryId: forkMessage.entryId }, tabId: tabContext.tabId });
|
|
2139
|
+
applyResponseTab(result);
|
|
2140
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2141
|
+
closeEditRetryDialog();
|
|
2142
|
+
await refreshAll(tabContext);
|
|
2143
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2144
|
+
if (send) {
|
|
2145
|
+
addEvent("forked session; sending edited prompt", "info");
|
|
2146
|
+
await sendPrompt("prompt", editedText, { targetTabId: tabContext.tabId, throwOnError: true });
|
|
2147
|
+
} else {
|
|
2148
|
+
elements.promptInput.value = editedText;
|
|
2149
|
+
resizePromptInput();
|
|
2150
|
+
focusPromptInput({ defer: true });
|
|
2151
|
+
addEvent("forked session; edited prompt restored in composer", "info");
|
|
2152
|
+
}
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
setEditRetryStatus(error.message || String(error), "error");
|
|
2155
|
+
if (send) {
|
|
2156
|
+
elements.promptInput.value = editedText;
|
|
2157
|
+
resizePromptInput();
|
|
2158
|
+
}
|
|
2159
|
+
} finally {
|
|
2160
|
+
setEditRetryBusy(false);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function attachMessageEditRetryButton(bubble, message, messageIndex, { streaming = false, transient = false } = {}) {
|
|
2165
|
+
if (!bubble || streaming || transient || message?.role !== "user") return null;
|
|
2166
|
+
const text = userMessageEditText(message);
|
|
2167
|
+
if (!text) return null;
|
|
2168
|
+
const existing = bubble.querySelector(":scope > .message-edit-retry-button");
|
|
2169
|
+
if (existing) return existing;
|
|
2170
|
+
const button = make("button", "message-edit-retry-button", "↺");
|
|
2171
|
+
button.type = "button";
|
|
2172
|
+
button.title = "Edit this prompt and retry from here";
|
|
2173
|
+
button.setAttribute("aria-label", button.title);
|
|
2174
|
+
button.addEventListener("click", (event) => {
|
|
2175
|
+
event.preventDefault();
|
|
2176
|
+
event.stopPropagation();
|
|
2177
|
+
openEditRetryDialog(message, messageIndex);
|
|
2178
|
+
});
|
|
2179
|
+
bubble.classList.add("has-edit-retry-action");
|
|
2180
|
+
bubble.append(button);
|
|
2181
|
+
return button;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1974
2184
|
function safeHttpUrl(value, base = window.location.href) {
|
|
1975
2185
|
const text = String(value || "").trim();
|
|
1976
2186
|
if (!text) return "";
|
|
@@ -3722,7 +3932,7 @@ function restoreActiveDraft() {
|
|
|
3722
3932
|
|
|
3723
3933
|
function focusPromptInput({ defer = false } = {}) {
|
|
3724
3934
|
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;
|
|
3935
|
+
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
3936
|
try {
|
|
3727
3937
|
elements.promptInput.focus({ preventScroll: true });
|
|
3728
3938
|
} catch {
|
|
@@ -4062,6 +4272,9 @@ function renderTabs() {
|
|
|
4062
4272
|
updateTerminalTabGroupOpenState();
|
|
4063
4273
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
4064
4274
|
updateDocumentTitle();
|
|
4275
|
+
renderWorkspaceDashboard();
|
|
4276
|
+
renderContextMeter();
|
|
4277
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
4065
4278
|
syncTabPolling();
|
|
4066
4279
|
}
|
|
4067
4280
|
|
|
@@ -5879,6 +6092,194 @@ function renderMinimalFooter() {
|
|
|
5879
6092
|
updateFooterModelPickerPosition();
|
|
5880
6093
|
}
|
|
5881
6094
|
|
|
6095
|
+
function contextUsageSnapshot() {
|
|
6096
|
+
const usage = latestStats?.contextUsage || currentState?.contextUsage || null;
|
|
6097
|
+
const contextWindow = contextWindowFromSources(usage, currentState?.model?.contextWindow);
|
|
6098
|
+
if (!contextWindow) return null;
|
|
6099
|
+
const rawPercent = Number(usage?.percent);
|
|
6100
|
+
const unknown = contextUsageUnknownAfterCompaction() || !Number.isFinite(rawPercent);
|
|
6101
|
+
const rawTokens = Number(usage?.tokens);
|
|
6102
|
+
return {
|
|
6103
|
+
tokens: Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : null,
|
|
6104
|
+
contextWindow,
|
|
6105
|
+
percent: unknown ? null : Math.max(0, Math.min(100, rawPercent)),
|
|
6106
|
+
unknown,
|
|
6107
|
+
autoCompactionEnabled: footerAutoCompactionEnabled(),
|
|
6108
|
+
};
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
function contextUsageDisplay(snapshot = contextUsageSnapshot()) {
|
|
6112
|
+
if (!snapshot) return "Context unknown";
|
|
6113
|
+
const windowText = formatFooterTokenCount(snapshot.contextWindow);
|
|
6114
|
+
if (typeof snapshot.percent === "number") return `${snapshot.percent.toFixed(1)}% of ${windowText}`;
|
|
6115
|
+
return `?/${windowText}`;
|
|
6116
|
+
}
|
|
6117
|
+
|
|
6118
|
+
function contextUsageDetail(snapshot = contextUsageSnapshot()) {
|
|
6119
|
+
if (!snapshot) return "Waiting for model context-window data.";
|
|
6120
|
+
const tokenText = snapshot.tokens === null ? "tokens unknown" : `${formatFooterTokenCount(snapshot.tokens)} tokens`;
|
|
6121
|
+
const autoText = snapshot.autoCompactionEnabled ? "auto-compaction on" : "auto-compaction off";
|
|
6122
|
+
return `${tokenText} · ${formatFooterTokenCount(snapshot.contextWindow)} window · ${autoText}`;
|
|
6123
|
+
}
|
|
6124
|
+
|
|
6125
|
+
function appendContextMeterFill(meter, snapshot) {
|
|
6126
|
+
const fill = make("span", "context-meter-fill");
|
|
6127
|
+
const percent = typeof snapshot?.percent === "number" ? snapshot.percent : 0;
|
|
6128
|
+
fill.style.width = `${Math.max(0, Math.min(100, percent)).toFixed(1)}%`;
|
|
6129
|
+
if (typeof snapshot?.percent === "number") {
|
|
6130
|
+
const activeColor = contextUsageActiveColor(snapshot.percent);
|
|
6131
|
+
fill.style.setProperty("--context-active-color", activeColor.color);
|
|
6132
|
+
fill.style.setProperty("--context-active-glow", activeColor.glow);
|
|
6133
|
+
}
|
|
6134
|
+
meter.append(fill);
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
async function requestManualCompaction({ triggerButton = null } = {}) {
|
|
6138
|
+
const tabContext = activeTabContext();
|
|
6139
|
+
if (!tabContext.tabId) return;
|
|
6140
|
+
const buttons = [...new Set([elements.compactButton, triggerButton].filter(Boolean))];
|
|
6141
|
+
try {
|
|
6142
|
+
for (const button of buttons) {
|
|
6143
|
+
button.disabled = true;
|
|
6144
|
+
button.textContent = "Compacting…";
|
|
6145
|
+
}
|
|
6146
|
+
setRunIndicatorActivity("Requesting context compaction…");
|
|
6147
|
+
scrollChatToBottom({ force: true });
|
|
6148
|
+
markContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
6149
|
+
renderFooter();
|
|
6150
|
+
renderContextMeter();
|
|
6151
|
+
renderWorkspaceDashboard();
|
|
6152
|
+
addEvent("manual compaction requested");
|
|
6153
|
+
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
6154
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6155
|
+
scheduleRefreshState(120, tabContext);
|
|
6156
|
+
scheduleRefreshMessages(600, tabContext);
|
|
6157
|
+
scheduleRefreshFooter(600, tabContext);
|
|
6158
|
+
} catch (error) {
|
|
6159
|
+
if (isCurrentTabContext(tabContext)) {
|
|
6160
|
+
clearContextUsageUnknownAfterCompaction(tabContext.tabId);
|
|
6161
|
+
clearRunIndicatorActivity();
|
|
6162
|
+
renderFooter();
|
|
6163
|
+
renderContextMeter();
|
|
6164
|
+
renderWorkspaceDashboard();
|
|
6165
|
+
addEvent(error.message, "error");
|
|
6166
|
+
}
|
|
6167
|
+
} finally {
|
|
6168
|
+
if (isCurrentTabContext(tabContext)) {
|
|
6169
|
+
for (const button of buttons) {
|
|
6170
|
+
button.disabled = !!currentState?.isCompacting;
|
|
6171
|
+
button.textContent = button === elements.compactButton && currentState?.isCompacting ? "Compacting…" : button === elements.compactButton ? "Compact" : "Compact now";
|
|
6172
|
+
}
|
|
6173
|
+
renderContextMeter();
|
|
6174
|
+
renderWorkspaceDashboard();
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
|
|
6179
|
+
function renderContextMeter() {
|
|
6180
|
+
const root = elements.contextMeterBar;
|
|
6181
|
+
if (!root) return;
|
|
6182
|
+
const tab = activeTab();
|
|
6183
|
+
if (!tab) {
|
|
6184
|
+
root.hidden = true;
|
|
6185
|
+
root.replaceChildren();
|
|
6186
|
+
return;
|
|
6187
|
+
}
|
|
6188
|
+
root.hidden = false;
|
|
6189
|
+
const snapshot = contextUsageSnapshot();
|
|
6190
|
+
if (!snapshot || typeof snapshot.percent !== "number" || snapshot.percent <= 50) {
|
|
6191
|
+
root.hidden = true;
|
|
6192
|
+
root.replaceChildren();
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
const meter = make("div", `context-meter${snapshot?.unknown ? " unknown" : ""}`);
|
|
6196
|
+
appendContextMeterFill(meter, snapshot);
|
|
6197
|
+
|
|
6198
|
+
const summary = make("div", "context-meter-summary");
|
|
6199
|
+
summary.append(
|
|
6200
|
+
make("strong", undefined, contextUsageDisplay(snapshot)),
|
|
6201
|
+
make("span", "muted", contextUsageDetail(snapshot)),
|
|
6202
|
+
);
|
|
6203
|
+
|
|
6204
|
+
const actions = make("div", "context-meter-actions");
|
|
6205
|
+
const compact = make("button", "context-meter-compact", currentState?.isCompacting ? "Compacting…" : "Compact now");
|
|
6206
|
+
compact.type = "button";
|
|
6207
|
+
compact.disabled = !!currentState?.isCompacting;
|
|
6208
|
+
compact.title = "Manually compact this tab's conversation context";
|
|
6209
|
+
compact.addEventListener("click", () => requestManualCompaction({ triggerButton: compact }));
|
|
6210
|
+
const auto = make("button", "context-meter-auto", footerAutoCompactionEnabled() ? "Auto on" : "Auto off");
|
|
6211
|
+
auto.type = "button";
|
|
6212
|
+
auto.setAttribute("aria-pressed", footerAutoCompactionEnabled() ? "true" : "false");
|
|
6213
|
+
auto.disabled = footerAutoCompactionToggleInFlight;
|
|
6214
|
+
auto.title = footerAutoCompactionToggleInFlight ? "Updating auto-compaction…" : footerAutoCompactionToggleAction();
|
|
6215
|
+
auto.addEventListener("click", () => toggleFooterAutoCompaction());
|
|
6216
|
+
actions.append(compact, auto);
|
|
6217
|
+
|
|
6218
|
+
root.replaceChildren(summary, meter, actions);
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
function dashboardMetric(label, value, detail = "") {
|
|
6222
|
+
const item = make("div", "workspace-dashboard-metric");
|
|
6223
|
+
item.append(make("span", "workspace-dashboard-metric-label", label), make("strong", undefined, value || "—"));
|
|
6224
|
+
if (detail) item.append(make("span", "workspace-dashboard-metric-detail", detail));
|
|
6225
|
+
return item;
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
function dashboardAction(label, handler, className = "") {
|
|
6229
|
+
const button = make("button", `workspace-dashboard-action ${className}`.trim(), label);
|
|
6230
|
+
button.type = "button";
|
|
6231
|
+
button.addEventListener("click", handler);
|
|
6232
|
+
return button;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
function renderWorkspaceDashboard() {
|
|
6236
|
+
const root = elements.workspaceDashboard;
|
|
6237
|
+
if (!root) return;
|
|
6238
|
+
const tab = activeTab();
|
|
6239
|
+
const snapshot = contextUsageSnapshot();
|
|
6240
|
+
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "Choose or create a tab to start");
|
|
6241
|
+
const queueCount = Number(currentState?.pendingMessageCount || 0) || 0;
|
|
6242
|
+
root.replaceChildren();
|
|
6243
|
+
|
|
6244
|
+
const header = make("div", "workspace-dashboard-header");
|
|
6245
|
+
const title = make("div", "workspace-dashboard-title");
|
|
6246
|
+
title.append(make("span", "workspace-dashboard-kicker", "Workspace"), make("h2", undefined, tab?.title || "Pi Web UI"), make("p", "muted", workspaceLabel));
|
|
6247
|
+
const actions = make("div", "workspace-dashboard-actions");
|
|
6248
|
+
actions.append(
|
|
6249
|
+
dashboardAction("Command palette", () => openCommandPalette(), "primary"),
|
|
6250
|
+
dashboardAction("New tab", () => createTerminalTab()),
|
|
6251
|
+
dashboardAction("Resume", () => runNativeCommandMenu("/resume")),
|
|
6252
|
+
dashboardAction("Model", () => runNativeCommandMenu("/model")),
|
|
6253
|
+
dashboardAction("Settings", () => runNativeCommandMenu("/settings")),
|
|
6254
|
+
);
|
|
6255
|
+
header.append(title, actions);
|
|
6256
|
+
|
|
6257
|
+
const metrics = make("div", "workspace-dashboard-metrics");
|
|
6258
|
+
metrics.append(
|
|
6259
|
+
dashboardMetric("Model", currentState?.model ? shortModelLabel(currentState.model) : "loading…", currentState?.thinkingLevel ? `thinking ${currentState.thinkingLevel}` : ""),
|
|
6260
|
+
dashboardMetric("Context", contextUsageDisplay(snapshot), contextUsageDetail(snapshot)),
|
|
6261
|
+
dashboardMetric("Session", currentState?.sessionName || currentState?.sessionId || "loading…", currentState?.sessionFile || "in-memory"),
|
|
6262
|
+
dashboardMetric("Queue", `${queueCount}`, queueCount === 1 ? "pending message" : "pending messages"),
|
|
6263
|
+
);
|
|
6264
|
+
|
|
6265
|
+
const tabsPanel = make("div", "workspace-dashboard-tabs");
|
|
6266
|
+
tabsPanel.append(make("span", "workspace-dashboard-tabs-title", `Open tabs (${tabs.length})`));
|
|
6267
|
+
const tabList = make("div", "workspace-dashboard-tab-list");
|
|
6268
|
+
for (const item of tabs.slice(0, 8)) {
|
|
6269
|
+
const indicator = tabIndicator(item);
|
|
6270
|
+
const button = make("button", `workspace-dashboard-tab activity-${indicator.state}${item.id === activeTabId ? " active" : ""}`);
|
|
6271
|
+
button.type = "button";
|
|
6272
|
+
button.title = `${item.title} · ${indicator.label}`;
|
|
6273
|
+
button.append(make("span", "workspace-dashboard-tab-dot", indicator.glyph), make("span", undefined, item.title));
|
|
6274
|
+
button.addEventListener("click", () => switchTab(item.id));
|
|
6275
|
+
tabList.append(button);
|
|
6276
|
+
}
|
|
6277
|
+
if (tabs.length > 8) tabList.append(make("span", "workspace-dashboard-tab-more", `+${tabs.length - 8} more`));
|
|
6278
|
+
tabsPanel.append(tabList);
|
|
6279
|
+
|
|
6280
|
+
root.append(header, metrics, tabsPanel);
|
|
6281
|
+
}
|
|
6282
|
+
|
|
5882
6283
|
function setFooterModelPickerOpen(open) {
|
|
5883
6284
|
footerModelPickerOpen = !!open;
|
|
5884
6285
|
if (footerModelPickerOpen) {
|
|
@@ -6892,6 +7293,8 @@ function renderStatus() {
|
|
|
6892
7293
|
elements.compactButton.textContent = state?.isCompacting ? "Compacting…" : "Compact";
|
|
6893
7294
|
syncModelSelectToState();
|
|
6894
7295
|
renderFooter();
|
|
7296
|
+
renderContextMeter();
|
|
7297
|
+
renderWorkspaceDashboard();
|
|
6895
7298
|
renderFeedbackTray();
|
|
6896
7299
|
}
|
|
6897
7300
|
|
|
@@ -8625,6 +9028,7 @@ function renderGitInitStackInput() {
|
|
|
8625
9028
|
function renderGitWorkflowManualCommitInput() {
|
|
8626
9029
|
const tabId = gitWorkflowActionTabId();
|
|
8627
9030
|
const workflow = gitWorkflowForTab(tabId, { create: false }) || gitWorkflow;
|
|
9031
|
+
const defaultCommitMessage = String(workflow?.manualCommitMessageDefault || "").trim();
|
|
8628
9032
|
const row = make("div", "git-workflow-message-input-row");
|
|
8629
9033
|
const field = make("label", "git-workflow-message-input-field");
|
|
8630
9034
|
field.setAttribute("for", "gitWorkflowManualCommitMessage");
|
|
@@ -8634,7 +9038,7 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
8634
9038
|
input.id = "gitWorkflowManualCommitMessage";
|
|
8635
9039
|
input.type = "text";
|
|
8636
9040
|
input.value = workflow?.manualCommitMessage || "";
|
|
8637
|
-
input.placeholder = "Type a commit message to use instead of short/long";
|
|
9041
|
+
input.placeholder = defaultCommitMessage || "Type a commit message to use instead of short/long";
|
|
8638
9042
|
input.autocomplete = "off";
|
|
8639
9043
|
input.spellcheck = true;
|
|
8640
9044
|
|
|
@@ -8642,7 +9046,10 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
8642
9046
|
commitButton.type = "button";
|
|
8643
9047
|
const updateCommitState = () => {
|
|
8644
9048
|
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
8645
|
-
|
|
9049
|
+
const message = String(input.value || "").trim() || String(currentWorkflow?.manualCommitMessageDefault || "").trim();
|
|
9050
|
+
commitButton.disabled = !currentWorkflow || !!currentWorkflow.busy || !message;
|
|
9051
|
+
if (message && !String(input.value || "").trim()) commitButton.title = `Use default commit message: ${message}`;
|
|
9052
|
+
else commitButton.removeAttribute("title");
|
|
8646
9053
|
};
|
|
8647
9054
|
input.addEventListener("input", () => {
|
|
8648
9055
|
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
@@ -8660,6 +9067,7 @@ function renderGitWorkflowManualCommitInput() {
|
|
|
8660
9067
|
commitGitWorkflow("input", tabId);
|
|
8661
9068
|
});
|
|
8662
9069
|
updateCommitState();
|
|
9070
|
+
loadGitWorkflowDefaultCommitMessage({ runId: workflow?.runId, tabId });
|
|
8663
9071
|
|
|
8664
9072
|
field.append(input);
|
|
8665
9073
|
row.append(field, commitButton);
|
|
@@ -8765,6 +9173,7 @@ function selectGitInitWorkflowProcess(processValue, tabId, workflow) {
|
|
|
8765
9173
|
initFilesStatus: null,
|
|
8766
9174
|
message: null,
|
|
8767
9175
|
manualCommitMessage: "",
|
|
9176
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
8768
9177
|
messageRequestedAt: 0,
|
|
8769
9178
|
branchName: "",
|
|
8770
9179
|
branchNameRequestedAt: 0,
|
|
@@ -8811,7 +9220,7 @@ function selectGitWorkflowProcess(processValue, tabId = gitWorkflowActionTabId()
|
|
|
8811
9220
|
const process = GIT_WORKFLOW_PROCESS_VALUES.has(processValue) ? processValue : "stage";
|
|
8812
9221
|
workflow.runId += 1;
|
|
8813
9222
|
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 };
|
|
9223
|
+
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
9224
|
|
|
8816
9225
|
if (process === "stage") {
|
|
8817
9226
|
setGitWorkflow({ ...base, step: "add", message: null, output: "Ready to stage all changes with git add ." }, { tabId });
|
|
@@ -9101,6 +9510,7 @@ function startGitWorkflow(tabId = activeTabId) {
|
|
|
9101
9510
|
initFilesStatus: null,
|
|
9102
9511
|
message: null,
|
|
9103
9512
|
manualCommitMessage: "",
|
|
9513
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
9104
9514
|
messageRequestedAt: 0,
|
|
9105
9515
|
branchName: "",
|
|
9106
9516
|
branchNameRequestedAt: 0,
|
|
@@ -9140,6 +9550,7 @@ function startGitInitWorkflow(tabId = activeTabId) {
|
|
|
9140
9550
|
initFilesStatus: null,
|
|
9141
9551
|
message: null,
|
|
9142
9552
|
manualCommitMessage: "",
|
|
9553
|
+
...resetGitWorkflowManualCommitDefaultPatch(),
|
|
9143
9554
|
messageRequestedAt: 0,
|
|
9144
9555
|
branchName: "",
|
|
9145
9556
|
branchNameRequestedAt: 0,
|
|
@@ -9430,17 +9841,47 @@ async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
|
9430
9841
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9431
9842
|
if (!workflow) return;
|
|
9432
9843
|
const runId = workflow.runId;
|
|
9433
|
-
setGitWorkflow({ step: "add", busy: true, error: "", output: "Running git add ." }, { tabId });
|
|
9844
|
+
setGitWorkflow({ step: "add", busy: true, error: "", ...resetGitWorkflowManualCommitDefaultPatch(), output: "Running git add ." }, { tabId });
|
|
9434
9845
|
try {
|
|
9435
9846
|
const result = await gitWorkflowRequest("/api/git-workflow/add", { runId, tabId });
|
|
9436
9847
|
if (!result) return;
|
|
9437
|
-
setGitWorkflow({ step: "generate", busy: false, ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
9848
|
+
setGitWorkflow({ step: "generate", busy: false, ...resetGitWorkflowManualCommitDefaultPatch(), ...gitWorkflowActionDonePatch(workflow, "stage"), output: `${formatGitCommandResult(result)}\n\nStaged. Next: run /git-staged-msg.` }, { tabId });
|
|
9438
9849
|
if (isCurrentTabContext(tabContext)) scheduleRefreshFooter();
|
|
9439
9850
|
} catch (error) {
|
|
9440
9851
|
if (isCurrentGitWorkflowRun(runId, tabId)) failGitWorkflow(error, "add", { tabId });
|
|
9441
9852
|
}
|
|
9442
9853
|
}
|
|
9443
9854
|
|
|
9855
|
+
async function loadGitWorkflowDefaultCommitMessage({ runId, tabId = activeTabId } = {}) {
|
|
9856
|
+
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
9857
|
+
const expectedRunId = runId ?? workflow?.runId;
|
|
9858
|
+
if (!workflow || workflow.manualCommitMessageDefaultLoading || workflow.manualCommitMessageDefaultRequestedAt) return;
|
|
9859
|
+
workflow.manualCommitMessageDefaultLoading = true;
|
|
9860
|
+
workflow.manualCommitMessageDefaultRequestedAt = Date.now();
|
|
9861
|
+
try {
|
|
9862
|
+
const data = await gitWorkflowRequest("/api/git-workflow/default-commit-message", { method: "GET", runId: expectedRunId, tabId });
|
|
9863
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9864
|
+
if (!data || !currentWorkflow || !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
9865
|
+
setGitWorkflow({
|
|
9866
|
+
manualCommitMessageDefault: String(data.message || "").trim(),
|
|
9867
|
+
manualCommitMessageDefaultReason: String(data.reason || ""),
|
|
9868
|
+
manualCommitMessageDefaultPath: String(data.path || ""),
|
|
9869
|
+
manualCommitMessageDefaultAction: String(data.action || ""),
|
|
9870
|
+
manualCommitMessageDefaultLoading: false,
|
|
9871
|
+
}, { tabId });
|
|
9872
|
+
} catch (error) {
|
|
9873
|
+
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
9874
|
+
if (!currentWorkflow || !isCurrentGitWorkflowRun(expectedRunId, tabId)) return;
|
|
9875
|
+
setGitWorkflow({
|
|
9876
|
+
manualCommitMessageDefault: "",
|
|
9877
|
+
manualCommitMessageDefaultReason: error?.message || String(error),
|
|
9878
|
+
manualCommitMessageDefaultPath: "",
|
|
9879
|
+
manualCommitMessageDefaultAction: "",
|
|
9880
|
+
manualCommitMessageDefaultLoading: false,
|
|
9881
|
+
}, { tabId });
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9884
|
+
|
|
9444
9885
|
async function runGitMessagePrompt(tabId = gitWorkflowActionTabId()) {
|
|
9445
9886
|
const tabContext = activeTabContext(tabId);
|
|
9446
9887
|
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
@@ -9642,9 +10083,9 @@ async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
|
9642
10083
|
if (!workflow) return;
|
|
9643
10084
|
const runId = workflow.runId;
|
|
9644
10085
|
const failureStep = variant === "input" && workflow.step === "generate" ? "generate" : "message";
|
|
9645
|
-
const inputMessage = variant === "input" ?
|
|
10086
|
+
const inputMessage = variant === "input" ? gitWorkflowManualCommitInputMessage(workflow) : "";
|
|
9646
10087
|
if (variant === "input" && !inputMessage) {
|
|
9647
|
-
failGitWorkflow(new Error("Type a commit message
|
|
10088
|
+
failGitWorkflow(new Error("Type a commit message, or stage exactly one created/updated/deleted file to use the default."), failureStep, { tabId });
|
|
9648
10089
|
return;
|
|
9649
10090
|
}
|
|
9650
10091
|
const preview = variant === "input" ? formatInputCommitMessagePreview(inputMessage) : formatCommitMessagePreview(workflow.message);
|
|
@@ -12010,6 +12451,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
12010
12451
|
bubble.append(header, body);
|
|
12011
12452
|
}
|
|
12012
12453
|
attachMessageCopyButton(bubble, message, body);
|
|
12454
|
+
attachMessageEditRetryButton(bubble, message, messageIndex, { streaming, transient });
|
|
12013
12455
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
12014
12456
|
appendChatMessageBubble(bubble);
|
|
12015
12457
|
return { bubble, body };
|
|
@@ -12815,6 +13257,30 @@ function resetOptionalFeatureAvailability() {
|
|
|
12815
13257
|
renderOptionalFeatureControls();
|
|
12816
13258
|
}
|
|
12817
13259
|
|
|
13260
|
+
function optionalFeaturePackageStatus(featureId) {
|
|
13261
|
+
return optionalFeaturePackageStatuses.get(featureId) || null;
|
|
13262
|
+
}
|
|
13263
|
+
|
|
13264
|
+
function optionalFeaturePackageVersionLabel(status) {
|
|
13265
|
+
if (!status?.installedVersion) return "";
|
|
13266
|
+
return status.declaredSpec ? `${status.installedVersion} (expects ${status.declaredSpec})` : status.installedVersion;
|
|
13267
|
+
}
|
|
13268
|
+
|
|
13269
|
+
async function refreshOptionalFeaturePackageStatuses({ announce = false } = {}) {
|
|
13270
|
+
try {
|
|
13271
|
+
const response = await api("/api/optional-features", { scoped: false });
|
|
13272
|
+
optionalFeaturePackageStatuses.clear();
|
|
13273
|
+
for (const status of response.data?.features || []) {
|
|
13274
|
+
if (status?.featureId) optionalFeaturePackageStatuses.set(status.featureId, status);
|
|
13275
|
+
}
|
|
13276
|
+
renderOptionalFeatureControls();
|
|
13277
|
+
return true;
|
|
13278
|
+
} catch (error) {
|
|
13279
|
+
if (announce) addEvent(`optional feature package status check failed: ${error.message || String(error)}`, "warn");
|
|
13280
|
+
return false;
|
|
13281
|
+
}
|
|
13282
|
+
}
|
|
13283
|
+
|
|
12818
13284
|
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
12819
13285
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
12820
13286
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
@@ -12854,9 +13320,16 @@ function updateOptionalFeatureAvailability() {
|
|
|
12854
13320
|
function optionalFeatureStatus(featureId) {
|
|
12855
13321
|
const detected = isOptionalFeatureDetected(featureId);
|
|
12856
13322
|
const disabled = isOptionalFeatureDisabled(featureId);
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
13323
|
+
const packageStatus = optionalFeaturePackageStatus(featureId);
|
|
13324
|
+
const installMessage = optionalFeatureInstallMessages.get(featureId);
|
|
13325
|
+
const versionLabel = optionalFeaturePackageVersionLabel(packageStatus);
|
|
13326
|
+
const versionSuffix = versionLabel ? ` · package ${versionLabel}` : "";
|
|
13327
|
+
if (optionalFeatureInstallInProgress.has(featureId)) return { label: "Installing", className: "updating", detail: installMessage || "npm install is running; waiting for the package manager to finish" };
|
|
13328
|
+
if (packageStatus?.updateAvailable) return { label: "Update available", className: "updating", detail: packageStatus.updateReason || `Installed package is older than the Web UI expects${versionSuffix}` };
|
|
13329
|
+
if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: `Detected and enabled in Web UI${versionSuffix}` };
|
|
13330
|
+
if (detected && disabled) return { label: "Disabled", className: "disabled", detail: `Detected, but disabled in Web UI${versionSuffix}` };
|
|
13331
|
+
if (packageStatus?.installed) return { label: "Installed", className: "installed", detail: `Package is installed but not loaded in the active Pi tab${versionSuffix}` };
|
|
13332
|
+
return { label: "Install needed", className: "missing", detail: installMessage || "Package is not installed or not visible from the Web UI package root" };
|
|
12860
13333
|
}
|
|
12861
13334
|
|
|
12862
13335
|
function optionalFeatureWidgetFeatureId(key) {
|
|
@@ -12875,6 +13348,7 @@ function renderOptionalFeaturePanel() {
|
|
|
12875
13348
|
const detected = isOptionalFeatureDetected(feature.id);
|
|
12876
13349
|
const enabled = isOptionalFeatureEnabled(feature.id);
|
|
12877
13350
|
const installing = optionalFeatureInstallInProgress.has(feature.id);
|
|
13351
|
+
const packageStatus = optionalFeaturePackageStatus(feature.id);
|
|
12878
13352
|
const status = optionalFeatureStatus(feature.id);
|
|
12879
13353
|
const row = make("div", `optional-feature-row ${status.className}`);
|
|
12880
13354
|
|
|
@@ -12902,9 +13376,16 @@ function renderOptionalFeaturePanel() {
|
|
|
12902
13376
|
action.disabled = installing;
|
|
12903
13377
|
if (installing) {
|
|
12904
13378
|
action.textContent = "Installing…";
|
|
13379
|
+
} else if (packageStatus?.updateAvailable) {
|
|
13380
|
+
action.textContent = "Update…";
|
|
13381
|
+
action.classList.add("update");
|
|
13382
|
+
action.addEventListener("click", () => installOptionalFeature(feature.id, { update: true }));
|
|
12905
13383
|
} else if (detected) {
|
|
12906
13384
|
action.textContent = enabled ? "Disable" : "Enable";
|
|
12907
13385
|
action.addEventListener("click", () => setOptionalFeatureDisabled(feature.id, enabled));
|
|
13386
|
+
} else if (packageStatus?.installed) {
|
|
13387
|
+
action.textContent = "Reload";
|
|
13388
|
+
action.addEventListener("click", () => sendPrompt("prompt", "/reload"));
|
|
12908
13389
|
} else {
|
|
12909
13390
|
action.textContent = "Install…";
|
|
12910
13391
|
action.classList.add("install");
|
|
@@ -12971,15 +13452,17 @@ function commandUnavailableMessage(commandName) {
|
|
|
12971
13452
|
return `Command unavailable: /${commandName} is not loaded in the active Pi tab.`;
|
|
12972
13453
|
}
|
|
12973
13454
|
|
|
12974
|
-
async function installOptionalFeature(featureId) {
|
|
13455
|
+
async function installOptionalFeature(featureId, { update = false } = {}) {
|
|
12975
13456
|
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
12976
13457
|
if (!feature || optionalFeatureInstallInProgress.has(featureId)) return;
|
|
12977
13458
|
|
|
13459
|
+
const actionLabel = update ? "Update" : "Install";
|
|
12978
13460
|
const warning = [
|
|
12979
|
-
|
|
13461
|
+
`${actionLabel} optional feature: ${feature.label}?`,
|
|
12980
13462
|
"",
|
|
12981
13463
|
`This will run npm install for ${feature.packageName} in the Web UI package install root.`,
|
|
12982
13464
|
"It can download code from npm and modify the local Pi/Web UI npm installation.",
|
|
13465
|
+
"Progress and failures will be shown in the optional-features row and activity log.",
|
|
12983
13466
|
"If this feature is already installed but disabled in Pi settings, cancel and enable it there instead.",
|
|
12984
13467
|
"",
|
|
12985
13468
|
"Continue?",
|
|
@@ -12987,14 +13470,20 @@ async function installOptionalFeature(featureId) {
|
|
|
12987
13470
|
if (!confirm(warning)) return;
|
|
12988
13471
|
|
|
12989
13472
|
optionalFeatureInstallInProgress.add(featureId);
|
|
13473
|
+
optionalFeatureInstallMessages.set(featureId, `${actionLabel} running via npm; waiting for package-manager output…`);
|
|
12990
13474
|
renderOptionalFeatureControls();
|
|
12991
|
-
addEvent(
|
|
13475
|
+
addEvent(`${update ? "updating" : "installing"} optional feature ${feature.label} (${feature.packageName})…`, "warn");
|
|
12992
13476
|
try {
|
|
12993
13477
|
const response = await api("/api/optional-feature-install", { method: "POST", body: { featureId }, scoped: false });
|
|
12994
13478
|
disabledOptionalFeatures.delete(featureId);
|
|
12995
13479
|
storeDisabledOptionalFeatures();
|
|
12996
|
-
|
|
12997
|
-
|
|
13480
|
+
const command = response.data?.command ? ` · ${response.data.command}` : "";
|
|
13481
|
+
optionalFeatureInstallMessages.set(featureId, `${response.data?.message || `${actionLabel} finished`}${command}`);
|
|
13482
|
+
addEvent(response.data?.message || `${update ? "updated" : "installed"} ${feature.packageName}`, "info");
|
|
13483
|
+
const output = [response.data?.stderr, response.data?.stdout].filter(Boolean).join("\n").trim();
|
|
13484
|
+
if (output) addEvent(`npm output for ${feature.packageName}:\n${output.slice(-4000)}`, "info");
|
|
13485
|
+
await refreshOptionalFeaturePackageStatuses({ announce: true });
|
|
13486
|
+
if (confirm(`${feature.label} ${actionLabel.toLowerCase()} finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
12998
13487
|
sendPrompt("prompt", "/reload");
|
|
12999
13488
|
} else {
|
|
13000
13489
|
const tabContext = activeTabContext();
|
|
@@ -13002,6 +13491,7 @@ async function installOptionalFeature(featureId) {
|
|
|
13002
13491
|
if (isCurrentTabContext(tabContext)) renderOptionalFeatureControls();
|
|
13003
13492
|
}
|
|
13004
13493
|
} catch (error) {
|
|
13494
|
+
optionalFeatureInstallMessages.set(featureId, `${actionLabel} failed: ${error.message || String(error)}`);
|
|
13005
13495
|
addEvent(error.message || String(error), "error");
|
|
13006
13496
|
} finally {
|
|
13007
13497
|
optionalFeatureInstallInProgress.delete(featureId);
|
|
@@ -14268,6 +14758,8 @@ async function refreshStats(tabContext = activeTabContext()) {
|
|
|
14268
14758
|
if (!isCurrentTabContext(tabContext)) return;
|
|
14269
14759
|
latestStats = response.data || null;
|
|
14270
14760
|
renderFooter();
|
|
14761
|
+
renderContextMeter();
|
|
14762
|
+
renderWorkspaceDashboard();
|
|
14271
14763
|
}
|
|
14272
14764
|
|
|
14273
14765
|
async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
@@ -14293,6 +14785,7 @@ async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
|
14293
14785
|
latestWorkspace = nextWorkspace;
|
|
14294
14786
|
rememberServerStartCwd(nextWorkspace?.cwd);
|
|
14295
14787
|
renderFooter();
|
|
14788
|
+
renderWorkspaceDashboard();
|
|
14296
14789
|
}
|
|
14297
14790
|
|
|
14298
14791
|
function renderNetworkStatus() {
|
|
@@ -14453,6 +14946,7 @@ async function refreshModels(tabContext = activeTabContext()) {
|
|
|
14453
14946
|
syncModelSelectToState();
|
|
14454
14947
|
renderFooter();
|
|
14455
14948
|
renderFeedbackTray();
|
|
14949
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
14456
14950
|
}
|
|
14457
14951
|
|
|
14458
14952
|
function syncModelSelectToState() {
|
|
@@ -14929,6 +15423,162 @@ async function refreshCommands(tabContext = activeTabContext()) {
|
|
|
14929
15423
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
14930
15424
|
updateOptionalFeatureAvailability();
|
|
14931
15425
|
renderCommands();
|
|
15426
|
+
if (elements.commandPaletteDialog?.open) renderCommandPalette();
|
|
15427
|
+
}
|
|
15428
|
+
|
|
15429
|
+
function paletteText(value) {
|
|
15430
|
+
return String(value || "").toLowerCase();
|
|
15431
|
+
}
|
|
15432
|
+
|
|
15433
|
+
function paletteItemMatches(item, query) {
|
|
15434
|
+
const text = [item.label, item.description, item.kind, item.keywords].map(paletteText).join(" ");
|
|
15435
|
+
return query.split(/\s+/).filter(Boolean).every((token) => text.includes(token));
|
|
15436
|
+
}
|
|
15437
|
+
|
|
15438
|
+
function commandPaletteCoreItems() {
|
|
15439
|
+
const items = [
|
|
15440
|
+
{ kind: "Action", label: "New tab", description: "Start an isolated Pi terminal in the current directory", keywords: "workspace session", run: () => createTerminalTab() },
|
|
15441
|
+
{ 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 }) },
|
|
15442
|
+
{ kind: "Action", label: "New session", description: "Start a fresh session in the active tab", keywords: "/new clear", run: () => elements.newSessionButton.click() },
|
|
15443
|
+
{ kind: "Action", label: "Compact context", description: contextUsageDetail(), keywords: "/compact context window tokens", run: () => requestManualCompaction() },
|
|
15444
|
+
{ kind: "Action", label: footerAutoCompactionEnabled() ? "Disable auto-compaction" : "Enable auto-compaction", description: footerAutoCompactionToggleAction(), keywords: "context automatic", run: () => toggleFooterAutoCompaction() },
|
|
15445
|
+
{ kind: "Action", label: workspaceDashboardCollapsed ? "Show workspace dashboard" : "Hide workspace dashboard", description: "Toggle the launch/workspace overview", keywords: "home overview", run: () => setWorkspaceDashboardCollapsed(!workspaceDashboardCollapsed) },
|
|
15446
|
+
{ 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 }) },
|
|
15447
|
+
{ kind: "Action", label: "Change working directory", description: "Restart active tab in another cwd", keywords: "cwd folder workspace", run: () => changeActiveTabCwd() },
|
|
15448
|
+
{ kind: "Action", label: "Search transcript", description: "Open transcript search", keywords: "find", run: () => openChatSearch() },
|
|
15449
|
+
{ kind: "Pi", label: "/model", description: "Select the active model", keywords: "provider llm", run: () => runNativeCommandMenu("/model") },
|
|
15450
|
+
{ kind: "Pi", label: "/resume", description: "Resume a previous session", keywords: "sessions history", run: () => runNativeCommandMenu("/resume") },
|
|
15451
|
+
{ kind: "Pi", label: "/fork", description: "Fork from a previous user message", keywords: "branch edit retry", run: () => runNativeCommandMenu("/fork") },
|
|
15452
|
+
{ kind: "Pi", label: "/tree", description: "Navigate the session tree", keywords: "branch history", run: () => runNativeCommandMenu("/tree") },
|
|
15453
|
+
{ kind: "Pi", label: "/settings", description: "Open settings", keywords: "configuration", run: () => runNativeCommandMenu("/settings") },
|
|
15454
|
+
{ kind: "Pi", label: "/scoped-models", description: "Manage model cycling scope", keywords: "models cycle ctrl p", run: () => runNativeCommandMenu("/scoped-models") },
|
|
15455
|
+
{ kind: "Pi", label: "/tools", description: "Manage active tools", keywords: "capabilities", run: () => runNativeCommandMenu("/tools") },
|
|
15456
|
+
{ kind: "Pi", label: "/skills", description: "Manage active skills", keywords: "system prompt", run: () => runNativeCommandMenu("/skills") },
|
|
15457
|
+
];
|
|
15458
|
+
if (isOptionalFeatureEnabled("statsCommand")) items.push({ kind: "Pi", label: "/stats-webui", description: "Open usage dashboard", keywords: "tokens cost budget", run: () => openStatsOverlay({ refresh: true }) });
|
|
15459
|
+
return items;
|
|
15460
|
+
}
|
|
15461
|
+
|
|
15462
|
+
function commandPaletteTabItems() {
|
|
15463
|
+
return tabs.map((tab) => {
|
|
15464
|
+
const indicator = tabIndicator(tab);
|
|
15465
|
+
return {
|
|
15466
|
+
kind: "Tab",
|
|
15467
|
+
label: tab.id === activeTabId ? `Current tab: ${tab.title}` : `Switch to tab: ${tab.title}`,
|
|
15468
|
+
description: `${indicator.label} · ${normalizeDisplayPath(tab.cwd || "")}`,
|
|
15469
|
+
keywords: `${tab.id} ${tab.cwd || ""}`,
|
|
15470
|
+
run: () => switchTab(tab.id),
|
|
15471
|
+
};
|
|
15472
|
+
});
|
|
15473
|
+
}
|
|
15474
|
+
|
|
15475
|
+
function commandPaletteModelItems() {
|
|
15476
|
+
return availableModels.map((model) => ({
|
|
15477
|
+
kind: "Model",
|
|
15478
|
+
label: `${model.provider}/${model.id}`,
|
|
15479
|
+
description: model.name || (model.contextWindow ? `context ${formatFooterTokenCount(model.contextWindow)}` : "Set active model"),
|
|
15480
|
+
keywords: `${model.provider} ${model.id} ${model.name || ""}`,
|
|
15481
|
+
run: async () => {
|
|
15482
|
+
const tabContext = activeTabContext();
|
|
15483
|
+
const response = await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
|
|
15484
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
15485
|
+
applyOptimisticModelSelection(response.data || model, tabContext);
|
|
15486
|
+
await refreshState(tabContext);
|
|
15487
|
+
await refreshModels(tabContext);
|
|
15488
|
+
},
|
|
15489
|
+
}));
|
|
15490
|
+
}
|
|
15491
|
+
|
|
15492
|
+
function commandPaletteSlashItems() {
|
|
15493
|
+
return visibleCommands().slice(0, 140).map((command) => ({
|
|
15494
|
+
kind: command.source || "Command",
|
|
15495
|
+
label: `/${command.name}`,
|
|
15496
|
+
description: command.description || "Run slash command",
|
|
15497
|
+
keywords: `${command.location || ""} ${command.path || ""}`,
|
|
15498
|
+
run: () => sendPrompt("prompt", `/${command.name}`),
|
|
15499
|
+
}));
|
|
15500
|
+
}
|
|
15501
|
+
|
|
15502
|
+
function buildCommandPaletteItems() {
|
|
15503
|
+
return [
|
|
15504
|
+
...commandPaletteCoreItems(),
|
|
15505
|
+
...commandPaletteTabItems(),
|
|
15506
|
+
...commandPaletteModelItems(),
|
|
15507
|
+
...commandPaletteSlashItems(),
|
|
15508
|
+
];
|
|
15509
|
+
}
|
|
15510
|
+
|
|
15511
|
+
function filteredCommandPaletteItems() {
|
|
15512
|
+
const query = paletteText(elements.commandPaletteInput?.value || "").trim();
|
|
15513
|
+
const items = buildCommandPaletteItems();
|
|
15514
|
+
return (query ? items.filter((item) => paletteItemMatches(item, query)) : items).slice(0, 80);
|
|
15515
|
+
}
|
|
15516
|
+
|
|
15517
|
+
function setCommandPaletteIndex(index) {
|
|
15518
|
+
const count = commandPaletteItems.length;
|
|
15519
|
+
commandPaletteIndex = count ? (index + count) % count : 0;
|
|
15520
|
+
renderCommandPaletteList();
|
|
15521
|
+
}
|
|
15522
|
+
|
|
15523
|
+
function renderCommandPaletteList() {
|
|
15524
|
+
const list = elements.commandPaletteList;
|
|
15525
|
+
if (!list) return;
|
|
15526
|
+
list.replaceChildren();
|
|
15527
|
+
if (!commandPaletteItems.length) {
|
|
15528
|
+
list.append(make("div", "command-palette-empty muted", "No matching actions."));
|
|
15529
|
+
return;
|
|
15530
|
+
}
|
|
15531
|
+
commandPaletteItems.forEach((item, index) => {
|
|
15532
|
+
const button = make("button", `command-palette-item${index === commandPaletteIndex ? " active" : ""}`);
|
|
15533
|
+
button.type = "button";
|
|
15534
|
+
button.setAttribute("role", "option");
|
|
15535
|
+
button.setAttribute("aria-selected", index === commandPaletteIndex ? "true" : "false");
|
|
15536
|
+
button.addEventListener("click", () => executeCommandPaletteItem(item));
|
|
15537
|
+
button.append(
|
|
15538
|
+
make("span", "command-palette-item-kind", item.kind || "Action"),
|
|
15539
|
+
make("span", "command-palette-item-label", item.label || "Untitled action"),
|
|
15540
|
+
make("span", "command-palette-item-description", item.description || ""),
|
|
15541
|
+
);
|
|
15542
|
+
list.append(button);
|
|
15543
|
+
});
|
|
15544
|
+
const active = list.children[commandPaletteIndex];
|
|
15545
|
+
active?.scrollIntoView({ block: "nearest" });
|
|
15546
|
+
}
|
|
15547
|
+
|
|
15548
|
+
function renderCommandPalette() {
|
|
15549
|
+
commandPaletteItems = filteredCommandPaletteItems();
|
|
15550
|
+
if (commandPaletteIndex >= commandPaletteItems.length) commandPaletteIndex = 0;
|
|
15551
|
+
renderCommandPaletteList();
|
|
15552
|
+
}
|
|
15553
|
+
|
|
15554
|
+
function openCommandPalette(initialQuery = "") {
|
|
15555
|
+
setComposerActionsOpen(false);
|
|
15556
|
+
setPublishMenuOpen(false);
|
|
15557
|
+
setNativeCommandMenuOpen(false);
|
|
15558
|
+
setAppRunnerMenuOpen(false);
|
|
15559
|
+
setOptionsMenuOpen(false);
|
|
15560
|
+
if (elements.commandPaletteInput) elements.commandPaletteInput.value = initialQuery;
|
|
15561
|
+
commandPaletteIndex = 0;
|
|
15562
|
+
renderCommandPalette();
|
|
15563
|
+
if (!elements.commandPaletteDialog.open) elements.commandPaletteDialog.showModal();
|
|
15564
|
+
queueMicrotask(() => {
|
|
15565
|
+
elements.commandPaletteInput?.focus();
|
|
15566
|
+
elements.commandPaletteInput?.select();
|
|
15567
|
+
});
|
|
15568
|
+
}
|
|
15569
|
+
|
|
15570
|
+
function closeCommandPalette() {
|
|
15571
|
+
if (elements.commandPaletteDialog?.open) elements.commandPaletteDialog.close();
|
|
15572
|
+
}
|
|
15573
|
+
|
|
15574
|
+
async function executeCommandPaletteItem(item = commandPaletteItems[commandPaletteIndex]) {
|
|
15575
|
+
if (!item) return;
|
|
15576
|
+
closeCommandPalette();
|
|
15577
|
+
try {
|
|
15578
|
+
await item.run?.();
|
|
15579
|
+
} catch (error) {
|
|
15580
|
+
addEvent(error.message || String(error), "error");
|
|
15581
|
+
}
|
|
14932
15582
|
}
|
|
14933
15583
|
|
|
14934
15584
|
async function refreshAll(tabContext = activeTabContext()) {
|
|
@@ -16066,6 +16716,8 @@ elements.newTabMenu?.addEventListener("focusout", () => {
|
|
|
16066
16716
|
elements.newTabCurrentDirectoryButton?.addEventListener("click", () => createTerminalTab(currentDirectoryForNewTab(), { triggerButton: elements.newTabCurrentDirectoryButton }));
|
|
16067
16717
|
elements.newTabChooseDirectoryButton?.addEventListener("click", () => createTerminalTabFromChosenDirectory({ triggerButton: elements.newTabChooseDirectoryButton }));
|
|
16068
16718
|
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
16719
|
+
elements.commandPaletteButton?.addEventListener("click", () => openCommandPalette());
|
|
16720
|
+
elements.workspaceDashboardToggleButton?.addEventListener("click", () => setWorkspaceDashboardCollapsed(!workspaceDashboardCollapsed));
|
|
16069
16721
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
16070
16722
|
setComposerActionsOpen(false);
|
|
16071
16723
|
startGitWorkflow();
|
|
@@ -16191,6 +16843,7 @@ elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/r
|
|
|
16191
16843
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
16192
16844
|
elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
|
|
16193
16845
|
elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu("/tools"));
|
|
16846
|
+
elements.optionsCommandPaletteButton.addEventListener("click", () => openCommandPalette());
|
|
16194
16847
|
elements.optionsResumeButton.addEventListener("click", () => runNativeCommandMenu("/resume"));
|
|
16195
16848
|
elements.optionsReloadButton.addEventListener("click", () => runNativeCommandMenu("/reload"));
|
|
16196
16849
|
elements.optionsNameButton.addEventListener("click", () => runNativeCommandMenu("/name"));
|
|
@@ -16248,6 +16901,42 @@ elements.nativeCommandDialog.addEventListener("close", () => {
|
|
|
16248
16901
|
elements.nativeCommandSearch.oninput = null;
|
|
16249
16902
|
nativeCommandTabId = null;
|
|
16250
16903
|
});
|
|
16904
|
+
elements.commandPaletteDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
16905
|
+
elements.commandPaletteDialog?.addEventListener("cancel", (event) => {
|
|
16906
|
+
event.preventDefault();
|
|
16907
|
+
closeCommandPalette();
|
|
16908
|
+
});
|
|
16909
|
+
elements.commandPaletteInput?.addEventListener("input", () => {
|
|
16910
|
+
commandPaletteIndex = 0;
|
|
16911
|
+
renderCommandPalette();
|
|
16912
|
+
});
|
|
16913
|
+
elements.commandPaletteInput?.addEventListener("keydown", (event) => {
|
|
16914
|
+
if (event.key === "ArrowDown") {
|
|
16915
|
+
event.preventDefault();
|
|
16916
|
+
setCommandPaletteIndex(commandPaletteIndex + 1);
|
|
16917
|
+
} else if (event.key === "ArrowUp") {
|
|
16918
|
+
event.preventDefault();
|
|
16919
|
+
setCommandPaletteIndex(commandPaletteIndex - 1);
|
|
16920
|
+
} else if (event.key === "Enter") {
|
|
16921
|
+
event.preventDefault();
|
|
16922
|
+
executeCommandPaletteItem();
|
|
16923
|
+
} else if (event.key === "Escape") {
|
|
16924
|
+
event.preventDefault();
|
|
16925
|
+
closeCommandPalette();
|
|
16926
|
+
}
|
|
16927
|
+
});
|
|
16928
|
+
elements.editRetryDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
16929
|
+
elements.editRetryDialog?.addEventListener("cancel", (event) => {
|
|
16930
|
+
event.preventDefault();
|
|
16931
|
+
closeEditRetryDialog();
|
|
16932
|
+
});
|
|
16933
|
+
elements.editRetryDialog?.addEventListener("close", () => {
|
|
16934
|
+
activeEditRetry = null;
|
|
16935
|
+
setEditRetryBusy(false);
|
|
16936
|
+
});
|
|
16937
|
+
elements.editRetryCancelButton?.addEventListener("click", closeEditRetryDialog);
|
|
16938
|
+
elements.editRetryForkButton?.addEventListener("click", () => submitEditRetry({ send: false }));
|
|
16939
|
+
elements.editRetrySendButton?.addEventListener("click", () => submitEditRetry({ send: true }));
|
|
16251
16940
|
|
|
16252
16941
|
function resetAbortLongPressAffordance() {
|
|
16253
16942
|
clearTimeout(abortLongPressTimer);
|
|
@@ -16333,33 +17022,7 @@ elements.newSessionButton.addEventListener("click", async () => {
|
|
|
16333
17022
|
});
|
|
16334
17023
|
elements.compactButton.addEventListener("click", async () => {
|
|
16335
17024
|
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
|
-
}
|
|
17025
|
+
await requestManualCompaction({ triggerButton: elements.compactButton });
|
|
16363
17026
|
});
|
|
16364
17027
|
elements.setModelButton.addEventListener("click", async () => {
|
|
16365
17028
|
if (!elements.modelSelect.value) return;
|
|
@@ -16504,7 +17167,7 @@ function isTextEntryTarget(target) {
|
|
|
16504
17167
|
|
|
16505
17168
|
function shouldHandleNativeAppShortcut(event) {
|
|
16506
17169
|
if (event.defaultPrevented) return false;
|
|
16507
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
17170
|
+
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
17171
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
16509
17172
|
}
|
|
16510
17173
|
|
|
@@ -16514,6 +17177,11 @@ function handleNativeAppShortcut(event) {
|
|
|
16514
17177
|
const lowerKey = String(key || "").toLowerCase();
|
|
16515
17178
|
const ctrlOrMeta = event.ctrlKey || event.metaKey;
|
|
16516
17179
|
|
|
17180
|
+
if (ctrlOrMeta && !event.altKey && !event.shiftKey && lowerKey === "k") {
|
|
17181
|
+
event.preventDefault();
|
|
17182
|
+
openCommandPalette();
|
|
17183
|
+
return;
|
|
17184
|
+
}
|
|
16517
17185
|
if (ctrlOrMeta && !event.altKey && lowerKey === "l") {
|
|
16518
17186
|
event.preventDefault();
|
|
16519
17187
|
openNativeModelSelector();
|
|
@@ -16675,7 +17343,7 @@ window.addEventListener("focus", () => scheduleForegroundReconcile("window focus
|
|
|
16675
17343
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
16676
17344
|
window.addEventListener("keydown", (event) => {
|
|
16677
17345
|
if (event.key !== "Escape") return;
|
|
16678
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open) return;
|
|
17346
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.commandPaletteDialog?.open || elements.editRetryDialog?.open) return;
|
|
16679
17347
|
if (publishMenuOpen) {
|
|
16680
17348
|
setPublishMenuOpen(false);
|
|
16681
17349
|
return;
|
|
@@ -16866,6 +17534,7 @@ restoreStoredSkillUsage();
|
|
|
16866
17534
|
restoreBusyPromptBehaviorSetting();
|
|
16867
17535
|
updateComposerModeButtons();
|
|
16868
17536
|
updateOptionalFeatureAvailability();
|
|
17537
|
+
refreshOptionalFeaturePackageStatuses({ announce: true });
|
|
16869
17538
|
renderAppRunnerControls();
|
|
16870
17539
|
renderLoadedPromptListPreview();
|
|
16871
17540
|
loadLastUserPromptCache();
|
|
@@ -16882,6 +17551,7 @@ restoreAgentDoneNotificationsSetting();
|
|
|
16882
17551
|
restoreThinkingVisibilitySetting();
|
|
16883
17552
|
restoreTerminalTabsLayoutSetting();
|
|
16884
17553
|
restoreToolOutputExpansionSetting();
|
|
17554
|
+
restoreWorkspaceDashboardState();
|
|
16885
17555
|
restoreSidePanelSectionState();
|
|
16886
17556
|
bindSidePanelSectionToggles();
|
|
16887
17557
|
restoreSidePanelState();
|