@firstpick/pi-package-webui 0.4.7 → 0.4.9

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 on first use; enabling it in **Controls → Network → Remote PIN auth** persists that preference for later Web UI starts.
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. Trusted-LAN opening/closing and Remote PIN auth controls are owned by the optional `@firstpick/pi-package-remote-webui` companion; when enabled, Remote PIN auth persists for later Web UI starts.
10
10
 
11
11
  ## Requirements
12
12
 
@@ -134,13 +134,13 @@ Environment variables:
134
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.
135
135
  - Prompt composer with uploads, drag/drop/paste, inline image support, slash-command autocomplete, and `@` file/path references with live suggestions.
136
136
  - Browser dialogs for common Pi selectors such as `/model`, `/settings`, `/theme`, `/fork`, `/clone`, `/resume`, `/tree`, `/scoped-models`, `/tools`, and `/skills`.
137
- - Model, thinking, session, workspace, theme, optional-feature, Codex usage, network, update/restart, event, and notification controls in the side panel.
137
+ - Model, thinking, session, workspace, theme, optional-feature, Codex usage, optional Remote WebUI, update/restart, event, and notification controls in the side panel.
138
138
  - Persistent context-window meter with manual compact and auto-compaction controls near the composer.
139
139
  - Side-panel theme picker backed by optional `@firstpick/pi-themes-bundle` themes when loaded.
140
140
  - Per-tab cwd changes, a clickable footer cwd picker, saved path fast picks, server-persisted fast picks, and restart-safe restoration of open tabs.
141
- - Detected app runner dropdown for the active tab cwd, including Cargo, Bun, npm/npx/pnpm, Python/uv, Go/Golang, Zig, C/C++, Docker Compose, root/dev/scripts shell scripts, and other common project runners with live output pinned at the top of the terminal. Projects can add browseable custom runners in `.pi-webui-runners.json` with a command (default `./`) plus a relative path to the file to run.
141
+ - Detected app runner dropdown for the active tab cwd, including Cargo, Bun, npm/npx/pnpm, Python/uv, Go/Golang, Zig, C/C++, Docker Compose, root/dev/scripts shell scripts, and other common project runners with live output pinned at the top of the terminal. Running app runners expose line-oriented stdin in the widget for interactive scripts. Projects can add browseable custom runners in `.pi-webui-runners.json` with a command (default `./`) plus a relative path to the file to run.
142
142
  - Browser support for Pi extension UI prompts, widgets, status updates, `/btw` side-question output widgets with optional context transfer/live steering, browser notifications when a tab needs an extension UI response, and an optional side-panel toggle for agent-done notifications.
143
- - Localhost-only Pi/Web UI update checks with a top-right update notification and a confirmed **Update & restart** action that runs `pi update` plus all detected local/global Web UI and Pi package-manager updates, then restarts the Web UI server.
143
+ - Localhost-only Pi/Web UI update checks with a top-right update notification and confirmed restart actions: **Update Pi & restart** runs `pi update` for Pi-only updates, while **Update Pi + Packages & Restart** runs `pi update --all` for Pi plus configured packages.
144
144
  - Feedback reactions (`👍`, `👎`, `?`) on final assistant output plus tool/bash action cards, which can ask Pi to create or update a LEARNING.
145
145
  - Mobile-friendly layout and PWA install support where the browser allows it.
146
146
 
@@ -150,7 +150,7 @@ Useful browser endpoints exposed by the local server include:
150
150
  - `POST /api/action-feedback?tab=<tabId>` for feedback on final assistant output and action cards.
151
151
  - `GET /api/optional-features` for optional companion package install/update status.
152
152
  - `POST /api/optional-feature-install` for installing or updating known optional companion packages from the side panel.
153
- - `GET /api/update-status` and localhost-only `POST /api/update` for checking Pi/Web UI updates and running `pi update` plus all detected local/global Web UI and Pi package-manager updates followed by a Web UI server restart.
153
+ - `GET /api/update-status` and localhost-only `POST /api/update` for checking Pi/Web UI updates and running `pi update` followed by a Web UI server restart. Use `POST /api/update?all=1` to run `pi update --all` for Pi plus configured packages.
154
154
  - `GET /api/remote-auth`, `POST /api/remote-auth`, and localhost-only `POST /api/remote-auth/settings` for optional 4-digit PIN authentication when serving non-local browser clients.
