pi2dsh 0.20.0 → 0.22.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.
@@ -963,6 +963,7 @@ async function createBridgedAgentSession(host, options) {
963
963
  const providedManager = options.sessionManager;
964
964
  const archiveFile = typeof providedManager?.getSessionFile === "function" ? providedManager.getSessionFile() : void 0;
965
965
  const resumeSessionId = host.resumeSessionIdFor?.(archiveFile);
966
+ const sideline = providedManager !== void 0 && archiveFile === void 0;
966
967
  subagentSerial += 1;
967
968
  const sessionId = `pi2dsh-sub-${Date.now().toString(36)}-${subagentSerial}`;
968
969
  let handle;
@@ -1076,7 +1077,7 @@ async function createBridgedAgentSession(host, options) {
1076
1077
  version: 2,
1077
1078
  mode: "continuable",
1078
1079
  provider: "pi2dsh",
1079
- label: childLabel(options.label, host.packageName)
1080
+ label: `${sideline ? "Side: " : ""}${childLabel(options.label, host.packageName)}`
1080
1081
  });
1081
1082
  } catch (error) {
1082
1083
  host.cordis.logger?.warn?.(`[pi2dsh] child session could not record its subagent identity: ${error instanceof Error ? error.message : String(error)}`);
@@ -1139,7 +1140,16 @@ function messageText(message) {
1139
1140
  */
1140
1141
  var BrowserSurfaces = class {
1141
1142
  #byParent = /* @__PURE__ */ new Map();
1143
+ #commandRunners = /* @__PURE__ */ new Map();
1142
1144
  #surfaces = /* @__PURE__ */ new Map();
1145
+ /**
1146
+ * Retained components from `(tui, theme) => Component` factories, keyed by
1147
+ * their slot (session + package + widget key). Retained because the driver
1148
+ * handed to the factory must stay LIVE: requestRender re-renders this
1149
+ * component into its slot for as long as the package holds the handle.
1150
+ * Replacing or clearing the slot disposes the previous component.
1151
+ */
1152
+ #factoryComponents = /* @__PURE__ */ new Map();
1143
1153
  #entrySources = /* @__PURE__ */ new Map();
1144
1154
  #draftRequests = /* @__PURE__ */ new Map();
1145
1155
  #liveDrafts = /* @__PURE__ */ new Map();
@@ -1267,6 +1277,32 @@ var BrowserSurfaces = class {
1267
1277
  };
1268
1278
  }
1269
1279
  /**
1280
+ * Register the executor for one package's Pi commands on one session, so
1281
+ * product UI (a side-chat window's input, its action buttons) can run the
1282
+ * package's OWN command handlers — the same code path the composer takes.
1283
+ * Generic by construction: the runner closure carries the package runtime;
1284
+ * this registry only routes (session, package) to it.
1285
+ * @param sessionId - the root session the package is mounted for.
1286
+ * @param packageName - the Pi package owning the commands.
1287
+ * @param runner - executes one registered Pi command by its Pi name.
1288
+ * @returns a disposer that unregisters the runner.
1289
+ */
1290
+ registerCommandRunner(sessionId, packageName, runner) {
1291
+ const runners = this.#commandRunners.get(sessionId) ?? /* @__PURE__ */ new Map();
1292
+ runners.set(packageName, runner);
1293
+ this.#commandRunners.set(sessionId, runners);
1294
+ return () => {
1295
+ const live = this.#commandRunners.get(sessionId);
1296
+ if (live === void 0) return;
1297
+ if (live.get(packageName) === runner) live.delete(packageName);
1298
+ if (live.size === 0) this.#commandRunners.delete(sessionId);
1299
+ };
1300
+ }
1301
+ /** The registered runner for (session, package), when one is mounted. */
1302
+ commandRunner(sessionId, packageName) {
1303
+ return this.#commandRunners.get(sessionId)?.get(packageName);
1304
+ }
1305
+ /**
1270
1306
  * The panel's view of one parent session.
1271
1307
  * @param parentSessionId - session the browser is currently showing.
1272
1308
  * @returns every side thread opened under it, oldest first.
@@ -1314,11 +1350,56 @@ var BrowserSurfaces = class {
1314
1350
  setWidget(sessionId, packageName, key, content, theme) {
1315
1351
  if (sessionId.length === 0) return;
1316
1352
  const view = this.#view(sessionId, packageName);
1353
+ const slot = `${sessionId}${packageName ?? "pi"}${key}`;
1354
+ this.#factoryComponents.get(slot)?.dispose?.();
1355
+ this.#factoryComponents.delete(slot);
1356
+ if (typeof content === "function") {
1357
+ this.#mountFactory(slot, content, theme, (text) => {
1358
+ const live = this.#view(sessionId, packageName);
1359
+ if (text === void 0) delete live.widgets[key];
1360
+ else live.widgets[key] = text;
1361
+ });
1362
+ return;
1363
+ }
1317
1364
  const text = surfaceText(content, theme);
1318
1365
  if (text === void 0) delete view.widgets[key];
1319
1366
  else view.widgets[key] = text;
1320
1367
  }
1321
1368
  /**
1369
+ * Mount one `(tui, theme) => Component` factory: run it with a live driver,
1370
+ * render the retained component now, and re-render it into the same slot on
1371
+ * every requestRender. A factory or render that throws leaves the slot
1372
+ * empty — the package's own bug stays its own — but a RETAINED handle keeps
1373
+ * working for the component's lifetime.
1374
+ * @param slot - identity for the retained component (dispose-on-replace).
1375
+ * @param factory - the package's factory, exactly as passed to setWidget.
1376
+ * @param theme - the bridge's headless theme.
1377
+ * @param store - writes rendered text (or undefined to clear) into the slot.
1378
+ */
1379
+ #mountFactory(slot, factory, theme, store) {
1380
+ let component;
1381
+ const render = () => {
1382
+ if (component === void 0) return;
1383
+ if (this.#factoryComponents.get(slot) !== component) return;
1384
+ store(surfaceText(component, theme));
1385
+ };
1386
+ const driver = { requestRender: () => {
1387
+ render();
1388
+ } };
1389
+ try {
1390
+ component = factory(driver, theme);
1391
+ } catch {
1392
+ store(void 0);
1393
+ return;
1394
+ }
1395
+ if (component === null || typeof component?.render !== "function") {
1396
+ store(void 0);
1397
+ return;
1398
+ }
1399
+ this.#factoryComponents.set(slot, component);
1400
+ render();
1401
+ }
1402
+ /**
1322
1403
  * Record one simple presentation call (working chrome, title, header,
1323
1404
  * footer). The value is a string, a Pi component, a working-indicator
1324
1405
  * options object, or a header/footer factory; each renders to text here so
@@ -1634,6 +1715,34 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1634
1715
  response.end(JSON.stringify(outcome ?? { ok: true }));
1635
1716
  return;
1636
1717
  }
1718
+ if (url.pathname === "/pi2dsh/pi-command" && method === "POST") {
1719
+ const chunks = [];
1720
+ const raw = await new Promise((settle) => {
1721
+ const stream = req;
1722
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1723
+ stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1724
+ });
1725
+ try {
1726
+ const payload = JSON.parse(raw || "{}");
1727
+ if (typeof payload.session !== "string" || typeof payload.package !== "string" || typeof payload.command !== "string") throw new TypeError("pi-command needs { session, package, command }");
1728
+ const runner = registry.commandRunner(payload.session, payload.package);
1729
+ if (runner === void 0) {
1730
+ response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
1731
+ response.end(JSON.stringify({ error: `no ${payload.package} command runner mounted for this session` }));
1732
+ return;
1733
+ }
1734
+ const notice = await runner(payload.command, typeof payload.args === "string" ? payload.args : "");
1735
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1736
+ response.end(JSON.stringify({
1737
+ ok: true,
1738
+ ...notice === void 0 ? {} : { notice }
1739
+ }));
1740
+ } catch (error) {
1741
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" });
1742
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1743
+ }
1744
+ return;
1745
+ }
1637
1746
  if (method === "POST" && (url.pathname === "/pi2dsh/scene-input" || url.pathname === "/pi2dsh/scene-close")) {
1638
1747
  const chunks = [];
1639
1748
  const body = await new Promise((settle) => {
@@ -4174,7 +4283,7 @@ function projectDialogPrompt(title, supplementalDetail, linkMode) {
4174
4283
  ...detail.length === 0 ? {} : { detail }
4175
4284
  };
4176
4285
  }
4177
- function contextFor(ctx, state, agent, signal, command = false, sessionOverride) {
4286
+ function contextFor(ctx, state, agent, signal, command = false, sessionOverride, headlessUi = false) {
4178
4287
  const notices = [];
4179
4288
  const userQuestions = optionalService(ctx, "userQuestions");
4180
4289
  const dialogLinkMode = optionalService(ctx, "tuiScenes") === void 0 ? "markdown" : "terminal";
@@ -4240,6 +4349,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
4240
4349
  setTitle: (title) => putSurface(ctx, state, agent, "title", title),
4241
4350
  custom: async (factory, options) => {
4242
4351
  if (typeof factory !== "function") return void 0;
4352
+ if (headlessUi) return void 0;
4243
4353
  const surfaces = state.tuiSurfaces;
4244
4354
  if (surfaces !== void 0) return surfaces.custom(factory, state.theme, getKeybindings(), options);
4245
4355
  const browser = state.shared.browserSurfaces;
@@ -4456,7 +4566,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
4456
4566
  scopedModels: [],
4457
4567
  thinkingLevel: thinkingLevelOf(state, agent),
4458
4568
  isIdle: () => command,
4459
- isProjectTrusted: () => false,
4569
+ isProjectTrusted: () => agent !== void 0 && contextCwd.length > 0,
4460
4570
  signal,
4461
4571
  abort: () => {
4462
4572
  const target = agent;
@@ -5872,16 +5982,16 @@ function ensureLoginCommand(ctx, state) {
5872
5982
  logger(ctx).debug("[pi2dsh] native terminal owns /login and the authorization service is composed; Pi OAuth flows are available through it");
5873
5983
  return;
5874
5984
  }
5875
- const scoped = scopeOf(ctx);
5876
- const owner = typeof scoped === "object" && scoped !== null ? scoped : ctx;
5877
- const registered = state.shared.loginRegistered ??= /* @__PURE__ */ new WeakSet();
5878
- if (registered.has(owner)) return;
5879
- registered.add(owner);
5985
+ if (state.shared.loginCommandRegistered === true) return;
5986
+ state.shared.loginCommandRegistered = true;
5880
5987
  try {
5881
5988
  registerLoginCommand(ctx, state);
5989
+ ctx.effect(() => () => {
5990
+ state.shared.loginCommandRegistered = false;
5991
+ });
5882
5992
  } catch (error) {
5883
- registered.delete(owner);
5884
- 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)})`);
5993
+ state.shared.loginCommandRegistered = false;
5994
+ 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)})`);
5885
5995
  }
