pi2dsh 0.22.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.
@@ -1139,6 +1139,8 @@ function messageText(message) {
1139
1139
  * is one surface, however many Pi packages contribute threads to it.
1140
1140
  */
1141
1141
  var BrowserSurfaces = class {
1142
+ /** Last time a browser client touched the /pi2dsh route (ms epoch). */
1143
+ lastClientContactMs;
1142
1144
  #byParent = /* @__PURE__ */ new Map();
1143
1145
  #commandRunners = /* @__PURE__ */ new Map();
1144
1146
  #surfaces = /* @__PURE__ */ new Map();
@@ -1163,102 +1165,6 @@ var BrowserSurfaces = class {
1163
1165
  imageToolNames() {
1164
1166
  return [...this.#imageToolNames];
1165
1167
  }
1166
- #scene;
1167
- #sceneRevision = 0;
1168
- #sceneWidth = 100;
1169
- openScene(packageName, factory, theme, keybindings, options) {
1170
- this.#finishScene(void 0);
1171
- return new Promise((resolve, reject) => {
1172
- const run = {
1173
- package: packageName,
1174
- component: void 0,
1175
- options,
1176
- resolve,
1177
- reject
1178
- };
1179
- this.#scene = run;
1180
- const driver = { requestRender: () => this.#invalidateScene() };
1181
- const done = (value) => {
1182
- if (this.#scene !== run) return;
1183
- this.#finishScene(value);
1184
- };
1185
- Promise.resolve().then(() => factory(driver, theme, keybindings, done)).then((component) => {
1186
- if (this.#scene !== run) {
1187
- component.dispose?.();
1188
- return;
1189
- }
1190
- if (typeof component?.render !== "function") {
1191
- this.#scene = void 0;
1192
- this.#invalidateScene();
1193
- console.warn(`[pi2dsh] web scene for ${packageName}: component lacks render(width)`);
1194
- reject(/* @__PURE__ */ new TypeError("Pi custom component must implement render(width)"));
1195
- return;
1196
- }
1197
- run.component = component;
1198
- this.#invalidateScene();
1199
- }, (error) => {
1200
- if (this.#scene !== run) return;
1201
- this.#scene = void 0;
1202
- this.#invalidateScene();
1203
- console.warn(`[pi2dsh] web scene for ${packageName}: component factory failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1204
- reject(error);
1205
- });
1206
- });
1207
- }
1208
- /** What the browser paints: the live component's frame, or a closed marker. */
1209
- sceneSnapshot() {
1210
- const scene = this.#scene;
1211
- if (scene?.component === void 0) return {
1212
- open: false,
1213
- revision: this.#sceneRevision
1214
- };
1215
- const requested = (typeof scene.options?.overlayOptions === "function" ? scene.options.overlayOptions() : scene.options?.overlayOptions)?.width;
1216
- const width = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(this.#sceneWidth, Math.floor(requested)) : this.#sceneWidth;
1217
- let lines;
1218
- try {
1219
- lines = scene.component.render(width);
1220
- } catch (error) {
1221
- const failed = this.#scene;
1222
- this.#scene = void 0;
1223
- this.#invalidateScene();
1224
- console.warn(`[pi2dsh] web scene for ${scene.package}: render(width) failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
1225
- failed?.reject(error);
1226
- return {
1227
- open: false,
1228
- revision: this.#sceneRevision
1229
- };
1230
- }
1231
- return {
1232
- open: true,
1233
- package: scene.package,
1234
- revision: this.#sceneRevision,
1235
- lines
1236
- };
1237
- }
1238
- /** Raw terminal input from the browser keyboard, verbatim into the component. */
1239
- sceneInput(sequence, width) {
1240
- if (typeof width === "number" && Number.isFinite(width)) this.#sceneWidth = Math.max(20, Math.min(400, Math.floor(width)));
1241
- const component = this.#scene?.component;
1242
- if (component === void 0 || sequence.length === 0) return;
1243
- component.handleInput?.(sequence);
1244
- component.invalidate?.();
1245
- this.#invalidateScene();
1246
- }
1247
- /** The browser dismissed the overlay: resolve the Pi caller with undefined. */
1248
- closeScene() {
1249
- this.#finishScene(void 0);
1250
- }
1251
- #finishScene(value) {
1252
- const scene = this.#scene;
1253
- if (scene === void 0) return;
1254
- this.#scene = void 0;
1255
- scene.component?.dispose?.();
1256
- this.#invalidateScene();
1257
- scene.resolve(value);
1258
- }
1259
- #invalidateScene() {
1260
- this.#sceneRevision += 1;
1261
- }
1262
1168
  /**
1263
1169
  * Track one child session under its parent.
1264
1170
  * @param parentSessionId - the session the panel floats over.
@@ -1298,9 +1204,20 @@ var BrowserSurfaces = class {
1298
1204
  if (live.size === 0) this.#commandRunners.delete(sessionId);
1299
1205
  };
1300
1206
  }
1301
- /** 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
+ */
1302
1215
  commandRunner(sessionId, packageName) {
1303
- 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
+ }
1304
1221
  }
1305
1222
  /**
1306
1223
  * The panel's view of one parent session.
@@ -1659,6 +1576,7 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1659
1576
  const response = res;
1660
1577
  const method = String(req.method ?? "GET");
1661
1578
  const url = new URL(String(req.url ?? "/"), "http://pi2dsh.invalid");
1579
+ registry.lastClientContactMs = Date.now();
1662
1580
  if (method === "POST" && url.pathname === "/pi2dsh/editor-draft") {
1663
1581
  const chunks = [];
1664
1582
  const body = await new Promise((settle) => {
@@ -1715,6 +1633,46 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1715
1633
  response.end(JSON.stringify(outcome ?? { ok: true }));
1716
1634
  return;
1717
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
+ }
1718
1676
  if (url.pathname === "/pi2dsh/pi-command" && method === "POST") {
1719
1677
  const chunks = [];
1720
1678
  const raw = await new Promise((settle) => {
@@ -1743,22 +1701,6 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1743
1701
  }
1744
1702
  return;
1745
1703
  }
1746
- if (method === "POST" && (url.pathname === "/pi2dsh/scene-input" || url.pathname === "/pi2dsh/scene-close")) {
1747
- const chunks = [];
1748
- const body = await new Promise((settle) => {
1749
- const stream = req;
1750
- stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1751
- stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1752
- });
1753
- if (url.pathname === "/pi2dsh/scene-close") registry.closeScene();
1754
- else try {
1755
- const payload = JSON.parse(body || "{}");
1756
- if (typeof payload.sequence === "string") registry.sceneInput(payload.sequence, typeof payload.width === "number" ? payload.width : void 0);
1757
- } catch {}
1758
- response.writeHead(204);
1759
- response.end();
1760
- return;
1761
- }
1762
1704
  if (method !== "GET" && method !== "HEAD") {
1763
1705
  response.writeHead(405);
1764
1706
  response.end();
@@ -1804,13 +1746,11 @@ function registerBrowserSurfaceRoute(ctx, registry, hooks) {
1804
1746
  const body = JSON.stringify(session === "" ? {
1805
1747
  threads: [],
1806
1748
  surfaces: [],
1807
- entries: [],
1808
- scene: registry.sceneSnapshot()
1749
+ entries: []
1809
1750
  } : {
1810
1751
  threads: registry.snapshot(session),
1811
1752
  surfaces: registry.surfaces(session),
1812
1753
  entries: registry.entries(session),
1813
- scene: registry.sceneSnapshot(),
1814
1754
  ...registry.draftRequest(session) === void 0 ? {} : { draft: registry.draftRequest(session) }
1815
1755
  });
1816
1756
  response.writeHead(200, {
@@ -3060,6 +3000,9 @@ const DSH_COMPAT_FIELD_PROTOCOLS = {
3060
3000
  supportsDeveloperRole: COMPLETIONS_AND_RESPONSES,
3061
3001
  supportsReasoningEffort: COMPLETIONS,
3062
3002
  supportsUsageInStreaming: COMPLETIONS,
3003
+ supportsFinishReason: COMPLETIONS,
3004
+ chatTemplateArgs: COMPLETIONS,
3005
+ supportsThinkingTokenBudget: COMPLETIONS,
3063
3006
  maxTokensField: COMPLETIONS,
3064
3007
  requiresToolResultName: COMPLETIONS,
3065
3008
  requiresAssistantAfterToolResult: COMPLETIONS,
@@ -4352,8 +4295,6 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4352
4295
  if (headlessUi) return void 0;
4353
4296
  const surfaces = state.tuiSurfaces;
4354
4297
  if (surfaces !== void 0) return surfaces.custom(factory, state.theme, getKeybindings(), options);
4355
- const browser = state.shared.browserSurfaces;
4356
- if (browser !== void 0 && state.shared.browserSurfacesRouted === true) return browser.openScene(state.packageName, factory, state.theme, getKeybindings(), options);
4357
4298
  },
4358
4299
  pasteToEditor(text) {
4359
4300
  const surfaces = state.shared.browserSurfaces;
@@ -4550,11 +4491,10 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride,
4550
4491
  },
4551
4492
  ui,
4552
4493
  get mode() {
4553
- if (state.tuiSurfaces?.available === true) return "tui";
4554
- return state.shared.browserSurfaces !== void 0 && state.shared.browserSurfacesRouted === true ? "tui" : "rpc";
4494
+ return state.tuiSurfaces?.available === true ? "tui" : "rpc";
4555
4495
  },
4556
4496
  get hasUI() {
4557
- return state.tuiSurfaces?.available === true || humanAnswererAvailable(userQuestions, agent);
4497
+ return state.tuiSurfaces?.available === true || humanAnswererAvailable(userQuestions, agent, state.shared);
4558
4498
  },
4559
4499
  cwd: contextCwd,
4560
4500
  sessionManager: session === void 0 ? state.bridge.readonlySessionManager({
@@ -6047,6 +5987,37 @@ async function supersedeActiveLogin(state) {
6047
5987
  * @param config - the provider config, whose oauth block made it loginable.
6048
5988
  * @returns whether this call put a route in place.
6049
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
+ }
6050
6021
  async function ensureLoggedInProviderRoute(ctx, state, name, config) {
6051
6022
  if (!providerSupportsOAuth(config)) return false;
6052
6023
  if (providerCarriesTransport(config) || state.providerRouteDisposers.has(name)) return false;
@@ -6358,11 +6329,16 @@ async function projectAuthorizationFlow(scope, state, providerId) {
6358
6329
  throw error;
6359
6330
  }
6360
6331
  },
6361
- deviceCode: async (title, detail) => {
6332
+ deviceCode: (title, detail, signal) => {
6362
6333
  session.notify({
6363
6334
  message: String(title),
6364
6335
  code: String(detail)
6365
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
+ });
6366
6342
  }
6367
6343
  };
6368
6344
  const canonical = canonicalOf();
@@ -6855,11 +6831,18 @@ function sessionNameOf(ctx, state, session) {
6855
6831
  * @param userQuestions - the mounted question service, if any.
6856
6832
  * @param agent - the agent whose turn this context belongs to.
6857
6833
  */
6858
- function humanAnswererAvailable(userQuestions, agent) {
6834
+ function humanAnswererAvailable(userQuestions, agent, shared) {
6859
6835
  if (userQuestions === void 0) return false;
6860
6836
  if (isSubagentOrigin(agent)) return false;
6861
6837
  const service = userQuestions;
6862
- if (typeof service.registerProvider !== "function") return false;
6838
+ if (typeof service.registerProvider !== "function") {
6839
+ const events = service.ctx?.events;
6840
+ if (typeof events?.dispatch === "function") try {
6841
+ if (events.dispatch("pi2dsh:hasui-probe", ["user-questions/request"]).length > 0) return true;
6842
+ } catch {}
6843
+ const last = shared?.browserSurfaces?.lastClientContactMs;
6844
+ return typeof last === "number" && Date.now() - last < 15e3;
6845
+ }
6863
6846
  let dispose;
6864
6847
  try {
6865
6848
  dispose = service.registerProvider({ ask: async () => {
@@ -7352,7 +7335,7 @@ async function applyPiPackage(ctx, options) {
7352
7335
  }
7353
7336
  ensureLoginCommand(ctx, state);
7354
7337
  for (const [name, config] of [...state.providers]) try {
7355
- await ensureLoggedInProviderRoute(ctx, state, name, config);
7338
+ await restoreLoggedInRouteWhenReady(ctx, state, name, config);
7356
7339
  } catch (error) {
7357
7340
  logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`);
7358
7341
  }
@@ -7435,6 +7418,131 @@ async function applyPiPackage(ctx, options) {
7435
7418
  ok: true,
7436
7419
  note: "run /reload (or restart the session) to apply"
7437
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
+ };
7438
7546
  }
7439
7547
  });
7440
7548
  }
@@ -7658,4 +7766,4 @@ const runtimeInternals = {
7658
7766
  //#endregion
7659
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 };
7660
7768
 
7661
- //# sourceMappingURL=runtime-BVrvJuiO.mjs.map
7769
+ //# sourceMappingURL=runtime-BiHMcD_8.mjs.map