pi-sdk-web 0.5.8 → 0.5.10

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/dist/server.js CHANGED
@@ -12,7 +12,7 @@ import { execFileSync } from "node:child_process";
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { readFile, unlink } from "node:fs/promises";
14
14
  import { createServer } from "node:http";
15
- import { dirname, join, resolve, sep } from "node:path";
15
+ import { dirname, join, relative, resolve, sep } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { ModelRegistry, VERSION, } from "@earendil-works/pi-coding-agent";
18
18
  import { WebSocket, WebSocketServer } from "ws";
@@ -22,6 +22,18 @@ const DEFAULT_PORT = 4080;
22
22
  // Static frontend: prefer the in-package copy (built by `npm run build` for
23
23
  // global installs), fall back to the repo-root static/ during development.
24
24
  const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
25
+ /** Format a local file extension path for the sidebar: relative to baseDir,
26
+ * with the ".pi/extensions/" prefix and the entry suffix (".ts"/".js"/"/index")
27
+ * stripped, e.g. ".../.pi/extensions/import-repro.ts" -> "import-repro",
28
+ * ".../extensions/refresh-deepinfra/index.ts" -> "refresh-deepinfra". */
29
+ function describeLocalExtension(entryPath, baseDir) {
30
+ const rel = baseDir
31
+ ? relative(baseDir, entryPath).replace(/\\/g, "/")
32
+ : entryPath.replace(/\\/g, "/");
33
+ let name = rel.replace(/^extensions\//, "");
34
+ name = name.replace(/\/index\.[jt]s$/, "").replace(/\.[jt]s$/, "");
35
+ return name || rel;
36
+ }
25
37
  // Static frontend source:
26
38
  // - dev (tsx runs src/server.ts): always the repo-root static/ (live files)
27
39
  // - published (dist/server.js): the built copy at dist/static (packaged)
@@ -435,9 +447,29 @@ export class PiWebServer {
435
447
  const list = [];
436
448
  const seen = new Set();
437
449
  for (const ext of result.extensions) {
438
- // ext.path points at the extension entry (e.g. .../lib/node_modules/
439
- // pi-magic-context/dist/index.js); walk up to the nearest package.json
440
- // for the name/version.
450
+ // Inline extensions (Pi built-ins like <inline:llama.cpp>, hidden:
451
+ // true) are virtual - no package.json, no disk entry. Skip them so
452
+ // the sidebar lists only real, on-disk extensions.
453
+ if (ext.path.startsWith("<") && ext.path.endsWith(">")) {
454
+ continue;
455
+ }
456
+ const sourceInfo = ext
457
+ .sourceInfo;
458
+ // Local file extensions (~/.pi/agent/extensions/*.ts or project
459
+ // .pi/extensions/*.ts): the nearest package.json is the HOST repo's
460
+ // (e.g. Pi's own monorepo root) and would show a misleading name.
461
+ // Show the extension file name + scope instead.
462
+ if (sourceInfo?.origin === "top-level") {
463
+ const name = describeLocalExtension(ext.path, sourceInfo.baseDir);
464
+ const key = `local:${ext.path}`;
465
+ if (!seen.has(key)) {
466
+ seen.add(key);
467
+ list.push({ name, version: sourceInfo.scope ?? "local" });
468
+ }
469
+ continue;
470
+ }
471
+ // Package extensions: walk up to the nearest package.json (e.g.
472
+ // .../node_modules/pi-magic-context/dist/index.js -> pi-magic-context)
441
473
  let dir = dirname(ext.path);
442
474
  while (dir !== dirname(dir)) {
443
475
  const pkgFile = join(dir, "package.json");
@@ -447,7 +479,14 @@ export class PiWebServer {
447
479
  const key = pkg.name ?? ext.path;
448
480
  if (!seen.has(key)) {
449
481
  seen.add(key);
450
- list.push({ name: pkg.name ?? key, version: pkg.version ?? "?" });
482
+ // Append the install scope (user/project, from sourceInfo) so
483
+ // package extensions show the same dimension as local ones.
484
+ const scope = sourceInfo?.scope;
485
+ const version = pkg.version ?? "?";
486
+ list.push({
487
+ name: pkg.name ?? key,
488
+ version: scope ? `${version} · ${scope}` : version,
489
+ });
451
490
  }
452
491
  }
453
492
  catch {
@@ -196,11 +196,38 @@ export class WebUIContext {
196
196
  // evaluate with a no-op tui stub and broadcast as text lines - the
197
197
  // browser renders them (with ANSI colors) in the widgets panel.
198
198
  try {
199
- const stubTui = { requestRender: () => { }, invalidate: () => { } };
200
- const component = content(stubTui, this.webTheme);
199
+ const width = 78;
200
+ let component;
201
+ const stubTui = {
202
+ // TUI semantics: after registration, updates flow through
203
+ // tui.requestRender() -> the component's render() is re-invoked
204
+ // with CURRENT state. Without this a widget is frozen at its
205
+ // first-registration content (magic-context's todos overlay
206
+ // updates via requestRender, never via setWidget, so the browser
207
+ // kept showing the initial todo list).
208
+ requestRender: () => {
209
+ if (!component || typeof component.render !== "function")
210
+ return;
211
+ let fresh;
212
+ try {
213
+ const out = component.render(width);
214
+ if (Array.isArray(out))
215
+ fresh = out.map((l) => String(l));
216
+ }
217
+ catch {
218
+ // A throwing render must not wipe the widget: keep last lines.
219
+ return;
220
+ }
221
+ this.broadcastWidget(key, fresh, options?.placement);
222
+ },
223
+ invalidate: () => {
224
+ // TUI calls invalidate while tearing the widget down; the web
225
+ // host never tears a widget down on its own.
226
+ },
227
+ };
228
+ component = content(stubTui, this.webTheme);
201
229
  const render = component && component.render;
202
230
  if (typeof render === "function") {
203
- const width = 78;
204
231
  const out = render(width);
205
232
  if (Array.isArray(out))
206
233
  lines = out.map((l) => String(l));
@@ -210,16 +237,20 @@ export class WebUIContext {
210
237
  // fall through with no lines - widget renders empty
211
238
  }
212
239
  }
240
+ this.broadcastWidget(key, lines, options?.placement);
241
+ }
242
+ /** Broadcast a widget update and store it as the snapshot for new clients. */
243
+ broadcastWidget(key, lines, placement) {
213
244
  this.sink({
214
245
  type: "extension_ui_request",
215
246
  id: crypto.randomUUID(),
216
247
  method: "setWidget",
217
248
  widgetKey: key,
218
249
  widgetLines: lines,
219
- widgetPlacement: options?.placement,
250
+ widgetPlacement: placement,
220
251
  });
221
252
  // Store for late-connecting browsers (replayed on WS connect).
222
- this.widgetMap.set(key, { lines, placement: options?.placement });
253
+ this.widgetMap.set(key, { lines, placement });
223
254
  }
224
255
  // ------------------------------------------------------------------
225
256
  // Terminal-specific features: no-op in web mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sdk-web",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Browser Web access for Pi (AI coding agent) via the Pi SDK - standalone module, zero modification to Pi itself",
5
5
  "type": "module",
6
6
  "bin": {