@firstpick/pi-package-webui 0.4.2 → 0.4.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 +6 -4
- package/WEBUI_TUI_NATIVE_PARITY.json +2 -2
- package/bin/pi-webui.mjs +94 -13
- package/package.json +5 -3
- package/public/app.js +473 -47
- package/public/index.html +10 -4
- package/public/styles.css +176 -55
- package/tests/http-endpoints-harness.test.mjs +2 -0
- package/tests/mobile-static.test.mjs +59 -10
- package/tests/remote-auth-settings-harness.test.mjs +81 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Local browser UI for [Pi coding agent](https://www.npmjs.com/package/@earendil-w
|
|
|
6
6
|
|
|
7
7
|
Pi Web UI gives you a local browser companion for Pi: multi-tab chat, streaming output, model controls, uploads, slash-command helpers, workspace navigation, and optional extension widgets.
|
|
8
8
|
|
|
9
|
-
> **Security:** Pi Web UI can control the spawned Pi session and run anything that session is allowed to run. It binds to `127.0.0.1` by default. Remote PIN authentication is off by default;
|
|
9
|
+
> **Security:** Pi Web UI can control the spawned Pi session and run anything that session is allowed to run. It binds to `127.0.0.1` by default. Remote PIN authentication is off by default on first use; enabling it in **Controls → Network → Remote PIN auth** persists that preference for later Web UI starts.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -123,6 +123,7 @@ Environment variables:
|
|
|
123
123
|
- `PI_WEBUI_PORT`
|
|
124
124
|
- `PI_WEBUI_PI_BIN`
|
|
125
125
|
- `PI_WEBUI_REMOTE_AUTH=1` to start with remote PIN authentication enabled
|
|
126
|
+
- `PI_WEBUI_SETTINGS_FILE=/path/to/settings.json` to override where Web UI stores persisted settings such as the Remote PIN auth preference
|
|
126
127
|
|
|
127
128
|
## Main features
|
|
128
129
|
|
|
@@ -130,7 +131,7 @@ Environment variables:
|
|
|
130
131
|
- Multi-tab Pi sessions with isolated processes, working directories, prompt drafts, activity state, and a workspace dashboard for common actions.
|
|
131
132
|
- Unified command palette (`Ctrl/Cmd+K`) for commands, tabs, models, sessions, settings, and frequent Web UI actions.
|
|
132
133
|
- Automatic tab naming from the first prompt, with `--name <name>` still available for an explicit initial tab name.
|
|
133
|
-
- Streaming chat transcript with Markdown, thinking output, tool/bash cards, queue and compaction events, edit-and-retry from user prompts, and abort controls.
|
|
134
|
+
- Streaming chat transcript with Markdown, thinking output, tool/bash cards, queue and compaction events, edit-and-retry from user prompts, and guarded abort controls that require holding Esc or the Abort button for 3 seconds.
|
|
134
135
|
- Prompt composer with uploads, drag/drop/paste, inline image support, slash-command autocomplete, and `@` file/path references with live suggestions.
|
|
135
136
|
- Browser dialogs for common Pi selectors such as `/model`, `/settings`, `/theme`, `/fork`, `/clone`, `/resume`, `/tree`, `/scoped-models`, `/tools`, and `/skills`.
|
|
136
137
|
- Model, thinking, session, workspace, theme, optional-feature, Codex usage, network, update/restart, event, and notification controls in the side panel.
|
|
@@ -175,6 +176,7 @@ Optional companions:
|
|
|
175
176
|
- `@firstpick/pi-extension-setup-skills` — TUI `/skills` setup command alongside WebUI-native skill toggles.
|
|
176
177
|
- `@firstpick/pi-extension-todo-progress` — todo-progress rendering.
|
|
177
178
|
- `@firstpick/pi-extension-tools` — TUI `/tools` active-tool manager alongside WebUI-native tool toggles.
|
|
179
|
+
- `@firstpick/pi-package-remote-webui` — `/remote` trusted-LAN QR helper for connecting mobile browsers to Web UI.
|
|
178
180
|
- `@firstpick/pi-extension-git-footer-status` — richer extension-owned git/footer status, including the structured Web UI footer payload.
|
|
179
181
|
- `@firstpick/pi-extension-stats` — stats commands and status data.
|
|
180
182
|
- `@firstpick/pi-themes-bundle` — Web UI and Pi theme resources.
|
|
@@ -205,8 +207,8 @@ This requires `/git-staged-msg` and `/pr` from `@firstpick/pi-prompts-git-pr`; b
|
|
|
205
207
|
|
|
206
208
|
- Default bind is localhost-only: `127.0.0.1:31415`.
|
|
207
209
|
- 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".
|
|
208
|
-
- The side-panel **Remote PIN auth** toggle is off by default. When enabled, the server generates a random 4-digit PIN, shows it in Controls and `/webui-status`, and requires it from non-local browser clients.
|
|
209
|
-
- Localhost clients stay frictionless and can toggle Remote PIN auth; changing the toggle disconnects existing event streams so remote clients must re-authenticate after enablement.
|
|
210
|
+
- The side-panel **Remote PIN auth** toggle is off by default on first use. When enabled, the server saves that preference, generates a fresh random 4-digit PIN for each server start, shows it in Controls and `/webui-status`, and requires it from non-local browser clients.
|
|
211
|
+
- Localhost clients stay frictionless and can toggle Remote PIN auth; changing the toggle persists the preference and disconnects existing event streams so remote clients must re-authenticate after enablement.
|
|
210
212
|
- `--host 0.0.0.0` also exposes the Web UI to the local network; pass `--remote-auth` to start with PIN auth already enabled.
|
|
211
213
|
- Any connected browser client with access (and the PIN, if enabled) can control Pi and run Web UI bash actions as the Web UI process user.
|
|
212
214
|
- Remote PIN auth is a simple trusted-LAN HTTP gate, not hardened multi-user authentication; do not expose it to untrusted networks.
|
|
@@ -445,8 +445,8 @@
|
|
|
445
445
|
"priority": "P1",
|
|
446
446
|
"sensitive": false,
|
|
447
447
|
"guards": ["confirmation"],
|
|
448
|
-
"currentBehavior": "Escape aborts active user bash first, then active agent runs, and closes UI surfaces with tab scoping.",
|
|
449
|
-
"targetBehavior": "Abort active bash first where applicable; keep agent/tab scoping and clear UI cancellation rules."
|
|
448
|
+
"currentBehavior": "Holding Escape for 3 seconds aborts active user bash first, then active agent runs, and closes UI surfaces with tab scoping before arming abort.",
|
|
449
|
+
"targetBehavior": "Abort active bash first where applicable; keep agent/tab scoping and clear UI cancellation rules with a guarded hold-to-abort affordance."
|
|
450
450
|
},
|
|
451
451
|
{
|
|
452
452
|
"id": "shortcut.editor.clear",
|
package/bin/pi-webui.mjs
CHANGED
|
@@ -43,6 +43,7 @@ const publicDir = path.join(packageRoot, "public");
|
|
|
43
43
|
const webuiHelperExtensionPath = path.join(packageRoot, "webui-rpc-helper.mjs");
|
|
44
44
|
const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(homedir(), ".pi", "agent");
|
|
45
45
|
const OPTIONAL_FEATURE_INSTALL_ROOT_ENV = "PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT";
|
|
46
|
+
const WEBUI_SETTINGS_FILE_ENV = "PI_WEBUI_SETTINGS_FILE";
|
|
46
47
|
const packageJson = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
47
48
|
let piPackageJson = {};
|
|
48
49
|
try {
|
|
@@ -227,6 +228,7 @@ const OPTIONAL_FEATURE_PACKAGES = new Map([
|
|
|
227
228
|
["tuiSkillsCommand", "@firstpick/pi-extension-setup-skills"],
|
|
228
229
|
["todoProgressWidget", "@firstpick/pi-extension-todo-progress"],
|
|
229
230
|
["tuiToolsCommand", "@firstpick/pi-extension-tools"],
|
|
231
|
+
["remoteWebui", "@firstpick/pi-package-remote-webui"],
|
|
230
232
|
["gitFooterStatus", "@firstpick/pi-extension-git-footer-status"],
|
|
231
233
|
["statsCommand", "@firstpick/pi-extension-stats"],
|
|
232
234
|
["themeBundle", "@firstpick/pi-themes-bundle"],
|
|
@@ -265,8 +267,8 @@ Examples:
|
|
|
265
267
|
|
|
266
268
|
Security:
|
|
267
269
|
The web UI controls Pi tools. It binds to localhost by default. Remote PIN
|
|
268
|
-
authentication is off by default;
|
|
269
|
-
|
|
270
|
+
authentication is off by default on first use; enabling it in Controls saves
|
|
271
|
+
that preference for later starts.
|
|
270
272
|
`);
|
|
271
273
|
}
|
|
272
274
|
|
|
@@ -289,6 +291,7 @@ function parseArgs(argv) {
|
|
|
289
291
|
noSession: false,
|
|
290
292
|
name: undefined,
|
|
291
293
|
remoteAuth: isTruthyEnv(process.env.PI_WEBUI_REMOTE_AUTH),
|
|
294
|
+
remoteAuthExplicit: process.env.PI_WEBUI_REMOTE_AUTH !== undefined,
|
|
292
295
|
piArgs: [],
|
|
293
296
|
help: false,
|
|
294
297
|
version: false,
|
|
@@ -345,10 +348,12 @@ function parseArgs(argv) {
|
|
|
345
348
|
}
|
|
346
349
|
if (arg === "--remote-auth") {
|
|
347
350
|
options.remoteAuth = true;
|
|
351
|
+
options.remoteAuthExplicit = true;
|
|
348
352
|
continue;
|
|
349
353
|
}
|
|
350
354
|
if (arg === "--no-remote-auth") {
|
|
351
355
|
options.remoteAuth = false;
|
|
356
|
+
options.remoteAuthExplicit = true;
|
|
352
357
|
continue;
|
|
353
358
|
}
|
|
354
359
|
throw new Error(`Unknown option: ${arg}. Pass Pi CLI args after --.`);
|
|
@@ -739,7 +744,7 @@ function sendRemoteAuthPage(res, returnPath = "/") {
|
|
|
739
744
|
<body>
|
|
740
745
|
<main>
|
|
741
746
|
<h1>Remote PIN required</h1>
|
|
742
|
-
<p>
|
|
747
|
+
<p>Scan a trusted /remote QR code to unlock automatically, or enter the 4-digit PIN shown in the local Pi terminal or local Web UI.</p>
|
|
743
748
|
<form id="pinForm" autocomplete="off">
|
|
744
749
|
<label for="pin">PIN</label>
|
|
745
750
|
<input id="pin" name="pin" inputmode="numeric" pattern="[0-9]{4}" maxlength="4" autofocus required>
|
|
@@ -753,16 +758,19 @@ function sendRemoteAuthPage(res, returnPath = "/") {
|
|
|
753
758
|
const input = document.getElementById("pin");
|
|
754
759
|
const button = document.getElementById("submit");
|
|
755
760
|
const error = document.getElementById("error");
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
761
|
+
function pinFromHash() {
|
|
762
|
+
const params = new URLSearchParams(String(window.location.hash || "").replace(/^#/, ""));
|
|
763
|
+
const pin = String(params.get("pin") || "").trim();
|
|
764
|
+
return /^\\d{4}$/.test(pin) ? pin : "";
|
|
765
|
+
}
|
|
766
|
+
async function submitPin(pin) {
|
|
759
767
|
button.disabled = true;
|
|
760
768
|
error.textContent = "";
|
|
761
769
|
try {
|
|
762
770
|
const response = await fetch("/api/remote-auth", {
|
|
763
771
|
method: "POST",
|
|
764
772
|
headers: { "content-type": "application/json" },
|
|
765
|
-
body: JSON.stringify({ pin
|
|
773
|
+
body: JSON.stringify({ pin }),
|
|
766
774
|
});
|
|
767
775
|
const data = await response.json().catch(() => ({}));
|
|
768
776
|
if (!response.ok || data.ok !== true) throw new Error(data.error || "Incorrect PIN");
|
|
@@ -773,7 +781,18 @@ function sendRemoteAuthPage(res, returnPath = "/") {
|
|
|
773
781
|
} finally {
|
|
774
782
|
button.disabled = false;
|
|
775
783
|
}
|
|
784
|
+
}
|
|
785
|
+
input.addEventListener("input", () => { input.value = input.value.replace(/\\D/g, "").slice(0, 4); error.textContent = ""; });
|
|
786
|
+
form.addEventListener("submit", async (event) => {
|
|
787
|
+
event.preventDefault();
|
|
788
|
+
await submitPin(input.value);
|
|
776
789
|
});
|
|
790
|
+
const autoPin = pinFromHash();
|
|
791
|
+
if (autoPin) {
|
|
792
|
+
input.value = autoPin;
|
|
793
|
+
window.history.replaceState(null, "", window.location.pathname + (window.location.search || ""));
|
|
794
|
+
submitPin(autoPin);
|
|
795
|
+
}
|
|
777
796
|
</script>
|
|
778
797
|
</body>
|
|
779
798
|
</html>`;
|
|
@@ -1332,6 +1351,50 @@ async function writePathFastPicks(picks) {
|
|
|
1332
1351
|
return normalized;
|
|
1333
1352
|
}
|
|
1334
1353
|
|
|
1354
|
+
function webuiSettingsFile() {
|
|
1355
|
+
if (process.env[WEBUI_SETTINGS_FILE_ENV]) return path.resolve(expandUserPath(process.env[WEBUI_SETTINGS_FILE_ENV]));
|
|
1356
|
+
const configRoot = process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
|
|
1357
|
+
return path.join(configRoot, "pi-webui", "settings.json");
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function normalizeWebuiSettings(value) {
|
|
1361
|
+
return {
|
|
1362
|
+
version: 1,
|
|
1363
|
+
remoteAuthEnabled: value?.remoteAuthEnabled === true,
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
let webuiSettingsCache = null;
|
|
1368
|
+
|
|
1369
|
+
async function readWebuiSettings() {
|
|
1370
|
+
if (webuiSettingsCache) return webuiSettingsCache;
|
|
1371
|
+
webuiSettingsCache = normalizeWebuiSettings(await readJsonFileIfExists(webuiSettingsFile()));
|
|
1372
|
+
return webuiSettingsCache;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
async function writeWebuiSettings(patch) {
|
|
1376
|
+
const current = await readWebuiSettings();
|
|
1377
|
+
const next = normalizeWebuiSettings({ ...current, ...(patch || {}) });
|
|
1378
|
+
const storageFile = webuiSettingsFile();
|
|
1379
|
+
await mkdir(path.dirname(storageFile), { recursive: true });
|
|
1380
|
+
const tmpFile = `${storageFile}.${process.pid}.${Date.now()}.tmp`;
|
|
1381
|
+
await writeFile(tmpFile, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
1382
|
+
await rename(tmpFile, storageFile);
|
|
1383
|
+
webuiSettingsCache = next;
|
|
1384
|
+
return next;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async function readPersistedRemoteAuthEnabled() {
|
|
1388
|
+
return (await readWebuiSettings()).remoteAuthEnabled === true;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
async function saveRemoteAuthPreference(enabled) {
|
|
1392
|
+
const nextEnabled = enabled === true;
|
|
1393
|
+
await writeWebuiSettings({ remoteAuthEnabled: nextEnabled });
|
|
1394
|
+
persistedRemoteAuthEnabled = nextEnabled;
|
|
1395
|
+
return persistedRemoteAuthEnabled;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1335
1398
|
function parseCliScopedModelPatterns() {
|
|
1336
1399
|
for (let index = 0; index < options.piArgs.length; index++) {
|
|
1337
1400
|
const arg = options.piArgs[index];
|
|
@@ -6606,6 +6669,7 @@ async function createInitialTabs() {
|
|
|
6606
6669
|
}
|
|
6607
6670
|
|
|
6608
6671
|
const serverStartedAt = new Date().toISOString();
|
|
6672
|
+
let persistedRemoteAuthEnabled = await readPersistedRemoteAuthEnabled();
|
|
6609
6673
|
const initialTabs = await createInitialTabs();
|
|
6610
6674
|
const initialTab = initialTabs[0];
|
|
6611
6675
|
let currentHost = options.host;
|
|
@@ -6650,6 +6714,19 @@ function resetRemoteAuth() {
|
|
|
6650
6714
|
remoteAuth.tokenExpiresAt = 0;
|
|
6651
6715
|
}
|
|
6652
6716
|
|
|
6717
|
+
function remoteAuthPreferenceEnabled() {
|
|
6718
|
+
return persistedRemoteAuthEnabled === true;
|
|
6719
|
+
}
|
|
6720
|
+
|
|
6721
|
+
function remoteAuthStartupEnabled() {
|
|
6722
|
+
return options.remoteAuthExplicit ? options.remoteAuth === true : remoteAuthPreferenceEnabled();
|
|
6723
|
+
}
|
|
6724
|
+
|
|
6725
|
+
function remoteAuthStartupReason() {
|
|
6726
|
+
if (options.remoteAuthExplicit) return "startup option";
|
|
6727
|
+
return "saved setting";
|
|
6728
|
+
}
|
|
6729
|
+
|
|
6653
6730
|
function remoteAuthStatus({ includePin = false } = {}) {
|
|
6654
6731
|
const enabled = !!remoteAuth.pin;
|
|
6655
6732
|
const status = {
|
|
@@ -6798,9 +6875,9 @@ async function closeNetworkAccess() {
|
|
|
6798
6875
|
await closeServerListener();
|
|
6799
6876
|
await listenOn(nextHost);
|
|
6800
6877
|
currentHost = nextHost;
|
|
6801
|
-
resetRemoteAuth();
|
|
6878
|
+
if (!remoteAuthPreferenceEnabled()) resetRemoteAuth();
|
|
6802
6879
|
console.warn("Web UI network access closed; listening on localhost only.");
|
|
6803
|
-
return networkStatus();
|
|
6880
|
+
return networkStatus({ includeAuthPin: true });
|
|
6804
6881
|
} catch (error) {
|
|
6805
6882
|
console.error("Failed to close Web UI network access:", sanitizeError(error));
|
|
6806
6883
|
if (!server.listening) {
|
|
@@ -6817,7 +6894,7 @@ async function closeNetworkAccess() {
|
|
|
6817
6894
|
}
|
|
6818
6895
|
}
|
|
6819
6896
|
|
|
6820
|
-
if (
|
|
6897
|
+
if (remoteAuthStartupEnabled()) enableRemoteAuth(remoteAuthStartupReason());
|
|
6821
6898
|
|
|
6822
6899
|
async function safeRpcData(tab, command, timeoutMs = STATUS_RPC_TIMEOUT_MS) {
|
|
6823
6900
|
try {
|
|
@@ -6961,9 +7038,13 @@ const server = createServer(async (req, res) => {
|
|
|
6961
7038
|
if (url.pathname === "/api/remote-auth/settings" && req.method === "POST") {
|
|
6962
7039
|
requireLocalhostRoute(req, url.pathname);
|
|
6963
7040
|
const body = await readJsonBody(req);
|
|
6964
|
-
if (body.enabled === true)
|
|
6965
|
-
|
|
6966
|
-
|
|
7041
|
+
if (body.enabled === true) {
|
|
7042
|
+
enableRemoteAuth("side panel toggle");
|
|
7043
|
+
await saveRemoteAuthPreference(true);
|
|
7044
|
+
} else if (body.enabled === false) {
|
|
7045
|
+
resetRemoteAuth();
|
|
7046
|
+
await saveRemoteAuthPreference(false);
|
|
7047
|
+
} else throw makeHttpError(400, "enabled must be true or false");
|
|
6967
7048
|
closeSseClientsForRemoteAuthChange();
|
|
6968
7049
|
const headers = body.enabled === false ? { "set-cookie": clearRemoteAuthCookie() } : {};
|
|
6969
7050
|
sendJson(res, 200, { ok: true, data: { auth: remoteAuthStatus({ includePin: true }), network: networkStatus({ includeAuthPin: true }) } }, headers);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-webui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/Firstp1ck/npm-packages/tree/main/pi-package-webui#readme",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"node_modules/@firstpick/pi-extension-setup-skills/index.ts",
|
|
31
31
|
"node_modules/@firstpick/pi-extension-stats/index.ts",
|
|
32
32
|
"node_modules/@firstpick/pi-extension-todo-progress/index.ts",
|
|
33
|
-
"node_modules/@firstpick/pi-extension-tools/index.ts"
|
|
33
|
+
"node_modules/@firstpick/pi-extension-tools/index.ts",
|
|
34
|
+
"node_modules/@firstpick/pi-package-remote-webui/index.ts"
|
|
34
35
|
],
|
|
35
36
|
"skills": [
|
|
36
37
|
"node_modules/@firstpick/pi-extension-release-aur/skills"
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"test": "node tests/run-all.mjs"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@earendil-works/pi-coding-agent": "^0.79.
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.79.3"
|
|
54
55
|
},
|
|
55
56
|
"optionalDependencies": {
|
|
56
57
|
"@firstpick/pi-extension-git-footer-status": "^0.3.3",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"@firstpick/pi-extension-stats": "^0.2.6",
|
|
62
63
|
"@firstpick/pi-extension-todo-progress": "^0.2.4",
|
|
63
64
|
"@firstpick/pi-extension-tools": "^0.1.6",
|
|
65
|
+
"@firstpick/pi-package-remote-webui": "^0.1.0",
|
|
64
66
|
"@firstpick/pi-prompts-git-pr": "^0.1.2",
|
|
65
67
|
"@firstpick/pi-themes-bundle": "^0.1.4"
|
|
66
68
|
},
|