pi2dsh 0.23.0 → 0.24.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.
@@ -1165,102 +1165,6 @@ var BrowserSurfaces = class {
1165
1165
  imageToolNames() {
1166
1166
  return [...this.#imageToolNames];
1167
1167
  }
1168
- #scene;
1169
- #sceneRevision = 0;
1170
- #sceneWidth = 100;
1171
- openScene(packageName, factory, theme, keybindings, options) {
1172
- this.#finishScene(void 0);
1173
- return new Promise((resolve, reject) => {
1174
- const run = {
1175
- package: packageName,
1176
- component: void 0,
1177
- options,
1178
- resolve,
1179
- reject
1180
- };
1181
- this.#scene = run;
1182
- const driver = { requestRender: () => this.#invalidateScene() };
1183
- const done = (value) => {
1184
- if (this.#scene !== run) return;
1185
- this.#finishScene(value);
1186
- };
1187
- Promise.resolve().then(() => factory(driver, theme, keybindings, done)).then((component) => {
1188
- if (this.#scene !== run) {
1189
- component.dispose?.();
1190
- return;
1191
- }
1192
- if (typeof component?.render !== "function") {
1193
- this.#scene = void 0;
1194
- this.#invalidateScene();
1195
- console.warn(`[pi2dsh] web scene for ${packageName}: component lacks render(width)`);
1196
- reject(/* @__PURE__ */ new TypeError("Pi custom component must implement render(width)"));
1197
- return;
1198
- }
1199
- run.component = component;
1200
- this.#invalidateScene();
1201
- }, (error) => {
1202
- if (this.#scene !== run) return;
1203
- this.#scene = void 0;
1204
- this.#invalidateScene();
1205
- console.warn(`[pi2dsh] web scene for ${packageName}: component factory failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1206
- reject(error);
1207
- });
1208
- });
1209
- }
1210
- /** What the browser paints: the live component's frame, or a closed marker. */
1211
- sceneSnapshot() {
1212
- const scene = this.#scene;
1213
- if (scene?.component === void 0) return {
1214
- open: false,
1215
- revision: this.#sceneRevision
1216
- };
1217
- const requested = (typeof scene.options?.overlayOptions === "function" ? scene.options.overlayOptions() : scene.options?.overlayOptions)?.width;
1218
- const width = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(this.#sceneWidth, Math.floor(requested)) : this.#sceneWidth;
1219
- let lines;
1220
- try {
1221
- lines = scene.component.render(width);
1222
- } catch (error) {
1223
- const failed = this.#scene;
1224
- this.#scene = void 0;
1225
- this.#invalidateScene();
1226
- console.warn(`[pi2dsh] web scene for ${scene.package}: render(width) failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1227
- failed?.reject(error);
1228
- return {
1229
- open: false,
1230
- revision: this.#sceneRevision
1231
- };
1232
- }
1233
- return {
1234
- open: true,
1235
- package: scene.package,
1236
- revision: this.#sceneRevision,
1237
- lines
1238
- };
1239
- }
1240
- /** Raw terminal input from the browser keyboard, verbatim into the component. */
1241
- sceneInput(sequence, width) {
1242
- if (typeof width === "number" && Number.isFinite(width)) this.#sceneWidth = Math.max(20, Math.min(400, Math.floor(width)));
1243
- const component = this.#scene?.component;
1244
- if (component === void 0 || sequence.length === 0) return;
1245
- component.handleInput?.(sequence);
1246
- component.invalidate?.();
1247
- this.#invalidateScene();
1248
- }
1249
- /** The browser dismissed the overlay: resolve the Pi caller with undefined. */
1250
- closeScene() {
1251
- this.#finishScene(void 0);
1252
- }
1253
- #finishScene(value) {
1254
- const scene = this.#scene;
1255
- if (scene === void 0) return;
1256
- this.#scene = void 0;
1257
- scene.component?.dispose?.();
1258
- this.#invalidateScene();
1259
- scene.resolve(value);
1260
- }
1261
- #invalidateScene() {
1262
- this.#sceneRevision += 1;
1263
- }
1264
1168
  /**
1265
1169
  * Track one child session under its parent.
1266
1170
  * @param parentSessionId - the session the panel floats over.
@@ -1300,9 +1204,20 @@ var BrowserSurfaces = class {
1300
1204
  if (live.size === 0) this.#commandRunners.delete(sessionId);
1301
1205
  };
1302
1206
  }
1303
- /** The registered runner for (session, package), when one is mounted. */
1207
+ /**
1208
+ * The registered runner for (session, package), when one is mounted.
1209
+ *
1210
+ * An empty sessionId means "any session where the package is mounted":
1211
+ * session-free product UI (a Settings page) invoking a package command
1212
+ * whose effect is global state — the handler itself is the package's own
1213
+ * either way, and every mounted copy routes to the same package storage.
1214
+ */
1304
1215
  commandRunner(sessionId, packageName) {
1305
- return this.#commandRunners.get(sessionId)?.get(packageName);
1216
+ if (sessionId !== "") return this.#commandRunners.get(sessionId)?.get(packageName);
1217
+ for (const runners of this.#commandRunners.values()) {
1218
+ const runner = runners.get(packageName);
1219
+ if (runner !== void 0) return runner;
1220
+ }
1306
1221
  }
1307
1222
  /**
1308
1223
  * The panel's view of one parent session.
@@ -1718,6 +1633,46 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1718
1633
  response.end(JSON.stringify(outcome ?? { ok: true }));
1719
1634
  return;
1720
1635
  }
1636
+ if (url.pathname === "/pi2dsh/login-state" && (method === "GET" || method === "HEAD")) {
1637
+ if (hooks?.loginState === void 0) {
1638
+ response.writeHead(404);
1639
+ response.end();
1640
+ return;
1641
+ }
1642
+ const body = JSON.stringify(await hooks.loginState());
1643
+ response.writeHead(200, {
1644
+ "content-type": "application/json; charset=utf-8",
1645
+ "cache-control": "no-store"
1646
+ });
1647
+ response.end(method === "HEAD" ? void 0 : body);
1648
+ return;
1649
+ }
1650
+ if (url.pathname === "/pi2dsh/login-action" && method === "POST") {
1651
+ if (hooks?.loginAction === void 0) {
1652
+ response.writeHead(404);
1653
+ response.end();
1654
+ return;
1655
+ }
1656
+ const chunks = [];
1657
+ const raw = await new Promise((settle) => {
1658
+ const stream = req;
1659
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1660
+ stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1661
+ });
1662
+ let outcome;
1663
+ try {
1664
+ const payload = JSON.parse(raw || "{}");
1665
+ if (typeof payload.action !== "string") throw new TypeError("login-action needs { action }");
1666
+ outcome = await hooks.loginAction(payload.action, typeof payload.provider === "string" ? payload.provider : "", typeof payload.value === "string" ? payload.value : "");
1667
+ } catch (error) {
1668
+ response.writeHead(400, { "content-type": "application/json; charset=utf-8" });
1669
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1670
+ return;
1671
+ }
1672
+ response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1673
+ response.end(JSON.stringify(outcome ?? { ok: true }));
1674
+ return;
1675
+ }
1721
1676
  if (url.pathname === "/pi2dsh/pi-command" && method === "POST") {
1722
1677
  const chunks = [];
1723
1678
  const raw = await new Promise((settle) => {
@@ -1746,22 +1701,6 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1746
1701
  }
1747
1702
  return;
1748
1703
  }
1749
- if (method === "POST" && (url.pathname === "/pi2dsh/scene-input" || url.pathname === "/pi2dsh/scene-close")) {
1750
- const chunks = [];
1751
- const body = await new Promise((settle) => {
1752
- const stream = req;
1753
- stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1754
- stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1755
- });
1756
- if (url.pathname === "/pi2dsh/scene-close") registry.closeScene();
1757
- else try {
1758
- const payload = JSON.parse(body || "{}");
1759
- if (typeof payload.sequence === "string") registry.sceneInput(payload.sequence, typeof payload.width === "number" ? payload.width : void 0);
1760
- } catch {}
1761
- response.writeHead(204);
1762
- response.end();
1763
- return;
1764
- }
1765
1704
  if (method !== "GET" && method !== "HEAD") {
1766
1705
  response.writeHead(405);
1767
1706
  response.end();
@@ -1807,13 +1746,11 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1807
1746
  const body = JSON.stringify(session === "" ? {
1808
1747
  threads: [],
1809
1748
  surfaces: [],
1810
- entries: [],
1811
- scene: registry.sceneSnapshot()
1749
+ entries: []
1812
1750
  } : {
1813
1751
  threads: registry.snapshot(session),
1814
1752
  surfaces: registry.surfaces(session),
1815
1753
  entries: registry.entries(session),
1816
- scene: registry.sceneSnapshot(),
1817
1754
  ...registry.draftRequest(session) === void 0 ? {} : { draft: registry.draftRequest(session) }
1818
1755
  });
1819
1756
  response.writeHead(200, {
@@ -4358,8 +4295,6 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4358
4295
  if (headlessUi) return void 0;
4359
4296
  const surfaces = state.tuiSurfaces;
4360
4297
  if (surfaces !== void 0) return surfaces.custom(factory, state.theme, getKeybindings(), options);
4361
- const browser = state.shared.browserSurfaces;
4362
- if (browser !== void 0 && state.shared.browserSurfacesRouted === true) return browser.openScene(state.packageName, factory, state.theme, getKeybindings(), options);
4363
4298
  },
4364
4299
  pasteToEditor(text) {
4365
4300
  const surfaces = state.shared.browserSurfaces;
@@ -4556,8 +4491,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4556
4491
  },
4557
4492
  ui,
4558
4493
  get mode() {
4559
- if (state.tuiSurfaces?.available === true) return "tui";
4560
- return state.shared.browserSurfaces !== void 0 && state.shared.browserSurfacesRouted === true ? "tui" : "rpc";
4494
+ return state.tuiSurfaces?.available === true ? "tui" : "rpc";
4561
4495
  },
4562
4496
  get hasUI() {
4563
4497
  return state.tuiSurfaces?.available === true || humanAnswererAvailable(userQuestions, agent, state.shared);
@@ -6053,6 +5987,37 @@ async function supersedeActiveLogin(state) {
6053
5987
  * @param config - the provider config, whose oauth block made it loginable.
6054
5988
  * @returns whether this call put a route in place.
6055
5989
  */
5990
+ /**
5991
+ * Mount-time route restore that tolerates composition order.
5992
+ *
5993
+ * The credentials service (credentials-local) can compose AFTER a
5994
+ * zero-package profile's host mount — probing immediately loses that race and
5995
+ * a stored login silently produces no route: an engine-only profile with a
5996
+ * valid auth.json hit MISSING_CREDENTIAL on every turn, while installing any
5997
+ * Pi package shifted mount timing enough to mask the bug (2026-08-30,
5998
+ * community/full-audit-work/zero-package-oauth-bug.md). When the service is
5999
+ * already there the restore stays awaited — that is mount readiness, see the
6000
+ * caller's comment. Otherwise the official inject seam re-runs it the moment
6001
+ * the service composes, the same pattern maybeProjectAuthorizationFlow uses.
6002
+ */
6003
+ async function restoreLoggedInRouteWhenReady(ctx, state, name, config) {
6004
+ if (optionalService(ctx, "credentials") !== void 0) {
6005
+ await ensureLoggedInProviderRoute(ctx, state, name, config);
6006
+ return;
6007
+ }
6008
+ const shared = state.shared;
6009
+ shared.routeRestoreArmedIds ??= /* @__PURE__ */ new Set();
6010
+ if (shared.routeRestoreArmedIds.has(name)) return;
6011
+ const inject = ctx.inject;
6012
+ if (typeof inject !== "function") {
6013
+ await ensureLoggedInProviderRoute(ctx, state, name, config);
6014
+ return;
6015
+ }
6016
+ shared.routeRestoreArmedIds.add(name);
6017
+ inject.call(ctx, ["credentials"], (scope) => {
6018
+ ensureLoggedInProviderRoute(scope, state, name, config).catch((error) => logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`));
6019
+ });
6020
+ }
6056
6021
  async function ensureLoggedInProviderRoute(ctx, state, name, config) {
6057
6022
  if (!providerSupportsOAuth(config)) return false;
6058
6023
  if (providerCarriesTransport(config) || state.providerRouteDisposers.has(name)) return false;
@@ -6364,11 +6329,16 @@ async function projectAuthorizationFlow(scope, state, providerId) {
6364
6329
  throw error;
6365
6330
  }
6366
6331
  },
6367
- deviceCode: async (title, detail) => {
6332
+ deviceCode: (title, detail, signal) => {
6368
6333
  session.notify({
6369
6334
  message: String(title),
6370
6335
  code: String(detail)
6371
6336
  });
6337
+ return new Promise((resolve) => {
6338
+ if (!(signal instanceof AbortSignal)) return;
6339
+ if (signal.aborted) resolve();
6340
+ else signal.addEventListener("abort", () => resolve(), { once: true });
6341
+ });
6372
6342
  }
6373
6343
  };
6374
6344
  const canonical = canonicalOf();
@@ -7365,7 +7335,7 @@ async function applyPiPackage(ctx, options) {
7365
7335
  }
7366
7336
  ensureLoginCommand(ctx, state);
7367
7337
  for (const [name, config] of [...state.providers]) try {
7368
- await ensureLoggedInProviderRoute(ctx, state, name, config);
7338
+ await restoreLoggedInRouteWhenReady(ctx, state, name, config);
7369
7339
  } catch (error) {
7370
7340
  logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`);
7371
7341
  }
@@ -7448,6 +7418,131 @@ async function applyPiPackage(ctx, options) {
7448
7418
  ok: true,
7449
7419
  note: "run /reload (or restart the session) to apply"
7450
7420
  };
7421
+ },
7422
+ async loginState() {
7423
+ const flow = state.shared.browserLogin;
7424
+ return {
7425
+ providers: await Promise.all([...state.providers.entries()].filter(([, config]) => providerSupportsOAuth(config)).map(async ([id, config]) => ({
7426
+ id,
7427
+ name: config.oauth?.name ?? id,
7428
+ signedIn: await storedOAuthCredential(oauthStoreOf(state), id).then((c) => c !== void 0, () => false)
7429
+ }))),
7430
+ ...flow === void 0 ? {} : { flow: {
7431
+ provider: flow.provider,
7432
+ providerName: flow.providerName,
7433
+ notices: flow.notices,
7434
+ ...flow.question === void 0 ? {} : { question: {
7435
+ id: flow.question.id,
7436
+ kind: flow.question.kind,
7437
+ title: flow.question.title,
7438
+ ...flow.question.placeholder === void 0 ? {} : { placeholder: flow.question.placeholder },
7439
+ ...flow.question.options === void 0 ? {} : { options: flow.question.options }
7440
+ } },
7441
+ ...flow.done === void 0 ? {} : { done: flow.done }
7442
+ } }
7443
+ };
7444
+ },
7445
+ async loginAction(action, provider, value) {
7446
+ const shared = state.shared;
7447
+ if (action === "dismiss") {
7448
+ shared.browserLogin = void 0;
7449
+ return { ok: true };
7450
+ }
7451
+ if (action === "answer") {
7452
+ const flow = shared.browserLogin;
7453
+ if (flow?.answer === void 0) throw new TypeError("no login question is waiting for an answer");
7454
+ const settle = flow.answer;
7455
+ flow.answer = void 0;
7456
+ delete flow.question;
7457
+ settle(value);
7458
+ return { ok: true };
7459
+ }
7460
+ if (action === "cancel") {
7461
+ const cancelled = await supersedeActiveLogin(state);
7462
+ const flow = shared.browserLogin;
7463
+ if (flow !== void 0 && flow.done === void 0) flow.done = {
7464
+ ok: false,
7465
+ summary: "Login cancelled"
7466
+ };
7467
+ return {
7468
+ ok: true,
7469
+ ...cancelled === void 0 ? {} : { cancelled }
7470
+ };
7471
+ }
7472
+ const config = state.providers.get(provider);
7473
+ if (config === void 0 || !providerSupportsOAuth(config)) throw new TypeError(`unknown OAuth provider ${JSON.stringify(provider)}`);
7474
+ const providerName = config.oauth?.name ?? provider;
7475
+ if (action === "signout") {
7476
+ await oauthStoreOf(state).delete(provider);
7477
+ if (!providerCarriesTransport(config)) retireSharedProviderRoute(shared, provider);
7478
+ return {
7479
+ ok: true,
7480
+ signedOut: provider
7481
+ };
7482
+ }
7483
+ if (action !== "begin") throw new TypeError(`unknown login action ${JSON.stringify(action)}`);
7484
+ let questionSeq = 0;
7485
+ const flow = {
7486
+ provider,
7487
+ providerName,
7488
+ notices: []
7489
+ };
7490
+ shared.browserLogin = flow;
7491
+ const ask = (kind, title, extras, signal) => new Promise((resolve) => {
7492
+ const id = ++questionSeq;
7493
+ flow.question = {
7494
+ id,
7495
+ kind,
7496
+ title,
7497
+ ...extras
7498
+ };
7499
+ flow.answer = (answered) => {
7500
+ resolve(answered);
7501
+ };
7502
+ const withdraw = () => {
7503
+ if (flow.question?.id !== id) return;
7504
+ delete flow.question;
7505
+ flow.answer = void 0;
7506
+ resolve(void 0);
7507
+ };
7508
+ if (signal !== void 0) {
7509
+ if (signal.aborted) withdraw();
7510
+ else signal.addEventListener("abort", withdraw, { once: true });
7511
+ }
7512
+ });
7513
+ runProviderLogin(ctx, state, provider, config, {
7514
+ notify: (message) => {
7515
+ flow.notices.push({ message: String(message) });
7516
+ },
7517
+ input: (title, placeholder, signal) => ask("input", String(title), typeof placeholder === "string" && placeholder.length > 0 ? { placeholder } : {}, signal),
7518
+ select: (title, options, signal) => ask("select", String(title), { options: options.map((option) => String(option)) }, signal),
7519
+ deviceCode: (title, detail, signal) => {
7520
+ const code = /`([^`]+)`/u.exec(String(detail))?.[1];
7521
+ flow.notices.push({
7522
+ message: String(title),
7523
+ ...code === void 0 ? {} : { code }
7524
+ });
7525
+ return new Promise((resolve) => {
7526
+ if (!(signal instanceof AbortSignal)) return;
7527
+ if (signal.aborted) resolve();
7528
+ else signal.addEventListener("abort", () => resolve(), { once: true });
7529
+ });
7530
+ }
7531
+ }).then((summary) => {
7532
+ if (shared.browserLogin === flow) flow.done = {
7533
+ ok: true,
7534
+ summary
7535
+ };
7536
+ }, (error) => {
7537
+ if (shared.browserLogin === flow) flow.done = {
7538
+ ok: false,
7539
+ summary: error instanceof Error ? error.message : String(error)
7540
+ };
7541
+ });
7542
+ return {
7543
+ ok: true,
7544
+ started: provider
7545
+ };
7451
7546
  }
7452
7547
  });
7453
7548
  }
@@ -7671,4 +7766,4 @@ const runtimeInternals = {
7671
7766
  //#endregion
7672
7767
  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 };
7673
7768
 
7674
- //# sourceMappingURL=runtime-Dnk9mhrc.mjs.map
7769
+ //# sourceMappingURL=runtime-BiHMcD_8.mjs.map