155
155
 
156
156
  For local development, run the checkout helper directly, for example:
@@ -178,7 +178,7 @@ Optional companions:
178
178
  - `@firstpick/pi-extension-setup-skills` — TUI `/skills` setup command alongside WebUI-native skill toggles.
179
179
  - `@firstpick/pi-extension-todo-progress` — todo-progress rendering.
180
180
  - `@firstpick/pi-extension-tools` — TUI `/tools` active-tool manager alongside WebUI-native tool toggles.
181
- - `@firstpick/pi-package-remote-webui` — `/remote` trusted-LAN QR helper for connecting mobile browsers to Web UI.
181
+ - `@firstpick/pi-package-remote-webui` — `/remote` trusted-LAN QR helper plus the optional browser controls for opening/closing LAN access and Remote PIN auth.
182
182
  - `@firstpick/pi-extension-git-footer-status` — richer extension-owned git/footer status, including the structured Web UI footer payload.
183
183
  - `@firstpick/pi-extension-stats` — stats commands and status data.
184
184
  - `@firstpick/pi-themes-bundle` — Web UI and Pi theme resources.
@@ -208,9 +208,9 @@ This requires `/git-staged-msg` and `/pr` from `@firstpick/pi-prompts-git-pr`; b
208
208
  ## Network safety
209
209
 
210
210
  - Default bind is localhost-only: `127.0.0.1:31415`.
211
- - 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".
212
- - 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.
213
- - 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.
211
+ - When `@firstpick/pi-package-remote-webui` is loaded and enabled, the side-panel **Remote WebUI** controls dispatch through `/remote`: opening rebinds the server to `0.0.0.0`, shows LAN URLs when available, and toggles to "Close for network".
212
+ - The optional **Remote PIN auth** toggle is off by default on first use. When enabled through `/remote auth on` or the Remote WebUI controls, the server saves that preference, generates a fresh random 4-digit PIN for each server start, shows it in the Remote WebUI controls and `/webui-status`, and requires it from non-local browser clients.
213
+ - Localhost clients stay frictionless and can toggle Remote PIN auth through the remote companion; changing the toggle persists the preference and disconnects existing event streams so remote clients must re-authenticate after enablement.
214
214
  - `--host 0.0.0.0` also exposes the Web UI to the local network; pass `--remote-auth` to start with PIN auth already enabled.
215
215
  - 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.
216
216
  - Remote PIN auth is a simple trusted-LAN HTTP gate, not hardened multi-user authentication; do not expose it to untrusted networks.
@@ -222,5 +222,5 @@ This requires `/git-staged-msg` and `/pr` from `@firstpick/pi-prompts-git-pr`; b
222
222
  - **`/webui-start` is missing:** restart Pi after installing the package.
223
223
  - **Wrong port or existing server:** use `/webui-status detailed`, or start on another port with `/webui-start --port 31500`.
224
224
  - **Optional feature is disabled or missing:** check the side panel, install the companion package if needed, then run `/reload` in the active Pi tab.
225
- - **Remote browser asks for a PIN:** read it from **Controls Network Remote PIN auth**, `/webui-status`, or the local Web UI server log. Disable the toggle from localhost to remove the PIN gate.
225
+ - **Remote browser asks for a PIN:** read it from the optional **Remote WebUI** side-panel controls, `/webui-status`, `/remote status`, or the local Web UI server log. Disable the toggle from localhost to remove the PIN gate.
226
226
  - **PWA install or notifications are unavailable:** use `localhost` or HTTPS; browser support varies on LAN HTTP URLs.
package/bin/pi-webui.mjs CHANGED
@@ -177,6 +177,7 @@ const APP_RUNNER_DETECTION_TIMEOUT_MS = 1_200;
177
177
  const APP_RUNNER_COMMAND_CACHE_TTL_MS = 30_000;
178
178
  const APP_RUNNER_OUTPUT_LINE_LIMIT = 1_000;
179
179
  const APP_RUNNER_OUTPUT_MAX_CHARS = 240_000;
180
+ const APP_RUNNER_INPUT_MAX_CHARS = 16_000;
180
181
  const APP_RUNNER_STOP_GRACE_MS = 2_500;
181
182
  const APP_RUNNER_PYTHON_ENTRIES = ["Main.py", "main.py", "src/main.py", "src/Main.py", "app.py", "src/app.py"];
