@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 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; enable it in **Controls → Network → Remote PIN auth** before exposing it on trusted networks.
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; enable it in Controls or with --remote-auth
269
- before exposing the server on trusted networks.
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>Enter the 4-digit PIN shown in the local Pi terminal or local Web UI to continue.</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
- input.addEventListener("input", () => { input.value = input.value.replace(/\\D/g, "").slice(0, 4); error.textContent = ""; });
757
- form.addEventListener("submit", async (event) => {
758
- event.preventDefault();
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: input.value }),
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 (!isLocalHost(currentHost) && options.remoteAuth !== false) enableRemoteAuth("startup network listener");
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) enableRemoteAuth("side panel toggle");
6965
- else if (body.enabled === false) resetRemoteAuth();
6966
- else throw makeHttpError(400, "enabled must be true or false");
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.2",
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.1"
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
  },