pi2dsh 0.19.0 → 0.21.0

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.
Files changed (35) hide show
  1. package/README.md +46 -2
  2. package/README.zh.md +26 -0
  3. package/dist/{analyzer-DLwDZoYB.mjs → analyzer-LpQ59yjZ.mjs} +2 -2
  4. package/dist/{analyzer-DLwDZoYB.mjs.map → analyzer-LpQ59yjZ.mjs.map} +1 -1
  5. package/dist/cli.mjs +3 -2
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client.js +17 -12
  8. package/dist/compat/pi-coding-agent.d.mts +2 -2
  9. package/dist/compat/pi-coding-agent.mjs +2 -2
  10. package/dist/{mcp-config-CyCPWeUs.mjs → compatibility-CsJEjhu8.mjs} +10 -187
  11. package/dist/compatibility-CsJEjhu8.mjs.map +1 -0
  12. package/dist/credentials-oauth.mjs +1 -1
  13. package/dist/host.d.mts +9 -2
  14. package/dist/host.d.mts.map +1 -1
  15. package/dist/host.mjs +7 -2
  16. package/dist/host.mjs.map +1 -1
  17. package/dist/index.d.mts +8 -0
  18. package/dist/index.d.mts.map +1 -1
  19. package/dist/index.mjs +154 -8
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/mcp-config-C1K-IzzB.mjs +180 -0
  22. package/dist/mcp-config-C1K-IzzB.mjs.map +1 -0
  23. package/dist/{pi-coding-agent-DXJ_Jzo0.mjs → pi-coding-agent-CPXFo0UO.mjs} +0 -0
  24. package/dist/pi-coding-agent-CPXFo0UO.mjs.map +1 -0
  25. package/dist/{pi-coding-agent-69yY0ke-.d.mts → pi-coding-agent-Pvy0m9tS.d.mts} +6 -2
  26. package/dist/{pi-coding-agent-69yY0ke-.d.mts.map → pi-coding-agent-Pvy0m9tS.d.mts.map} +1 -1
  27. package/dist/{runtime-CW52OWvW.mjs → runtime-CGDq6LKi.mjs} +557 -27
  28. package/dist/runtime-CGDq6LKi.mjs.map +1 -0
  29. package/dist/runtime.d.mts +83 -41
  30. package/dist/runtime.d.mts.map +1 -1
  31. package/dist/runtime.mjs +2 -2
  32. package/package.json +7 -2
  33. package/dist/mcp-config-CyCPWeUs.mjs.map +0 -1
  34. package/dist/pi-coding-agent-DXJ_Jzo0.mjs.map +0 -1
  35. package/dist/runtime-CW52OWvW.mjs.map +0 -1
@@ -1,15 +1,16 @@
1
1
 
