@firstpick/pi-package-webui 0.2.1 → 0.2.3
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 +4 -2
- package/WEBUI_TUI_NATIVE_PARITY.json +26 -0
- package/bin/pi-webui.mjs +371 -4
- package/package.json +10 -3
- package/public/app.js +409 -35
- package/public/index.html +26 -6
- package/public/service-worker.js +1 -1
- package/public/styles.css +225 -7
- package/start-webui.sh +9 -48
- package/tests/mobile-static.test.mjs +47 -6
- package/tests/native-parity.test.mjs +14 -0
- package/webui-rpc-helper.mjs +231 -0
package/public/app.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
const $ = (selector) => document.querySelector(selector);
|
|
2
2
|
|
|
3
3
|
const elements = {
|
|
4
|
-
|
|
4
|
+
webuiVersionBadge: $("#webuiVersionBadge"),
|
|
5
|
+
webuiDevBadge: $("#webuiDevBadge"),
|
|
5
6
|
tabBar: $("#tabBar"),
|
|
6
7
|
terminalTabsToggleButton: $("#terminalTabsToggleButton"),
|
|
7
8
|
newTabButton: $("#newTabButton"),
|
|
8
9
|
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
9
10
|
statusBar: $("#statusBar"),
|
|
10
11
|
serverOfflinePanel: $("#serverOfflinePanel"),
|
|
12
|
+
serverRestartPanel: $("#serverRestartPanel"),
|
|
13
|
+
serverRestartMessage: $("#serverRestartMessage"),
|
|
11
14
|
serverOfflineCommand: $("#serverOfflineCommand"),
|
|
12
15
|
serverOfflineSlashCommand: $("#serverOfflineSlashCommand"),
|
|
13
16
|
copyServerCommandButton: $("#copyServerCommandButton"),
|
|
@@ -60,7 +63,9 @@ const elements = {
|
|
|
60
63
|
backgroundStatus: $("#backgroundStatus"),
|
|
61
64
|
networkStatus: $("#networkStatus"),
|
|
62
65
|
openNetworkButton: $("#openNetworkButton"),
|
|
63
|
-
|
|
66
|
+
serverActionSelect: $("#serverActionSelect"),
|
|
67
|
+
runServerActionButton: $("#runServerActionButton"),
|
|
68
|
+
serverActionStatus: $("#serverActionStatus"),
|
|
64
69
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
65
70
|
agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
|
|
66
71
|
optionalFeaturesBox: $("#optionalFeaturesBox"),
|
|
@@ -139,6 +144,7 @@ let pathFastPicksLoadPromise = null;
|
|
|
139
144
|
let mobileTabsExpanded = false;
|
|
140
145
|
let openTerminalTabGroupKey = null;
|
|
141
146
|
let availableCommands = [];
|
|
147
|
+
let rawAvailableCommands = [];
|
|
142
148
|
let commandSuggestions = [];
|
|
143
149
|
let pathSuggestions = [];
|
|
144
150
|
let suggestionMode = "none";
|
|
@@ -150,12 +156,15 @@ let pathSuggestAbortController = null;
|
|
|
150
156
|
let latestStats = null;
|
|
151
157
|
let latestWorkspace = null;
|
|
152
158
|
let latestNetwork = null;
|
|
159
|
+
let webuiVersion = "";
|
|
160
|
+
let webuiDevServer = false;
|
|
153
161
|
let latestCodexUsage = null;
|
|
154
162
|
let codexUsageError = null;
|
|
155
163
|
let codexUsageLoading = false;
|
|
156
164
|
let refreshCodexUsageTimer = null;
|
|
157
165
|
let codexUsageRenderTimer = null;
|
|
158
166
|
let backendOffline = false;
|
|
167
|
+
let serverRestartInProgress = false;
|
|
159
168
|
let backendOfflineNoticeShown = false;
|
|
160
169
|
let latestMessages = [];
|
|
161
170
|
let promptHistoryByTab = new Map();
|
|
@@ -263,6 +272,7 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
|
263
272
|
const statusEntries = new Map();
|
|
264
273
|
const widgets = new Map();
|
|
265
274
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
275
|
+
const releaseNpmOutputExpandedByTab = new Map();
|
|
266
276
|
const liveToolRuns = new Map();
|
|
267
277
|
const liveToolCards = new Map();
|
|
268
278
|
const liveToolRenderQueue = new Map();
|
|
@@ -276,7 +286,9 @@ const optionalFeatureAvailability = {
|
|
|
276
286
|
releaseAur: false,
|
|
277
287
|
statsCommand: false,
|
|
278
288
|
gitFooterStatus: false,
|
|
289
|
+
tuiSkillsCommand: false,
|
|
279
290
|
todoProgressWidget: false,
|
|
291
|
+
tuiToolsCommand: false,
|
|
280
292
|
themeBundle: false,
|
|
281
293
|
};
|
|
282
294
|
const OPTIONAL_FEATURES = [
|
|
@@ -301,6 +313,13 @@ const OPTIONAL_FEATURES = [
|
|
|
301
313
|
capabilityLabel: "/release-aur",
|
|
302
314
|
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
303
315
|
},
|
|
316
|
+
{
|
|
317
|
+
id: "tuiSkillsCommand",
|
|
318
|
+
label: "TUI Skills command",
|
|
319
|
+
packageName: "@firstpick/pi-extension-setup-skills",
|
|
320
|
+
capabilityLabel: "RPC /skills from setup-skills extension",
|
|
321
|
+
description: "Terminal-native skill setup command alongside WebUI-native /skills toggles.",
|
|
322
|
+
},
|
|
304
323
|
{
|
|
305
324
|
id: "todoProgressWidget",
|
|
306
325
|
label: "Todo progress widget",
|
|
@@ -308,6 +327,13 @@ const OPTIONAL_FEATURES = [
|
|
|
308
327
|
capabilityLabel: "/todo-progress-status or todo-progress widget event",
|
|
309
328
|
description: "Styled live checklist rendering for assistant todo updates.",
|
|
310
329
|
},
|
|
330
|
+
{
|
|
331
|
+
id: "tuiToolsCommand",
|
|
332
|
+
label: "TUI Tools command",
|
|
333
|
+
packageName: "@firstpick/pi-extension-tools",
|
|
334
|
+
capabilityLabel: "RPC /tools from tools extension",
|
|
335
|
+
description: "Terminal-native active-tool manager alongside WebUI-native /tools toggles.",
|
|
336
|
+
},
|
|
311
337
|
{
|
|
312
338
|
id: "gitFooterStatus",
|
|
313
339
|
label: "Git footer status",
|
|
@@ -339,8 +365,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
339
365
|
["git-footer-refresh", "gitFooterStatus"],
|
|
340
366
|
["todo-progress-status", "todoProgressWidget"],
|
|
341
367
|
]);
|
|
342
|
-
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
343
|
-
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
368
|
+
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
|
|
369
|
+
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
|
|
344
370
|
const optionalFeatureInstallInProgress = new Set();
|
|
345
371
|
|
|
346
372
|
function createGitWorkflowState() {
|
|
@@ -899,13 +925,22 @@ function renderServerOfflinePanel() {
|
|
|
899
925
|
if (elements.serverOfflineSlashCommand) elements.serverOfflineSlashCommand.textContent = serverStartSlashCommandText();
|
|
900
926
|
}
|
|
901
927
|
|
|
928
|
+
function setServerRestartOverlay(active, message = "Waiting for the server to come back…") {
|
|
929
|
+
serverRestartInProgress = !!active;
|
|
930
|
+
document.body.classList.toggle("server-restarting", serverRestartInProgress);
|
|
931
|
+
if (elements.serverRestartPanel) elements.serverRestartPanel.hidden = !serverRestartInProgress;
|
|
932
|
+
if (elements.serverRestartMessage) elements.serverRestartMessage.textContent = message;
|
|
933
|
+
if (serverRestartInProgress && elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = true;
|
|
934
|
+
}
|
|
935
|
+
|
|
902
936
|
function setBackendOffline(offline, error) {
|
|
903
937
|
backendOffline = !!offline;
|
|
904
|
-
|
|
905
|
-
|
|
938
|
+
const showOfflinePanel = backendOffline && !serverRestartInProgress;
|
|
939
|
+
document.body.classList.toggle("server-offline", showOfflinePanel);
|
|
940
|
+
if (elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = !showOfflinePanel;
|
|
906
941
|
renderServerOfflinePanel();
|
|
907
942
|
if (backendOffline) {
|
|
908
|
-
if (!backendOfflineNoticeShown) {
|
|
943
|
+
if (!serverRestartInProgress && !backendOfflineNoticeShown) {
|
|
909
944
|
backendOfflineNoticeShown = true;
|
|
910
945
|
addEvent(`Pi Web UI server is offline${error?.message ? `: ${error.message}` : ""}`, "warn");
|
|
911
946
|
}
|
|
@@ -1015,6 +1050,52 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
|
|
|
1015
1050
|
return data;
|
|
1016
1051
|
}
|
|
1017
1052
|
|
|
1053
|
+
function formatWebuiVersion(version) {
|
|
1054
|
+
const text = String(version || "").trim();
|
|
1055
|
+
if (!text) return "";
|
|
1056
|
+
return text.startsWith("v") ? text : `v${text}`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function isWebuiDevMetadata(data) {
|
|
1060
|
+
return data?.webuiDev === true || String(data?.webuiMode || "").toLowerCase() === "dev";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function renderWebuiVersion() {
|
|
1064
|
+
const badge = elements.webuiVersionBadge;
|
|
1065
|
+
if (!badge) return;
|
|
1066
|
+
const label = formatWebuiVersion(webuiVersion);
|
|
1067
|
+
badge.hidden = !label;
|
|
1068
|
+
badge.textContent = label;
|
|
1069
|
+
if (label) badge.title = `Pi Web UI ${label}`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function renderWebuiDevBadge() {
|
|
1073
|
+
const badge = elements.webuiDevBadge;
|
|
1074
|
+
if (!badge) return;
|
|
1075
|
+
badge.hidden = !webuiDevServer;
|
|
1076
|
+
badge.title = "Pi Web UI dev server";
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function setWebuiVersion(version) {
|
|
1080
|
+
const text = String(version || "").trim();
|
|
1081
|
+
if (text === webuiVersion) return;
|
|
1082
|
+
webuiVersion = text;
|
|
1083
|
+
renderWebuiVersion();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function setWebuiDevServer(dev) {
|
|
1087
|
+
const next = !!dev;
|
|
1088
|
+
if (next === webuiDevServer) return;
|
|
1089
|
+
webuiDevServer = next;
|
|
1090
|
+
renderWebuiDevBadge();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async function refreshWebuiVersion() {
|
|
1094
|
+
const health = await api("/api/health", { scoped: false });
|
|
1095
|
+
setWebuiVersion(health.webuiVersion);
|
|
1096
|
+
setWebuiDevServer(isWebuiDevMetadata(health));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1018
1099
|
function formatBytes(bytes) {
|
|
1019
1100
|
const value = Number(bytes) || 0;
|
|
1020
1101
|
if (value < 1024) return `${value} B`;
|
|
@@ -2334,6 +2415,7 @@ function resetActiveTabUi() {
|
|
|
2334
2415
|
liveToolRuns.clear();
|
|
2335
2416
|
liveToolCards.clear();
|
|
2336
2417
|
availableCommands = [];
|
|
2418
|
+
rawAvailableCommands = [];
|
|
2337
2419
|
resetOptionalFeatureAvailability();
|
|
2338
2420
|
commandSuggestions = [];
|
|
2339
2421
|
pathSuggestions = [];
|
|
@@ -2357,7 +2439,6 @@ function resetActiveTabUi() {
|
|
|
2357
2439
|
}
|
|
2358
2440
|
elements.commandsBox.textContent = "Loading…";
|
|
2359
2441
|
elements.commandsBox.classList.add("muted");
|
|
2360
|
-
elements.sessionLine.textContent = activeTab() ? "Connecting…" : "No terminal tabs.";
|
|
2361
2442
|
renderWidgets();
|
|
2362
2443
|
renderGitWorkflow();
|
|
2363
2444
|
renderFooter();
|
|
@@ -3899,13 +3980,6 @@ function renderStatus() {
|
|
|
3899
3980
|
updateComposerModeButtons();
|
|
3900
3981
|
const running = state?.isStreaming ? "running" : "idle";
|
|
3901
3982
|
const compacting = state?.isCompacting ? " · compacting" : "";
|
|
3902
|
-
const queue = state?.pendingMessageCount ? ` · queued ${state.pendingMessageCount}` : "";
|
|
3903
|
-
const extra = [...statusEntries.entries()].map(([key, value]) => formatStatusEntry(key, value)).filter(Boolean).join(" · ");
|
|
3904
|
-
const statusText = state?.isStreaming ? "Running" : "Idle";
|
|
3905
|
-
const compactingText = state?.isCompacting ? " · Compacting" : "";
|
|
3906
|
-
const queueText = state?.pendingMessageCount ? ` · Queue: ${state.pendingMessageCount}` : "";
|
|
3907
|
-
|
|
3908
|
-
elements.sessionLine.textContent = `Status: ${statusText}${compactingText}${queueText}${extra ? ` · ${extra}` : ""} · Model: ${modelLabel(state?.model)} · Session: ${shortSessionLabel(state)}`;
|
|
3909
3983
|
|
|
3910
3984
|
elements.stateDetails.replaceChildren();
|
|
3911
3985
|
const details = {
|
|
@@ -4181,6 +4255,26 @@ function releaseNpmStreamHeader(label, lineCount, { live = false } = {}) {
|
|
|
4181
4255
|
return header;
|
|
4182
4256
|
}
|
|
4183
4257
|
|
|
4258
|
+
function renderReleaseNpmOutputDetails(key, streamHeader, terminal, controls = null) {
|
|
4259
|
+
const tabId = activeTabId || "default";
|
|
4260
|
+
const stateKey = `${tabId}:${key}`;
|
|
4261
|
+
const node = make("details", "release-npm-output-details");
|
|
4262
|
+
node.open = releaseNpmOutputExpandedByTab.get(stateKey) !== false;
|
|
4263
|
+
node.addEventListener("toggle", () => {
|
|
4264
|
+
releaseNpmOutputExpandedByTab.set(stateKey, node.open);
|
|
4265
|
+
if (node.open) requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
4266
|
+
});
|
|
4267
|
+
|
|
4268
|
+
const summary = make("summary", "release-npm-output-summary");
|
|
4269
|
+
summary.title = "Expand or collapse command output in the Web UI";
|
|
4270
|
+
const toggle = make("span", "release-npm-output-toggle", "›");
|
|
4271
|
+
toggle.setAttribute("aria-hidden", "true");
|
|
4272
|
+
summary.append(toggle, streamHeader);
|
|
4273
|
+
node.append(summary, terminal);
|
|
4274
|
+
if (controls) node.append(controls);
|
|
4275
|
+
return node;
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4184
4278
|
function renderReleaseNpmOutputWidget() {
|
|
4185
4279
|
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
4186
4280
|
const outputLines = getWidgetLines("release-npm:output");
|
|
@@ -4216,8 +4310,9 @@ function renderReleaseNpmOutputWidget() {
|
|
|
4216
4310
|
}
|
|
4217
4311
|
|
|
4218
4312
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
|
|
4219
|
-
|
|
4220
|
-
|
|
4313
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-npm:output", streamHeader, terminal, controls);
|
|
4314
|
+
node.append(header, outputDetails);
|
|
4315
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4221
4316
|
return node;
|
|
4222
4317
|
}
|
|
4223
4318
|
|
|
@@ -4246,8 +4341,9 @@ function renderReleaseNpmLogWidget() {
|
|
|
4246
4341
|
for (const line of logLines) {
|
|
4247
4342
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4248
4343
|
}
|
|
4249
|
-
|
|
4250
|
-
|
|
4344
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-npm:logs", streamHeader, terminal);
|
|
4345
|
+
node.append(header, outputDetails);
|
|
4346
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4251
4347
|
return node;
|
|
4252
4348
|
}
|
|
4253
4349
|
|
|
@@ -4286,8 +4382,9 @@ function renderReleaseAurOutputWidget() {
|
|
|
4286
4382
|
}
|
|
4287
4383
|
|
|
4288
4384
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
|
|
4289
|
-
|
|
4290
|
-
|
|
4385
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-aur:output", streamHeader, terminal, controls);
|
|
4386
|
+
node.append(header, outputDetails);
|
|
4387
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4291
4388
|
return node;
|
|
4292
4389
|
}
|
|
4293
4390
|
|
|
@@ -4316,8 +4413,9 @@ function renderReleaseAurLogWidget() {
|
|
|
4316
4413
|
for (const line of logLines) {
|
|
4317
4414
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4318
4415
|
}
|
|
4319
|
-
|
|
4320
|
-
|
|
4416
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-aur:logs", streamHeader, terminal);
|
|
4417
|
+
node.append(header, outputDetails);
|
|
4418
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4321
4419
|
return node;
|
|
4322
4420
|
}
|
|
4323
4421
|
|
|
@@ -6850,6 +6948,7 @@ function optionalFeatureIdForCommand(name) {
|
|
|
6850
6948
|
|
|
6851
6949
|
function isCommandVisible(command) {
|
|
6852
6950
|
if (HIDDEN_COMMAND_NAMES.has(command.name)) return false;
|
|
6951
|
+
if (command.enabled === false) return false;
|
|
6853
6952
|
const featureId = optionalFeatureIdForCommand(command.name);
|
|
6854
6953
|
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
6855
6954
|
}
|
|
@@ -6862,6 +6961,10 @@ function hasAvailableCommand(name) {
|
|
|
6862
6961
|
return availableCommands.some((command) => command.name === name);
|
|
6863
6962
|
}
|
|
6864
6963
|
|
|
6964
|
+
function hasLoadedRpcCommand(name) {
|
|
6965
|
+
return rawAvailableCommands.some((command) => command.name === name && command.source !== "native");
|
|
6966
|
+
}
|
|
6967
|
+
|
|
6865
6968
|
function optionalFeatureUnavailableMessage(featureId) {
|
|
6866
6969
|
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
6867
6970
|
if (!feature) return "Optional feature unavailable.";
|
|
@@ -6908,7 +7011,9 @@ function updateOptionalFeatureAvailability() {
|
|
|
6908
7011
|
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
6909
7012
|
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
6910
7013
|
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer");
|
|
7014
|
+
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
6911
7015
|
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
7016
|
+
optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
|
|
6912
7017
|
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
6913
7018
|
renderOptionalFeatureControls();
|
|
6914
7019
|
}
|
|
@@ -7051,7 +7156,7 @@ function runPublishWorkflow(command) {
|
|
|
7051
7156
|
|
|
7052
7157
|
function slashCommandName(message) {
|
|
7053
7158
|
const match = String(message || "").trim().match(/^\/([^\s]+)$/);
|
|
7054
|
-
return match ? match[1] : "";
|
|
7159
|
+
return match ? match[1].toLowerCase() : "";
|
|
7055
7160
|
}
|
|
7056
7161
|
|
|
7057
7162
|
function openNativeCommandDialog({ title, message = "", searchPlaceholder = "" } = {}) {
|
|
@@ -7124,7 +7229,17 @@ function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect,
|
|
|
7124
7229
|
button.addEventListener("click", () => onSelect?.(item));
|
|
7125
7230
|
const title = make("span", "native-selector-title");
|
|
7126
7231
|
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
7127
|
-
if (item.badge)
|
|
7232
|
+
if (item.badge) {
|
|
7233
|
+
const badgeState = String(item.badge).toLowerCase();
|
|
7234
|
+
const badge = make("span", `native-selector-badge${item.badgeClass ? ` ${item.badgeClass}` : ""}`, item.badge);
|
|
7235
|
+
badge.dataset.badgeState = badgeState;
|
|
7236
|
+
if (badgeState === "disabled" || String(item.badgeClass || "").includes("disabled")) {
|
|
7237
|
+
badge.style.borderColor = "rgba(255, 159, 67, 0.62)";
|
|
7238
|
+
badge.style.color = "#ff9f43";
|
|
7239
|
+
badge.style.background = "rgba(255, 159, 67, 0.10)";
|
|
7240
|
+
}
|
|
7241
|
+
title.append(badge);
|
|
7242
|
+
}
|
|
7128
7243
|
const detail = make("span", "native-selector-detail", item.description || "");
|
|
7129
7244
|
const meta = make("span", "native-selector-meta", item.meta || "");
|
|
7130
7245
|
button.append(title);
|
|
@@ -7458,6 +7573,145 @@ function openNativeScopedModelsInfo() {
|
|
|
7458
7573
|
elements.nativeCommandBody.append(make("p", "native-command-note", "Use the footer model chip to choose among scoped models. The full native scoped-models editor is still TUI-only."));
|
|
7459
7574
|
}
|
|
7460
7575
|
|
|
7576
|
+
function nativeResourceSourceLabel(resource) {
|
|
7577
|
+
const info = resource?.sourceInfo || {};
|
|
7578
|
+
return [info.source, info.scope, info.origin].filter(Boolean).join(" · ") || resource?.location || "loaded resource";
|
|
7579
|
+
}
|
|
7580
|
+
|
|
7581
|
+
function nativeResourceCounts(resources) {
|
|
7582
|
+
const disabled = resources.filter((resource) => resource.enabled === false).length;
|
|
7583
|
+
return { total: resources.length, disabled, enabled: resources.length - disabled };
|
|
7584
|
+
}
|
|
7585
|
+
|
|
7586
|
+
function nativeResourceFilterMatches(resource, filter) {
|
|
7587
|
+
if (filter === "enabled") return resource.enabled !== false;
|
|
7588
|
+
if (filter === "disabled") return resource.enabled === false;
|
|
7589
|
+
return true;
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7592
|
+
function renderNativeResourceToggles(resources, { savingName, filter = "all", onToggle } = {}) {
|
|
7593
|
+
const filteredResources = resources.filter((resource) => nativeResourceFilterMatches(resource, filter));
|
|
7594
|
+
const counts = nativeResourceCounts(resources);
|
|
7595
|
+
const items = filteredResources.map((resource) => ({
|
|
7596
|
+
id: resource.name,
|
|
7597
|
+
label: resource.name,
|
|
7598
|
+
description: resource.description || "No description provided.",
|
|
7599
|
+
meta: nativeResourceSourceLabel(resource),
|
|
7600
|
+
badge: resource.enabled === false ? "disabled" : "enabled",
|
|
7601
|
+
badgeClass: resource.enabled === false ? "disabled native-selector-badge-disabled" : "enabled native-selector-badge-enabled",
|
|
7602
|
+
disabled: Boolean(savingName),
|
|
7603
|
+
resource,
|
|
7604
|
+
}));
|
|
7605
|
+
const filterLabel = filter === "enabled" ? "enabled" : filter === "disabled" ? "disabled" : "all";
|
|
7606
|
+
renderNativeSelectorItems(items, {
|
|
7607
|
+
emptyText: `No ${filterLabel} entries match this filter.`,
|
|
7608
|
+
onSelect: (item) => onToggle?.(item.resource),
|
|
7609
|
+
});
|
|
7610
|
+
elements.nativeCommandBody.prepend(make("div", "native-resource-summary muted", `${counts.total} total · ${counts.enabled} enabled · ${counts.disabled} disabled · showing ${filterLabel}`));
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
function renderNativeResourceFilterActions(filter, setFilter, render) {
|
|
7614
|
+
elements.nativeCommandActions.replaceChildren();
|
|
7615
|
+
for (const option of [
|
|
7616
|
+
{ value: "all", label: "All" },
|
|
7617
|
+
{ value: "enabled", label: "Enabled" },
|
|
7618
|
+
{ value: "disabled", label: "Disabled" },
|
|
7619
|
+
]) {
|
|
7620
|
+
addNativeCommandAction(option.label, () => {
|
|
7621
|
+
setFilter(option.value);
|
|
7622
|
+
render();
|
|
7623
|
+
}, filter === option.value ? "primary" : undefined);
|
|
7624
|
+
}
|
|
7625
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
7626
|
+
}
|
|
7627
|
+
|
|
7628
|
+
async function openNativeToolsSelector() {
|
|
7629
|
+
openNativeCommandDialog({ title: "/tools", message: "Enable or disable tools for the active Pi tab. Changes apply to the next model turn and persist on this session branch.", searchPlaceholder: "Filter tools…" });
|
|
7630
|
+
renderNativeLoading("Loading tools…");
|
|
7631
|
+
let tools = [];
|
|
7632
|
+
let savingName = "";
|
|
7633
|
+
let filter = "all";
|
|
7634
|
+
const render = () => {
|
|
7635
|
+
renderNativeResourceToggles(tools, {
|
|
7636
|
+
savingName,
|
|
7637
|
+
filter,
|
|
7638
|
+
onToggle: async (tool) => {
|
|
7639
|
+
if (!tool || savingName) return;
|
|
7640
|
+
const enabledTools = new Set(tools.filter((item) => item.enabled !== false).map((item) => item.name));
|
|
7641
|
+
if (tool.enabled === false) enabledTools.add(tool.name);
|
|
7642
|
+
else enabledTools.delete(tool.name);
|
|
7643
|
+
savingName = tool.name;
|
|
7644
|
+
setNativeCommandError("");
|
|
7645
|
+
render();
|
|
7646
|
+
try {
|
|
7647
|
+
const response = await nativeCommandApi("/api/tools", { method: "POST", body: { enabledTools: [...enabledTools] } });
|
|
7648
|
+
tools = Array.isArray(response.data?.tools) ? response.data.tools : [];
|
|
7649
|
+
addTransientMessage({ role: "native", title: "/tools", content: `Tool ${tool.name} ${enabledTools.has(tool.name) ? "enabled" : "disabled"}.`, level: "info" });
|
|
7650
|
+
} catch (error) {
|
|
7651
|
+
setNativeCommandError(error.message || String(error));
|
|
7652
|
+
} finally {
|
|
7653
|
+
savingName = "";
|
|
7654
|
+
render();
|
|
7655
|
+
}
|
|
7656
|
+
},
|
|
7657
|
+
});
|
|
7658
|
+
renderNativeResourceFilterActions(filter, (value) => { filter = value; }, render);
|
|
7659
|
+
};
|
|
7660
|
+
try {
|
|
7661
|
+
const response = await nativeCommandApi("/api/tools");
|
|
7662
|
+
tools = Array.isArray(response.data?.tools) ? response.data.tools : [];
|
|
7663
|
+
elements.nativeCommandSearch.oninput = render;
|
|
7664
|
+
render();
|
|
7665
|
+
} catch (error) {
|
|
7666
|
+
setNativeCommandError(error.message || String(error));
|
|
7667
|
+
elements.nativeCommandBody.replaceChildren();
|
|
7668
|
+
}
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
async function openNativeSkillsSelector() {
|
|
7672
|
+
openNativeCommandDialog({ title: "/skills", message: "Enable or disable skills for automatic model invocation in the active Pi tab. Disabled skills are removed from the system prompt and their /skill:name commands are blocked by Web UI.", searchPlaceholder: "Filter skills…" });
|
|
7673
|
+
renderNativeLoading("Loading skills…");
|
|
7674
|
+
let skills = [];
|
|
7675
|
+
let savingName = "";
|
|
7676
|
+
let filter = "all";
|
|
7677
|
+
const render = () => {
|
|
7678
|
+
renderNativeResourceToggles(skills, {
|
|
7679
|
+
savingName,
|
|
7680
|
+
filter,
|
|
7681
|
+
onToggle: async (skill) => {
|
|
7682
|
+
if (!skill || savingName) return;
|
|
7683
|
+
const enabledSkills = new Set(skills.filter((item) => item.enabled !== false).map((item) => item.name));
|
|
7684
|
+
if (skill.enabled === false) enabledSkills.add(skill.name);
|
|
7685
|
+
else enabledSkills.delete(skill.name);
|
|
7686
|
+
savingName = skill.name;
|
|
7687
|
+
setNativeCommandError("");
|
|
7688
|
+
render();
|
|
7689
|
+
try {
|
|
7690
|
+
const response = await nativeCommandApi("/api/skills", { method: "POST", body: { enabledSkills: [...enabledSkills] } });
|
|
7691
|
+
skills = Array.isArray(response.data?.skills) ? response.data.skills : [];
|
|
7692
|
+
addTransientMessage({ role: "native", title: "/skills", content: `Skill ${skill.name} ${enabledSkills.has(skill.name) ? "enabled" : "disabled"}.`, level: "info" });
|
|
7693
|
+
refreshCommands(activeTabContext()).catch((error) => addEvent(error.message || String(error), "error"));
|
|
7694
|
+
} catch (error) {
|
|
7695
|
+
setNativeCommandError(error.message || String(error));
|
|
7696
|
+
} finally {
|
|
7697
|
+
savingName = "";
|
|
7698
|
+
render();
|
|
7699
|
+
}
|
|
7700
|
+
},
|
|
7701
|
+
});
|
|
7702
|
+
renderNativeResourceFilterActions(filter, (value) => { filter = value; }, render);
|
|
7703
|
+
};
|
|
7704
|
+
try {
|
|
7705
|
+
const response = await nativeCommandApi("/api/skills");
|
|
7706
|
+
skills = Array.isArray(response.data?.skills) ? response.data.skills : [];
|
|
7707
|
+
elements.nativeCommandSearch.oninput = render;
|
|
7708
|
+
render();
|
|
7709
|
+
} catch (error) {
|
|
7710
|
+
setNativeCommandError(error.message || String(error));
|
|
7711
|
+
elements.nativeCommandBody.replaceChildren();
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
|
|
7461
7715
|
function openNativeAuthInfo(mode) {
|
|
7462
7716
|
const command = mode === "logout" ? "/logout" : "/login";
|
|
7463
7717
|
openNativeCommandDialog({ title: command, message: "Provider credential entry is intentionally not implemented in the browser yet." });
|
|
@@ -7502,6 +7756,12 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
|
|
|
7502
7756
|
case "scoped-models":
|
|
7503
7757
|
openNativeScopedModelsInfo();
|
|
7504
7758
|
return true;
|
|
7759
|
+
case "tools":
|
|
7760
|
+
await openNativeToolsSelector();
|
|
7761
|
+
return true;
|
|
7762
|
+
case "skills":
|
|
7763
|
+
await openNativeSkillsSelector();
|
|
7764
|
+
return true;
|
|
7505
7765
|
case "login":
|
|
7506
7766
|
case "logout":
|
|
7507
7767
|
openNativeAuthInfo(name);
|
|
@@ -7889,7 +8149,7 @@ function syncModelSelectToState() {
|
|
|
7889
8149
|
}
|
|
7890
8150
|
}
|
|
7891
8151
|
|
|
7892
|
-
function normalizeCommands(commands) {
|
|
8152
|
+
function normalizeCommands(commands, { dedupe = true } = {}) {
|
|
7893
8153
|
const seen = new Set();
|
|
7894
8154
|
return (commands || [])
|
|
7895
8155
|
.map((command) => ({
|
|
@@ -7897,9 +8157,12 @@ function normalizeCommands(commands) {
|
|
|
7897
8157
|
description: String(command.description || "").trim(),
|
|
7898
8158
|
source: String(command.source || "command").trim(),
|
|
7899
8159
|
location: String(command.location || "").trim(),
|
|
8160
|
+
enabled: command.enabled !== false,
|
|
7900
8161
|
}))
|
|
7901
8162
|
.filter((command) => {
|
|
7902
|
-
if (!command.name
|
|
8163
|
+
if (!command.name) return false;
|
|
8164
|
+
if (!dedupe) return true;
|
|
8165
|
+
if (seen.has(command.name)) return false;
|
|
7903
8166
|
seen.add(command.name);
|
|
7904
8167
|
return true;
|
|
7905
8168
|
})
|
|
@@ -8278,6 +8541,7 @@ async function refreshCommands(tabContext = activeTabContext()) {
|
|
|
8278
8541
|
if (!tabContext.tabId) return;
|
|
8279
8542
|
const response = await api("/api/commands", { tabId: tabContext.tabId });
|
|
8280
8543
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8544
|
+
rawAvailableCommands = normalizeCommands(response.data?.commands || [], { dedupe: false });
|
|
8281
8545
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
8282
8546
|
updateOptionalFeatureAvailability();
|
|
8283
8547
|
renderCommands();
|
|
@@ -8293,6 +8557,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
8293
8557
|
refreshStats(tabContext),
|
|
8294
8558
|
refreshWorkspace(tabContext),
|
|
8295
8559
|
refreshNetworkStatus(),
|
|
8560
|
+
refreshWebuiVersion(),
|
|
8296
8561
|
]);
|
|
8297
8562
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8298
8563
|
for (const result of results) {
|
|
@@ -8404,12 +8669,108 @@ async function closeNetworkAccess() {
|
|
|
8404
8669
|
}
|
|
8405
8670
|
}
|
|
8406
8671
|
|
|
8672
|
+
function setServerActionStatus(message = "", level = "info") {
|
|
8673
|
+
const status = elements.serverActionStatus;
|
|
8674
|
+
if (!status) return;
|
|
8675
|
+
status.textContent = message;
|
|
8676
|
+
status.hidden = !message;
|
|
8677
|
+
status.className = `server-action-status ${level} ${message ? "" : "muted"}`.trim();
|
|
8678
|
+
}
|
|
8679
|
+
|
|
8680
|
+
function updateServerActionButton() {
|
|
8681
|
+
const action = elements.serverActionSelect?.value || "";
|
|
8682
|
+
const button = elements.runServerActionButton;
|
|
8683
|
+
if (!button) return;
|
|
8684
|
+
button.disabled = !action;
|
|
8685
|
+
button.textContent = action === "restart" ? "Restart" : action === "stop" ? "Stop" : "Run";
|
|
8686
|
+
button.classList.toggle("danger", action === "stop");
|
|
8687
|
+
if (action) setServerActionStatus(action === "restart" ? "Ready to restart the Web UI server." : "Ready to stop the Web UI server.", "info");
|
|
8688
|
+
else setServerActionStatus();
|
|
8689
|
+
}
|
|
8690
|
+
|
|
8691
|
+
function setServerActionBusy(label) {
|
|
8692
|
+
if (elements.serverActionSelect) elements.serverActionSelect.disabled = true;
|
|
8693
|
+
if (elements.runServerActionButton) {
|
|
8694
|
+
elements.runServerActionButton.disabled = true;
|
|
8695
|
+
elements.runServerActionButton.textContent = label;
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
|
|
8699
|
+
function resetServerActionControls() {
|
|
8700
|
+
if (elements.serverActionSelect) {
|
|
8701
|
+
elements.serverActionSelect.disabled = false;
|
|
8702
|
+
elements.serverActionSelect.value = "";
|
|
8703
|
+
}
|
|
8704
|
+
updateServerActionButton();
|
|
8705
|
+
}
|
|
8706
|
+
|
|
8707
|
+
async function waitForServerRestart() {
|
|
8708
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
8709
|
+
await delay(attempt === 0 ? 900 : 500);
|
|
8710
|
+
const message = `Restarting… reconnect attempt ${attempt + 1}/40`;
|
|
8711
|
+
setServerActionStatus(message, "warn");
|
|
8712
|
+
setServerRestartOverlay(true, message);
|
|
8713
|
+
try {
|
|
8714
|
+
await api("/api/health", { scoped: false });
|
|
8715
|
+
setBackendOffline(false);
|
|
8716
|
+
await initializeTabs();
|
|
8717
|
+
setServerRestartOverlay(false);
|
|
8718
|
+
setServerActionStatus("Server restarted and reconnected.", "success");
|
|
8719
|
+
addEvent("Pi Web UI server restarted", "warn");
|
|
8720
|
+
return true;
|
|
8721
|
+
} catch (error) {
|
|
8722
|
+
setBackendOffline(true, error);
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
return false;
|
|
8726
|
+
}
|
|
8727
|
+
|
|
8728
|
+
async function restartServer() {
|
|
8729
|
+
if (!confirm("Restart the Pi Web UI server?\n\nThis briefly disconnects browser clients and restarts the Pi tabs managed by this Web UI.")) return;
|
|
8730
|
+
|
|
8731
|
+
setServerActionBusy("Restarting…");
|
|
8732
|
+
setServerActionStatus("Restart requested. Waiting for the server to come back…", "warn");
|
|
8733
|
+
setServerRestartOverlay(true, "Restart requested. Waiting for the server to come back…");
|
|
8734
|
+
try {
|
|
8735
|
+
await api("/api/restart", { method: "POST", scoped: false });
|
|
8736
|
+
addEvent("Pi Web UI server restart requested", "warn");
|
|
8737
|
+
} catch (error) {
|
|
8738
|
+
if (!error?.backendOffline) {
|
|
8739
|
+
const missingRestartEndpoint = error.statusCode === 404 || /not found/i.test(error.message || "");
|
|
8740
|
+
const message = missingRestartEndpoint
|
|
8741
|
+
? "Restart is not available in the currently running server. Stop/start manually once to load the new backend."
|
|
8742
|
+
: error.message || String(error);
|
|
8743
|
+
addEvent(message, "error");
|
|
8744
|
+
setServerRestartOverlay(false);
|
|
8745
|
+
resetServerActionControls();
|
|
8746
|
+
setServerActionStatus(message, "error");
|
|
8747
|
+
return;
|
|
8748
|
+
}
|
|
8749
|
+
addEvent("Pi Web UI server connection dropped during restart request", "warn");
|
|
8750
|
+
}
|
|
8751
|
+
|
|
8752
|
+
setBackendOffline(true, new Error("restart requested from side panel"));
|
|
8753
|
+
const restarted = await waitForServerRestart();
|
|
8754
|
+
if (elements.serverActionSelect) {
|
|
8755
|
+
elements.serverActionSelect.disabled = false;
|
|
8756
|
+
elements.serverActionSelect.value = "";
|
|
8757
|
+
}
|
|
8758
|
+
updateServerActionButton();
|
|
8759
|
+
if (restarted) {
|
|
8760
|
+
setServerActionStatus("Server restarted and reconnected.", "success");
|
|
8761
|
+
} else {
|
|
8762
|
+
setServerRestartOverlay(false);
|
|
8763
|
+
setBackendOffline(true, new Error("restart reconnect timed out"));
|
|
8764
|
+
setServerActionStatus("Restart requested, but the server did not reconnect automatically.", "error");
|
|
8765
|
+
addEvent("Pi Web UI server did not come back online after restart request", "error");
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
|
|
8407
8769
|
async function stopServer() {
|
|
8408
8770
|
if (!confirm("Stop the Pi Web UI server?\n\nThis disconnects all browser clients and stops the Pi tabs managed by this Web UI.")) return;
|
|
8409
8771
|
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
button.textContent = "Stopping…";
|
|
8772
|
+
setServerActionBusy("Stopping…");
|
|
8773
|
+
setServerActionStatus("Stop requested. The Web UI will disconnect.", "warn");
|
|
8413
8774
|
try {
|
|
8414
8775
|
await api("/api/shutdown", { method: "POST", scoped: false });
|
|
8415
8776
|
addEvent("Pi Web UI server stop requested", "warn");
|
|
@@ -8421,11 +8782,17 @@ async function stopServer() {
|
|
|
8421
8782
|
return;
|
|
8422
8783
|
}
|
|
8423
8784
|
addEvent(error.message || String(error), "error");
|
|
8424
|
-
|
|
8425
|
-
|
|
8785
|
+
resetServerActionControls();
|
|
8786
|
+
setServerActionStatus(error.message || String(error), "error");
|
|
8426
8787
|
}
|
|
8427
8788
|
}
|
|
8428
8789
|
|
|
8790
|
+
async function runSelectedServerAction() {
|
|
8791
|
+
const action = elements.serverActionSelect?.value || "";
|
|
8792
|
+
if (action === "restart") await restartServer();
|
|
8793
|
+
else if (action === "stop") await stopServer();
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8429
8796
|
function appShortcutModelLabel(model) {
|
|
8430
8797
|
return model ? `${model.provider}/${model.id}` : "unknown model";
|
|
8431
8798
|
}
|
|
@@ -8840,8 +9207,11 @@ function showNextDialog() {
|
|
|
8840
9207
|
button.type = "button";
|
|
8841
9208
|
if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
|
|
8842
9209
|
if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
|
|
8843
|
-
if (isReleaseDialog && /^Yes
|
|
8844
|
-
if (isReleaseDialog && /^
|
|
9210
|
+
if (isReleaseDialog && /^(?:Yes|All eligible packages\b|Publish selected packages \([1-9]\d*\))/.test(optionLabel)) button.classList.add("primary", "release-publish-action");
|
|
9211
|
+
if (isReleaseDialog && /^Publish selected packages \(select at least one\)$/i.test(optionLabel)) button.classList.add("release-publish-disabled-action");
|
|
9212
|
+
if (isReleaseDialog && /^\[x\]/.test(optionLabel)) button.classList.add("release-target-option", "release-target-selected");
|
|
9213
|
+
if (isReleaseDialog && /^\[ \]/.test(optionLabel)) button.classList.add("release-target-option");
|
|
9214
|
+
if (isReleaseDialog && /^(?:No|Cancel)$/i.test(optionLabel)) button.classList.add("release-cancel-action");
|
|
8845
9215
|
button.addEventListener("click", () => sendDialogResponse({ type: "extension_ui_response", id: request.id, value: optionLabel, tabId: request.tabId }));
|
|
8846
9216
|
options.append(button);
|
|
8847
9217
|
}
|
|
@@ -8889,6 +9259,8 @@ function handleEvent(event) {
|
|
|
8889
9259
|
const tabContext = activeTabContext(event.tabId || activeTabId);
|
|
8890
9260
|
switch (event.type) {
|
|
8891
9261
|
case "webui_connected":
|
|
9262
|
+
setWebuiVersion(event.version);
|
|
9263
|
+
setWebuiDevServer(isWebuiDevMetadata(event));
|
|
8892
9264
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
8893
9265
|
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
8894
9266
|
break;
|
|
@@ -9299,7 +9671,9 @@ if (elements.backgroundClearButton) {
|
|
|
9299
9671
|
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
9300
9672
|
}
|
|
9301
9673
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
9302
|
-
elements.
|
|
9674
|
+
elements.serverActionSelect.addEventListener("change", updateServerActionButton);
|
|
9675
|
+
elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
|
|
9676
|
+
updateServerActionButton();
|
|
9303
9677
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
9304
9678
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
9305
9679
|
requestPermission: elements.agentDoneNotificationsToggle.checked,
|