@firstpick/pi-package-webui 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/pi-webui.mjs +67 -1
- package/package.json +1 -1
- package/public/app.js +219 -29
- package/public/index.html +24 -4
- package/public/service-worker.js +1 -1
- package/public/styles.css +200 -7
- package/start-webui.sh +9 -48
- package/tests/mobile-static.test.mjs +34 -3
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ This requires `/git-staged-msg` from `@firstpick/pi-prompts-git-pr`. Review the
|
|
|
161
161
|
## Network safety
|
|
162
162
|
|
|
163
163
|
- Default bind is localhost-only: `127.0.0.1:31415`.
|
|
164
|
-
- The side-panel **Open to network** button rebinds the server to `0.0.0.0
|
|
164
|
+
- The side-panel **Open to network** button rebinds the server to `0.0.0.0`, shows LAN URLs when available, and toggles to "Close for network".
|
|
165
165
|
- `--host 0.0.0.0` also exposes the Web UI to the local network.
|
|
166
166
|
- Any connected browser client can control Pi and run Web UI bash actions as the Web UI process user.
|
|
167
167
|
- Treat Pi Web UI as a local companion, not a hardened multi-user web service.
|
package/bin/pi-webui.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const packageRoot = path.resolve(__dirname, "..");
|
|
|
17
17
|
const publicDir = path.join(packageRoot, "public");
|
|
18
18
|
const packageJson = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
19
19
|
const nativeParityMatrix = JSON.parse(await readFile(path.join(packageRoot, "WEBUI_TUI_NATIVE_PARITY.json"), "utf8"));
|
|
20
|
+
const webuiDevServer = isTruthyEnv(process.env.PI_WEBUI_DEV) || isSourceCheckout(packageRoot);
|
|
20
21
|
|
|
21
22
|
const DEFAULT_HOST = "127.0.0.1";
|
|
22
23
|
const DEFAULT_PORT = 31415;
|
|
@@ -100,6 +101,15 @@ const MIME_TYPES = new Map([
|
|
|
100
101
|
[".webmanifest", "application/manifest+json; charset=utf-8"],
|
|
101
102
|
]);
|
|
102
103
|
|
|
104
|
+
function isTruthyEnv(value) {
|
|
105
|
+
return ["1", "true", "yes", "dev"].includes(String(value || "").trim().toLowerCase());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isSourceCheckout(root) {
|
|
109
|
+
const normalized = String(root || "").replace(/\\/g, "/");
|
|
110
|
+
return normalized.includes("/npm-packages/") && !normalized.includes("/node_modules/");
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
function nativeParitySurfaces(matrix = nativeParityMatrix) {
|
|
104
114
|
return Array.isArray(matrix?.surfaces) ? matrix.surfaces : [];
|
|
105
115
|
}
|
|
@@ -2004,6 +2014,12 @@ if (options.version) {
|
|
|
2004
2014
|
process.exit(0);
|
|
2005
2015
|
}
|
|
2006
2016
|
|
|
2017
|
+
const startupDelayMs = Number.parseInt(process.env.PI_WEBUI_START_DELAY_MS || "", 10);
|
|
2018
|
+
delete process.env.PI_WEBUI_START_DELAY_MS;
|
|
2019
|
+
if (Number.isFinite(startupDelayMs) && startupDelayMs > 0) {
|
|
2020
|
+
await delay(Math.min(startupDelayMs, 10_000));
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2007
2023
|
const restoreTabs = readRestoreTabsFromEnv();
|
|
2008
2024
|
|
|
2009
2025
|
function normalizedRestoreString(value, maxLength) {
|
|
@@ -2547,6 +2563,33 @@ function mergeRestorableTabDescriptors(...sources) {
|
|
|
2547
2563
|
.slice(0, RESTORE_TAB_LIMIT);
|
|
2548
2564
|
}
|
|
2549
2565
|
|
|
2566
|
+
async function restorableTabsForRestart() {
|
|
2567
|
+
const liveDescriptors = await Promise.all([...tabs.values()].map(async (tab) => {
|
|
2568
|
+
const state = await currentSessionState(tab).catch(() => tab.lastState || null);
|
|
2569
|
+
return restorableTabDescriptor(tab, state);
|
|
2570
|
+
}));
|
|
2571
|
+
return mergeRestorableTabDescriptors(liveDescriptors, closedRestorableTabs);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function spawnRestartServer(restorableTabs) {
|
|
2575
|
+
const env = {
|
|
2576
|
+
...process.env,
|
|
2577
|
+
PI_WEBUI_RESTORE_TABS: JSON.stringify(restorableTabs || []),
|
|
2578
|
+
PI_WEBUI_START_DELAY_MS: "1200",
|
|
2579
|
+
};
|
|
2580
|
+
if (webuiDevServer) env.PI_WEBUI_DEV = "1";
|
|
2581
|
+
else delete env.PI_WEBUI_DEV;
|
|
2582
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
2583
|
+
cwd: process.cwd(),
|
|
2584
|
+
env,
|
|
2585
|
+
detached: true,
|
|
2586
|
+
stdio: "ignore",
|
|
2587
|
+
windowsHide: true,
|
|
2588
|
+
});
|
|
2589
|
+
child.unref();
|
|
2590
|
+
return child;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2550
2593
|
function rememberClosedRestorableTab(tab, state = null) {
|
|
2551
2594
|
const descriptor = restorableTabDescriptor(tab, state);
|
|
2552
2595
|
if (!descriptor) return;
|
|
@@ -3461,6 +3504,8 @@ async function webuiStatus({ detailed = false, eventLimit = 40 } = {}) {
|
|
|
3461
3504
|
const data = {
|
|
3462
3505
|
online: true,
|
|
3463
3506
|
webuiVersion: packageJson.version,
|
|
3507
|
+
webuiDev: webuiDevServer,
|
|
3508
|
+
webuiMode: webuiDevServer ? "dev" : "production",
|
|
3464
3509
|
webuiPid: process.pid,
|
|
3465
3510
|
startedAt: serverStartedAt,
|
|
3466
3511
|
cwd: options.cwd,
|
|
@@ -3537,6 +3582,8 @@ const server = createServer(async (req, res) => {
|
|
|
3537
3582
|
sendSse(res, {
|
|
3538
3583
|
type: "webui_connected",
|
|
3539
3584
|
version: packageJson.version,
|
|
3585
|
+
webuiDev: webuiDevServer,
|
|
3586
|
+
webuiMode: webuiDevServer ? "dev" : "production",
|
|
3540
3587
|
tabId: tab.id,
|
|
3541
3588
|
tabTitle: tab.title,
|
|
3542
3589
|
pid: tab.rpc.child?.pid,
|
|
@@ -3559,6 +3606,8 @@ const server = createServer(async (req, res) => {
|
|
|
3559
3606
|
sendJson(res, 200, {
|
|
3560
3607
|
ok: true,
|
|
3561
3608
|
webuiVersion: status.webuiVersion,
|
|
3609
|
+
webuiDev: status.webuiDev,
|
|
3610
|
+
webuiMode: status.webuiMode,
|
|
3562
3611
|
webuiPid: status.webuiPid,
|
|
3563
3612
|
piPid: status.piPid,
|
|
3564
3613
|
piRunning: status.piRunning,
|
|
@@ -3629,6 +3678,15 @@ const server = createServer(async (req, res) => {
|
|
|
3629
3678
|
return;
|
|
3630
3679
|
}
|
|
3631
3680
|
|
|
3681
|
+
if (url.pathname === "/api/restart" && req.method === "POST") {
|
|
3682
|
+
if (!isLocalAddress(req.socket.remoteAddress)) throw makeHttpError(403, "Restart is only allowed from localhost");
|
|
3683
|
+
const restorableTabs = await restorableTabsForRestart();
|
|
3684
|
+
const child = spawnRestartServer(restorableTabs);
|
|
3685
|
+
sendJson(res, 200, { ok: true, message: "Pi Web UI restarting", webuiPid: process.pid, nextWebuiPid: child.pid, restorableTabCount: restorableTabs.length });
|
|
3686
|
+
setTimeout(() => shutdown("api restart"), 20).unref();
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3632
3690
|
if (url.pathname === "/api/shutdown" && req.method === "POST") {
|
|
3633
3691
|
if (!isLocalAddress(req.socket.remoteAddress)) throw makeHttpError(403, "Shutdown is only allowed from localhost");
|
|
3634
3692
|
sendJson(res, 200, { ok: true, message: "Pi Web UI shutting down", webuiPid: process.pid });
|
|
@@ -3879,7 +3937,15 @@ server.listen(options.port, currentHost, () => {
|
|
|
3879
3937
|
|
|
3880
3938
|
function shutdown(signal) {
|
|
3881
3939
|
console.log(`\n${signal}: shutting down Pi Web UI...`);
|
|
3882
|
-
|
|
3940
|
+
const forceCloseTimer = setTimeout(() => {
|
|
3941
|
+
server.closeAllConnections?.();
|
|
3942
|
+
}, NETWORK_REBIND_FORCE_CLOSE_MS);
|
|
3943
|
+
forceCloseTimer.unref?.();
|
|
3944
|
+
server.close(() => {
|
|
3945
|
+
clearTimeout(forceCloseTimer);
|
|
3946
|
+
process.exit(0);
|
|
3947
|
+
});
|
|
3948
|
+
server.closeIdleConnections?.();
|
|
3883
3949
|
for (const tab of tabs.values()) tab.rpc.stop();
|
|
3884
3950
|
setTimeout(() => process.exit(0), 4000).unref();
|
|
3885
3951
|
}
|
package/package.json
CHANGED
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"),
|
|
@@ -150,12 +155,15 @@ let pathSuggestAbortController = null;
|
|
|
150
155
|
let latestStats = null;
|
|
151
156
|
let latestWorkspace = null;
|
|
152
157
|
let latestNetwork = null;
|
|
158
|
+
let webuiVersion = "";
|
|
159
|
+
let webuiDevServer = false;
|
|
153
160
|
let latestCodexUsage = null;
|
|
154
161
|
let codexUsageError = null;
|
|
155
162
|
let codexUsageLoading = false;
|
|
156
163
|
let refreshCodexUsageTimer = null;
|
|
157
164
|
let codexUsageRenderTimer = null;
|
|
158
165
|
let backendOffline = false;
|
|
166
|
+
let serverRestartInProgress = false;
|
|
159
167
|
let backendOfflineNoticeShown = false;
|
|
160
168
|
let latestMessages = [];
|
|
161
169
|
let promptHistoryByTab = new Map();
|
|
@@ -263,6 +271,7 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
|
263
271
|
const statusEntries = new Map();
|
|
264
272
|
const widgets = new Map();
|
|
265
273
|
const todoProgressWidgetExpandedByTab = new Map();
|
|
274
|
+
const releaseNpmOutputExpandedByTab = new Map();
|
|
266
275
|
const liveToolRuns = new Map();
|
|
267
276
|
const liveToolCards = new Map();
|
|
268
277
|
const liveToolRenderQueue = new Map();
|
|
@@ -899,13 +908,22 @@ function renderServerOfflinePanel() {
|
|
|
899
908
|
if (elements.serverOfflineSlashCommand) elements.serverOfflineSlashCommand.textContent = serverStartSlashCommandText();
|
|
900
909
|
}
|
|
901
910
|
|
|
911
|
+
function setServerRestartOverlay(active, message = "Waiting for the server to come back…") {
|
|
912
|
+
serverRestartInProgress = !!active;
|
|
913
|
+
document.body.classList.toggle("server-restarting", serverRestartInProgress);
|
|
914
|
+
if (elements.serverRestartPanel) elements.serverRestartPanel.hidden = !serverRestartInProgress;
|
|
915
|
+
if (elements.serverRestartMessage) elements.serverRestartMessage.textContent = message;
|
|
916
|
+
if (serverRestartInProgress && elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = true;
|
|
917
|
+
}
|
|
918
|
+
|
|
902
919
|
function setBackendOffline(offline, error) {
|
|
903
920
|
backendOffline = !!offline;
|
|
904
|
-
|
|
905
|
-
|
|
921
|
+
const showOfflinePanel = backendOffline && !serverRestartInProgress;
|
|
922
|
+
document.body.classList.toggle("server-offline", showOfflinePanel);
|
|
923
|
+
if (elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = !showOfflinePanel;
|
|
906
924
|
renderServerOfflinePanel();
|
|
907
925
|
if (backendOffline) {
|
|
908
|
-
if (!backendOfflineNoticeShown) {
|
|
926
|
+
if (!serverRestartInProgress && !backendOfflineNoticeShown) {
|
|
909
927
|
backendOfflineNoticeShown = true;
|
|
910
928
|
addEvent(`Pi Web UI server is offline${error?.message ? `: ${error.message}` : ""}`, "warn");
|
|
911
929
|
}
|
|
@@ -1015,6 +1033,52 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
|
|
|
1015
1033
|
return data;
|
|
1016
1034
|
}
|
|
1017
1035
|
|
|
1036
|
+
function formatWebuiVersion(version) {
|
|
1037
|
+
const text = String(version || "").trim();
|
|
1038
|
+
if (!text) return "";
|
|
1039
|
+
return text.startsWith("v") ? text : `v${text}`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function isWebuiDevMetadata(data) {
|
|
1043
|
+
return data?.webuiDev === true || String(data?.webuiMode || "").toLowerCase() === "dev";
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function renderWebuiVersion() {
|
|
1047
|
+
const badge = elements.webuiVersionBadge;
|
|
1048
|
+
if (!badge) return;
|
|
1049
|
+
const label = formatWebuiVersion(webuiVersion);
|
|
1050
|
+
badge.hidden = !label;
|
|
1051
|
+
badge.textContent = label;
|
|
1052
|
+
if (label) badge.title = `Pi Web UI ${label}`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function renderWebuiDevBadge() {
|
|
1056
|
+
const badge = elements.webuiDevBadge;
|
|
1057
|
+
if (!badge) return;
|
|
1058
|
+
badge.hidden = !webuiDevServer;
|
|
1059
|
+
badge.title = "Pi Web UI dev server";
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function setWebuiVersion(version) {
|
|
1063
|
+
const text = String(version || "").trim();
|
|
1064
|
+
if (text === webuiVersion) return;
|
|
1065
|
+
webuiVersion = text;
|
|
1066
|
+
renderWebuiVersion();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function setWebuiDevServer(dev) {
|
|
1070
|
+
const next = !!dev;
|
|
1071
|
+
if (next === webuiDevServer) return;
|
|
1072
|
+
webuiDevServer = next;
|
|
1073
|
+
renderWebuiDevBadge();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function refreshWebuiVersion() {
|
|
1077
|
+
const health = await api("/api/health", { scoped: false });
|
|
1078
|
+
setWebuiVersion(health.webuiVersion);
|
|
1079
|
+
setWebuiDevServer(isWebuiDevMetadata(health));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1018
1082
|
function formatBytes(bytes) {
|
|
1019
1083
|
const value = Number(bytes) || 0;
|
|
1020
1084
|
if (value < 1024) return `${value} B`;
|
|
@@ -2357,7 +2421,6 @@ function resetActiveTabUi() {
|
|
|
2357
2421
|
}
|
|
2358
2422
|
elements.commandsBox.textContent = "Loading…";
|
|
2359
2423
|
elements.commandsBox.classList.add("muted");
|
|
2360
|
-
elements.sessionLine.textContent = activeTab() ? "Connecting…" : "No terminal tabs.";
|
|
2361
2424
|
renderWidgets();
|
|
2362
2425
|
renderGitWorkflow();
|
|
2363
2426
|
renderFooter();
|
|
@@ -3899,13 +3962,6 @@ function renderStatus() {
|
|
|
3899
3962
|
updateComposerModeButtons();
|
|
3900
3963
|
const running = state?.isStreaming ? "running" : "idle";
|
|
3901
3964
|
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
3965
|
|
|
3910
3966
|
elements.stateDetails.replaceChildren();
|
|
3911
3967
|
const details = {
|
|
@@ -4181,6 +4237,26 @@ function releaseNpmStreamHeader(label, lineCount, { live = false } = {}) {
|
|
|
4181
4237
|
return header;
|
|
4182
4238
|
}
|
|
4183
4239
|
|
|
4240
|
+
function renderReleaseNpmOutputDetails(key, streamHeader, terminal, controls = null) {
|
|
4241
|
+
const tabId = activeTabId || "default";
|
|
4242
|
+
const stateKey = `${tabId}:${key}`;
|
|
4243
|
+
const node = make("details", "release-npm-output-details");
|
|
4244
|
+
node.open = releaseNpmOutputExpandedByTab.get(stateKey) !== false;
|
|
4245
|
+
node.addEventListener("toggle", () => {
|
|
4246
|
+
releaseNpmOutputExpandedByTab.set(stateKey, node.open);
|
|
4247
|
+
if (node.open) requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
4248
|
+
});
|
|
4249
|
+
|
|
4250
|
+
const summary = make("summary", "release-npm-output-summary");
|
|
4251
|
+
summary.title = "Expand or collapse command output in the Web UI";
|
|
4252
|
+
const toggle = make("span", "release-npm-output-toggle", "›");
|
|
4253
|
+
toggle.setAttribute("aria-hidden", "true");
|
|
4254
|
+
summary.append(toggle, streamHeader);
|
|
4255
|
+
node.append(summary, terminal);
|
|
4256
|
+
if (controls) node.append(controls);
|
|
4257
|
+
return node;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4184
4260
|
function renderReleaseNpmOutputWidget() {
|
|
4185
4261
|
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
4186
4262
|
const outputLines = getWidgetLines("release-npm:output");
|
|
@@ -4216,8 +4292,9 @@ function renderReleaseNpmOutputWidget() {
|
|
|
4216
4292
|
}
|
|
4217
4293
|
|
|
4218
4294
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
|
|
4219
|
-
|
|
4220
|
-
|
|
4295
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-npm:output", streamHeader, terminal, controls);
|
|
4296
|
+
node.append(header, outputDetails);
|
|
4297
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4221
4298
|
return node;
|
|
4222
4299
|
}
|
|
4223
4300
|
|
|
@@ -4246,8 +4323,9 @@ function renderReleaseNpmLogWidget() {
|
|
|
4246
4323
|
for (const line of logLines) {
|
|
4247
4324
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4248
4325
|
}
|
|
4249
|
-
|
|
4250
|
-
|
|
4326
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-npm:logs", streamHeader, terminal);
|
|
4327
|
+
node.append(header, outputDetails);
|
|
4328
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4251
4329
|
return node;
|
|
4252
4330
|
}
|
|
4253
4331
|
|
|
@@ -4286,8 +4364,9 @@ function renderReleaseAurOutputWidget() {
|
|
|
4286
4364
|
}
|
|
4287
4365
|
|
|
4288
4366
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
|
|
4289
|
-
|
|
4290
|
-
|
|
4367
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-aur:output", streamHeader, terminal, controls);
|
|
4368
|
+
node.append(header, outputDetails);
|
|
4369
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4291
4370
|
return node;
|
|
4292
4371
|
}
|
|
4293
4372
|
|
|
@@ -4316,8 +4395,9 @@ function renderReleaseAurLogWidget() {
|
|
|
4316
4395
|
for (const line of logLines) {
|
|
4317
4396
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4318
4397
|
}
|
|
4319
|
-
|
|
4320
|
-
|
|
4398
|
+
const outputDetails = renderReleaseNpmOutputDetails("release-aur:logs", streamHeader, terminal);
|
|
4399
|
+
node.append(header, outputDetails);
|
|
4400
|
+
requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
|
|
4321
4401
|
return node;
|
|
4322
4402
|
}
|
|
4323
4403
|
|
|
@@ -8293,6 +8373,7 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
8293
8373
|
refreshStats(tabContext),
|
|
8294
8374
|
refreshWorkspace(tabContext),
|
|
8295
8375
|
refreshNetworkStatus(),
|
|
8376
|
+
refreshWebuiVersion(),
|
|
8296
8377
|
]);
|
|
8297
8378
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8298
8379
|
for (const result of results) {
|
|
@@ -8404,12 +8485,108 @@ async function closeNetworkAccess() {
|
|
|
8404
8485
|
}
|
|
8405
8486
|
}
|
|
8406
8487
|
|
|
8488
|
+
function setServerActionStatus(message = "", level = "info") {
|
|
8489
|
+
const status = elements.serverActionStatus;
|
|
8490
|
+
if (!status) return;
|
|
8491
|
+
status.textContent = message;
|
|
8492
|
+
status.hidden = !message;
|
|
8493
|
+
status.className = `server-action-status ${level} ${message ? "" : "muted"}`.trim();
|
|
8494
|
+
}
|
|
8495
|
+
|
|
8496
|
+
function updateServerActionButton() {
|
|
8497
|
+
const action = elements.serverActionSelect?.value || "";
|
|
8498
|
+
const button = elements.runServerActionButton;
|
|
8499
|
+
if (!button) return;
|
|
8500
|
+
button.disabled = !action;
|
|
8501
|
+
button.textContent = action === "restart" ? "Restart" : action === "stop" ? "Stop" : "Run";
|
|
8502
|
+
button.classList.toggle("danger", action === "stop");
|
|
8503
|
+
if (action) setServerActionStatus(action === "restart" ? "Ready to restart the Web UI server." : "Ready to stop the Web UI server.", "info");
|
|
8504
|
+
else setServerActionStatus();
|
|
8505
|
+
}
|
|
8506
|
+
|
|
8507
|
+
function setServerActionBusy(label) {
|
|
8508
|
+
if (elements.serverActionSelect) elements.serverActionSelect.disabled = true;
|
|
8509
|
+
if (elements.runServerActionButton) {
|
|
8510
|
+
elements.runServerActionButton.disabled = true;
|
|
8511
|
+
elements.runServerActionButton.textContent = label;
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
|
|
8515
|
+
function resetServerActionControls() {
|
|
8516
|
+
if (elements.serverActionSelect) {
|
|
8517
|
+
elements.serverActionSelect.disabled = false;
|
|
8518
|
+
elements.serverActionSelect.value = "";
|
|
8519
|
+
}
|
|
8520
|
+
updateServerActionButton();
|
|
8521
|
+
}
|
|
8522
|
+
|
|
8523
|
+
async function waitForServerRestart() {
|
|
8524
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
8525
|
+
await delay(attempt === 0 ? 900 : 500);
|
|
8526
|
+
const message = `Restarting… reconnect attempt ${attempt + 1}/40`;
|
|
8527
|
+
setServerActionStatus(message, "warn");
|
|
8528
|
+
setServerRestartOverlay(true, message);
|
|
8529
|
+
try {
|
|
8530
|
+
await api("/api/health", { scoped: false });
|
|
8531
|
+
setBackendOffline(false);
|
|
8532
|
+
await initializeTabs();
|
|
8533
|
+
setServerRestartOverlay(false);
|
|
8534
|
+
setServerActionStatus("Server restarted and reconnected.", "success");
|
|
8535
|
+
addEvent("Pi Web UI server restarted", "warn");
|
|
8536
|
+
return true;
|
|
8537
|
+
} catch (error) {
|
|
8538
|
+
setBackendOffline(true, error);
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8541
|
+
return false;
|
|
8542
|
+
}
|
|
8543
|
+
|
|
8544
|
+
async function restartServer() {
|
|
8545
|
+
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;
|
|
8546
|
+
|
|
8547
|
+
setServerActionBusy("Restarting…");
|
|
8548
|
+
setServerActionStatus("Restart requested. Waiting for the server to come back…", "warn");
|
|
8549
|
+
setServerRestartOverlay(true, "Restart requested. Waiting for the server to come back…");
|
|
8550
|
+
try {
|
|
8551
|
+
await api("/api/restart", { method: "POST", scoped: false });
|
|
8552
|
+
addEvent("Pi Web UI server restart requested", "warn");
|
|
8553
|
+
} catch (error) {
|
|
8554
|
+
if (!error?.backendOffline) {
|
|
8555
|
+
const missingRestartEndpoint = error.statusCode === 404 || /not found/i.test(error.message || "");
|
|
8556
|
+
const message = missingRestartEndpoint
|
|
8557
|
+
? "Restart is not available in the currently running server. Stop/start manually once to load the new backend."
|
|
8558
|
+
: error.message || String(error);
|
|
8559
|
+
addEvent(message, "error");
|
|
8560
|
+
setServerRestartOverlay(false);
|
|
8561
|
+
resetServerActionControls();
|
|
8562
|
+
setServerActionStatus(message, "error");
|
|
8563
|
+
return;
|
|
8564
|
+
}
|
|
8565
|
+
addEvent("Pi Web UI server connection dropped during restart request", "warn");
|
|
8566
|
+
}
|
|
8567
|
+
|
|
8568
|
+
setBackendOffline(true, new Error("restart requested from side panel"));
|
|
8569
|
+
const restarted = await waitForServerRestart();
|
|
8570
|
+
if (elements.serverActionSelect) {
|
|
8571
|
+
elements.serverActionSelect.disabled = false;
|
|
8572
|
+
elements.serverActionSelect.value = "";
|
|
8573
|
+
}
|
|
8574
|
+
updateServerActionButton();
|
|
8575
|
+
if (restarted) {
|
|
8576
|
+
setServerActionStatus("Server restarted and reconnected.", "success");
|
|
8577
|
+
} else {
|
|
8578
|
+
setServerRestartOverlay(false);
|
|
8579
|
+
setBackendOffline(true, new Error("restart reconnect timed out"));
|
|
8580
|
+
setServerActionStatus("Restart requested, but the server did not reconnect automatically.", "error");
|
|
8581
|
+
addEvent("Pi Web UI server did not come back online after restart request", "error");
|
|
8582
|
+
}
|
|
8583
|
+
}
|
|
8584
|
+
|
|
8407
8585
|
async function stopServer() {
|
|
8408
8586
|
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
8587
|
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
button.textContent = "Stopping…";
|
|
8588
|
+
setServerActionBusy("Stopping…");
|
|
8589
|
+
setServerActionStatus("Stop requested. The Web UI will disconnect.", "warn");
|
|
8413
8590
|
try {
|
|
8414
8591
|
await api("/api/shutdown", { method: "POST", scoped: false });
|
|
8415
8592
|
addEvent("Pi Web UI server stop requested", "warn");
|
|
@@ -8421,11 +8598,17 @@ async function stopServer() {
|
|
|
8421
8598
|
return;
|
|
8422
8599
|
}
|
|
8423
8600
|
addEvent(error.message || String(error), "error");
|
|
8424
|
-
|
|
8425
|
-
|
|
8601
|
+
resetServerActionControls();
|
|
8602
|
+
setServerActionStatus(error.message || String(error), "error");
|
|
8426
8603
|
}
|
|
8427
8604
|
}
|
|
8428
8605
|
|
|
8606
|
+
async function runSelectedServerAction() {
|
|
8607
|
+
const action = elements.serverActionSelect?.value || "";
|
|
8608
|
+
if (action === "restart") await restartServer();
|
|
8609
|
+
else if (action === "stop") await stopServer();
|
|
8610
|
+
}
|
|
8611
|
+
|
|
8429
8612
|
function appShortcutModelLabel(model) {
|
|
8430
8613
|
return model ? `${model.provider}/${model.id}` : "unknown model";
|
|
8431
8614
|
}
|
|
@@ -8840,8 +9023,11 @@ function showNextDialog() {
|
|
|
8840
9023
|
button.type = "button";
|
|
8841
9024
|
if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
|
|
8842
9025
|
if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
|
|
8843
|
-
if (isReleaseDialog && /^Yes
|
|
8844
|
-
if (isReleaseDialog && /^
|
|
9026
|
+
if (isReleaseDialog && /^(?:Yes|All eligible packages\b|Publish selected packages \([1-9]\d*\))/.test(optionLabel)) button.classList.add("primary", "release-publish-action");
|
|
9027
|
+
if (isReleaseDialog && /^Publish selected packages \(select at least one\)$/i.test(optionLabel)) button.classList.add("release-publish-disabled-action");
|
|
9028
|
+
if (isReleaseDialog && /^\[x\]/.test(optionLabel)) button.classList.add("release-target-option", "release-target-selected");
|
|
9029
|
+
if (isReleaseDialog && /^\[ \]/.test(optionLabel)) button.classList.add("release-target-option");
|
|
9030
|
+
if (isReleaseDialog && /^(?:No|Cancel)$/i.test(optionLabel)) button.classList.add("release-cancel-action");
|
|
8845
9031
|
button.addEventListener("click", () => sendDialogResponse({ type: "extension_ui_response", id: request.id, value: optionLabel, tabId: request.tabId }));
|
|
8846
9032
|
options.append(button);
|
|
8847
9033
|
}
|
|
@@ -8889,6 +9075,8 @@ function handleEvent(event) {
|
|
|
8889
9075
|
const tabContext = activeTabContext(event.tabId || activeTabId);
|
|
8890
9076
|
switch (event.type) {
|
|
8891
9077
|
case "webui_connected":
|
|
9078
|
+
setWebuiVersion(event.version);
|
|
9079
|
+
setWebuiDevServer(isWebuiDevMetadata(event));
|
|
8892
9080
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
8893
9081
|
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
8894
9082
|
break;
|
|
@@ -9299,7 +9487,9 @@ if (elements.backgroundClearButton) {
|
|
|
9299
9487
|
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
9300
9488
|
}
|
|
9301
9489
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
9302
|
-
elements.
|
|
9490
|
+
elements.serverActionSelect.addEventListener("change", updateServerActionButton);
|
|
9491
|
+
elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
|
|
9492
|
+
updateServerActionButton();
|
|
9303
9493
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
9304
9494
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
9305
9495
|
requestPermission: elements.agentDoneNotificationsToggle.checked,
|
package/public/index.html
CHANGED
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
</div>
|
|
37
37
|
</section>
|
|
38
38
|
|
|
39
|
+
<section id="serverRestartPanel" class="server-restart-panel" aria-live="assertive" hidden>
|
|
40
|
+
<div class="server-restart-card" role="status">
|
|
41
|
+
<div class="server-restart-spinner" aria-hidden="true"></div>
|
|
42
|
+
<span class="server-restart-kicker">Restarting</span>
|
|
43
|
+
<h1>Restarting Pi Web UI server</h1>
|
|
44
|
+
<p id="serverRestartMessage">Waiting for the server to come back…</p>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
39
48
|
<main class="layout">
|
|
40
49
|
<section class="chat-panel">
|
|
41
50
|
<header class="terminal-tabs-shell">
|
|
@@ -151,8 +160,11 @@
|
|
|
151
160
|
<div class="side-panel-header">
|
|
152
161
|
<div>
|
|
153
162
|
<span class="side-panel-kicker">Pi Web UI</span>
|
|
154
|
-
<strong
|
|
155
|
-
|
|
163
|
+
<strong class="side-panel-title">
|
|
164
|
+
<span>Control Deck</span>
|
|
165
|
+
<span id="webuiVersionBadge" class="webui-version-badge" aria-label="Pi Web UI version" hidden></span>
|
|
166
|
+
<span id="webuiDevBadge" class="webui-dev-badge" aria-label="Pi Web UI dev server" hidden>DEV</span>
|
|
167
|
+
</strong>
|
|
156
168
|
</div>
|
|
157
169
|
<button id="toggleSidePanelButton" class="side-panel-toggle-button" type="button" aria-controls="sidePanel" aria-expanded="true" aria-label="Collapse side panel" title="Collapse side panel">
|
|
158
170
|
<span class="side-panel-button-icon" aria-hidden="true">
|
|
@@ -217,8 +229,16 @@
|
|
|
217
229
|
<button id="openNetworkButton" type="button">Open to network</button>
|
|
218
230
|
</div>
|
|
219
231
|
<div class="control-field server-control-field">
|
|
220
|
-
<label>Server</label>
|
|
221
|
-
<
|
|
232
|
+
<label for="serverActionSelect">Server</label>
|
|
233
|
+
<div class="server-action-row">
|
|
234
|
+
<select id="serverActionSelect" title="Server action" aria-label="Server action">
|
|
235
|
+
<option value="" selected>Choose action…</option>
|
|
236
|
+
<option value="restart">Restart Server</option>
|
|
237
|
+
<option value="stop">Stop Server</option>
|
|
238
|
+
</select>
|
|
239
|
+
<button id="runServerActionButton" type="button" disabled>Run</button>
|
|
240
|
+
</div>
|
|
241
|
+
<div id="serverActionStatus" class="server-action-status muted" role="status" aria-live="polite"></div>
|
|
222
242
|
</div>
|
|
223
243
|
<div class="control-field notification-control-field">
|
|
224
244
|
<span class="control-label">Notifications</span>
|
package/public/service-worker.js
CHANGED
package/public/styles.css
CHANGED
|
@@ -375,6 +375,65 @@ body.server-offline .layout {
|
|
|
375
375
|
filter: blur(2px);
|
|
376
376
|
pointer-events: none;
|
|
377
377
|
}
|
|
378
|
+
.server-restart-panel[hidden] {
|
|
379
|
+
display: none !important;
|
|
380
|
+
}
|
|
381
|
+
.server-restart-panel {
|
|
382
|
+
position: fixed;
|
|
383
|
+
inset: 1rem;
|
|
384
|
+
z-index: 62;
|
|
385
|
+
display: grid;
|
|
386
|
+
place-items: center;
|
|
387
|
+
padding: 1rem;
|
|
388
|
+
pointer-events: none;
|
|
389
|
+
}
|
|
390
|
+
.server-restart-card {
|
|
391
|
+
position: relative;
|
|
392
|
+
display: grid;
|
|
393
|
+
justify-items: center;
|
|
394
|
+
width: min(34rem, 100%);
|
|
395
|
+
pointer-events: auto;
|
|
396
|
+
padding: clamp(1.35rem, 4vw, 2.2rem);
|
|
397
|
+
text-align: center;
|
|
398
|
+
border: 1px solid rgba(148, 226, 213, 0.34);
|
|
399
|
+
border-radius: 1.2rem;
|
|
400
|
+
background:
|
|
401
|
+
radial-gradient(circle at 50% 0, rgba(148, 226, 213, 0.18), transparent 18rem),
|
|
402
|
+
linear-gradient(145deg, rgba(var(--ctp-base-rgb), 0.96), rgba(var(--ctp-crust-rgb), 0.98));
|
|
403
|
+
box-shadow: 0 1.2rem 4rem rgba(var(--ctp-crust-rgb), 0.74), 0 0 2rem rgba(148, 226, 213, 0.14), inset 0 1px 0 rgba(255,255,255,0.07);
|
|
404
|
+
}
|
|
405
|
+
.server-restart-spinner {
|
|
406
|
+
width: 2.8rem;
|
|
407
|
+
height: 2.8rem;
|
|
408
|
+
margin-bottom: 0.9rem;
|
|
409
|
+
border: 0.22rem solid rgba(148, 226, 213, 0.18);
|
|
410
|
+
border-top-color: var(--ctp-teal);
|
|
411
|
+
border-radius: 999px;
|
|
412
|
+
animation: server-restart-spin 900ms linear infinite;
|
|
413
|
+
}
|
|
414
|
+
.server-restart-kicker {
|
|
415
|
+
color: var(--ctp-teal);
|
|
416
|
+
font-size: 0.76rem;
|
|
417
|
+
font-weight: 900;
|
|
418
|
+
letter-spacing: 0.12em;
|
|
419
|
+
text-transform: uppercase;
|
|
420
|
+
}
|
|
421
|
+
.server-restart-card h1 {
|
|
422
|
+
margin: 0.35rem 0 0.45rem;
|
|
423
|
+
font-size: clamp(1.35rem, 4vw, 2rem);
|
|
424
|
+
}
|
|
425
|
+
.server-restart-card p {
|
|
426
|
+
margin: 0;
|
|
427
|
+
color: rgba(var(--ctp-subtext-rgb), 0.9);
|
|
428
|
+
}
|
|
429
|
+
body.server-restarting .layout {
|
|
430
|
+
opacity: 0.56;
|
|
431
|
+
filter: blur(1.5px);
|
|
432
|
+
pointer-events: none;
|
|
433
|
+
}
|
|
434
|
+
@keyframes server-restart-spin {
|
|
435
|
+
to { transform: rotate(360deg); }
|
|
436
|
+
}
|
|
378
437
|
.side-panel-expand-button {
|
|
379
438
|
position: fixed;
|
|
380
439
|
top: 1rem;
|
|
@@ -504,13 +563,36 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
504
563
|
color: var(--ctp-text);
|
|
505
564
|
letter-spacing: 0.03em;
|
|
506
565
|
}
|
|
507
|
-
.side-panel-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
566
|
+
.side-panel-header .side-panel-title {
|
|
567
|
+
display: flex;
|
|
568
|
+
align-items: center;
|
|
569
|
+
gap: 0.46rem;
|
|
570
|
+
flex-wrap: wrap;
|
|
571
|
+
}
|
|
572
|
+
.webui-version-badge,
|
|
573
|
+
.webui-dev-badge {
|
|
574
|
+
display: inline-flex;
|
|
575
|
+
align-items: center;
|
|
576
|
+
min-height: 1.18rem;
|
|
577
|
+
padding: 0.05rem 0.44rem;
|
|
578
|
+
border: 1px solid rgba(180, 190, 254, 0.24);
|
|
579
|
+
border-radius: 999px;
|
|
580
|
+
color: var(--ctp-subtext);
|
|
581
|
+
background: rgba(var(--ctp-surface-rgb), 0.74);
|
|
582
|
+
font-size: 0.68rem;
|
|
583
|
+
font-weight: 800;
|
|
584
|
+
letter-spacing: 0.04em;
|
|
585
|
+
line-height: 1;
|
|
586
|
+
}
|
|
587
|
+
.webui-dev-badge {
|
|
588
|
+
border-color: rgba(249, 226, 175, 0.38);
|
|
589
|
+
color: var(--ctp-yellow);
|
|
590
|
+
background: rgba(249, 226, 175, 0.12);
|
|
591
|
+
box-shadow: 0 0 0.7rem rgba(249, 226, 175, 0.1);
|
|
592
|
+
}
|
|
593
|
+
.webui-version-badge[hidden],
|
|
594
|
+
.webui-dev-badge[hidden] {
|
|
595
|
+
display: none;
|
|
514
596
|
}
|
|
515
597
|
.side-panel-kicker {
|
|
516
598
|
display: block;
|
|
@@ -647,6 +729,35 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
647
729
|
gap: 0.42rem;
|
|
648
730
|
align-items: center;
|
|
649
731
|
}
|
|
732
|
+
.server-action-row {
|
|
733
|
+
display: grid;
|
|
734
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
735
|
+
gap: 0.42rem;
|
|
736
|
+
align-items: center;
|
|
737
|
+
}
|
|
738
|
+
.server-action-row button {
|
|
739
|
+
width: auto;
|
|
740
|
+
min-width: 4.4rem;
|
|
741
|
+
}
|
|
742
|
+
.server-action-status {
|
|
743
|
+
min-height: 1.05rem;
|
|
744
|
+
color: rgba(var(--ctp-subtext-rgb), 0.82);
|
|
745
|
+
font-size: 0.72rem;
|
|
746
|
+
font-weight: 750;
|
|
747
|
+
line-height: 1.35;
|
|
748
|
+
}
|
|
749
|
+
.server-action-status.warn {
|
|
750
|
+
color: var(--ctp-yellow);
|
|
751
|
+
}
|
|
752
|
+
.server-action-status.error {
|
|
753
|
+
color: var(--ctp-red);
|
|
754
|
+
}
|
|
755
|
+
.server-action-status.success {
|
|
756
|
+
color: var(--ctp-green);
|
|
757
|
+
}
|
|
758
|
+
.server-action-status[hidden] {
|
|
759
|
+
display: none;
|
|
760
|
+
}
|
|
650
761
|
.background-clear-button {
|
|
651
762
|
width: 44px !important;
|
|
652
763
|
min-width: 44px !important;
|
|
@@ -1311,6 +1422,15 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1311
1422
|
color: rgba(var(--ctp-text-rgb), 0.78);
|
|
1312
1423
|
background: linear-gradient(90deg, rgba(203, 166, 247, 0.10), rgba(137, 180, 250, 0.05), rgba(148, 226, 213, 0.08));
|
|
1313
1424
|
}
|
|
1425
|
+
.widget-area:has(.release-npm-live-widget .release-npm-output-details[open]),
|
|
1426
|
+
.widget-area:has(.release-aur-live-widget .release-npm-output-details[open]) {
|
|
1427
|
+
flex: 0 0 min(44rem, 68dvh);
|
|
1428
|
+
min-height: 0;
|
|
1429
|
+
overflow: auto;
|
|
1430
|
+
overscroll-behavior: contain;
|
|
1431
|
+
scrollbar-gutter: stable;
|
|
1432
|
+
overflow-anchor: none;
|
|
1433
|
+
}
|
|
1314
1434
|
.statusbar {
|
|
1315
1435
|
position: relative;
|
|
1316
1436
|
flex: 0 0 auto;
|
|
@@ -1806,6 +1926,52 @@ button.footer-meta {
|
|
|
1806
1926
|
text-transform: none;
|
|
1807
1927
|
white-space: nowrap;
|
|
1808
1928
|
}
|
|
1929
|
+
.release-npm-output-details {
|
|
1930
|
+
display: grid;
|
|
1931
|
+
gap: 0.58rem;
|
|
1932
|
+
min-width: 0;
|
|
1933
|
+
}
|
|
1934
|
+
.release-npm-output-summary {
|
|
1935
|
+
display: grid;
|
|
1936
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1937
|
+
align-items: center;
|
|
1938
|
+
gap: 0.38rem;
|
|
1939
|
+
min-width: 0;
|
|
1940
|
+
color: rgba(var(--ctp-text-rgb), 0.92);
|
|
1941
|
+
cursor: pointer;
|
|
1942
|
+
list-style: none;
|
|
1943
|
+
}
|
|
1944
|
+
.release-npm-output-summary::-webkit-details-marker { display: none; }
|
|
1945
|
+
.release-npm-output-summary:focus-visible {
|
|
1946
|
+
outline: 2px solid rgba(137, 180, 250, 0.72);
|
|
1947
|
+
outline-offset: 0.18rem;
|
|
1948
|
+
border-radius: 0.72rem;
|
|
1949
|
+
}
|
|
1950
|
+
.release-npm-output-summary:hover .release-npm-stream-header,
|
|
1951
|
+
.release-npm-output-summary:focus-visible .release-npm-stream-header {
|
|
1952
|
+
border-color: rgba(137, 180, 250, 0.54);
|
|
1953
|
+
box-shadow: inset 0 0 0 1px rgba(137, 180, 250, 0.08), 0 0 0.8rem rgba(137, 180, 250, 0.08);
|
|
1954
|
+
}
|
|
1955
|
+
.release-npm-output-toggle {
|
|
1956
|
+
display: inline-grid;
|
|
1957
|
+
place-items: center;
|
|
1958
|
+
width: 1rem;
|
|
1959
|
+
height: 1rem;
|
|
1960
|
+
flex: 0 0 auto;
|
|
1961
|
+
color: rgba(var(--ctp-subtext-rgb), 0.78);
|
|
1962
|
+
font-size: 1rem;
|
|
1963
|
+
font-weight: 950;
|
|
1964
|
+
line-height: 1;
|
|
1965
|
+
transition: transform 0.16s ease, color 0.16s ease;
|
|
1966
|
+
}
|
|
1967
|
+
.release-npm-output-details[open] .release-npm-output-toggle {
|
|
1968
|
+
color: var(--ctp-blue);
|
|
1969
|
+
transform: rotate(90deg);
|
|
1970
|
+
}
|
|
1971
|
+
.release-npm-output-summary .release-npm-stream-header {
|
|
1972
|
+
min-width: 0;
|
|
1973
|
+
width: 100%;
|
|
1974
|
+
}
|
|
1809
1975
|
.release-npm-terminal {
|
|
1810
1976
|
max-height: min(34rem, 42vh);
|
|
1811
1977
|
min-height: 5.25rem;
|
|
@@ -1825,6 +1991,11 @@ button.footer-meta {
|
|
|
1825
1991
|
line-height: 1.5;
|
|
1826
1992
|
overscroll-behavior: contain;
|
|
1827
1993
|
}
|
|
1994
|
+
.release-npm-live-widget .release-npm-output-details[open] .release-npm-terminal,
|
|
1995
|
+
.release-aur-live-widget .release-npm-output-details[open] .release-npm-terminal {
|
|
1996
|
+
height: clamp(15rem, 42dvh, 31rem);
|
|
1997
|
+
min-height: 0;
|
|
1998
|
+
}
|
|
1828
1999
|
.release-npm-line {
|
|
1829
2000
|
display: block;
|
|
1830
2001
|
width: max-content;
|
|
@@ -3189,6 +3360,28 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3189
3360
|
text-align: center;
|
|
3190
3361
|
font-weight: 800;
|
|
3191
3362
|
}
|
|
3363
|
+
.extension-dialog.release-dialog .dialog-options button.release-publish-disabled-action {
|
|
3364
|
+
color: rgba(var(--ctp-subtext-rgb), 0.72);
|
|
3365
|
+
border-color: rgba(var(--ctp-overlay-rgb), 0.32);
|
|
3366
|
+
background: linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.58), rgba(var(--ctp-crust-rgb), 0.72));
|
|
3367
|
+
}
|
|
3368
|
+
.extension-dialog.release-dialog .dialog-options button.release-target-option {
|
|
3369
|
+
text-align: left;
|
|
3370
|
+
border-color: rgba(137, 180, 250, 0.34);
|
|
3371
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
3372
|
+
font-size: 0.78rem;
|
|
3373
|
+
line-height: 1.35;
|
|
3374
|
+
overflow-wrap: anywhere;
|
|
3375
|
+
white-space: normal;
|
|
3376
|
+
}
|
|
3377
|
+
.extension-dialog.release-dialog .dialog-options button.release-target-selected {
|
|
3378
|
+
color: var(--ctp-green);
|
|
3379
|
+
border-color: rgba(166, 227, 161, 0.58);
|
|
3380
|
+
background:
|
|
3381
|
+
linear-gradient(120deg, rgba(166, 227, 161, 0.16), rgba(137, 180, 250, 0.08)),
|
|
3382
|
+
linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.80), rgba(var(--ctp-crust-rgb), 0.78));
|
|
3383
|
+
box-shadow: 0 0 1rem rgba(166, 227, 161, 0.14);
|
|
3384
|
+
}
|
|
3192
3385
|
.extension-dialog.release-dialog .dialog-options button.release-cancel-action {
|
|
3193
3386
|
border-color: rgba(249, 226, 175, 0.38);
|
|
3194
3387
|
color: var(--ctp-yellow);
|
package/start-webui.sh
CHANGED
|
@@ -183,51 +183,10 @@ connect_host_for_port() {
|
|
|
183
183
|
esac
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
print_manual_url() {
|
|
187
187
|
local url="$1"
|
|
188
|
-
local platform=""
|
|
189
|
-
platform="$(uname -s 2>/dev/null || true)"
|
|
190
188
|
|
|
191
|
-
|
|
192
|
-
MINGW*|MSYS*|CYGWIN*)
|
|
193
|
-
if command -v cmd.exe >/dev/null 2>&1; then
|
|
194
|
-
cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
|
|
195
|
-
return 0
|
|
196
|
-
fi
|
|
197
|
-
if command -v powershell.exe >/dev/null 2>&1; then
|
|
198
|
-
powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
|
|
199
|
-
return 0
|
|
200
|
-
fi
|
|
201
|
-
;;
|
|
202
|
-
Linux*)
|
|
203
|
-
if grep -qi microsoft /proc/version 2>/dev/null; then
|
|
204
|
-
if command -v wslview >/dev/null 2>&1; then
|
|
205
|
-
wslview "$url" </dev/null >/dev/null 2>&1 &
|
|
206
|
-
return 0
|
|
207
|
-
fi
|
|
208
|
-
if command -v cmd.exe >/dev/null 2>&1; then
|
|
209
|
-
cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
|
|
210
|
-
return 0
|
|
211
|
-
fi
|
|
212
|
-
fi
|
|
213
|
-
;;
|
|
214
|
-
esac
|
|
215
|
-
|
|
216
|
-
if command -v xdg-open >/dev/null 2>&1; then
|
|
217
|
-
xdg-open "$url" </dev/null >/dev/null 2>&1 &
|
|
218
|
-
elif command -v open >/dev/null 2>&1; then
|
|
219
|
-
open "$url" </dev/null >/dev/null 2>&1 &
|
|
220
|
-
elif command -v wslview >/dev/null 2>&1; then
|
|
221
|
-
wslview "$url" </dev/null >/dev/null 2>&1 &
|
|
222
|
-
elif command -v cmd.exe >/dev/null 2>&1; then
|
|
223
|
-
cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
|
|
224
|
-
elif command -v powershell.exe >/dev/null 2>&1; then
|
|
225
|
-
powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
|
|
226
|
-
else
|
|
227
|
-
echo "Could not find a browser opener. Open manually:" >&2
|
|
228
|
-
echo " $url" >&2
|
|
229
|
-
return 1
|
|
230
|
-
fi
|
|
189
|
+
echo "Open manually: $url"
|
|
231
190
|
}
|
|
232
191
|
|
|
233
192
|
http_ok() {
|
|
@@ -456,8 +415,7 @@ main() {
|
|
|
456
415
|
if [[ "$dev_mode" -eq 1 ]]; then
|
|
457
416
|
echo "--dev only affects newly started servers; stop the existing server first to run this checkout."
|
|
458
417
|
fi
|
|
459
|
-
|
|
460
|
-
open_url "$target_url" || true
|
|
418
|
+
print_manual_url "$target_url"
|
|
461
419
|
exit 0
|
|
462
420
|
fi
|
|
463
421
|
|
|
@@ -474,10 +432,12 @@ main() {
|
|
|
474
432
|
if [[ "$dev_mode" -eq 1 ]]; then
|
|
475
433
|
local_webui_bin="$(local_pi_webui_bin)"
|
|
476
434
|
webui_cmd=(node "$local_webui_bin")
|
|
435
|
+
export PI_WEBUI_DEV=1
|
|
477
436
|
echo "Dev mode: using local Pi Web UI server: $local_webui_bin"
|
|
478
437
|
else
|
|
479
438
|
ensure_pi_webui
|
|
480
439
|
webui_cmd=("$PI_WEBUI_COMMAND")
|
|
440
|
+
unset PI_WEBUI_DEV
|
|
481
441
|
fi
|
|
482
442
|
|
|
483
443
|
echo "Starting Pi Web UI in: $cwd"
|
|
@@ -491,7 +451,8 @@ main() {
|
|
|
491
451
|
trap 'cleanup; exit 143' TERM
|
|
492
452
|
|
|
493
453
|
if wait_until_ready "$url" "$SERVER_PID"; then
|
|
494
|
-
|
|
454
|
+
echo "Pi Web UI is ready."
|
|
455
|
+
print_manual_url "$url"
|
|
495
456
|
else
|
|
496
457
|
ready_status="$?"
|
|
497
458
|
if [[ "$ready_status" -eq 2 ]]; then
|
|
@@ -500,8 +461,8 @@ main() {
|
|
|
500
461
|
exit $?
|
|
501
462
|
fi
|
|
502
463
|
|
|
503
|
-
echo "Server did not respond yet; opening
|
|
504
|
-
|
|
464
|
+
echo "Server did not respond yet; not opening a browser automatically." >&2
|
|
465
|
+
print_manual_url "$url"
|
|
505
466
|
fi
|
|
506
467
|
|
|
507
468
|
wait "$SERVER_PID"
|
|
@@ -41,11 +41,15 @@ assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png"
|
|
|
41
41
|
assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
|
|
42
42
|
assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
|
|
43
43
|
assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
|
|
44
|
+
assert.match(html, /<strong class="side-panel-title">[\s\S]*Control Deck[\s\S]*id="webuiVersionBadge"[\s\S]*id="webuiDevBadge"/, "Control Deck title should expose Web UI version and dev badges");
|
|
45
|
+
assert.doesNotMatch(html, /id="sessionLine"/, "Control Deck title should not show verbose session status metadata");
|
|
44
46
|
assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
|
|
45
47
|
assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
|
|
46
48
|
assert.match(html, /id="backgroundInput"[^>]*type="file"[^>]*accept="image\/png,image\/jpeg,image\/webp,image\/gif"/, "side panel should expose an image picker for custom backgrounds");
|
|
47
49
|
assert.match(html, /id="backgroundClearButton"[\s\S]*?>×<\/button>/, "side-panel background control should expose an X remove button");
|
|
48
|
-
assert.match(html, /id="
|
|
50
|
+
assert.match(html, /id="serverActionSelect"[\s\S]*<option value="restart">Restart Server<\/option>[\s\S]*<option value="stop">Stop Server<\/option>/, "side panel should expose restart and stop server actions in a dropdown");
|
|
51
|
+
assert.match(html, /id="runServerActionButton"[^>]*disabled[^>]*>Run<\/button>/, "side panel should expose a guarded button for selected server actions");
|
|
52
|
+
assert.match(html, /id="serverActionStatus"[^>]*aria-live="polite"/, "server actions should expose visible restart feedback");
|
|
49
53
|
assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
|
|
50
54
|
assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
|
|
51
55
|
assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
|
|
@@ -56,6 +60,7 @@ assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optiona
|
|
|
56
60
|
assert.match(html, /id="codexUsageBox"/, "side panel should expose Codex subscription usage status");
|
|
57
61
|
assert.match(html, /data-side-panel-section="codex-usage"/, "Codex usage should live in a collapsible side-panel section");
|
|
58
62
|
assert.match(html, /id="serverOfflinePanel"/, "PWA/offline shell should expose a backend-offline recovery panel");
|
|
63
|
+
assert.match(html, /id="serverRestartPanel"[\s\S]*id="serverRestartMessage"/, "server restart should expose a loading overlay instead of the generic offline shell");
|
|
59
64
|
assert.match(html, /id="copyServerCommandButton"/, "backend-offline recovery panel should expose a start-command copy button");
|
|
60
65
|
assert.match(html, /id="retryServerConnectionButton"/, "backend-offline recovery panel should expose a retry button");
|
|
61
66
|
assert.match(html, /data-side-panel-section="controls"/, "side panel controls should live in a collapsible section");
|
|
@@ -135,6 +140,10 @@ assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*
|
|
|
135
140
|
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
136
141
|
assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
|
|
137
142
|
assert.match(css, /\.side-panel-section-toggle \{[\s\S]*?justify-content:\s*space-between/, "side panel section toggles should align labels and chevrons");
|
|
143
|
+
assert.match(css, /\.server-restart-panel \{[\s\S]*?z-index:\s*62/, "server restart overlay should render above the offline shell");
|
|
144
|
+
assert.match(css, /@keyframes server-restart-spin/, "server restart overlay should show a loading spinner");
|
|
145
|
+
assert.match(css, /\.webui-version-badge,\n\.webui-dev-badge \{[\s\S]*?border-radius:\s*999px/, "Web UI version and dev indicators should render as compact title badges");
|
|
146
|
+
assert.match(css, /\.webui-dev-badge \{[\s\S]*?color:\s*var\(--ctp-yellow\)/, "Web UI dev indicator should have distinct warning styling");
|
|
138
147
|
assert.match(css, /\.side-panel-section\.collapsed \.side-panel-section-content,\n\.side-panel-section-content\[hidden\] \{\n\s+display:\s*none;/, "collapsed side panel section content should be hidden");
|
|
139
148
|
assert.match(css, /\.side-panel-section:not\(\.collapsed\) \.side-panel-section-chevron/, "expanded side panel sections should rotate the chevron");
|
|
140
149
|
assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
|
|
@@ -145,6 +154,10 @@ assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-prog
|
|
|
145
154
|
assert.match(css, /\.todo-widget-item\.done \.todo-widget-text[\s\S]*?text-decoration:\s*line-through/, "todo-progress completed items should be visually crossed out");
|
|
146
155
|
assert.match(css, /\.release-npm-widget \{[\s\S]*?border-left:\s*0\.28rem solid/, "release-npm output should stand apart from the page background");
|
|
147
156
|
assert.match(css, /\.release-npm-stream-header \{[\s\S]*?text-transform:\s*uppercase/, "release-npm output should label the output stream clearly");
|
|
157
|
+
assert.match(css, /\.release-npm-output-summary \{[\s\S]*?cursor:\s*pointer/, "release-npm output should expose a local expand/collapse summary");
|
|
158
|
+
assert.match(css, /\.release-npm-output-details\[open\] \.release-npm-output-toggle/, "release-npm expanded output should rotate the summary chevron");
|
|
159
|
+
assert.match(css, /\.widget-area:has\(\.release-npm-live-widget \.release-npm-output-details\[open\]\)[\s\S]*?flex:\s*0 0 min\(44rem, 68dvh\)/, "live release output should reserve a stable widget slot instead of resizing the transcript while streaming");
|
|
160
|
+
assert.match(css, /\.release-npm-live-widget \.release-npm-output-details\[open\] \.release-npm-terminal,[\s\S]*?height:\s*clamp\(15rem, 42dvh, 31rem\)/, "live release terminals should keep a fixed viewport height while output streams");
|
|
148
161
|
assert.match(css, /\.release-npm-terminal \{[\s\S]*?rgba\(3, 4, 10, 0\.98\)/, "release-npm terminal should use a high-contrast stream panel");
|
|
149
162
|
assert.match(css, /\.release-aur-widget \{[\s\S]*?border-color/, "release-aur output should render as a specialized Web UI widget variant");
|
|
150
163
|
assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
|
|
@@ -239,8 +252,22 @@ assert.match(app, /Restart Web UI to load themes/, "frontend should explain when
|
|
|
239
252
|
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
240
253
|
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
241
254
|
assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
|
|
242
|
-
assert.match(app, /
|
|
255
|
+
assert.match(app, /webuiVersionBadge: \$\("#webuiVersionBadge"\)/, "frontend should bind the Control Deck version badge");
|
|
256
|
+
assert.match(app, /webuiDevBadge: \$\("#webuiDevBadge"\)/, "frontend should bind the Control Deck dev badge");
|
|
257
|
+
assert.match(app, /function refreshWebuiVersion\(\)[\s\S]*api\("\/api\/health", \{ scoped: false \}\)[\s\S]*setWebuiVersion\(health\.webuiVersion\)[\s\S]*setWebuiDevServer\(isWebuiDevMetadata\(health\)\)/, "frontend should load Web UI version and dev mode from health metadata");
|
|
258
|
+
assert.match(app, /case "webui_connected":[\s\S]*setWebuiVersion\(event\.version\)[\s\S]*setWebuiDevServer\(isWebuiDevMetadata\(event\)\)/, "frontend should refresh Web UI version and dev mode from reconnect events");
|
|
259
|
+
assert.match(server, /const webuiDevServer = isTruthyEnv\(process\.env\.PI_WEBUI_DEV\) \|\| isSourceCheckout\(packageRoot\)/, "server should derive dev mode from PI_WEBUI_DEV or a source checkout");
|
|
260
|
+
assert.match(server, /webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "server status should expose Web UI dev mode");
|
|
261
|
+
assert.match(server, /type: "webui_connected",[\s\S]*webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "SSE connect event should expose Web UI dev mode");
|
|
262
|
+
assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
|
|
263
|
+
assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
|
|
264
|
+
assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
|
|
265
|
+
assert.match(app, /setServerActionStatus\(message, "warn"\);\n\s+setServerRestartOverlay\(true, message\)/, "Restart Server action should show reconnect progress in the side panel and loading overlay");
|
|
266
|
+
assert.match(app, /const showOfflinePanel = backendOffline && !serverRestartInProgress/, "intentional restart should suppress the generic offline shell while reconnecting");
|
|
243
267
|
assert.match(app, /api\("\/api\/shutdown", \{ method: "POST", scoped: false \}\)/, "Stop Server action should call the unscoped shutdown endpoint");
|
|
268
|
+
assert.match(server, /url\.pathname === "\/api\/restart" && req\.method === "POST"/, "server should expose restart endpoint");
|
|
269
|
+
assert.match(server, /PI_WEBUI_RESTORE_TABS: JSON\.stringify\(restorableTabs \|\| \[\]\)/, "server restart should preserve restorable tab metadata");
|
|
270
|
+
assert.match(server, /if \(webuiDevServer\) env\.PI_WEBUI_DEV = "1";/, "server restart should explicitly preserve dev mode");
|
|
244
271
|
assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
|
|
245
272
|
assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
|
|
246
273
|
assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
|
|
@@ -324,7 +351,10 @@ assert.match(app, /function bindGitWorkflowToActiveTab\(\) \{\n\s+gitWorkflow =
|
|
|
324
351
|
assert.match(app, /function setGitWorkflow\(patch, \{ tabId = activeTabId \} = \{\}\)[\s\S]*if \(tabId === activeTabId\) \{[\s\S]*renderGitWorkflow\(\);/, "guided git workflow should not render inactive terminal workflows globally");
|
|
325
352
|
assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided git workflow should not pin or show workflows outside their owning terminal tab");
|
|
326
353
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
354
|
+
assert.match(app, /const releaseNpmOutputExpandedByTab = new Map\(\)/, "release-npm output collapse state should be tracked per browser tab");
|
|
355
|
+
assert.match(app, /function renderReleaseNpmOutputDetails\(key, streamHeader, terminal, controls = null\)[\s\S]*node\.open = releaseNpmOutputExpandedByTab\.get\(stateKey\) !== false[\s\S]*release-npm-output-toggle/, "release-npm output should render as a browser-side details expander");
|
|
327
356
|
assert.match(app, /releaseNpmStreamHeader\("Live output stream", outputLines\.length, \{ live: true \}\)/, "release-npm live output should expose a clear stream heading");
|
|
357
|
+
assert.match(app, /renderReleaseNpmOutputDetails\("release-npm:output", streamHeader, terminal, controls\)/, "release-npm live stream should be wrapped in the local expander");
|
|
328
358
|
assert.match(app, /function renderReleaseAurOutputWidget\(\)/, "release-aur live output should use a specialized Web UI renderer");
|
|
329
359
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-abort", "danger"\)/, "release-npm Web UI output should expose an abort action");
|
|
330
360
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-aur abort", "danger"\)/, "release-aur Web UI output should expose an abort action");
|
|
@@ -566,7 +596,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
566
596
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/apple-touch-icon.png" && icon.sizes === "180x180"), "PWA manifest should include a conventional 180px apple touch icon");
|
|
567
597
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
568
598
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
569
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
599
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v18"/, "PWA service worker should define an app-shell cache");
|
|
570
600
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
571
601
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
572
602
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
@@ -715,6 +745,7 @@ assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "REA
|
|
|
715
745
|
assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
|
|
716
746
|
assert.match(startScript, /local_pi_webui_bin\(\)/, "start-webui.sh should resolve this checkout's local server entrypoint");
|
|
717
747
|
assert.match(startScript, /webui_cmd=\(node "\$local_webui_bin"\)/, "start-webui.sh --dev should run the local bin with node");
|
|
748
|
+
assert.match(startScript, /export PI_WEBUI_DEV=1/, "start-webui.sh --dev should mark the Web UI server as dev mode");
|
|
718
749
|
assert.match(startScript, /"\$\{webui_cmd\[@\]\}" --cwd "\$cwd" --host "\$host" --port "\$port" "\$\{pass_args\[@\]\}"/, "start-webui.sh should launch through the selected server command without forwarding --dev");
|
|
719
750
|
|
|
720
751
|
assert.match(pkg.scripts?.test || "", /node tests\/mobile-static\.test\.mjs/, "package test script should run the mobile static harness");
|