2
- import { En as PiCapabilityError, F as generateBranchSummary, Tn as CapabilityLedger, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-DXJ_Jzo0.mjs";
2
+ import { Dn as PiCapabilityError, En as CapabilityLedger, F as generateBranchSummary, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-CPXFo0UO.mjs";
3
3
  import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
4
4
  import { $ as AssistantMessageEventStream, S as getSupportedThinkingLevels, c as builtinProviders, et as isContextOverflow, o as __runWithPiAiRuntime, r as __createPiAiRuntimeRegistry } from "./pi-ai-DQPHUR9S.mjs";
5
5
  import { W as getKeybindings, _t as stripTerminalSequences } from "./pi-tui-DruSeKmd.mjs";
6
+ import { r as piMcpConfigPaths, t as collectPiMcpServers } from "./mcp-config-C1K-IzzB.mjs";
6
7
  import { c as resolvePiProviderAuth, i as oauthCredentialRef, l as storedOAuthCredential, n as joinSignals, o as providerSupportsOAuth, r as loginPiProvider, t as FileCredentialStore } from "./oauth-bridge-C_MAPMGA.mjs";
7
8
  import { createRequire } from "node:module";
8
9
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
9
10
  import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
10
11
  import { basename, dirname, join, resolve } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
- import { tmpdir } from "node:os";
13
+ import { homedir, tmpdir } from "node:os";
13
14
  import { EventEmitter } from "node:events";
14
15
  import { AsyncLocalStorage } from "node:async_hooks";
15
16
  import { createJiti } from "jiti";
@@ -912,7 +913,21 @@ var PiBridgedAgentSession = class {
912
913
  this.#restrictionDispose = toolsService.restrict({ allow: this.#activeToolNames.map(nativeToolNameOf).filter((name) => known.has(name)) });
913
914
  } catch {}
914
915
  }
915
- async bindExtensions(_bindings = {}) {}
916
+ /** Per-extension mount failures recorded at creation, for bindExtensions' onError. */
917
+ #extensionFailures = [];
918
+ adoptExtensionFailures(failures) {
919
+ this.#extensionFailures = failures;
920
+ }
921
+ async bindExtensions(bindings = {}) {
922
+ const onError = bindings.onError;
923
+ if (typeof onError !== "function") return;
924
+ for (const failure of this.#extensionFailures) try {
925
+ onError({
926
+ extensionPath: failure.name,
927
+ error: failure.error
928
+ });
929
+ } catch {}
930
+ }
916
931
  async dispose() {
917
932
  for (const dispose of this.#disposers.splice(0)) try {
918
933
  dispose();
@@ -948,6 +963,7 @@ async function createBridgedAgentSession(host, options) {
948
963
  const providedManager = options.sessionManager;
949
964
  const archiveFile = typeof providedManager?.getSessionFile === "function" ? providedManager.getSessionFile() : void 0;
950
965
  const resumeSessionId = host.resumeSessionIdFor?.(archiveFile);
966
+ const sideline = providedManager !== void 0 && archiveFile === void 0;
951
967
  subagentSerial += 1;
952
968
  const sessionId = `pi2dsh-sub-${Date.now().toString(36)}-${subagentSerial}`;
953
969
  let handle;
@@ -1061,15 +1077,30 @@ async function createBridgedAgentSession(host, options) {
1061
1077
  version: 2,
1062
1078
  mode: "continuable",
1063
1079
  provider: "pi2dsh",
1064
- label: childLabel(options.label, host.packageName)
1080
+ label: `${sideline ? "Side: " : ""}${childLabel(options.label, host.packageName)}`
1065
1081
  });
1066
1082
  } catch (error) {
1067
1083
  host.cordis.logger?.warn?.(`[pi2dsh] child session could not record its subagent identity: ${error instanceof Error ? error.message : String(error)}`);
1068
1084
  }
1069
1085
  const thinkingLevel = typeof options.thinkingLevel === "string" ? options.thinkingLevel : void 0;
1070
1086
  host.adoptChildAgent?.(handle.agent, thinkingLevel);
1087
+ let extensionFailures = [];
1088
+ try {
1089
+ extensionFailures = await host.mountChildExtensions?.(handle.agent, options.resourceLoader) ?? [];
1090
+ } catch (error) {
1091
+ extensionFailures = [{
1092
+ name: "pi2dsh:child-extensions",
1093
+ error: error instanceof Error ? error.message : String(error)
1094
+ }];
1095
+ }
1096
+ for (const failure of extensionFailures) {
1097
+ const message = `[pi2dsh] child extension ${JSON.stringify(failure.name)} did not mount: ${failure.error}`;
1098
+ host.cordis.logger?.warn?.(message);
1099
+ console.warn(message);
1100
+ }
1071
1101
  const session = new PiBridgedAgentSession(host, handle, tools, thinkingLevel);
1072
1102
  session.adoptRestriction(initialRestrictionDispose);
1103
+ session.adoptExtensionFailures(extensionFailures);
1073
1104
  return { session };
1074
1105
  }
1075
1106
  //#endregion
@@ -1109,6 +1140,7 @@ function messageText(message) {
1109
1140
  */
1110
1141
  var BrowserSurfaces = class {
1111
1142
  #byParent = /* @__PURE__ */ new Map();
1143
+ #commandRunners = /* @__PURE__ */ new Map();
1112
1144
  #surfaces = /* @__PURE__ */ new Map();
1113
1145
  #entrySources = /* @__PURE__ */ new Map();
1114
1146
  #draftRequests = /* @__PURE__ */ new Map();
@@ -1150,6 +1182,7 @@ var BrowserSurfaces = class {
1150
1182
  if (typeof component?.render !== "function") {
1151
1183
  this.#scene = void 0;
1152
1184
  this.#invalidateScene();
1185
+ console.warn(`[pi2dsh] web scene for ${packageName}: component lacks render(width)`);
1153
1186
  reject(/* @__PURE__ */ new TypeError("Pi custom component must implement render(width)"));
1154
1187
  return;
1155
1188
  }
@@ -1159,6 +1192,7 @@ var BrowserSurfaces = class {
1159
1192
  if (this.#scene !== run) return;
1160
1193
  this.#scene = void 0;
1161
1194
  this.#invalidateScene();
1195
+ console.warn(`[pi2dsh] web scene for ${packageName}: component factory failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1162
1196
  reject(error);
1163
1197
  });
1164
1198
  });
@@ -1179,6 +1213,7 @@ var BrowserSurfaces = class {
1179
1213
  const failed = this.#scene;
1180
1214
  this.#scene = void 0;
1181
1215
  this.#invalidateScene();
1216
+ console.warn(`[pi2dsh] web scene for ${scene.package}: render(width) failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1182
1217
  failed?.reject(error);
1183
1218
  return {
1184
1219
  open: false,
@@ -1234,6 +1269,32 @@ var BrowserSurfaces = class {
1234
1269
  };
1235
1270
  }
1236
1271
  /**
1272
+ * Register the executor for one package's Pi commands on one session, so
1273
+ * product UI (a side-chat window's input, its action buttons) can run the
1274
+ * package's OWN command handlers — the same code path the composer takes.
1275
+ * Generic by construction: the runner closure carries the package runtime;
1276
+ * this registry only routes (session, package) to it.
1277
+ * @param sessionId - the root session the package is mounted for.
1278
+ * @param packageName - the Pi package owning the commands.
1279
+ * @param runner - executes one registered Pi command by its Pi name.
1280
+ * @returns a disposer that unregisters the runner.
1281
+ */
1282
+ registerCommandRunner(sessionId, packageName, runner) {
1283
+ const runners = this.#commandRunners.get(sessionId) ?? /* @__PURE__ */ new Map();
1284
+ runners.set(packageName, runner);
1285
+ this.#commandRunners.set(sessionId, runners);
1286
+ return () => {
1287
+ const live = this.#commandRunners.get(sessionId);
1288
+ if (live === void 0) return;
1289
+ if (live.get(packageName) === runner) live.delete(packageName);
1290
+ if (live.size === 0) this.#commandRunners.delete(sessionId);
1291
+ };
1292
+ }
1293
+ /** The registered runner for (session, package), when one is mounted. */
1294
+ commandRunner(sessionId, packageName) {
1295
+ return this.#commandRunners.get(sessionId)?.get(packageName);
1296
+ }
1297
+ /**
1237
1298
  * The panel's view of one parent session.
1238
1299
  * @param parentSessionId - session the browser is currently showing.
1239
1300
  * @returns every side thread opened under it, oldest first.
@@ -1535,7 +1596,7 @@ function publishAuthorization(url) {
1535
1596
  function revokeAuthorization(path) {
1536
1597
  pendingAuthorizations.delete(path.slice(18));
1537
1598
  }
1538
- function registerBrowserSurfaceRoute(ctx, registry) {
1599
+ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1539
1600
  const web = ctx.get("webServer");
1540
1601
  if (web === void 0 || typeof web.register !== "function") return false;
1541
1602
  ctx.effect(() => web.register({
@@ -1560,6 +1621,75 @@ function registerBrowserSurfaceRoute(ctx, registry) {
1560
1621
  response.end();
1561
1622
  return;
1562
1623
  }
1624
+ if (url.pathname === "/pi2dsh/mcp-state" && (method === "GET" || method === "HEAD")) {
1625
+ if (hooks?.mcpState === void 0) {
1626
+ response.writeHead(404);
1627
+ response.end();
1628
+ return;
1629
+ }
1630
+ const body = JSON.stringify(await hooks.mcpState(url.searchParams.get("session") ?? ""));
1631
+ response.writeHead(200, {
1632
+ "content-type": "application/json; charset=utf-8",
1633
+ "cache-control": "no-store"
1634
+ });
1635
+ response.end(method === "HEAD" ? void 0 : body);
1636
+ return;
1637
+ }
1638
+ if (url.pathname === "/pi2dsh/mcp-action" && method === "POST") {
1639
+ if (hooks?.mcpAction === void 0) {
1640
+ response.writeHead(404);
1641
+ response.end();
1642
+ return;
1643
+ }
1644
+ const chunks = [];
1645
+ const raw = await new Promise((settle) => {
1646
+ const stream = req;
1647
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1648
+ stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1649
+ });
1650
+ let outcome;
1651
+ try {
1652
+ const payload = JSON.parse(raw || "{}");
1653
+ if (typeof payload.session !== "string" || typeof payload.server !== "string" || typeof payload.disabled !== "boolean") throw new TypeError("mcp-action needs { session, server, disabled }");
1654
+ const scope = payload.scope === "global" ? "global" : "project";
1655
+ outcome = await hooks.mcpAction(payload.session, payload.server, payload.disabled, scope);
1656
+ } catch (error) {
1657
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" });
1658
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1659
+ return;
1660
+ }
1661
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1662
+ response.end(JSON.stringify(outcome ?? { ok: true }));
1663
+ return;
1664
+ }
1665
+ if (url.pathname === "/pi2dsh/pi-command" && method === "POST") {
1666
+ const chunks = [];
1667
+ const raw = await new Promise((settle) => {
1668
+ const stream = req;
1669
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1670
+ stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1671
+ });
1672
+ try {
1673
+ const payload = JSON.parse(raw || "{}");
1674
+ if (typeof payload.session !== "string" || typeof payload.package !== "string" || typeof payload.command !== "string") throw new TypeError("pi-command needs { session, package, command }");
1675
+ const runner = registry.commandRunner(payload.session, payload.package);
1676
+ if (runner === void 0) {
1677
+ response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
1678
+ response.end(JSON.stringify({ error: `no ${payload.package} command runner mounted for this session` }));
1679
+ return;
1680
+ }
1681
+ const notice = await runner(payload.command, typeof payload.args === "string" ? payload.args : "");
1682
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1683
+ response.end(JSON.stringify({
1684
+ ok: true,
1685
+ ...notice === void 0 ? {} : { notice }
1686
+ }));
1687
+ } catch (error) {
1688
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" });
1689
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1690
+ }
1691
+ return;
1692
+ }
1563
1693
  if (method === "POST" && (url.pathname === "/pi2dsh/scene-input" || url.pathname === "/pi2dsh/scene-close")) {
1564
1694
  const chunks = [];
1565
1695
  const body = await new Promise((settle) => {
@@ -3112,7 +3242,7 @@ function registerPiProviderRoute(options) {
3112
3242
  }
3113
3243
  //#endregion
3114
3244
  //#region src/tui-surfaces.ts
3115
- function service(ctx, name) {
3245
+ function service$1(ctx, name) {
3116
3246
  try {
3117
3247
  return ctx.get(name, false);
3118
3248
  } catch {
@@ -3159,6 +3289,8 @@ function preferredWidth(options) {
3159
3289
  var TuiSurfaceAdapter = class {
3160
3290
  packageName;
3161
3291
  instanceKey;
3292
+ kind = "tuiScenes";
3293
+ nativeCommands = dshTuiLocalCommands();
3162
3294
  sceneId;
3163
3295
  scenes;
3164
3296
  status;
@@ -3171,10 +3303,10 @@ var TuiSurfaceAdapter = class {
3171
3303
  constructor(ctx, packageName, instanceKey) {
3172
3304
  this.packageName = packageName;
3173
3305
  this.instanceKey = instanceKey;
3174
- const scenes = service(ctx, "tuiScenes");
3306
+ const scenes = service$1(ctx, "tuiScenes");
3175
3307
  if (scenes === void 0) throw new Error("pi2dsh: dsh-TUI scene service is unavailable");
3176
3308
  this.scenes = scenes;
3177
- this.status = service(ctx, "tuiStatus");
3309
+ this.status = service$1(ctx, "tuiStatus");
3178
3310
  this.sceneId = sceneIdFor(instanceKey === void 0 ? packageName : `${packageName}-${instanceKey}`);
3179
3311
  this.unregisterScene = scenes.register({
3180
3312
  id: this.sceneId,
@@ -3307,7 +3439,7 @@ function mountTuiSurfaceAdapter(ctx, packageName, publish, instanceKey) {
3307
3439
  publish(void 0);
3308
3440
  });
3309
3441
  };
3310
- if (service(ctx, "tuiScenes") !== void 0) {
3442
+ if (service$1(ctx, "tuiScenes") !== void 0) {
3311
3443
  attach(ctx);
3312
3444
  return;
3313
3445
  }
@@ -3400,8 +3532,252 @@ function dshTuiLocalCommands() {
3400
3532
  cachedTuiLocalCommands = tuiLocalCommandsForVersion(version);
3401
3533
  return cachedTuiLocalCommands;
3402
3534
  }
3403
- function commandNameForDshTui(name, tuiAvailable) {
3404
- return tuiAvailable && dshTuiLocalCommands().has(name) ? `pi-${name}` : name;
3535
+ /** Host-native command ownership, independent of the concrete TUI package. */
3536
+ function commandNameForTerminal(name, nativeCommands) {
3537
+ return nativeCommands?.has(name) === true ? `pi-${name}` : name;
3538
+ }
3539
+ /** Bridge fallback commands disappear when the active Host already owns one. */
3540
+ function shouldRegisterCompatibilityCommand(name, nativeCommands) {
3541
+ return nativeCommands?.has(name) !== true;
3542
+ }
3543
+ //#endregion
3544
+ //#region src/pi-component-relay.ts
3545
+ let relaySerial = 0;
3546
+ function relayId() {
3547
+ relaySerial += 1;
3548
+ return `pi2dsh-custom-${relaySerial}`;
3549
+ }
3550
+ /**
3551
+ * One Host-owned Pi component session expressed as data messages.
3552
+ *
3553
+ * The result value never crosses the presentation boundary: Pi's own `done`
3554
+ * callback completes it on the Host. Frames and input are deliberately plain
3555
+ * data so the presentation transport can change independently.
3556
+ */
3557
+ var PiComponentRelay = class {
3558
+ sessionId = relayId();
3559
+ options;
3560
+ ready;
3561
+ result;
3562
+ component;
3563
+ invalidations = /* @__PURE__ */ new Set();
3564
+ resolveResult;
3565
+ rejectResult;
3566
+ revision = 0;
3567
+ closed = false;
3568
+ constructor(factory, theme, keybindings, options) {
3569
+ this.options = options;
3570
+ this.result = new Promise((resolve, reject) => {
3571
+ this.resolveResult = resolve;
3572
+ this.rejectResult = reject;
3573
+ });
3574
+ const driver = { requestRender: () => this.invalidate() };
3575
+ const done = (value) => this.close(value);
3576
+ this.ready = Promise.resolve().then(() => factory(driver, theme, keybindings, done)).then((component) => {
3577
+ if (this.closed) {
3578
+ component.dispose?.();
3579
+ return false;
3580
+ }
3581
+ if (typeof component?.render !== "function") throw new TypeError("Pi custom component must implement render(width)");
3582
+ this.component = component;
3583
+ this.invalidate();
3584
+ return true;
3585
+ }).catch((error) => {
3586
+ this.fail(error);
3587
+ return false;
3588
+ });
3589
+ }
3590
+ get active() {
3591
+ return !this.closed;
3592
+ }
3593
+ onInvalidate(listener) {
3594
+ this.invalidations.add(listener);
3595
+ return () => this.invalidations.delete(listener);
3596
+ }
3597
+ frame(request) {
3598
+ if (request.sessionId !== this.sessionId) throw new Error("stale Pi component render session");
3599
+ const width = Math.max(1, Math.floor(request.width));
3600
+ return {
3601
+ sessionId: this.sessionId,
3602
+ revision: this.revision,
3603
+ width,
3604
+ lines: this.component?.render(width) ?? []
3605
+ };
3606
+ }
3607
+ input(message) {
3608
+ if (message.sessionId !== this.sessionId || this.closed) return;
3609
+ this.component?.handleInput?.(message.data);
3610
+ }
3611
+ invalidate() {
3612
+ if (this.closed) return;
3613
+ this.revision += 1;
3614
+ for (const listener of this.invalidations) listener(this.revision);
3615
+ }
3616
+ close(value) {
3617
+ if (this.closed) return;
3618
+ this.closed = true;
3619
+ const component = this.component;
3620
+ this.component = void 0;
3621
+ component?.dispose?.();
3622
+ this.invalidations.clear();
3623
+ this.resolveResult(value);
3624
+ }
3625
+ fail(error) {
3626
+ if (this.closed) return;
3627
+ this.closed = true;
3628
+ const component = this.component;
3629
+ this.component = void 0;
3630
+ component?.dispose?.();
3631
+ this.invalidations.clear();
3632
+ this.rejectResult(error);
3633
+ }
3634
+ };
3635
+ //#endregion
3636
+ //#region src/pi-tui-extension-surface.ts
3637
+ function service(ctx) {
3638
+ try {
3639
+ return ctx.get("piTuiExtensions", false);
3640
+ } catch {
3641
+ return;
3642
+ }
3643
+ }
3644
+ function statusId(packageName, instanceKey, key) {
3645
+ return `pi2dsh-status-${`${packageName}${instanceKey === void 0 ? "" : `-${instanceKey}`}-${key}`.toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "") || "entry"}`;
3646
+ }
3647
+ function overlayOptions(options) {
3648
+ const raw = typeof options?.overlayOptions === "function" ? options.overlayOptions() : options?.overlayOptions;
3649
+ return raw === void 0 ? void 0 : { ...raw };
3650
+ }
3651
+ /** dsh-pi-tui backend over the public `piTuiExtensions` service. */
3652
+ var PiTuiExtensionSurfaceAdapter = class {
3653
+ extensionService;
3654
+ unstableFacade;
3655
+ packageName;
3656
+ instanceKey;
3657
+ kind = "piTuiExtensions";
3658
+ nativeCommands;
3659
+ statusHandles = /* @__PURE__ */ new Map();
3660
+ mounts = /* @__PURE__ */ new Set();
3661
+ disposed = false;
3662
+ constructor(extensionService, unstableFacade, nativeCommands, packageName, instanceKey) {
3663
+ this.extensionService = extensionService;
3664
+ this.unstableFacade = unstableFacade;
3665
+ this.packageName = packageName;
3666
+ this.instanceKey = instanceKey;
3667
+ this.nativeCommands = new Set(nativeCommands);
3668
+ }
3669
+ get available() {
3670
+ if (this.disposed) return false;
3671
+ try {
3672
+ return this.extensionService.api().capabilities.has("unstable.surface.handle");
3673
+ } catch {
3674
+ return false;
3675
+ }
3676
+ }
3677
+ setStatus(key, text) {
3678
+ if (this.disposed) return;
3679
+ const id = statusId(this.packageName, this.instanceKey, key);
3680
+ const previous = this.statusHandles.get(id);
3681
+ if (text === void 0) {
3682
+ previous?.dispose();
3683
+ this.statusHandles.delete(id);
3684
+ return;
3685
+ }
3686
+ const value = {
3687
+ spans: [{
3688
+ text: String(text),
3689
+ tone: "textDim"
3690
+ }],
3691
+ minWidth: 4,
3692
+ importance: 50
3693
+ };
3694
+ if (previous !== void 0) {
3695
+ previous.replace(value);
3696
+ return;
3697
+ }
3698
+ if (!this.extensionService.api().capabilities.has("slot.chrome.footer.status")) return;
3699
+ this.statusHandles.set(id, this.extensionService.register("chrome.footer.status", {
3700
+ id,
3701
+ order: 500,
3702
+ description: `Pi status ${key} from ${this.packageName}`
3703
+ }, value));
3704
+ }
3705
+ async custom(factory, theme, keybindings, options) {
3706
+ if (!this.available) return void 0;
3707
+ for (const active of [...this.mounts]) active.relay.close(void 0);
3708
+ const relay = new PiComponentRelay(factory, theme, keybindings, options);
3709
+ const record = { relay };
3710
+ this.mounts.add(record);
3711
+ try {
3712
+ if (!await relay.ready || !relay.active) return await relay.result;
3713
+ if (this.disposed) {
3714
+ relay.close(void 0);
3715
+ return await relay.result;
3716
+ }
3717
+ const lease = this.unstableFacade.surface.handle.mountComponent({
3718
+ render: (width) => relay.frame({
3719
+ sessionId: relay.sessionId,
3720
+ width
3721
+ }).lines,
3722
+ handleInput: (data) => relay.input({
3723
+ sessionId: relay.sessionId,
3724
+ data
3725
+ }),
3726
+ dispose: () => relay.close(void 0)
3727
+ }, overlayOptions(options));
3728
+ record.lease = lease;
3729
+ const offInvalidate = relay.onInvalidate(() => lease.invalidate());
3730
+ const release = () => {
3731
+ offInvalidate();
3732
+ lease.close();
3733
+ };
3734
+ relay.result.then(release, release);
3735
+ if (!lease.active) relay.close(void 0);
3736
+ lease.focus();
3737
+ return await relay.result;
3738
+ } catch (error) {
3739
+ relay.fail(error);
3740
+ throw error;
3741
+ } finally {
3742
+ this.mounts.delete(record);
3743
+ }
3744
+ }
3745
+ dispose() {
3746
+ if (this.disposed) return;
3747
+ this.disposed = true;
3748
+ for (const handle of this.statusHandles.values()) handle.dispose();
3749
+ this.statusHandles.clear();
3750
+ for (const active of this.mounts) {
3751
+ active.relay.close(void 0);
3752
+ active.lease?.close();
3753
+ }
3754
+ this.mounts.clear();
3755
+ }
3756
+ };
3757
+ /**
3758
+ * Build the optional backend from published public entry points.
3759
+ * Returns undefined when the service is absent or cannot provide the exact
3760
+ * raw-component capability; callers then keep the existing dsh-TUI/Web path.
3761
+ */
3762
+ async function createPiTuiExtensionSurfaceAdapter(ctx, packageName, instanceKey) {
3763
+ const extensionService = service(ctx);
3764
+ if (extensionService === void 0) return void 0;
3765
+ const api = extensionService.api();
3766
+ if (api.apiVersion !== 1 || !api.capabilities.has("unstable.surface.handle")) {
3767
+ ctx.logger?.warn(`pi2dsh: piTuiExtensions cannot host Pi components (api=${api.apiVersion}, raw surface=${api.capabilities.has("unstable.surface.handle")})`);
3768
+ return;
3769
+ }
3770
+ try {
3771
+ const [hostModule, unstableModule] = await Promise.all([import("@xmoon76/dsh-pi-tui"), import("@xmoon76/dsh-pi-tui/extensions/unstable")]);
3772
+ if (unstableModule.UNSTABLE_API_LEVEL !== 1) {
3773
+ ctx.logger?.warn(`pi2dsh: unsupported dsh-pi-tui unstable API level ${unstableModule.UNSTABLE_API_LEVEL}`);
3774
+ return;
3775
+ }
3776
+ return new PiTuiExtensionSurfaceAdapter(extensionService, unstableModule.unstable(extensionService), hostModule.LOCAL_COMMANDS, packageName, instanceKey);
3777
+ } catch (error) {
3778
+ ctx.logger?.warn(`pi2dsh: could not attach the public piTuiExtensions backend: ${error instanceof Error ? error.message : String(error)}`);
3779
+ return;
3780
+ }
3405
3781
  }
3406
3782
  //#endregion
3407
3783
  //#region src/runtime.ts
@@ -3854,7 +4230,7 @@ function projectDialogPrompt(title, supplementalDetail, linkMode) {
3854
4230
  ...detail.length === 0 ? {} : { detail }
3855
4231
  };
3856
4232
  }
3857
- function contextFor(ctx, state, agent, signal, command = false, sessionOverride) {
4233
+ function contextFor(ctx, state, agent, signal, command = false, sessionOverride, headlessUi = false) {
3858
4234
  const notices = [];
3859
4235
  const userQuestions = optionalService(ctx, "userQuestions");
3860
4236
  const dialogLinkMode = optionalService(ctx, "tuiScenes") === void 0 ? "markdown" : "terminal";
@@ -3920,6 +4296,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
3920
4296
  setTitle: (title) => putSurface(ctx, state, agent, "title", title),
3921
4297
  custom: async (factory, options) => {
3922
4298
  if (typeof factory !== "function") return void 0;
4299
+ if (headlessUi) return void 0;
3923
4300
  const surfaces = state.tuiSurfaces;
3924
4301
  if (surfaces !== void 0) return surfaces.custom(factory, state.theme, getKeybindings(), options);
3925
4302
  const browser = state.shared.browserSurfaces;
@@ -4120,7 +4497,8 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
4120
4497
  },
4121
4498
  ui,
4122
4499
  get mode() {
4123
- return state.tuiSurfaces?.available === true ? "tui" : "rpc";
4500
+ if (state.tuiSurfaces?.available === true) return "tui";
4501
+ return state.shared.browserSurfaces !== void 0 && state.shared.browserSurfacesRouted === true ? "tui" : "rpc";
4124
4502
  },
4125
4503
  get hasUI() {
4126
4504
  return state.tuiSurfaces?.available === true || humanAnswererAvailable(userQuestions, agent);
@@ -5234,6 +5612,59 @@ function oauthStoreOf(state) {
5234
5612
  }
5235
5613
  const COMPANION_ROUTE_PREFIX = "pi2dsh-companion/";
5236
5614
  const SHARED_HOST_STATE = /* @__PURE__ */ new WeakMap();
5615
+ function registerChildExtensionCatalog(ctx, catalog) {
5616
+ sharedHostStateOf(ctx).childExtensions = catalog;
5617
+ }
5618
+ /** Read the installed-package catalog registered on shared host state, or undefined. */
5619
+ function getSharedChildExtensionCatalog(ctx) {
5620
+ return sharedHostStateOf(ctx).childExtensions;
5621
+ }
5622
+ /**
5623
+ * Which installed packages a creator's resource loader selects for its child
5624
+ * — the creator's OWN filter code decides (the loader's getExtensions applies
5625
+ * noExtensions and extensionsOverride), this only maps the surviving entry
5626
+ * paths back to package names. No loader at all means Pi's default: the full
5627
+ * discovered set (sdk.ts builds a default loader and loads everything).
5628
+ */
5629
+ function resolveChildExtensionPackages(loader, catalog) {
5630
+ if (loader === void 0 || loader === null) return {
5631
+ names: [...new Set(catalog.packageByEntryPath.values())],
5632
+ failures: []
5633
+ };
5634
+ const getExtensions = loader.getExtensions;
5635
+ if (typeof getExtensions !== "function") return {
5636
+ names: [],
5637
+ failures: []
5638
+ };
5639
+ const names = /* @__PURE__ */ new Set();
5640
+ const failures = [];
5641
+ let entries;
5642
+ try {
5643
+ entries = getExtensions.call(loader)?.extensions ?? [];
5644
+ } catch (error) {
5645
+ return {
5646
+ names: [],
5647
+ failures: [{
5648
+ name: "resourceLoader.getExtensions",
5649
+ error: error instanceof Error ? error.message : String(error)
5650
+ }]
5651
+ };
5652
+ }
5653
+ for (const entry of entries) {
5654
+ const path = entry?.path;
5655
+ if (typeof path !== "string" || path.length === 0) continue;
5656
+ const name = catalog.packageByEntryPath.get(resolve(path));
5657
+ if (name !== void 0) names.add(name);
5658
+ else failures.push({
5659
+ name: path,
5660
+ error: "not an installed DSH plugin — path-loaded extensions are not supported on this host; install it with `dsh plugin add <pkg>`"
5661
+ });
5662
+ }
5663
+ return {
5664
+ names: [...names],
5665
+ failures
5666
+ };
5667
+ }
5237
5668
  /**
5238
5669
  * Pi's provider ledger is layered, not flat (model-runtime.ts:742-786 at the
5239
5670
  * pinned upstream): a builtin base map, an extension overlay where defined
@@ -5494,16 +5925,20 @@ async function materializeAttachmentImage(ctx, attachment) {
5494
5925
  }
5495
5926
  }
5496
5927
  function ensureLoginCommand(ctx, state) {
5497
- const scoped = scopeOf(ctx);
5498
- const owner = typeof scoped === "object" && scoped !== null ? scoped : ctx;
5499
- const registered = state.shared.loginRegistered ??= /* @__PURE__ */ new WeakSet();
5500
- if (registered.has(owner)) return;
5501
- registered.add(owner);
5928
+ if (!shouldRegisterCompatibilityCommand("login", state.tuiSurfaces?.nativeCommands) && optionalService(ctx, "authorization") !== void 0) {
5929
+ logger(ctx).debug("[pi2dsh] native terminal owns /login and the authorization service is composed; Pi OAuth flows are available through it");
5930
+ return;
5931
+ }
5932
+ if (state.shared.loginCommandRegistered === true) return;
5933
+ state.shared.loginCommandRegistered = true;
5502
5934
  try {
5503
5935
  registerLoginCommand(ctx, state);
5936
+ ctx.effect(() => () => {
5937
+ state.shared.loginCommandRegistered = false;
5938
+ });
5504
5939
  } catch (error) {
5505
- registered.delete(owner);
5506
- logger(ctx).warn(`[pi2dsh] /login is already registered by an earlier package in this host; this package's providers use that command (${error instanceof Error ? error.message : String(error)})`);
5940
+ state.shared.loginCommandRegistered = false;
5941
+ logger(ctx).warn(`[pi2dsh] /login could not be registered; provider logins remain available on the host's own sign-in surface (${error instanceof Error ? error.message : String(error)})`);
5507
5942
  }
5508
5943
  }
5509
5944
  /**
@@ -6279,9 +6714,9 @@ function normalizedPiCommandName(piName) {
6279
6714
  }
6280
6715
  function dshCommandName(ctx, state, piName) {
6281
6716
  const validName = normalizedPiCommandName(piName);
6282
- const name = commandNameForDshTui(validName, state.tuiSurfaces?.available === true || optionalService(ctx, "tuiScenes") !== void 0);
6717
+ const name = commandNameForTerminal(validName, state.tuiSurfaces?.nativeCommands);
6283
6718
  if (name !== piName) {
6284
- const reason = name === `pi-${validName}` ? `because dsh-TUI reserves /${validName} for its own local command (locals win on name collisions)` : "to satisfy DSH command naming";
6719
+ const reason = name === `pi-${validName}` ? `because the active terminal reserves /${validName} for its native command (host commands win on collisions)` : "to satisfy DSH command naming";
6285
6720
  logger(ctx).warn(`[pi2dsh] Pi command /${piName} registered as /${name} ${reason}`);
6286
6721
  }
6287
6722
  return name;
@@ -6767,6 +7202,12 @@ async function applyPiPackage(ctx, options) {
6767
7202
  const shared = sharedHostStateOf(ctx);
6768
7203
  const scopedOwner = options.ownerAgent ?? scopeOf(ctx);
6769
7204
  const ownerAgent = typeof scopedOwner === "object" && scopedOwner !== null ? scopedOwner : void 0;
7205
+ if (ownerAgent !== void 0 && options.hostAnchor !== true) {
7206
+ shared.childPackageTools ??= /* @__PURE__ */ new WeakMap();
7207
+ const byPackage = shared.childPackageTools.get(ownerAgent) ?? /* @__PURE__ */ new Map();
7208
+ byPackage.set(options.manifest.package.name, runtimeTools);
7209
+ shared.childPackageTools.set(ownerAgent, byPackage);
7210
+ }
6770
7211
  const state = {
6771
7212
  shared,
6772
7213
  packageName: options.manifest.package.name,
@@ -6829,9 +7270,17 @@ async function applyPiPackage(ctx, options) {
6829
7270
  publishedOAuthKeys: shared.publishedOAuthKeys,
6830
7271
  tuiSurfaces: void 0
6831
7272
  };
6832
- mountTuiSurfaceAdapter(ctx, state.packageName, (adapter) => {
7273
+ const surfaceInstanceKey = typeof ownerAgent?.id === "string" ? ownerAgent.id : void 0;
7274
+ const piTuiSurface = await createPiTuiExtensionSurfaceAdapter(ctx, state.packageName, surfaceInstanceKey);
7275
+ if (piTuiSurface !== void 0) {
7276
+ state.tuiSurfaces = piTuiSurface;
7277
+ ctx.effect(() => () => {
7278
+ piTuiSurface.dispose();
7279
+ if (state.tuiSurfaces === piTuiSurface) state.tuiSurfaces = void 0;
7280
+ });
7281
+ } else mountTuiSurfaceAdapter(ctx, state.packageName, (adapter) => {
6833
7282
  state.tuiSurfaces = adapter;
6834
- }, typeof ownerAgent?.id === "string" ? ownerAgent.id : void 0);
7283
+ }, surfaceInstanceKey);
6835
7284
  subscribeLifecycle(ctx, state);
6836
7285
  subscribeInterceptors(ctx, state);
6837
7286
  shared.providerBuiltins ??= /* @__PURE__ */ new Map();
@@ -6885,7 +7334,57 @@ async function applyPiPackage(ctx, options) {
6885
7334
  });
6886
7335
  }
6887
7336
  const browserSurfaces = state.shared.browserSurfaces ??= new BrowserSurfaces();
6888
- if (state.shared.browserSurfacesRouted !== true) state.shared.browserSurfacesRouted = registerBrowserSurfaceRoute(ctx, browserSurfaces);
7337
+ if (state.shared.browserSurfacesRouted !== true) {
7338
+ const sessionCwd = (session) => {
7339
+ const cwd = ctx.get("sessions")?.get?.(session)?.header?.cwd;
7340
+ if (typeof cwd === "string" && cwd.length > 0) return cwd;
7341
+ return session === "" ? homedir() : process.cwd();
7342
+ };
7343
+ const globalLayerPaths = (cwd) => new Set(piMcpConfigPaths(cwd).slice(0, 4));
7344
+ state.shared.browserSurfacesRouted = registerBrowserSurfaceRoute(ctx, browserSurfaces, {
7345
+ async mcpState(session) {
7346
+ const cwd = sessionCwd(session);
7347
+ const globals = globalLayerPaths(cwd);
7348
+ const { servers, sources } = collectPiMcpServers(cwd);
7349
+ return {
7350
+ cwd,
7351
+ sources,
7352
+ servers: [...servers.values()].map((server) => ({
7353
+ name: server.name,
7354
+ transport: server.transport,
7355
+ target: server.transport === "stdio" ? [server.command, ...server.args ?? []].filter(Boolean).join(" ") : server.url ?? "",
7356
+ disabled: server.disabled,
7357
+ sourcePath: server.sourcePath,
7358
+ layer: globals.has(server.sourcePath) ? "global" : "project",
7359
+ envKeys: Object.keys(server.env ?? {}),
7360
+ headerKeys: Object.keys(server.headers ?? {})
7361
+ }))
7362
+ };
7363
+ },
7364
+ async mcpAction(session, server, disabled, scope) {
7365
+ const cwd = sessionCwd(session);
7366
+ const { servers } = collectPiMcpServers(cwd);
7367
+ if (!servers.has(server)) throw new TypeError(`unknown MCP server ${JSON.stringify(server)}`);
7368
+ const overridePath = scope === "global" ? [...globalLayerPaths(cwd)][3] : join(cwd, ".pi", "mcp.json");
7369
+ let current = {};
7370
+ try {
7371
+ current = JSON.parse(await readFile(overridePath, "utf8"));
7372
+ } catch {}
7373
+ const mcpServers = typeof current.mcpServers === "object" && current.mcpServers !== null ? current.mcpServers : {};
7374
+ mcpServers[server] = {
7375
+ ...typeof mcpServers[server] === "object" && mcpServers[server] !== null ? mcpServers[server] : {},
7376
+ disabled
7377
+ };
7378
+ current.mcpServers = mcpServers;
7379
+ await mkdir(dirname(overridePath), { recursive: true });
7380
+ await writeFile(overridePath, `${JSON.stringify(current, null, 2)}\n`);
7381
+ return {
7382
+ ok: true,
7383
+ note: "run /reload (or restart the session) to apply"
7384
+ };
7385
+ }
7386
+ });
7387
+ }
6889
7388
  ctx.effect(() => browserSurfaces.trackCompletions(state.packageName ?? "pi", async (trigger, query) => {
6890
7389
  if (state.autocompleteProviders.length === 0) return [];
6891
7390
  let provider = { getSuggestions: async () => null };
@@ -6941,6 +7440,21 @@ async function applyPiPackage(ctx, options) {
6941
7440
  }
6942
7441
  return rendered;
6943
7442
  }));
7443
+ const runnerSessionId = ownerAgent === void 0 ? "" : String(agentSession(ownerAgent)?.id ?? "");
7444
+ if (runnerSessionId.length > 0) {
7445
+ const disposeRunner = browserSurfaces.registerCommandRunner(runnerSessionId, state.packageName, async (name, args) => {
7446
+ const live = state.commands.get(name);
7447
+ if (live === void 0) throw new Error(`${state.packageName} has no command ${JSON.stringify(name)}`);
7448
+ const commandContext = contextFor(ctx, state, ownerAgent, void 0, true, void 0, true);
7449
+ await runInPiRuntime(state, ownerAgent, async () => {
7450
+ await ensurePiSessionStarted(ctx, state, ownerAgent, void 0);
7451
+ await live.handler(args, commandContext);
7452
+ });
7453
+ const notices = commandContext.__notices;
7454
+ return notices.length > 0 ? notices.join("\n") : void 0;
7455
+ });
7456
+ ctx.effect(() => disposeRunner);
7457
+ }
6944
7458
  state.subagentSessionFactory = async (subagentOptions) => {
6945
7459
  const created = await createBridgedAgentSession(subagentHost(), subagentOptions);
6946
7460
  const parent = agentSession(currentAgent(state));
@@ -6995,6 +7509,21 @@ async function applyPiPackage(ctx, options) {
6995
7509
  return state.bridge.readonlySessionManager(session, cwd);
6996
7510
  },
6997
7511
  resumeSessionIdFor: (file) => state.bridge.sessionIdOfArchiveFile(file),
7512
+ mountChildExtensions: async (childAgent, loader) => {
7513
+ const shared = sharedHostStateOf(ctx);
7514
+ const catalog = shared.childExtensions;
7515
+ if (catalog === void 0) return [];
7516
+ const { names, failures } = resolveChildExtensionPackages(loader, catalog);
7517
+ if (names.length === 0) return failures;
7518
+ const mountFailures = await catalog.mount(childAgent, names);
7519
+ const attachable = loader;
7520
+ if (typeof attachable?.attachChildToolResolver === "function") attachable.attachChildToolResolver((path) => {
7521
+ const packageName = catalog.packageByEntryPath.get(resolve(path));
7522
+ if (packageName === void 0) return void 0;
7523
+ return shared.childPackageTools?.get(childAgent)?.get(packageName);
7524
+ });
7525
+ return [...failures, ...mountFailures];
7526
+ },
6998
7527
  sessionGoneFromPersistence: async (sessionId) => {
6999
7528
  const persistence = optionalService(ctx, "sessionPersistence");
7000
7529
  if (typeof persistence?.list !== "function") return void 0;
@@ -7063,6 +7592,7 @@ const runtimeInternals = {
7063
7592
  currentPiModel,
7064
7593
  lastRequestRouteOf,
7065
7594
  resolveCallerRoute,
7595
+ resolveChildExtensionPackages,
7066
7596
  dshToPiContent,
7067
7597
  expandPrompt,
7068
7598
  isKnownImageTool,
@@ -7073,6 +7603,6 @@ const runtimeInternals = {
7073
7603
  textBlocks
7074
7604
  };
7075
7605
  //#endregion
7076
- export { registerVisionCompanions as a, overlayProviderConfig as i, mergeProviderRegistration as n, runtimeInternals as o, normalizeToolSchema as r, applyPiPackage as t };
7606
+ export { overlayProviderConfig as a, runtimeInternals as c, normalizeToolSchema as i, getSharedChildExtensionCatalog as n, registerChildExtensionCatalog as o, mergeProviderRegistration as r, registerVisionCompanions as s, applyPiPackage as t };
7077
7607
 
7078
- //# sourceMappingURL=runtime-CW52OWvW.mjs.map
7608
+ //# sourceMappingURL=runtime-CGDq6LKi.mjs.map