@firstpick/pi-package-webui 0.4.0 → 0.4.1

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
@@ -120,12 +120,14 @@ Environment variables:
120
120
  ## Main features
121
121
 
122
122
  - Pathless `pi-webui` startup: the server opens first, then the browser prompts for the first terminal CWD.
123
- - Multi-tab Pi sessions with isolated processes, working directories, prompt drafts, and activity state.
123
+ - Multi-tab Pi sessions with isolated processes, working directories, prompt drafts, activity state, and a workspace dashboard for common actions.
124
+ - Unified command palette (`Ctrl/Cmd+K`) for commands, tabs, models, sessions, settings, and frequent Web UI actions.
124
125
  - Automatic tab naming from the first prompt, with `--name <name>` still available for an explicit initial tab name.
125
- - Streaming chat transcript with Markdown, thinking output, tool/bash cards, queue and compaction events, and abort controls.
126
+ - Streaming chat transcript with Markdown, thinking output, tool/bash cards, queue and compaction events, edit-and-retry from user prompts, and abort controls.
126
127
  - Prompt composer with uploads, drag/drop/paste, inline image support, slash-command autocomplete, and `@` file/path references with live suggestions.
127
128
  - Browser dialogs for common Pi selectors such as `/model`, `/settings`, `/theme`, `/fork`, `/clone`, `/resume`, `/tree`, `/scoped-models`, `/tools`, and `/skills`.
128
129
  - Model, thinking, session, workspace, theme, optional-feature, Codex usage, network, update/restart, event, and notification controls in the side panel.
130
+ - Persistent context-window meter with manual compact and auto-compaction controls near the composer.
129
131
  - Side-panel theme picker backed by optional `@firstpick/pi-themes-bundle` themes when loaded.
130
132
  - Per-tab cwd changes, a clickable footer cwd picker, saved path fast picks, server-persisted fast picks, and restart-safe restoration of open tabs.
131
133
  - 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.
@@ -138,7 +140,8 @@ Useful browser endpoints exposed by the local server include:
138
140
 
139
141
  - `GET /api/path-suggestions?tab=<tabId>&query=<path>` for `@` file/path references with live suggestions.
140
142
  - `POST /api/action-feedback?tab=<tabId>` for feedback on final assistant output and action cards.