5886
5996
  }
5887
5997
  /**
@@ -7277,7 +7387,7 @@ async function applyPiPackage(ctx, options) {
7277
7387
  });
7278
7388
  }
7279
7389
  const browserSurfaces = state.shared.browserSurfaces ??= new BrowserSurfaces();
7280
- if (state.shared.browserSurfacesRouted !== true) {
7390
+ if (options.config?.browserPresentation === true && state.shared.browserSurfacesRouted !== true) {
7281
7391
  const sessionCwd = (session) => {
7282
7392
  const cwd = ctx.get("sessions")?.get?.(session)?.header?.cwd;
7283
7393
  if (typeof cwd === "string" && cwd.length > 0) return cwd;
@@ -7383,6 +7493,21 @@ async function applyPiPackage(ctx, options) {
7383
7493
  }
7384
7494
  return rendered;
7385
7495
  }));
7496
+ const runnerSessionId = ownerAgent === void 0 ? "" : String(agentSession(ownerAgent)?.id ?? "");
7497
+ if (runnerSessionId.length > 0) {
7498
+ const disposeRunner = browserSurfaces.registerCommandRunner(runnerSessionId, state.packageName, async (name, args) => {
7499
+ const live = state.commands.get(name);
7500
+ if (live === void 0) throw new Error(`${state.packageName} has no command ${JSON.stringify(name)}`);
7501
+ const commandContext = contextFor(ctx, state, ownerAgent, void 0, true, void 0, true);
7502
+ await runInPiRuntime(state, ownerAgent, async () => {
7503
+ await ensurePiSessionStarted(ctx, state, ownerAgent, void 0);
7504
+ await live.handler(args, commandContext);
7505
+ });
7506
+ const notices = commandContext.__notices;
7507
+ return notices.length > 0 ? notices.join("\n") : void 0;
7508
+ });
7509
+ ctx.effect(() => disposeRunner);
7510
+ }
7386
7511
  state.subagentSessionFactory = async (subagentOptions) => {
7387
7512
  const created = await createBridgedAgentSession(subagentHost(), subagentOptions);
7388
7513
  const parent = agentSession(currentAgent(state));
@@ -7533,4 +7658,4 @@ const runtimeInternals = {
7533
7658
  //#endregion
7534
7659
  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 };
7535
7660
 
7536
- //# sourceMappingURL=runtime-CBZcd1uU.mjs.map
7661
+ //# sourceMappingURL=runtime-BVrvJuiO.mjs.map