182
183
  const APP_RUNNER_JS_ENTRIES = ["main.js", "src/main.js", "index.js", "src/index.js", "server.js", "src/server.js", "app.js", "src/app.js"];
@@ -838,10 +839,18 @@ function safeDownloadFileName(name, fallback = "pi-export") {
838
839
  return (text || fallback).slice(0, 180);
839
840
  }
840
841
 
841
- function contentDispositionAttachment(fileName) {
842
+ function contentDispositionHeader(fileName, disposition = "attachment") {
842
843
  const safeName = safeDownloadFileName(fileName);
843
844
  const asciiName = safeName.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
844
- return `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(safeName)}`;
845
+ return `${disposition}; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(safeName)}`;
846
+ }
847
+
848
+ function contentDispositionAttachment(fileName) {
849
+ return contentDispositionHeader(fileName, "attachment");
850
+ }
851
+
852
+ function contentDispositionInline(fileName) {
853
+ return contentDispositionHeader(fileName, "inline");
845
854
  }
846
855
 
847
856
  function registerNativeDownload(filePath, { fileName, contentType, command = "native" } = {}) {
@@ -856,15 +865,17 @@ function registerNativeDownload(filePath, { fileName, contentType, command = "na
856
865
  expiresAt,
857
866
  };
858
867
  nativeDownloadTokens.set(token, record);
868
+ const url = `/api/native-download/${encodeURIComponent(token)}`;
859
869
  return {
860
- url: `/api/native-download/${encodeURIComponent(token)}`,
870
+ url,
871
+ openUrl: record.contentType === MIME_TYPES.get(".html") ? `${url}?disposition=inline` : undefined,
861
872
  fileName: record.fileName,
862
873
  contentType: record.contentType,
863
874
  expiresAt: new Date(expiresAt).toISOString(),
864
875
  };
865
876
  }
866
877
 
867
- async function sendNativeDownload(res, token) {
878
+ async function sendNativeDownload(res, token, { inline = false } = {}) {
868
879
  pruneNativeDownloadTokens();
869
880
  const item = nativeDownloadTokens.get(token);
870
881
  if (!item) throw makeHttpError(404, "Download token expired or not found");
@@ -873,10 +884,11 @@ async function sendNativeDownload(res, token) {
873
884
  nativeDownloadTokens.delete(token);
874
885
  throw makeHttpError(404, "Download file expired or not found");
875
886
  }
887
+ const canRenderInline = inline === true && item.contentType === MIME_TYPES.get(".html");
876
888
  res.writeHead(200, {
877
889
  "content-type": item.contentType,
878
890
  "content-length": String(fileStats.size),
879
- "content-disposition": contentDispositionAttachment(item.fileName),
891
+ "content-disposition": canRenderInline ? contentDispositionInline(item.fileName) : contentDispositionAttachment(item.fileName),
880
892
  "cache-control": "no-store",
881
893
  "x-content-type-options": "nosniff",
882
894
  });
@@ -2274,6 +2286,11 @@ async function detectAppRunners(tab) {
2274
2286
  .map(publicAppRunner);
2275
2287
  }
2276
2288
 
2289
+ function appRunnerPendingLine(run) {
2290
+ if (!run || run.status !== "running") return "";
2291
+ return [run.stdoutRemainder, run.stderrRemainder].map((part) => String(part || "")).filter(Boolean).join("");
2292
+ }
2293
+
2277
2294
  function publicAppRunnerState(run) {
2278
2295
  if (!run) return null;
2279
2296
  return {
@@ -2295,6 +2312,11 @@ function publicAppRunnerState(run) {
2295
2312
  truncated: run.truncated === true,
2296
2313
  lineCount: run.lineCount || run.lines?.length || 0,
2297
2314
  lines: Array.isArray(run.lines) ? [...run.lines] : [],
2315
+ pendingLine: appRunnerPendingLine(run),
2316
+ stdinClosed: run.stdinClosed === true,
2317
+ stdinError: run.stdinError || "",
2318
+ stdinWrites: run.stdinWrites || 0,
2319
+ lastStdinAt: run.lastStdinAt || "",
2298
2320
  };
2299
2321
  }
2300
2322
 
@@ -2398,6 +2420,7 @@ function finishAppRunner(tab, run, patch = {}) {
2398
2420
  run.error = patch.error;
2399
2421
  run.status = patch.error ? "error" : patch.exitCode === 0 ? "done" : "failed";
2400
2422
  run.child = null;
2423
+ run.stdinClosed = true;
2401
2424
  run.stopping = false;
2402
2425
  appendAppRunnerLine(run, `# ${appRunnerStatusLabel(run)} after ${Math.max(0, Math.round((Date.parse(run.endedAt) - Date.parse(run.startedAt)) / 1000))}s`);
2403
2426
  if (patch.error) appendAppRunnerLine(run, `# ${patch.error}`);
@@ -2407,6 +2430,45 @@ function finishAppRunner(tab, run, patch = {}) {
2407
2430
  broadcastAppRunnerState(tab);
2408
2431
  }
2409
2432
 
2433
+ function normalizeAppRunnerInputText(value) {
2434
+ const text = String(value ?? "");
2435
+ if (text.includes("\0")) throw makeHttpError(400, "App runner input cannot contain null bytes");
2436
+ if (text.length > APP_RUNNER_INPUT_MAX_CHARS) throw makeHttpError(413, `App runner input is too long; limit is ${APP_RUNNER_INPUT_MAX_CHARS} characters`);
2437
+ return text;
2438
+ }
2439
+
2440
+ function sendAppRunnerInput(tab, value, { appendNewline = true, closeStdin = false } = {}) {
2441
+ const run = tab?.appRunner;
2442
+ if (!run || run.status !== "running") throw makeHttpError(409, "No app runner is running in this tab");
2443
+ const stdin = run.child?.stdin;
2444
+ if (!stdin || stdin.destroyed || stdin.writableEnded || run.stdinClosed === true) throw makeHttpError(409, "App runner stdin is closed");
2445
+ const text = normalizeAppRunnerInputText(value);
2446
+ const chunk = `${text}${appendNewline === false ? "" : "\n"}`;
2447
+ if (!chunk && !closeStdin) throw makeHttpError(400, "App runner input is empty");
2448
+ let buffered = false;
2449
+ try {
2450
+ if (closeStdin) {
2451
+ if (chunk) stdin.end(chunk, "utf8");
2452
+ else stdin.end();
2453
+ run.stdinClosed = true;
2454
+ } else {
2455
+ buffered = stdin.write(chunk, "utf8") === false;
2456
+ }
2457
+ } catch (error) {
2458
+ run.stdinClosed = true;
2459
+ run.stdinError = sanitizeError(error);
2460
+ throw makeHttpError(409, `App runner stdin write failed: ${run.stdinError}`);
2461
+ }
2462
+ run.stdinWrites = (run.stdinWrites || 0) + 1;
2463
+ run.lastStdinAt = new Date().toISOString();
2464
+ const closeSuffix = closeStdin ? " and closed" : "";
2465
+ if (chunk) appendAppRunnerLine(run, text ? `# stdin sent (${text.length} char${text.length === 1 ? "" : "s"})${closeSuffix}` : `# stdin sent (Enter)${closeSuffix}`);
2466
+ else appendAppRunnerLine(run, "# stdin closed (EOF)");
2467
+ recordEvent({ type: "webui_app_runner_stdin", tabId: tab.id, tabTitle: tab.title, command: run.displayCommand, chars: text.length, newline: appendNewline !== false, closed: closeStdin === true });
2468
+ scheduleAppRunnerBroadcast(tab);
2469
+ return { cwd: tab.cwd, activeRun: publicAppRunnerState(run), inputBuffered: buffered };
2470
+ }
2471
+
2410
2472
  async function startAppRunner(tab, runnerId) {
2411
2473
  if (tab.appRunner?.status === "running") throw makeHttpError(409, `App runner already running: ${tab.appRunner.displayCommand}`);
2412
2474
  const runners = await detectAppRunners(tab);
@@ -2427,12 +2489,14 @@ async function startAppRunner(tab, runnerId) {
2427
2489
  lines: [],
2428
2490
  lineCount: 0,
2429
2491
  outputChars: 0,
2492
+ stdinClosed: false,
2493
+ stdinWrites: 0,
2430
2494
  };
2431
2495
  appendAppRunnerLine(run, `$ ${run.displayCommand}`);
2432
2496
  const child = spawn(run.command, run.args, {
2433
2497
  cwd: run.cwd,
2434
2498
  env: process.env,
2435
- stdio: ["ignore", "pipe", "pipe"],
2499
+ stdio: ["pipe", "pipe", "pipe"],
2436
2500
  windowsHide: true,
2437
2501
  detached: process.platform !== "win32",
2438
2502
  });
@@ -2440,6 +2504,18 @@ async function startAppRunner(tab, runnerId) {
2440
2504
  run.pid = child.pid;
2441
2505
  tab.appRunner = run;
2442
2506
 
2507
+ child.stdin?.on("error", (error) => {
2508
+ run.stdinClosed = true;
2509
+ run.stdinError = sanitizeError(error);
2510
+ if (run.status === "running") {
2511
+ appendAppRunnerLine(run, `# stdin error: ${run.stdinError}`);
2512
+ scheduleAppRunnerBroadcast(tab);
2513
+ }
2514
+ });
2515
+ child.stdin?.on("close", () => {
2516
+ run.stdinClosed = true;
2517
+ if (run.status === "running") scheduleAppRunnerBroadcast(tab);
2518
+ });
2443
2519
  child.stdout.on("data", (chunk) => appendAppRunnerChunk(tab, run, chunk, "stdout"));
2444
2520
  child.stderr.on("data", (chunk) => appendAppRunnerChunk(tab, run, chunk, "stderr"));
2445
2521
  child.on("error", (error) => finishAppRunner(tab, run, { error: sanitizeError(error) }));
@@ -3155,40 +3231,43 @@ async function runGitReadCommand(root, args, { timeoutMs = GIT_CHANGES_COMMAND_T
3155
3231
  throw new Error(String(message).trim());
3156
3232
  }
3157
3233
 
3158
- function gitBranchFromStatus(statusText) {
3159
- const branchLine = String(statusText || "").split(/\r?\n/).find((line) => line.startsWith("## ")) || "";
3160
- return branchLine.slice(3).trim().replace(/\.\.\..*$/, "") || "detached";
3234
+ function gitBranchFromPorcelainStatus(statusText) {
3235
+ for (const line of String(statusText || "").split(/\r?\n/)) {
3236
+ if (!line.startsWith("# branch.head ")) continue;
3237
+ const branch = line.slice("# branch.head ".length).trim();
3238
+ return branch && branch !== "(detached)" ? branch : "detached";
3239
+ }
3240
+ return "detached";
3161
3241
  }
3162
3242
 
3163
- function gitDivergenceFromBranchStatus(line) {
3164
- const details = String(line || "").match(/\[(.+)\]\s*$/)?.[1] || "";
3165
- const ahead = Number.parseInt(details.match(/ahead\s+(\d+)/i)?.[1] || "0", 10) || 0;
3166
- const behind = Number.parseInt(details.match(/behind\s+(\d+)/i)?.[1] || "0", 10) || 0;
3167
- return { ahead, behind };
3243
+ function addGitPorcelainTrackedSummary(summary, xy) {
3244
+ const x = xy?.[0] || ".";
3245
+ const y = xy?.[1] || ".";
3246
+ if (x !== ".") summary.staged += 1;
3247
+ if (y !== ".") summary.unstaged += 1;
3168
3248
  }
3169
3249
 
3170
- function summarizeGitShortStatus(statusText) {
3250
+ function summarizeGitPorcelainStatus(statusText) {
3171
3251
  const summary = { staged: 0, unstaged: 0, untracked: 0, conflicted: 0, ahead: 0, behind: 0 };
3172
3252
  for (const line of String(statusText || "").split(/\r?\n/)) {
3173
3253
  if (!line) continue;
3174
- if (line.startsWith("## ")) {
3175
- const divergence = gitDivergenceFromBranchStatus(line);
3176
- summary.ahead = divergence.ahead;
3177
- summary.behind = divergence.behind;
3254
+ if (line.startsWith("# branch.ab ")) {
3255
+ const match = line.match(/\+(\d+)\s+-(\d+)/);
3256
+ if (match) {
3257
+ summary.ahead = Number.parseInt(match[1] || "0", 10) || 0;
3258
+ summary.behind = Number.parseInt(match[2] || "0", 10) || 0;
3259
+ }
3178
3260
  continue;
3179
3261
  }
3180
- const x = line[0] || " ";
3181
- const y = line[1] || " ";
3182
- if (x === "?" && y === "?") {
3183
- summary.untracked += 1;
3262
+ if (line.startsWith("1 ") || line.startsWith("2 ")) {
3263
+ addGitPorcelainTrackedSummary(summary, line.split(" ")[1] || "..");
3184
3264
  continue;
3185
3265
  }
3186
- if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) {
3266
+ if (line.startsWith("u ")) {
3187
3267
  summary.conflicted += 1;
3188
3268
  continue;
3189
3269
  }
3190
- if (x && x !== " ") summary.staged += 1;
3191
- if (y && y !== " ") summary.unstaged += 1;
3270
+ if (line.startsWith("? ")) summary.untracked += 1;
3192
3271
  }
3193
3272
  return summary;
3194
3273
  }
@@ -3293,20 +3372,21 @@ async function pullGitChanges(cwd) {
3293
3372
  async function readGitChanges(cwd) {
3294
3373
  const root = await getGitRoot(cwd);
3295
3374
  const diffArgs = ["diff", "--no-ext-diff", "--no-color", "--find-renames", "--unified=0", "--src-prefix=a/", "--dst-prefix=b/"];
3296
- const [statusText, unstagedDiff, stagedDiff, untrackedText] = await Promise.all([
3375
+ const [statusText, porcelainStatusText, unstagedDiff, stagedDiff, untrackedText] = await Promise.all([
3297
3376
  runGitReadCommand(root, ["status", "--short", "--branch", "--untracked-files=all"], { maxOutputLength: 120_000 }),
3377
+ runGitReadCommand(root, ["status", "--porcelain=2", "--branch", "--untracked-files=all"], { maxOutputLength: 120_000 }),
3298
3378
  runGitReadCommand(root, diffArgs),
3299
3379
  runGitReadCommand(root, ["diff", "--cached", "--no-ext-diff", "--no-color", "--find-renames", "--unified=0", "--src-prefix=a/", "--dst-prefix=b/"]),
3300
3380
  runGitReadCommand(root, ["ls-files", "--others", "--exclude-standard"], { maxOutputLength: 120_000 }),
3301
3381
  ]);
3302
- const summary = summarizeGitShortStatus(statusText);
3382
+ const summary = summarizeGitPorcelainStatus(porcelainStatusText);
3303
3383
  const incoming = await readGitIncomingChanges(root, summary);
3304
3384
  const untrackedFiles = untrackedText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3305
3385
  const untracked = await readGitUntrackedEntries(root, untrackedFiles);
3306
3386
  return {
3307
3387
  cwd,
3308
3388
  root,
3309
- branch: gitBranchFromStatus(statusText),
3389
+ branch: gitBranchFromPorcelainStatus(porcelainStatusText),
3310
3390
  generatedAt: new Date().toISOString(),
3311
3391
  summary,
3312
3392
  remote: incoming.remote,
@@ -4276,6 +4356,9 @@ try {
4276
4356
  process.exit(2);
4277
4357
  }
4278
4358
 
4359
+ process.env.PI_WEBUI_HOST = options.host;
4360
+ process.env.PI_WEBUI_PORT = String(options.port);
4361
+
4279
4362
  const startupDelayMs = Number.parseInt(process.env.PI_WEBUI_START_DELAY_MS || "", 10);
4280
4363
  delete process.env.PI_WEBUI_START_DELAY_MS;
4281
4364
  if (Number.isFinite(startupDelayMs) && startupDelayMs > 0) {
@@ -5198,32 +5281,35 @@ async function getUpdateStatus({ force = false } = {}) {
5198
5281
  checkedAt: new Date(now).toISOString(),
5199
5282
  updateAvailable,
5200
5283
  restartRequired: true,
5201
- command: "pi update + Web UI/Pi package-manager updates",
5284
+ command: "pi update",
5285
+ allCommand: "pi update --all",
5202
5286
  webuiDev: webuiDevServer,
5203
5287
  pi: piStatus,
5204
5288
  webui: webuiStatus,
5205
5289
  packages: {
5206
5290
  checked: false,
5207
- note: "Update runs pi update plus all detected local, Pi-agent, npm-global, and Bun-global Web UI/Pi package roots."
5291
+ note: "Default update runs pi update for Pi only. Use update all to run pi update --all for Pi and configured packages."
5208
5292
  },
5209
5293
  };
5210
5294
  updateStatusCacheAt = now;
5211
5295
  return updateStatusCache;
5212
5296
  }
5213
5297
 
5214
- async function resolvePiUpdateCommand() {
5298
+ async function resolvePiUpdateCommand({ all = false } = {}) {
5299
+ const updateArgs = all ? ["update", "--all"] : ["update"];
5300
+ const label = all ? "Pi CLI and configured packages" : "Pi CLI";
5215
5301
  if (options.piBinExplicit) {
5216
- const command = await resolvePiCommand(["update"]);
5217
- return { ...command, label: "Pi CLI and configured packages" };
5302
+ const command = await resolvePiCommand(updateArgs);
5303
+ return { ...command, label, timeoutMs: PI_UPDATE_TIMEOUT_MS, maxOutputLength: PI_UPDATE_OUTPUT_MAX_CHARS };
5218
5304
  }
5219
5305
 
5220
5306
  const pathPi = await runCommand(options.piBin, ["--version"], { timeoutMs: 3000, maxOutputLength: 4000 });
5221
5307
  if (pathPi.exitCode === 0 && !pathPi.timedOut && !pathPi.error) {
5222
- return { label: "Pi CLI and configured packages", command: options.piBin, args: ["update"], displayCommand: formatCommandForDisplay(options.piBin, ["update"]) };
5308
+ return { label, command: options.piBin, args: updateArgs, displayCommand: formatCommandForDisplay(options.piBin, updateArgs), timeoutMs: PI_UPDATE_TIMEOUT_MS, maxOutputLength: PI_UPDATE_OUTPUT_MAX_CHARS };
5223
5309
  }
5224
5310
 
5225
- const fallback = await resolvePiCommand(["update"]);
5226
- return { ...fallback, label: "bundled Pi CLI and configured packages" };
5311
+ const fallback = await resolvePiCommand(updateArgs);
5312
+ return { ...fallback, label: `bundled ${label}`, timeoutMs: PI_UPDATE_TIMEOUT_MS, maxOutputLength: PI_UPDATE_OUTPUT_MAX_CHARS };
5227
5313
  }
5228
5314
 
5229
5315
  function packageNodeModulesPath(nodeModulesRoot, packageName) {
@@ -5398,13 +5484,9 @@ function uniqueUpdateTasks(tasks) {
5398
5484
  return unique;
5399
5485
  }
5400
5486
 
5401
- async function resolveUpdateTasks() {
5487
+ async function resolveUpdateTasks({ all = false } = {}) {
5402
5488
  return uniqueUpdateTasks([
5403
- await resolvePiUpdateCommand(),
5404
- await currentWebuiPackageUpdateTask(),
5405
- await agentPackageRootUpdateTask(),
5406
- await npmGlobalPackageRootUpdateTask(),
5407
- await bunGlobalPackageRootUpdateTask(),
5489
+ await resolvePiUpdateCommand({ all }),
5408
5490
  ]);
5409
5491
  }
5410
5492
 
@@ -5445,16 +5527,17 @@ function combinedUpdateOutput(results, field) {
5445
5527
  .join("\n\n");
5446
5528
  }
5447
5529
 
5448
- async function runPiUpdateAndPrepareRestart() {
5530
+ async function runPiUpdateAndPrepareRestart({ all = false } = {}) {
5449
5531
  if (piUpdateInProgress) throw makeHttpError(409, "A Pi update is already running.");
5450
5532
  piUpdateInProgress = true;
5451
5533
  let restartPrepared = false;
5452
5534
  try {
5453
5535
  const restorableTabs = await restorableTabsForRestart();
5454
- const updateTasks = await resolveUpdateTasks();
5455
- if (!updateTasks.length) throw makeHttpError(500, "No Pi/Web UI update commands could be resolved.");
5536
+ const updateTasks = await resolveUpdateTasks({ all });
5537
+ if (!updateTasks.length) throw makeHttpError(500, "No Pi update command could be resolved.");
5456
5538
  const command = updateTasks.map(updateTaskDisplay).join(" && ");
5457
- recordEvent({ type: "webui_update_started", command, restorableTabCount: restorableTabs.length });
5539
+ const updateLabel = all ? "Pi and package updates" : "Pi update";
5540
+ recordEvent({ type: "webui_update_started", command, updateAll: all, restorableTabCount: restorableTabs.length });
5458
5541
  const results = [];
5459
5542
  for (const task of updateTasks) results.push(await runUpdateTask(task));
5460
5543
 
@@ -5462,9 +5545,9 @@ async function runPiUpdateAndPrepareRestart() {
5462
5545
  updateStatusCacheAt = 0;
5463
5546
  const child = spawnRestartServer(restorableTabs);
5464
5547
  restartPrepared = true;
5465
- recordEvent({ type: "webui_update_restarting", command, nextWebuiPid: child.pid, restorableTabCount: restorableTabs.length });
5548
+ recordEvent({ type: "webui_update_restarting", command, updateAll: all, nextWebuiPid: child.pid, restorableTabCount: restorableTabs.length });
5466
5549
  return {
5467
- message: "Pi/Web UI package updates completed. Pi Web UI is restarting.",
5550
+ message: `${updateLabel} completed. Pi Web UI is restarting.`,
5468
5551
  command,
5469
5552
  commands: results.map((result) => ({ label: result.label, command: result.command })),
5470
5553
  stdout: combinedUpdateOutput(results, "stdout"),
@@ -6537,7 +6620,7 @@ async function handleNativeExportCommand(tab, args, req) {
6537
6620
  return respondNative("export", {
6538
6621
  status: "succeeded",
6539
6622
  level: "info",
6540
- message: `Exported current session to HTML.\nDownload: ${download.fileName}\nLink expires: ${download.expiresAt}`,
6623
+ message: `Exported current session to HTML.\nDownload: ${download.fileName}\nOpen it in your browser when prompted.\nOpen URL: ${download.openUrl || download.url}\nDownload URL: ${download.url}\nLink expires: ${download.expiresAt}`,
6541
6624
  download,
6542
6625
  result: response.data,
6543
6626
  refresh: ["state"],
@@ -7297,7 +7380,9 @@ const server = createServer(async (req, res) => {
7297
7380
  }
7298
7381
 
7299
7382
  if (url.pathname.startsWith("/api/native-download/") && req.method === "GET") {
7300
- await sendNativeDownload(res, decodeURIComponent(url.pathname.slice("/api/native-download/".length)));
7383
+ await sendNativeDownload(res, decodeURIComponent(url.pathname.slice("/api/native-download/".length)), {
7384
+ inline: url.searchParams.get("disposition") === "inline",
7385
+ });
7301
7386
  return;
7302
7387
  }
7303
7388
 
@@ -7354,7 +7439,10 @@ const server = createServer(async (req, res) => {
7354
7439
 
7355
7440
  if (url.pathname === "/api/update" && req.method === "POST") {
7356
7441
  requireLocalhostRoute(req, url.pathname);
7357
- const data = await runPiUpdateAndPrepareRestart();
7442
+ const body = await readJsonBody(req);
7443
+ const queryAll = ["1", "true", "yes", "all"].includes(String(url.searchParams.get("all") || "").toLowerCase());
7444
+ const bodyAll = body?.all === true || String(body?.mode || "").toLowerCase() === "all";
7445
+ const data = await runPiUpdateAndPrepareRestart({ all: queryAll || bodyAll });
7358
7446
  sendJson(res, 200, { ok: true, data });
7359
7447
  setTimeout(() => shutdown("api update"), 20).unref();
7360
7448
  return;
@@ -7389,6 +7477,14 @@ const server = createServer(async (req, res) => {
7389
7477
  return;
7390
7478
  }
7391
7479
 
7480
+ if (url.pathname === "/api/app-runner/input" && req.method === "POST") {
7481
+ const body = await readJsonBody(req);
7482
+ const tab = getRequestedTab(req, url, body);
7483
+ const text = Object.prototype.hasOwnProperty.call(body, "text") ? body.text : body.input;
7484
+ sendJson(res, 200, { ok: true, data: sendAppRunnerInput(tab, text, { appendNewline: body.newline !== false, closeStdin: body.closeStdin === true || body.close === true }) });
7485
+ return;
7486
+ }
7487
+
7392
7488
  if (url.pathname === "/api/app-runner/stop" && req.method === "POST") {
7393
7489
  const body = await readJsonBody(req);
7394
7490
  const tab = getRequestedTab(req, url, body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-package-webui",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
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",
@@ -53,7 +53,7 @@
53
53
  "test": "node tests/run-all.mjs"
54
54
  },
55
55
  "dependencies": {
56
- "@earendil-works/pi-coding-agent": "^0.79.7"
56
+ "@earendil-works/pi-coding-agent": "^0.79.8"
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "@firstpick/pi-extension-btw": "^0.1.0",