141
- - `POST /api/optional-feature-install` for installing known optional companion packages from the side panel.
143
+ - `GET /api/optional-features` for optional companion package install/update status.
144
+ - `POST /api/optional-feature-install` for installing or updating known optional companion packages from the side panel.
142
145
  - `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.
143
146
 
144
147
  For local development, run the checkout helper directly, for example:
@@ -151,9 +154,9 @@ Run `../dev/scripts/sync-pi-package-symlinks.sh` first when developing companion
151
154
 
152
155
  ## Optional companion packages
153
156
 
154
- A normal Pi/npm install includes the optional companion packages unless optional dependencies are disabled. Each Web UI tab curates Pi resources from the Web UI package that started the server, while preserving unrelated user/project resources; separately installed Web UI companion packages are ignored to avoid loading two copies. Startup checks loaded Pi capabilities directly through RPC-visible commands and live widget events, then the side panel shows each optional feature as enabled, disabled, or install-needed. Installing a missing feature is an explicit, warned action; it is localhost-only, limited to known packages, and requires reloading the active Pi tab after installation.
157
+ A normal Pi/npm install includes the optional companion packages unless optional dependencies are disabled. Each Web UI tab curates Pi resources from the Web UI package that started the server, while preserving unrelated user/project resources. Companion packages installed as global/npm-prefix siblings of the started Web UI package are reused when the Web UI package does not have its own nested optional dependency copy, avoiding duplicate loads while keeping global `pi-webui` launches working. Startup checks loaded Pi capabilities directly through RPC-visible commands and live widget events, then the side panel shows each optional feature as enabled, disabled, installed-but-not-loaded, update-available, or install-needed. Installing or updating a feature is an explicit, warned action with running/failure feedback in the row and activity log; it is localhost-only, limited to known packages, and requires reloading the active Pi tab after installation.
155
158
 
156
- When the standalone global `pi-webui` launcher is used, optional companion installs should target the Pi agent npm root instead of the global npm prefix. Override the target explicitly with `PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT=/path/to/package-root` when needed.
159
+ When the standalone global `pi-webui` launcher is used, optional companion installs target the npm prefix containing the Web UI package when that prefix is safe, otherwise the Pi agent npm root if it contains Web UI. Override the target explicitly with `PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT=/path/to/package-root` when needed.
157
160
 
158
161
  Optional companions:
159
162
 
package/bin/pi-webui.mjs CHANGED
@@ -933,6 +933,10 @@ async function installRootDeclaresPackage(root, packageName) {
933
933
  return declaredDependencySpec(pkg, packageName) !== undefined;
934
934
  }
935
935
 
936
+ async function installRootContainsPackage(root, packageName) {
937
+ return directoryExists(packageNodeModulesPath(path.join(root, "node_modules"), packageName));
938
+ }
939
+
936
940
  function configuredAgentNpmRoot() {
937
941
  const root = process.env.PI_CODING_AGENT_DIR ? path.resolve(expandUserPath(process.env.PI_CODING_AGENT_DIR)) : agentDir;
938
942
  return path.join(root, "npm");
@@ -943,10 +947,10 @@ async function optionalDependencyInstallRoot() {
943
947
  if (configuredRoot) return path.resolve(expandUserPath(configuredRoot));
944
948
 
945
949
  const installRoot = nodeModulesParentForPackageRoot(packageRoot);
946
- if (await installRootDeclaresPackage(installRoot, "@firstpick/pi-package-webui")) return installRoot;
950
+ if (await installRootDeclaresPackage(installRoot, "@firstpick/pi-package-webui") || await installRootContainsPackage(installRoot, "@firstpick/pi-package-webui")) return installRoot;
947
951
 
948
952
  const agentNpmRoot = configuredAgentNpmRoot();
949
- if (installRoot !== agentNpmRoot && await installRootDeclaresPackage(agentNpmRoot, "@firstpick/pi-package-webui")) return agentNpmRoot;
953
+ if (installRoot !== agentNpmRoot && (await installRootDeclaresPackage(agentNpmRoot, "@firstpick/pi-package-webui") || await installRootContainsPackage(agentNpmRoot, "@firstpick/pi-package-webui"))) return agentNpmRoot;
950
954
 
951
955
  if (webuiDevServer) return installRoot;
952
956
 
@@ -956,13 +960,102 @@ async function optionalDependencyInstallRoot() {
956
960
  );
957
961
  }
958
962
 
963
+ function minimumPackageVersionFromSpec(spec) {
964
+ const match = String(spec || "").match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/);
965
+ return match?.[0] || "";
966
+ }
967
+
968
+ function packageVersionBelowSpec(currentVersion, spec) {
969
+ const minimum = minimumPackageVersionFromSpec(spec);
970
+ return !!(currentVersion && minimum && isNewerPackageVersion(minimum, currentVersion));
971
+ }
972
+
959
973
  function formatCommandForDisplay(command, args) {
960
974
  return [command, ...args].map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)).join(" ");
961
975
  }
962
976
 
963
- async function installOptionalFeaturePackage(featureId) {
977
+ let optionalPackageNodeModulesRootsCache = null;
978
+ async function optionalPackageNodeModulesRoots() {
979
+ if (optionalPackageNodeModulesRootsCache) return optionalPackageNodeModulesRootsCache;
980
+ const roots = [];
981
+ const seen = new Set();
982
+ const add = (root) => {
983
+ if (!root) return;
984
+ const normalized = path.resolve(root);
985
+ if (seen.has(normalized)) return;
986
+ seen.add(normalized);
987
+ roots.push(normalized);
988
+ };
989
+ const configuredRoot = process.env[OPTIONAL_FEATURE_INSTALL_ROOT_ENV];
990
+ if (configuredRoot) add(path.join(path.resolve(expandUserPath(configuredRoot)), "node_modules"));
991
+ add(path.join(packageRoot, "node_modules"));
992
+ add(path.join(nodeModulesParentForPackageRoot(packageRoot), "node_modules"));
993
+ add(path.join(configuredAgentNpmRoot(), "node_modules"));
994
+ const npmGlobalRoot = await npmGlobalNodeModulesRoot();
995
+ if (npmGlobalRoot) add(npmGlobalRoot);
996
+ for (const bunRoot of await bunGlobalNodeModulesRoots()) add(bunRoot);
997
+ optionalPackageNodeModulesRootsCache = roots;
998
+ return roots;
999
+ }
1000
+
1001
+ async function optionalPackageCandidateRoots(packageName) {
1002
+ return (await optionalPackageNodeModulesRoots()).map((root) => packageNodeModulesPath(root, packageName));
1003
+ }
1004
+
1005
+ async function resolveInstalledPackageRoot(packageName) {
1006
+ const workspaceRoot = await workspacePackageRootForName(packageName);
1007
+ if (workspaceRoot) return workspaceRoot;
1008
+ for (const candidate of await optionalPackageCandidateRoots(packageName)) {
1009
+ if (await directoryExists(candidate)) return candidate;
1010
+ }
1011
+ return null;
1012
+ }
1013
+
1014
+ async function resolveInstalledPackageSubpath(packageName, subpath = "") {
1015
+ const root = await resolveInstalledPackageRoot(packageName);
1016
+ if (!root) return null;
1017
+ const candidate = path.join(root, subpath || "");
1018
+ try {
1019
+ await access(candidate);
1020
+ return candidate;
1021
+ } catch {
1022
+ return null;
1023
+ }
1024
+ }
1025
+
1026
+ function optionalFeatureDeclaredSpec(packageName) {
1027
+ return declaredDependencySpec(packageJson, packageName) || "";
1028
+ }
1029
+
1030
+ async function optionalFeaturePackageStatus(featureId) {
964
1031
  const packageName = OPTIONAL_FEATURE_PACKAGES.get(featureId);
965
1032
  if (!packageName) throw makeHttpError(400, `Unknown optional feature: ${featureId}`);
1033
+ const declaredSpec = optionalFeatureDeclaredSpec(packageName);
1034
+ const installedRoot = await resolveInstalledPackageRoot(packageName);
1035
+ const manifest = installedRoot ? await readJsonFileIfExists(path.join(installedRoot, "package.json")) : null;
1036
+ const installedVersion = typeof manifest?.version === "string" ? manifest.version : "";
1037
+ const updateAvailable = !!(installedVersion && packageVersionBelowSpec(installedVersion, declaredSpec));
1038
+ return {
1039
+ featureId,
1040
+ packageName,
1041
+ declaredSpec,
1042
+ installed: !!installedRoot,
1043
+ installedVersion,
1044
+ installedRoot,
1045
+ updateAvailable,
1046
+ updateReason: updateAvailable ? `installed ${installedVersion} is older than Web UI expects (${declaredSpec})` : "",
1047
+ };
1048
+ }
1049
+
1050
+ async function optionalFeaturePackageStatuses() {
1051
+ const features = [];
1052
+ for (const featureId of OPTIONAL_FEATURE_PACKAGES.keys()) features.push(await optionalFeaturePackageStatus(featureId));
1053
+ return { features };
1054
+ }
1055
+
1056
+ async function installOptionalFeaturePackage(featureId) {
1057
+ const beforeStatus = await optionalFeaturePackageStatus(featureId);
1058
+ const packageName = beforeStatus.packageName;
966
1059
 
967
1060
  const installRoot = await optionalDependencyInstallRoot();
968
1061
  const npmCommand = process.env.PI_WEBUI_NPM_BIN || "npm";
@@ -978,6 +1071,8 @@ async function installOptionalFeaturePackage(featureId) {
978
1071
  const details = [result.error, result.timedOut ? "timed out" : undefined, result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join("\n");
979
1072
  throw makeHttpError(500, `Optional feature install failed: ${command}${details ? `\n${details}` : ""}`);
980
1073
  }
1074
+ const afterStatus = await optionalFeaturePackageStatus(featureId);
1075
+ const operation = beforeStatus.installed ? "Updated" : "Installed";
981
1076
  return {
982
1077
  featureId,
983
1078
  packageName,
@@ -985,7 +1080,8 @@ async function installOptionalFeaturePackage(featureId) {
985
1080
  command,
986
1081
  stdout: result.stdout,
987
1082
  stderr: result.stderr,
988
- message: `Installed optional feature package ${packageName}. Reload the active Pi tab to load new resources.`,
1083
+ status: afterStatus,
1084
+ message: `${operation} optional feature package ${packageName}${afterStatus.installedVersion ? ` to ${afterStatus.installedVersion}` : ""}. Reload the active Pi tab to load new resources.`,
989
1085
  };
990
1086
  }
991
1087
 
@@ -2955,6 +3051,58 @@ function cleanGitCommitMessageInput(value) {
2955
3051
  return message;
2956
3052
  }
2957
3053
 
3054
+ function parseGitPorcelainZEntries(text) {
3055
+ const fields = String(text || "").split("\0").filter(Boolean);
3056
+ const entries = [];
3057
+ for (let index = 0; index < fields.length; index++) {
3058
+ const field = fields[index];
3059
+ if (field.length < 4) {
3060
+ entries.push({ x: "", y: "", path: field, unsupported: true });
3061
+ continue;
3062
+ }
3063
+ const x = field[0] || " ";
3064
+ const y = field[1] || " ";
3065
+ const filePath = field.slice(3);
3066
+ const entry = { x, y, path: filePath };
3067
+ if ((x === "R" || x === "C") && index + 1 < fields.length) entry.oldPath = fields[++index];
3068
+ entries.push(entry);
3069
+ }
3070
+ return entries;
3071
+ }
3072
+
3073
+ function gitWorkflowDefaultCommitAction(entry) {
3074
+ if (!entry || entry.y !== " ") return "";
3075
+ if (entry.x === "A") return "created";
3076
+ if (entry.x === "M" || entry.x === "T") return "updated";
3077
+ if (entry.x === "D") return "deleted";
3078
+ return "";
3079
+ }
3080
+
3081
+ function formatGitWorkflowDefaultCommitPath(filePath) {
3082
+ return String(filePath || "").replace(/[\0\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 4000);
3083
+ }
3084
+
3085
+ async function readGitWorkflowDefaultCommitMessage(cwd) {
3086
+ const root = await getGitRoot(cwd);
3087
+ const statusText = await runGitReadCommand(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { maxOutputLength: 120_000 });
3088
+ const entries = parseGitPorcelainZEntries(statusText);
3089
+ const empty = (reason, extra = {}) => ({ root, message: "", reason, ...extra });
3090
+ if (entries.length === 0) return empty("No changed files are ready for a default commit message.");
3091
+ if (entries.length !== 1) return empty(`Expected exactly one changed file for a default commit message; found ${entries.length}.`);
3092
+ const [entry] = entries;
3093
+ const action = gitWorkflowDefaultCommitAction(entry);
3094
+ const displayPath = formatGitWorkflowDefaultCommitPath(entry.path);
3095
+ if (!action || !displayPath) {
3096
+ return empty("The only changed file is not a staged created, updated, or deleted file.", { path: entry.path || "" });
3097
+ }
3098
+ return {
3099
+ root,
3100
+ message: `${action} ${displayPath}`,
3101
+ action,
3102
+ path: entry.path,
3103
+ };
3104
+ }
3105
+
2958
3106
  function cleanGitHubUsername(value) {
2959
3107
  const username = String(value || "").trim().replace(/^@+/, "");
2960
3108
  if (!username) throw new Error("GitHub username is required");
@@ -3300,6 +3448,8 @@ async function handleGitWorkflowRequest(pathname, body = {}, cwd = options.cwd)
3300
3448
  switch (pathname) {
3301
3449
  case "/api/git-workflow/message":
3302
3450
  return { ok: true, data: await readGitWorkflowMessages(cwd) };
3451
+ case "/api/git-workflow/default-commit-message":
3452
+ return { ok: true, data: await readGitWorkflowDefaultCommitMessage(cwd) };
3303
3453
  case "/api/git-workflow/branch-name":
3304
3454
  return { ok: true, data: await readGitWorkflowBranchName(cwd) };
3305
3455
  case "/api/git-workflow/pr-description":
@@ -3917,16 +4067,8 @@ function parseNodeModulesPackageRef(manifestEntry) {
3917
4067
  async function resolveStartedWebuiManifestResource(manifestEntry) {
3918
4068
  const nodeModulesRef = parseNodeModulesPackageRef(manifestEntry);
3919
4069
  if (nodeModulesRef && WEBUI_CONTROLLED_PACKAGES.has(nodeModulesRef.packageName)) {
3920
- const workspaceRoot = await workspacePackageRootForName(nodeModulesRef.packageName);
3921
- if (workspaceRoot) {
3922
- const devCandidate = path.join(workspaceRoot, nodeModulesRef.subpath);
3923
- try {
3924
- await access(devCandidate);
3925
- return devCandidate;
3926
- } catch {
3927
- // Fall back to the started package's node_modules copy below.
3928
- }
3929
- }
4070
+ const installedCandidate = await resolveInstalledPackageSubpath(nodeModulesRef.packageName, nodeModulesRef.subpath);
4071
+ if (installedCandidate) return installedCandidate;
3930
4072
  }
3931
4073
 
3932
4074
  const candidate = path.resolve(packageRoot, manifestEntry);
@@ -6964,6 +7106,11 @@ const server = createServer(async (req, res) => {
6964
7106
  return;
6965
7107
  }
6966
7108
 
7109
+ if (url.pathname === "/api/optional-features" && req.method === "GET") {
7110
+ sendJson(res, 200, { ok: true, data: await optionalFeaturePackageStatuses() });
7111
+ return;
7112
+ }
7113
+
6967
7114
  if (url.pathname === "/api/optional-feature-install" && req.method === "POST") {
6968
7115
  requireLocalhostRoute(req, url.pathname);
6969
7116
  const body = await readJsonBody(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-package-webui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",