@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/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
- commitButton.disabled = !currentWorkflow || !!currentWorkflow.busy || !input.value.trim();
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" ? String(workflow.manualCommitMessage || "").trim() : "";
10092
+ const inputMessage = variant === "input" ? gitWorkflowManualCommitInputMessage(workflow) : "";
9646
10093
  if (variant === "input" && !inputMessage) {
9647
- failGitWorkflow(new Error("Type a commit message before using Commit input."), failureStep, { tabId });
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
- if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: "Detected and enabled in Web UI" };
12858
- if (detected && disabled) return { label: "Disabled", className: "disabled", detail: "Detected, but disabled in Web UI" };
12859
- return { label: "Install needed", className: "missing", detail: "Not detected in the active Pi tab" };
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
- `Install optional feature: ${feature.label}?`,
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(`installing optional feature ${feature.label} (${feature.packageName})…`, "warn");
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
- addEvent(response.data?.message || `installed ${feature.packageName}`, "info");
12997
- if (confirm(`${feature.label} install finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
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
- elements.networkStatus.replaceChildren(heading, detail, list);
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("Open Pi Web UI to your local network?\n\nThe Web UI has no authentication and can control Pi/tools. Only do this on a trusted LAN.")) return;
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
- const tabContext = activeTabContext();
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();