pi2dsh 0.12.3 → 0.13.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 (41) hide show
  1. package/README.md +82 -22
  2. package/README.zh.md +67 -19
  3. package/dist/{analyzer-2Nx5XYOA.mjs → analyzer-CXNbeBAe.mjs} +2 -2
  4. package/dist/{analyzer-2Nx5XYOA.mjs.map → analyzer-CXNbeBAe.mjs.map} +1 -1
  5. package/dist/cli.mjs +2 -2
  6. package/dist/client.js +604 -31
  7. package/dist/compat/pi-ai.d.mts +36 -1
  8. package/dist/compat/pi-ai.d.mts.map +1 -1
  9. package/dist/compat/pi-ai.mjs +2 -2
  10. package/dist/compat/pi-coding-agent.d.mts +2 -1411
  11. package/dist/compat/pi-coding-agent.mjs +1 -1
  12. package/dist/compat/pi-tui.mjs +1 -1
  13. package/dist/{oauth-bridge-CfyImWx4.mjs → credentials-oauth-BakY-0PA.mjs} +172 -13
  14. package/dist/credentials-oauth-BakY-0PA.mjs.map +1 -0
  15. package/dist/credentials-oauth.mjs +1 -89
  16. package/dist/host.mjs +4 -2
  17. package/dist/host.mjs.map +1 -1
  18. package/dist/index.mjs +3 -3
  19. package/dist/{mcp-config-Cjae0vot.mjs → mcp-config-CZepacin.mjs} +19 -19
  20. package/dist/mcp-config-CZepacin.mjs.map +1 -0
  21. package/dist/{pi-ai-Dyg4zyLZ.mjs → pi-ai-BA3V_J0V.mjs} +40 -2
  22. package/dist/{pi-ai-Dyg4zyLZ.mjs.map → pi-ai-BA3V_J0V.mjs.map} +1 -1
  23. package/dist/pi-coding-agent-B0PT23gE.d.mts +1412 -0
  24. package/dist/pi-coding-agent-B0PT23gE.d.mts.map +1 -0
  25. package/dist/{pi-coding-agent-CYnmeJEx.mjs → pi-coding-agent-D9skobb6.mjs} +0 -0
  26. package/dist/pi-coding-agent-D9skobb6.mjs.map +1 -0
  27. package/dist/{pi-tui-5CYLcj-_.mjs → pi-tui-DruSeKmd.mjs} +3 -3
  28. package/dist/pi-tui-DruSeKmd.mjs.map +1 -0
  29. package/dist/{runtime-B_Ig28og.mjs → runtime-B-SlR8mJ.mjs} +970 -75
  30. package/dist/runtime-B-SlR8mJ.mjs.map +1 -0
  31. package/dist/runtime.d.mts +600 -0
  32. package/dist/runtime.d.mts.map +1 -1
  33. package/dist/runtime.mjs +1 -1
  34. package/package.json +2 -1
  35. package/dist/compat/pi-coding-agent.d.mts.map +0 -1
  36. package/dist/credentials-oauth.mjs.map +0 -1
  37. package/dist/mcp-config-Cjae0vot.mjs.map +0 -1
  38. package/dist/oauth-bridge-CfyImWx4.mjs.map +0 -1
  39. package/dist/pi-coding-agent-CYnmeJEx.mjs.map +0 -1
  40. package/dist/pi-tui-5CYLcj-_.mjs.map +0 -1
  41. package/dist/runtime-B_Ig28og.mjs.map +0 -1
@@ -1,8 +1,8 @@
1
1
 
2
- import { F as AssistantMessageEventStream, I as isContextOverflow, a as builtinProviders, h as getSupportedThinkingLevels, i as __setPiAiLlmBridge } from "./pi-ai-Dyg4zyLZ.mjs";
3
- import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-CYnmeJEx.mjs";
2
+ import { J as AssistantMessageEventStream, Y as isContextOverflow, c as builtinProviders, i as __setPiAiLlmBridge, x as getSupportedThinkingLevels } from "./pi-ai-BA3V_J0V.mjs";
3
+ import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-D9skobb6.mjs";
4
4
  import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
5
- import { a as resolvePiProviderAuth, n as loginPiProvider, r as providerSupportsOAuth, t as FileCredentialStore } from "./oauth-bridge-CfyImWx4.mjs";
5
+ import { a as FileCredentialStore, c as providerSupportsOAuth, i as oauthCredentialRef, l as resolvePiProviderAuth, o as joinSignals, s as loginPiProvider, u as storedOAuthCredential } from "./credentials-oauth-BakY-0PA.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
8
8
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
@@ -16,6 +16,7 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmError, QUOTA_EXCE
16
16
  import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
17
17
  import { randomUUID } from "node:crypto";
18
18
  import { foldSurface } from "@deepseek-ai/dsh-session";
19
+ import { spawn } from "node:child_process";
19
20
  import { Compile } from "typebox/compile";
20
21
  import { Value } from "typebox/value";
21
22
  //#region src/session-bridge.ts
@@ -102,6 +103,29 @@ var PiSessionBridge = class {
102
103
  });
103
104
  return id;
104
105
  }
106
+ /**
107
+ * The custom entries a package appended to one session, oldest first.
108
+ *
109
+ * Addressed by id alone: the browser half asks for a session it is showing,
110
+ * and has no DSH session object to hand over.
111
+ * @param sessionId - session whose sidecar to read.
112
+ * @returns each custom entry with its type, data and id.
113
+ */
114
+ customEntries(sessionId) {
115
+ this.load(sessionId);
116
+ const out = [];
117
+ for (const record of this.records.get(sessionId) ?? []) {
118
+ if (record.kind !== "custom") continue;
119
+ const entry = record;
120
+ out.push({
121
+ id: entry.id,
122
+ customType: entry.customType,
123
+ data: entry.data,
124
+ timestamp: entry.timestamp
125
+ });
126
+ }
127
+ return out;
128
+ }
105
129
  appendBranchSummary(sessionId, summary, fromId) {
106
130
  const id = randomUUID();
107
131
  this.persist(sessionId, {
@@ -721,7 +745,24 @@ async function createBridgedAgentSession(host, options) {
721
745
  return { session: new PiBridgedAgentSession(host, handle, tools) };
722
746
  }
723
747
  //#endregion
724
- //#region src/side-panel.ts
748
+ //#region src/compat/vendor/pi-open-browser.ts
749
+ /**
750
+ * Open a URL or file in the platform browser/default handler.
751
+ *
752
+ * This intentionally never invokes a shell. On Windows, do not use
753
+ * `cmd /c start`: cmd.exe re-parses metacharacters (&, |, ^, ...) before
754
+ * `start` runs, which would make attacker-controlled URLs injectable.
755
+ * @param target - the URL or file to hand to the platform handler.
756
+ */
757
+ function openBrowser(target) {
758
+ const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", target]] : ["xdg-open", [target]];
759
+ spawn(cmd, args, {
760
+ stdio: "ignore",
761
+ detached: true
762
+ }).on("error", () => {}).unref();
763
+ }
764
+ //#endregion
765
+ //#region src/browser-surfaces.ts
725
766
  /** Flatten one Pi message's content to the text a panel row shows. */
726
767
  function messageText(message) {
727
768
  const content = message.content;
@@ -738,8 +779,22 @@ function messageText(message) {
738
779
  * Host-level (one per engine instance, like the provider directory): the panel
739
780
  * is one surface, however many Pi packages contribute threads to it.
740
781
  */
741
- var SidePanelRegistry = class {
782
+ var BrowserSurfaces = class {
742
783
  #byParent = /* @__PURE__ */ new Map();
784
+ #surfaces = /* @__PURE__ */ new Map();
785
+ #entrySources = /* @__PURE__ */ new Map();
786
+ #draftRequests = /* @__PURE__ */ new Map();
787
+ #liveDrafts = /* @__PURE__ */ new Map();
788
+ #completionSources = /* @__PURE__ */ new Map();
789
+ #imageToolNames = /* @__PURE__ */ new Set();
790
+ /** Register one known image tool's exact DSH wire name. */
791
+ registerImageTool(name) {
792
+ if (name.length > 0) this.#imageToolNames.add(name);
793
+ }
794
+ /** Exact wire names needing the generic Pi image-result browser view. */
795
+ imageToolNames() {
796
+ return [...this.#imageToolNames];
797
+ }
743
798
  /**
744
799
  * Track one child session under its parent.
745
800
  * @param parentSessionId - the session the panel floats over.
@@ -776,11 +831,246 @@ var SidePanelRegistry = class {
776
831
  })).filter((entry) => !entry.text.startsWith(`<${SEED_CARRIER_TAG}>`)).filter((entry) => entry.text.length > 0)
777
832
  }));
778
833
  }
834
+ /**
835
+ * Pi's `setStatus(key, text)`: one keyed status entry in the status bar.
836
+ * The key is the entry's identity; passing undefined removes that entry,
837
+ * exactly like Pi clears one status.
838
+ * @param sessionId - session the calling agent belongs to.
839
+ * @param packageName - Pi package making the call, when known.
840
+ * @param key - the status entry's key.
841
+ * @param text - the text to show, or undefined to remove the entry.
842
+ */
843
+ setStatus(sessionId, packageName, key, text) {
844
+ if (sessionId.length === 0) return;
845
+ const view = this.#view(sessionId, packageName);
846
+ const rendered = text === void 0 || text === null ? void 0 : String(text);
847
+ if (rendered === void 0 || rendered.length === 0) delete view.statuses[key];
848
+ else view.statuses[key] = rendered;
849
+ }
850
+ /**
851
+ * Pi's `setWidget(key, content)`: one keyed widget. Only string arrays are
852
+ * recorded — anything else is ignored exactly like Pi's own rpc mode, where
853
+ * widgets are transmitted to a host as lines and factory content needs TUI
854
+ * access the host never provides.
855
+ * @param sessionId - session the calling agent belongs to.
856
+ * @param packageName - Pi package making the call, when known.
857
+ * @param key - the widget's key.
858
+ * @param content - the widget's lines, or undefined to remove the widget.
859
+ */
860
+ setWidget(sessionId, packageName, key, content, theme) {
861
+ if (sessionId.length === 0) return;
862
+ const view = this.#view(sessionId, packageName);
863
+ const text = surfaceText(content, theme);
864
+ if (text === void 0) delete view.widgets[key];
865
+ else view.widgets[key] = text;
866
+ }
867
+ /**
868
+ * Record one simple presentation call (working chrome, title, header,
869
+ * footer). The value is a string, a Pi component, a working-indicator
870
+ * options object, or a header/footer factory; each renders to text here so
871
+ * the browser half never runs Pi code.
872
+ * @param sessionId - session the calling agent belongs to.
873
+ * @param packageName - Pi package making the call, when known.
874
+ * @param key - which surface.
875
+ * @param value - the value the package passed; undefined clears it.
876
+ * @param theme - the bridge's headless theme, for factories that style with it.
877
+ */
878
+ setSurface(sessionId, packageName, key, value, theme) {
879
+ if (sessionId.length === 0) return;
880
+ const view = this.#view(sessionId, packageName);
881
+ const text = surfaceText(value, theme);
882
+ if (text === void 0) delete view.values[key];
883
+ else view.values[key] = text;
884
+ }
885
+ /**
886
+ * Pi's `setWorkingVisible`: hide the working chrome without forgetting it.
887
+ * @param sessionId - session the calling agent belongs to.
888
+ * @param packageName - Pi package making the call, when known.
889
+ * @param visible - whether the working chrome should show.
890
+ */
891
+ setWorkingVisible(sessionId, packageName, visible) {
892
+ if (sessionId.length === 0) return;
893
+ this.#view(sessionId, packageName).workingVisible = visible;
894
+ }
895
+ /**
896
+ * Register one package's custom-entry renderer.
897
+ *
898
+ * Pulled rather than pushed: a renderer's output depends on the session being
899
+ * looked at, and the package registers once at mount for every session it
900
+ * will ever be asked about.
901
+ * @param packageName - owning Pi package.
902
+ * @param source - called with the session id the browser is showing.
903
+ * @returns a disposer that unregisters the source.
904
+ */
905
+ trackEntries(packageName, source) {
906
+ this.#entrySources.set(packageName, source);
907
+ return () => {
908
+ this.#entrySources.delete(packageName);
909
+ };
910
+ }
911
+ /**
912
+ * Custom entries every package renders for one session, in package order.
913
+ * @param sessionId - session the browser is showing.
914
+ * @returns rendered entries; a renderer that throws contributes nothing.
915
+ */
916
+ entries(sessionId) {
917
+ const out = [];
918
+ for (const [packageName, source] of this.#entrySources) try {
919
+ for (const entry of source(sessionId)) out.push({
920
+ ...entry,
921
+ package: packageName
922
+ });
923
+ } catch {}
924
+ return out;
925
+ }
926
+ /**
927
+ * Ask the browser to put text in the composer.
928
+ *
929
+ * A revision rather than a flag: the seat applies a request once, and the
930
+ * next call — even with identical text — is a new request rather than a
931
+ * no-op, which is what a package retrying a paste means.
932
+ * @param sessionId - session whose composer to write.
933
+ * @param text - the full next draft.
934
+ */
935
+ requestDraft(sessionId, text) {
936
+ if (sessionId.length === 0) return;
937
+ const current = this.#draftRequests.get(sessionId);
938
+ this.#draftRequests.set(sessionId, {
939
+ text,
940
+ rev: (current?.rev ?? 0) + 1
941
+ });
942
+ this.#liveDrafts.set(sessionId, text);
943
+ }
944
+ /**
945
+ * The pending composer write for one session, if any.
946
+ * @param sessionId - session the browser is showing.
947
+ * @returns the request, or undefined when nothing is pending.
948
+ */
949
+ draftRequest(sessionId) {
950
+ return this.#draftRequests.get(sessionId);
951
+ }
952
+ /**
953
+ * Record what the composer actually holds, as reported by the browser.
954
+ * @param sessionId - session the report is about.
955
+ * @param text - the live draft.
956
+ */
957
+ reportDraft(sessionId, text) {
958
+ if (sessionId.length === 0) return;
959
+ this.#liveDrafts.set(sessionId, text);
960
+ }
961
+ /**
962
+ * The composer text for one session.
963
+ * @param sessionId - session to read.
964
+ * @returns the browser's last report, or the last requested text.
965
+ */
966
+ liveDraft(sessionId) {
967
+ return this.#liveDrafts.get(sessionId) ?? "";
968
+ }
969
+ /**
970
+ * Register one package's completion provider.
971
+ * @param packageName - owning Pi package.
972
+ * @param source - asked with the trigger character and the typed query.
973
+ * @returns a disposer that unregisters the source.
974
+ */
975
+ trackCompletions(packageName, source) {
976
+ this.#completionSources.set(packageName, source);
977
+ return () => {
978
+ this.#completionSources.delete(packageName);
979
+ };
980
+ }
981
+ /**
982
+ * Completions every package offers for one trigger token.
983
+ * @param trigger - the trigger character the menu opened on.
984
+ * @param query - what the user has typed after it.
985
+ * @returns candidates in package order; a provider that throws contributes none.
986
+ */
987
+ async completions(trigger, query) {
988
+ const out = [];
989
+ for (const source of this.#completionSources.values()) try {
990
+ out.push(...await source(trigger, query));
991
+ } catch {}
992
+ return out;
993
+ }
994
+ /**
995
+ * What packages have put on screen for one session.
996
+ * @param sessionId - session the browser is showing.
997
+ * @returns one entry per package that has driven a surface, empties dropped.
998
+ */
999
+ surfaces(sessionId) {
1000
+ const bySession = this.#surfaces.get(sessionId);
1001
+ if (bySession === void 0) return [];
1002
+ return [...bySession.values()].filter((view) => viewHasContent(view));
1003
+ }
1004
+ /** Get-or-create one package's view for a session. */
1005
+ #view(sessionId, packageName) {
1006
+ const owner = packageName ?? "pi";
1007
+ const bySession = this.#surfaces.get(sessionId) ?? /* @__PURE__ */ new Map();
1008
+ let view = bySession.get(owner);
1009
+ if (view === void 0) {
1010
+ view = {
1011
+ ...packageName === void 0 ? {} : { package: packageName },
1012
+ values: {},
1013
+ statuses: {},
1014
+ widgets: {},
1015
+ workingVisible: true
1016
+ };
1017
+ bySession.set(owner, view);
1018
+ this.#surfaces.set(sessionId, bySession);
1019
+ }
1020
+ return view;
1021
+ }
779
1022
  };
1023
+ /** Whether a view still carries something to show. */
1024
+ function viewHasContent(view) {
1025
+ return Object.keys(view.values).length > 0 || Object.keys(view.statuses).length > 0 || Object.keys(view.widgets).length > 0;
1026
+ }
1027
+ /**
1028
+ * Render whatever Pi handed a presentation call into displayable text.
1029
+ *
1030
+ * Pi's UI methods accept a string, a string array (widget lines), one of its
1031
+ * components (`render(width): string[]`), a working-indicator options object
1032
+ * (`{ frames?: string[] }`), or a header/footer factory
1033
+ * (`(tui, theme) => Component`). Every shape is projected to text here — the
1034
+ * pi-tui shims honour `render(width)` headlessly, and a factory is called with
1035
+ * the bridge's headless theme (and no TUI, which Pi's own rpc mode never
1036
+ * provides either); anything that cannot render leaves the surface empty
1037
+ * rather than faking output.
1038
+ * @param value - the value the package passed.
1039
+ * @param theme - the bridge's headless theme, for factories that style with it.
1040
+ * @returns the text to show, or undefined to clear the surface.
1041
+ */
1042
+ function surfaceText(value, theme) {
1043
+ if (value === void 0 || value === null) return void 0;
1044
+ if (typeof value === "string") return value.length === 0 ? void 0 : value;
1045
+ if (Array.isArray(value)) {
1046
+ const text = value.map(String).join("\n").trimEnd();
1047
+ return text.length === 0 ? void 0 : text;
1048
+ }
1049
+ const record = value;
1050
+ if (typeof record.render === "function") try {
1051
+ const lines = record.render(80);
1052
+ const text = (Array.isArray(lines) ? lines : [lines]).map((line) => String(line)).join("\n").trimEnd();
1053
+ return text.length === 0 ? void 0 : text;
1054
+ } catch {
1055
+ return;
1056
+ }
1057
+ if (Array.isArray(record.frames)) {
1058
+ const text = record.frames.map(String).join("").trimEnd();
1059
+ return text.length === 0 ? void 0 : text;
1060
+ }
1061
+ if (typeof value === "function") try {
1062
+ return surfaceText(value(void 0, theme), theme);
1063
+ } catch {
1064
+ return;
1065
+ }
1066
+ const text = String(value);
1067
+ return text.length === 0 || text === "[object Object]" ? void 0 : text;
1068
+ }
780
1069
  /**
781
- * Serve the panel's own read route on the host's web server.
1070
+ * Serve the browser half's own read route on the host's web server.
782
1071
  *
783
- * One prefix, one GET: the client half polls it for the session it is showing.
1072
+ * One prefix, one GET, one payload: the client half polls it for the session
1073
+ * it is showing and gets every surface at once.
784
1074
  * A composition with no web server (the CLI profile) simply has no panel, which
785
1075
  * is the correct outcome rather than an error — the side conversation itself
786
1076
  * still runs, and the host's subagent catalog still lists it.
@@ -788,7 +1078,43 @@ var SidePanelRegistry = class {
788
1078
  * @param registry - live thread registry to read from.
789
1079
  * @returns whether the route was registered.
790
1080
  */
791
- function registerSidePanelRoute(ctx, registry) {
1081
+ /**
1082
+ * Short, clickable stand-ins for the authorization URLs a login flow announces.
1083
+ *
1084
+ * An OAuth authorize URL is 400 characters of query string. Putting it in a
1085
+ * dialog produces six wrapped lines with the question buried under them, and a
1086
+ * link that long is unusable even when the surface makes it clickable. These
1087
+ * are the short links shown instead; hitting one redirects to the real address.
1088
+ */
1089
+ const pendingAuthorizations = /* @__PURE__ */ new Map();
1090
+ /**
1091
+ * Publish a short link for one authorization URL.
1092
+ * @param url - the address the flow wants the user to open.
1093
+ * @returns the path to show, or undefined when the URL is not http(s).
1094
+ */
1095
+ function publishAuthorization(url) {
1096
+ if (!/^https?:\/\//u.test(url)) return void 0;
1097
+ const token = Math.random().toString(36).slice(2, 10);
1098
+ pendingAuthorizations.set(token, url);
1099
+ if (pendingAuthorizations.size > 8) {
1100
+ const oldest = pendingAuthorizations.keys().next().value;
1101
+ if (oldest !== void 0) pendingAuthorizations.delete(oldest);
1102
+ }
1103
+ return `/pi2dsh/authorize/${token}`;
1104
+ }
1105
+ /**
1106
+ * Retire a published link when its flow is over.
1107
+ *
1108
+ * A finished (or superseded) flow's address still resolves at the provider, and
1109
+ * following it produces a callback the flow that is now listening cannot match
1110
+ * — the provider answers "State mismatch", which reads like a bug in the login
1111
+ * rather than a link that has expired. Saying so is the honest answer.
1112
+ * @param path - what {@link publishAuthorization} returned.
1113
+ */
1114
+ function revokeAuthorization(path) {
1115
+ pendingAuthorizations.delete(path.slice(18));
1116
+ }
1117
+ function registerBrowserSurfaceRoute(ctx, registry) {
792
1118
  const web = ctx.get("webServer");
793
1119
  if (web === void 0 || typeof web.register !== "function") return false;
794
1120
  ctx.effect(() => web.register({
@@ -797,26 +1123,81 @@ function registerSidePanelRoute(ctx, registry) {
797
1123
  handler: async (req, res) => {
798
1124
  const response = res;
799
1125
  const method = String(req.method ?? "GET");
1126
+ const url = new URL(String(req.url ?? "/"), "http://pi2dsh.invalid");
1127
+ if (method === "POST" && url.pathname === "/pi2dsh/editor-draft") {
1128
+ const chunks = [];
1129
+ const body = await new Promise((settle) => {
1130
+ const stream = req;
1131
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1132
+ stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
1133
+ });
1134
+ try {
1135
+ const payload = JSON.parse(body || "{}");
1136
+ if (typeof payload.session === "string" && typeof payload.draft === "string") registry.reportDraft(payload.session, payload.draft);
1137
+ } catch {}
1138
+ response.writeHead(204);
1139
+ response.end();
1140
+ return;
1141
+ }
800
1142
  if (method !== "GET" && method !== "HEAD") {
801
1143
  response.writeHead(405);
802
1144
  response.end();
803
1145
  return;
804
1146
  }
805
- const url = new URL(String(req.url ?? "/"), "http://pi2dsh.invalid");
806
- if (url.pathname !== "/pi2dsh/side-panel") {
1147
+ if (url.pathname.startsWith("/pi2dsh/authorize/")) {
1148
+ const target = pendingAuthorizations.get(url.pathname.slice(18));
1149
+ if (target === void 0) {
1150
+ response.writeHead(404);
1151
+ response.end("this login link has expired");
1152
+ return;
1153
+ }
1154
+ response.writeHead(302, {
1155
+ location: target,
1156
+ "cache-control": "no-store"
1157
+ });
1158
+ response.end();
1159
+ return;
1160
+ }
1161
+ if (url.pathname === "/pi2dsh/completions") {
1162
+ const items = await registry.completions(url.searchParams.get("trigger") ?? "@", url.searchParams.get("query") ?? "");
1163
+ response.writeHead(200, {
1164
+ "content-type": "application/json; charset=utf-8",
1165
+ "cache-control": "no-store"
1166
+ });
1167
+ response.end(JSON.stringify({ items }));
1168
+ return;
1169
+ }
1170
+ if (url.pathname === "/pi2dsh/image-tool-names") {
1171
+ response.writeHead(200, {
1172
+ "content-type": "application/json; charset=utf-8",
1173
+ "cache-control": "no-store"
1174
+ });
1175
+ response.end(method === "HEAD" ? void 0 : JSON.stringify({ names: registry.imageToolNames() }));
1176
+ return;
1177
+ }
1178
+ if (url.pathname !== "/pi2dsh/browser-state") {
807
1179
  response.writeHead(404);
808
1180
  response.end();
809
1181
  return;
810
1182
  }
811
1183
  const session = url.searchParams.get("session") ?? "";
812
- const body = JSON.stringify({ threads: session === "" ? [] : registry.snapshot(session) });
1184
+ const body = JSON.stringify(session === "" ? {
1185
+ threads: [],
1186
+ surfaces: [],
1187
+ entries: []
1188
+ } : {
1189
+ threads: registry.snapshot(session),
1190
+ surfaces: registry.surfaces(session),
1191
+ entries: registry.entries(session),
1192
+ ...registry.draftRequest(session) === void 0 ? {} : { draft: registry.draftRequest(session) }
1193
+ });
813
1194
  response.writeHead(200, {
814
1195
  "content-type": "application/json; charset=utf-8",
815
1196
  "cache-control": "no-store"
816
1197
  });
817
1198
  response.end(method === "HEAD" ? void 0 : body);
818
1199
  }
819
- }), "pi2dsh: side-panel route");
1200
+ }), "pi2dsh: browser-state route");
820
1201
  return true;
821
1202
  }
822
1203
  //#endregion
@@ -1776,12 +2157,27 @@ function providerCarriesTransport(value) {
1776
2157
  }
1777
2158
  function providerModels(provider) {
1778
2159
  try {
1779
- const models = provider.getModels?.();
2160
+ const models = typeof provider.getModels === "function" ? provider.getModels() : provider.models;
1780
2161
  return Array.isArray(models) ? models : [];
1781
2162
  } catch {
1782
2163
  return [];
1783
2164
  }
1784
2165
  }
2166
+ /**
2167
+ * The environment variable a Pi config value names, when that is all it names.
2168
+ *
2169
+ * Pi's config values (`resolve-config-value.ts`) are `$VAR` / `${VAR}`
2170
+ * interpolation, `!command` execution, or a literal. Only a value that is
2171
+ * exactly one reference can become a DSH credential reference; a command, a
2172
+ * template, or a literal secret must not be copied into settings, which carry
2173
+ * references and never values.
2174
+ * @param value - the package's declared config value, if it declared one.
2175
+ */
2176
+ function envReferenceOf(value) {
2177
+ if (typeof value !== "string") return void 0;
2178
+ const match = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$|^\$([A-Za-z_][A-Za-z0-9_]*)$/u.exec(value.trim());
2179
+ return match?.[1] ?? match?.[2];
2180
+ }
1785
2181
  const PI_CARRIED_FIELDS = [
1786
2182
  "api",
1787
2183
  "baseUrl",
@@ -1986,6 +2382,159 @@ function imageAdmissionCompanionAdapter(options) {
1986
2382
  }
1987
2383
  };
1988
2384
  }
2385
+ /** A wire protocol DSH's official adapter can serve, or undefined. */
2386
+ function protocolOf(value) {
2387
+ return typeof value === "string" && DSH_PROTOCOLS.has(value) ? value : void 0;
2388
+ }
2389
+ const DSH_PROTOCOLS = /* @__PURE__ */ new Set([
2390
+ "openai-completions",
2391
+ "openai-responses",
2392
+ "anthropic-messages"
2393
+ ]);
2394
+ const DSH_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
2395
+ const DSH_THINKING_FORMATS = /* @__PURE__ */ new Set([
2396
+ "openai",
2397
+ "deepseek",
2398
+ "openrouter",
2399
+ "together",
2400
+ "zai",
2401
+ "qwen",
2402
+ "string-thinking",
2403
+ "ant-ling"
2404
+ ]);
2405
+ /** A positive whole number, or undefined — the profile schema rejects 0 and fractions. */
2406
+ function wholeAbove(value) {
2407
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : void 0;
2408
+ }
2409
+ /**
2410
+ * One Pi model as a DSH model profile.
2411
+ *
2412
+ * Field by field on purpose. The two vocabularies overlap but do not match:
2413
+ * Pi's `maxTokens` is routinely 0 for "unstated" while DSH requires at least 1,
2414
+ * Pi's `compat` carries gateway dialect flags DSH's profile has no slot for,
2415
+ * and Pi's `reasoning` is a boolean where DSH's `reasoningEfforts` is a map of
2416
+ * offered levels. Forwarding any of those verbatim rejects the whole settings
2417
+ * section — one bad key takes down every route in it.
2418
+ * @param model - the Pi model descriptor as its package declared it.
2419
+ * @param api - the wire protocol this route resolved to, which decides whether
2420
+ * the reasoning compat switches are even a legal thing to state.
2421
+ */
2422
+ function modelProfileOf(model, api) {
2423
+ const compat = model.compat;
2424
+ const reasoningCompatAllowed = api === "openai-completions";
2425
+ const thinkingFormat = reasoningCompatAllowed && typeof compat?.thinkingFormat === "string" && DSH_THINKING_FORMATS.has(compat.thinkingFormat) ? { thinkingFormat: compat.thinkingFormat } : {};
2426
+ const supportsReasoningEffort = reasoningCompatAllowed && typeof compat?.supportsReasoningEffort === "boolean" ? { supportsReasoningEffort: compat.supportsReasoningEffort } : {};
2427
+ const carriedCompat = {
2428
+ ...thinkingFormat,
2429
+ ...supportsReasoningEffort
2430
+ };
2431
+ const input = Array.isArray(model.input) ? model.input.filter((entry) => typeof entry === "string" && DSH_MODALITIES.has(entry)) : [];
2432
+ const contextWindow = wholeAbove(model.contextWindow);
2433
+ const maxTokens = wholeAbove(model.maxTokens);
2434
+ return {
2435
+ id: String(model.id ?? ""),
2436
+ ...typeof model.name === "string" ? { name: model.name } : {},
2437
+ ...contextWindow === void 0 ? {} : { contextWindow },
2438
+ ...maxTokens === void 0 ? {} : { maxTokens },
2439
+ ...input.length === 0 ? {} : { input },
2440
+ ...Object.keys(carriedCompat).length === 0 ? {} : { compat: carriedCompat }
2441
+ };
2442
+ }
2443
+ /**
2444
+ * Serve a catalog-only Pi provider through DSH's OWN llm adapter.
2445
+ *
2446
+ * A Pi package may declare a gateway without shipping any code to call it —
2447
+ * on Pi that is complete, because pi-ai supplies the transport for the wire
2448
+ * protocol the models name. The bridge used to refuse those and tell the user
2449
+ * to configure the gateway by hand in DSH settings, which is the translation
2450
+ * this package exists to do for them.
2451
+ *
2452
+ * The translation is configuration, not transport: the declaration becomes an
2453
+ * `llm-pi-ai` provider profile — the official adapter's own shape, which
2454
+ * carries `api`, `baseURL` and per-model `compat` — and that official plugin
2455
+ * is mounted with it. Nothing here speaks HTTP.
2456
+ * @returns a disposer for the mounted route, or undefined when it cannot be built.
2457
+ */
2458
+ function registerCatalogOnlyRoute(options) {
2459
+ const { ctx, providerId, provider, host } = options;
2460
+ if (ctx === void 0) return void 0;
2461
+ return mountOfficialAdapter(ctx, providerId, host, () => {
2462
+ const models = providerModels(provider);
2463
+ const first = models[0] ?? {};
2464
+ const apiKeyEnv = envReferenceOf(provider.apiKey);
2465
+ const api = protocolOf(provider.api) ?? protocolOf(first.api);
2466
+ const baseURL = typeof provider.baseUrl === "string" ? provider.baseUrl : typeof first.baseUrl === "string" ? first.baseUrl : void 0;
2467
+ return {
2468
+ displayName: typeof provider.name === "string" ? provider.name : providerId,
2469
+ ...apiKeyEnv === void 0 ? {} : { apiKeyEnv },
2470
+ ...api === void 0 ? {} : { api },
2471
+ ...baseURL === void 0 ? {} : { baseURL },
2472
+ ...models.length === 0 ? {} : { models: models.map((model) => modelProfileOf(model, api)) }
2473
+ };
2474
+ }, `[pi2dsh] Pi provider ${JSON.stringify(providerId)} declares a catalog only; DSH's official llm-pi-ai adapter now serves it as a native route`);
2475
+ }
2476
+ /**
2477
+ * Mount DSH's official llm-pi-ai adapter for one route, rebuildable on demand.
2478
+ * @param buildProfile - called per mount, so a re-announce picks up new facts.
2479
+ * @param announcement - what to tell the user once the route is up.
2480
+ */
2481
+ function mountOfficialAdapter(ctx, providerId, host, buildProfile, announcement) {
2482
+ let mounted;
2483
+ let disposed = false;
2484
+ let generation = 0;
2485
+ const mount = () => {
2486
+ const profile = buildProfile();
2487
+ const mine = ++generation;
2488
+ (async () => {
2489
+ try {
2490
+ if (await declareInSettings(ctx, providerId, profile)) {
2491
+ if (!disposed && mine === generation) mounted = void 0;
2492
+ host.warn(announcement);
2493
+ return;
2494
+ }
2495
+ const official = await import("@deepseek-ai/dsh-llm-pi-ai");
2496
+ const fiber = await ctx.plugin(official.default ?? official, { providers: { [providerId]: profile } });
2497
+ if (disposed || mine !== generation) fiber?.dispose?.();
2498
+ else mounted = fiber;
2499
+ host.warn(announcement);
2500
+ } catch (error) {
2501
+ host.warn(`[pi2dsh] Pi provider ${JSON.stringify(providerId)} could not be served by the official adapter (${error instanceof Error ? error.message : String(error)}); configure the gateway in the host's llm settings instead`);
2502
+ }
2503
+ })();
2504
+ };
2505
+ mount();
2506
+ const route = (() => {
2507
+ disposed = true;
2508
+ mounted?.dispose?.();
2509
+ });
2510
+ route.reannounce = () => {
2511
+ if (disposed) return;
2512
+ mounted?.dispose?.();
2513
+ mounted = void 0;
2514
+ mount();
2515
+ };
2516
+ return route;
2517
+ }
2518
+ /**
2519
+ * Hand one provider profile to the official adapter through DSH's settings.
2520
+ *
2521
+ * `ctx.settings.update` merges into the user layer of a namespace its OWNER
2522
+ * registered, so this only works where that adapter is mounted — which is the
2523
+ * normal composition, and exactly where mounting a second copy would collide.
2524
+ * A composition without it (a bare test host, an embedded runtime) rejects and
2525
+ * the caller falls back to mounting the plugin itself.
2526
+ * @returns whether the profile was stored.
2527
+ */
2528
+ async function declareInSettings(ctx, providerId, profile) {
2529
+ const settings = ctx.get?.("settings");
2530
+ if (typeof settings?.update !== "function") return false;
2531
+ try {
2532
+ await settings.update("llm-pi-ai", { providers: { [providerId]: profile } });
2533
+ return true;
2534
+ } catch {
2535
+ return false;
2536
+ }
2537
+ }
1989
2538
  /**
1990
2539
  * Register the provider as a live DSH route when an llm service is mounted.
1991
2540
  * A provider carrying its own transport streams through it; a config-only
@@ -1998,11 +2547,16 @@ function registerPiProviderRoute(options) {
1998
2547
  const { llm, providerId, provider, host } = options;
1999
2548
  if (llm === void 0) return void 0;
2000
2549
  if (!providerCarriesTransport(provider)) {
2001
- host.warn(`[pi2dsh] Pi provider ${JSON.stringify(providerId)} declares a model catalog but no transport; it was not added as a DSH llm route — configure the gateway in the host's llm settings instead`);
2002
- return;
2550
+ if (providerModels(provider).length === 0 && provider.oauth !== void 0) return void 0;
2551
+ return registerCatalogOnlyRoute(options);
2003
2552
  }
2004
2553
  try {
2005
- return llm.registerAdapter([providerId], piProviderDshAdapter(providerId, provider, host));
2554
+ const registration = llm.registerAdapter([providerId], piProviderDshAdapter(providerId, provider, host));
2555
+ const route = (() => {
2556
+ registration();
2557
+ });
2558
+ if (typeof registration.replace === "function") route.reannounce = () => registration.replace?.([providerId]);
2559
+ return route;
2006
2560
  } catch (error) {
2007
2561
  host.warn(`[pi2dsh] Pi provider ${JSON.stringify(providerId)} was not added as a DSH llm route (${error instanceof Error ? error.message : String(error)}); the existing route keeps the name`);
2008
2562
  return;
@@ -2012,6 +2566,10 @@ function registerPiProviderRoute(options) {
2012
2566
  //#region src/runtime.ts
2013
2567
  /** Fallback thread ids when a child session reports none. */
2014
2568
  let sidePanelSerial = 0;
2569
+ const KNOWN_IMAGE_TOOLS_BY_PACKAGE = /* @__PURE__ */ new Map([["@crazygit/pi-codex-image-gen", /* @__PURE__ */ new Set(["codex_generate_image"])]]);
2570
+ function isKnownImageTool(packageName, toolName) {
2571
+ return KNOWN_IMAGE_TOOLS_BY_PACKAGE.get(packageName)?.has(toolName) === true;
2572
+ }
2015
2573
  function logger(ctx) {
2016
2574
  const candidate = ctx.logger;
2017
2575
  return {
@@ -2277,6 +2835,30 @@ function currentPiModel(state, agent) {
2277
2835
  reasoning: false
2278
2836
  };
2279
2837
  }
2838
+ /** A session's durable events, whether the store exposes them as value or method. */
2839
+ function eventsOf(session) {
2840
+ const events = session.events;
2841
+ if (typeof events === "function") return events.call(session) ?? [];
2842
+ return Array.isArray(events) ? events : [];
2843
+ }
2844
+ /** The session id a presentation call belongs to, or '' outside a session. */
2845
+ function sessionIdOf(state, agent) {
2846
+ const session = agentSession(agent ?? currentAgent(state));
2847
+ return session === void 0 ? "" : String(session.id ?? "");
2848
+ }
2849
+ /**
2850
+ * Record one Pi presentation call against the session that made it.
2851
+ * @param ctx - context, for the logger on an unmounted host.
2852
+ * @param state - runtime state carrying the shared surface registry.
2853
+ * @param agent - the agent whose context the call came through.
2854
+ * @param key - which surface.
2855
+ * @param value - a string or Pi component; undefined clears it.
2856
+ */
2857
+ function putSurface(ctx, state, agent, key, value) {
2858
+ const surfaces = state.shared.browserSurfaces;
2859
+ if (surfaces === void 0) return;
2860
+ surfaces.setSurface(sessionIdOf(state, agent), state.packageName, key, value, state.theme);
2861
+ }
2280
2862
  function thinkingLevelOf(state, agent) {
2281
2863
  if (agent !== void 0) {
2282
2864
  const scoped = state.thinkingLevels.get(agent);
@@ -2333,11 +2915,15 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
2333
2915
  const level = type === "warning" || type === "error" ? "warn" : "info";
2334
2916
  logger(ctx)[level](`[pi2dsh] ${text}`);
2335
2917
  },
2336
- select: (title, options) => askOne(ctx, agent, signal, {
2337
- id: "pi2dsh-select",
2338
- question: String(title),
2339
- options: options.map((option) => ({ label: String(option) }))
2340
- }),
2918
+ select: async (title, options, promptSignal) => {
2919
+ const labels = options.map((option) => String(option));
2920
+ const answered = await askOne(ctx, agent, joinSignals(signal, promptSignal), {
2921
+ id: "pi2dsh-select",
2922
+ question: String(title),
2923
+ options: labels.map((label) => ({ label }))
2924
+ });
2925
+ return answered === void 0 ? void 0 : resolveOfferedChoice(answered, labels);
2926
+ },
2341
2927
  async confirm(title, message) {
2342
2928
  return await askOne(ctx, agent, signal, {
2343
2929
  id: "pi2dsh-confirm",
@@ -2346,7 +2932,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
2346
2932
  options: [{ label: "Yes" }, { label: "No" }]
2347
2933
  }) === "Yes";
2348
2934
  },
2349
- input: (title, placeholder) => askOne(ctx, agent, signal, {
2935
+ input: (title, placeholder, promptSignal) => askOne(ctx, agent, joinSignals(signal, promptSignal), {
2350
2936
  id: "pi2dsh-input",
2351
2937
  question: String(title),
2352
2938
  ...placeholder === void 0 ? {} : { detail: String(placeholder) }
@@ -2356,24 +2942,45 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
2356
2942
  question: String(title),
2357
2943
  ...prefill === void 0 ? {} : { detail: `Current text:\n${String(prefill)}` }
2358
2944
  }),
2359
- setStatus: () => void 0,
2360
- setWidget: () => void 0,
2945
+ setStatus: (key, text) => {
2946
+ const surfaces = state.shared.browserSurfaces;
2947
+ if (surfaces === void 0) return;
2948
+ surfaces.setStatus(sessionIdOf(state, agent), state.packageName, String(key), text);
2949
+ },
2950
+ setWidget: (key, content) => {
2951
+ const surfaces = state.shared.browserSurfaces;
2952
+ if (surfaces === void 0) return;
2953
+ surfaces.setWidget(sessionIdOf(state, agent), state.packageName, String(key), content, state.theme);
2954
+ },
2361
2955
  onTerminalInput: () => () => void 0,
2362
- setWorkingMessage: () => void 0,
2363
- setWorkingVisible: () => void 0,
2364
- setWorkingIndicator: () => void 0,
2365
- setHiddenThinkingLabel: () => void 0,
2366
- setFooter: () => void 0,
2367
- setHeader: () => void 0,
2368
- setTitle: () => void 0,
2956
+ setWorkingMessage: (message) => putSurface(ctx, state, agent, "workingMessage", message),
2957
+ setWorkingVisible: (visible) => {
2958
+ const surfaces = state.shared.browserSurfaces;
2959
+ if (surfaces === void 0) return;
2960
+ surfaces.setWorkingVisible(sessionIdOf(state, agent), state.packageName, visible !== false);
2961
+ },
2962
+ setWorkingIndicator: (options) => putSurface(ctx, state, agent, "workingIndicator", options),
2963
+ setHiddenThinkingLabel: (label) => putSurface(ctx, state, agent, "hiddenThinkingLabel", label),
2964
+ setFooter: (factory) => putSurface(ctx, state, agent, "footer", factory),
2965
+ setHeader: (factory) => putSurface(ctx, state, agent, "header", factory),
2966
+ setTitle: (title) => putSurface(ctx, state, agent, "title", title),
2369
2967
  custom: async () => void 0,
2370
2968
  pasteToEditor(text) {
2371
- if (agent !== void 0) state.editorBuffers.set(agent, (state.editorBuffers.get(agent) ?? "") + String(text));
2969
+ const surfaces = state.shared.browserSurfaces;
2970
+ const sessionId = sessionIdOf(state, agent);
2971
+ const next = (surfaces?.liveDraft(sessionId) ?? (agent === void 0 ? "" : state.editorBuffers.get(agent) ?? "")) + String(text);
2972
+ if (agent !== void 0) state.editorBuffers.set(agent, next);
2973
+ surfaces?.requestDraft(sessionId, next);
2372
2974
  },
2373
2975
  setEditorText(text) {
2374
2976
  if (agent !== void 0) state.editorBuffers.set(agent, String(text));
2977
+ state.shared.browserSurfaces?.requestDraft(sessionIdOf(state, agent), String(text));
2978
+ },
2979
+ getEditorText: () => {
2980
+ const live = state.shared.browserSurfaces?.liveDraft(sessionIdOf(state, agent)) ?? "";
2981
+ if (live.length > 0) return live;
2982
+ return agent === void 0 ? "" : state.editorBuffers.get(agent) ?? "";
2375
2983
  },
2376
- getEditorText: () => agent === void 0 ? "" : state.editorBuffers.get(agent) ?? "",
2377
2984
  addAutocompleteProvider(factory) {
2378
2985
  state.autocompleteProviders.push(factory);
2379
2986
  },
@@ -3206,7 +3813,7 @@ function subscribeInterceptors(ctx, state) {
3206
3813
  });
3207
3814
  const systemPrompt = optionalService(ctx, "systemPrompt");
3208
3815
  if (systemPrompt !== void 0) ctx.effect(() => systemPrompt.section({
3209
- name: "pi2dsh:tool-guidance",
3816
+ name: `pi2dsh:tool-guidance:${state.packageName ?? "pi"}`,
3210
3817
  order: 150,
3211
3818
  text: () => piToolPromptContribution(ctx, state)
3212
3819
  }));
@@ -3423,6 +4030,7 @@ function sharedHostStateOf(ctx) {
3423
4030
  shared = {
3424
4031
  companionRoutes: /* @__PURE__ */ new Map(),
3425
4032
  providerRouteDisposers: /* @__PURE__ */ new Map(),
4033
+ publishedOAuthKeys: /* @__PURE__ */ new Map(),
3426
4034
  providers: /* @__PURE__ */ new Map()
3427
4035
  };
3428
4036
  SHARED_HOST_STATE.set(key, shared);
@@ -3561,6 +4169,206 @@ function ensureLoginCommand(ctx, state) {
3561
4169
  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)})`);
3562
4170
  }
3563
4171
  }
4172
+ /**
4173
+ * Turn what the user answered into one of the offered options.
4174
+ * @param answer - the label, a differently-cased label, or a 1-based position.
4175
+ * @param offered - the options as they were shown, in order.
4176
+ * @returns the matching option, or undefined when nothing matches.
4177
+ */
4178
+ function resolveOfferedChoice(answer, offered) {
4179
+ const trimmed = answer.trim();
4180
+ if (offered.includes(trimmed)) return trimmed;
4181
+ const insensitive = offered.find((name) => name.toLowerCase() === trimmed.toLowerCase());
4182
+ if (insensitive !== void 0) return insensitive;
4183
+ if (/^\d+$/u.test(trimmed)) {
4184
+ const at = Number(trimmed) - 1;
4185
+ if (at >= 0 && at < offered.length) return offered[at];
4186
+ }
4187
+ }
4188
+ /**
4189
+ * Cancel the login already in flight, if any, and wait for it to let go.
4190
+ *
4191
+ * Waiting matters: the next flow binds the same fixed callback port, and Pi's
4192
+ * flow answers a taken port by silently degrading to a listener that never
4193
+ * receives a code. Starting the new flow before the old one has closed
4194
+ * reproduces exactly that.
4195
+ * @param state - the mounting package's state, holding the shared host state.
4196
+ * @returns the cancelled provider's name, or undefined when nothing was running.
4197
+ */
4198
+ async function supersedeActiveLogin(state) {
4199
+ const previous = state.shared.activeLogin;
4200
+ if (previous === void 0) return void 0;
4201
+ state.shared.activeLogin = void 0;
4202
+ previous.controller.abort();
4203
+ for (const path of previous.published) revokeAuthorization(path);
4204
+ let timer;
4205
+ const released = await Promise.race([previous.finished.then(() => true), new Promise((resolve) => {
4206
+ timer = setTimeout(() => resolve(false), 5e3);
4207
+ })]);
4208
+ if (timer !== void 0) clearTimeout(timer);
4209
+ if (!released) throw new Error(`the ${previous.providerName} login is still shutting down; try /login again in a moment`);
4210
+ return previous.providerName;
4211
+ }
4212
+ /**
4213
+ * Give a logged-in provider a route, if nothing else already gave it one.
4214
+ *
4215
+ * Pi's built-in OAuth providers (openai-codex, anthropic, github-copilot,
4216
+ * kimi-coding) are seeded so `/login` can offer them, but they carry no
4217
+ * transport and no catalog — so the registration path above never builds them
4218
+ * a route, and a successful login used to leave the model picker unchanged.
4219
+ * A stored credential is the whole precondition: with one, the models pi-ai's
4220
+ * installed catalog describes become selectable through DSH's own adapter.
4221
+ * @param name - the Pi provider id, which is also its DSH route name.
4222
+ * @param config - the provider config, whose oauth block made it loginable.
4223
+ * @returns whether this call put a route in place.
4224
+ */
4225
+ async function ensureLoggedInProviderRoute(ctx, state, name, config) {
4226
+ if (!providerSupportsOAuth(config)) return false;
4227
+ if (providerCarriesTransport(config) || state.providerRouteDisposers.has(name)) return false;
4228
+ const alreadyRouted = llmOf(ctx)?.listProviders().some((provider) => provider.id === name) === true;
4229
+ if (await storedOAuthCredential(oauthStoreOf(state), name).catch(() => void 0) === void 0) return false;
4230
+ const published = await publishOAuthCredential(ctx, state, name, config);
4231
+ if (!published.ok) return false;
4232
+ keepOAuthCredentialFresh(ctx, state);
4233
+ if (alreadyRouted) return false;
4234
+ const settings = optionalService(ctx, "settings");
4235
+ if (settings === void 0) {
4236
+ logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this composition has no settings service to declare its model route in`);
4237
+ return false;
4238
+ }
4239
+ await settings.update("llm-pi-ai", { providers: { [name]: {
4240
+ ...typeof config.name === "string" ? { displayName: config.name } : {},
4241
+ ...published.baseUrl === void 0 ? {} : { baseURL: published.baseUrl },
4242
+ apiKeyEnv: oauthCredentialRef(name)
4243
+ } } });
4244
+ return true;
4245
+ }
4246
+ /**
4247
+ * Put a logged-in provider's current key where the official adapter reads it.
4248
+ *
4249
+ * Pi keeps the OAuth credential (access + refresh + expiry) in its own store;
4250
+ * DSH's adapter reads one opaque value through the credentials seam. This is
4251
+ * the join: Pi's resolution runs — including its double-checked-lock refresh
4252
+ * when the token is inside the five-minute window — and whatever key that
4253
+ * yields is stored under the reference the profile names.
4254
+ * @returns whether a key is now behind the reference, plus any endpoint the
4255
+ * credential itself decided (some providers read their host out of the token).
4256
+ */
4257
+ async function publishOAuthCredential(ctx, state, name, config) {
4258
+ const credentials = optionalService(ctx, "credentials");
4259
+ if (credentials === void 0) {
4260
+ logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this composition has no credentials service to hand the token to`);
4261
+ return { ok: false };
4262
+ }
4263
+ const resolvedAuth = (await resolvePiProviderAuth({
4264
+ providerId: name,
4265
+ providerConfig: config,
4266
+ store: oauthStoreOf(state)
4267
+ }).catch(() => void 0))?.auth;
4268
+ const key = typeof resolvedAuth?.apiKey === "string" ? resolvedAuth.apiKey : void 0;
4269
+ if (key === void 0 || key.length === 0) {
4270
+ logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this provider authenticates with a request header rather than an api key — DSH routes name a credential reference, so no model route was added for it`);
4271
+ return { ok: false };
4272
+ }
4273
+ const ref = oauthCredentialRef(name);
4274
+ const endpoint = typeof resolvedAuth?.baseUrl === "string" ? { baseUrl: resolvedAuth.baseUrl } : {};
4275
+ if ((await credentials.describe(ref).catch(() => void 0))?.writable === false) return {
4276
+ ok: true,
4277
+ ...endpoint
4278
+ };
4279
+ if (state.publishedOAuthKeys.get(name) === key) return {
4280
+ ok: true,
4281
+ ...endpoint
4282
+ };
4283
+ await credentials.set(ref, key);
4284
+ state.publishedOAuthKeys.set(name, key);
4285
+ return {
4286
+ ok: true,
4287
+ ...endpoint
4288
+ };
4289
+ }
4290
+ /**
4291
+ * Re-publish rotating OAuth keys at the moment a request needs them.
4292
+ *
4293
+ * An access token expires (codex's in about an hour), so a value stored once at
4294
+ * login goes stale and every later request fails on a credential the user
4295
+ * believes they supplied. DSH's own credentials contract is per-operation for
4296
+ * exactly this reason, and `llm/stream` is the per-operation seam.
4297
+ *
4298
+ * Whether the refresh is awaited depends on whether this request can still be
4299
+ * served by what is stored. Inside Pi's refresh window the stored token is
4300
+ * still VALID, so refreshing beside the request costs nothing and the rotated
4301
+ * key is there for the next one. Past expiry it is not: publishing in the
4302
+ * background there means the first request after an idle stretch goes out with
4303
+ * a dead token and fails, every time, and only the second one works. So an
4304
+ * expired credential blocks its own request until it has been renewed.
4305
+ */
4306
+ function keepOAuthCredentialFresh(ctx, state) {
4307
+ if (state.shared.oauthRefreshHooked === true) return;
4308
+ state.shared.oauthRefreshHooked = true;
4309
+ ctx.on("llm/stream", ((options, next) => {
4310
+ const provider = typeof options.provider === "string" ? options.provider : void 0;
4311
+ const config = provider === void 0 ? void 0 : state.providers.get(provider);
4312
+ if (provider === void 0 || config === void 0 || !providerSupportsOAuth(config)) return next();
4313
+ const publish = () => publishOAuthCredential(ctx, state, provider, config).catch((error) => logger(ctx).warn(`[pi2dsh] could not refresh the stored credential for ${JSON.stringify(provider)}: ${error instanceof Error ? error.message : String(error)}`));
4314
+ return (async function* () {
4315
+ const stored = await storedOAuthCredential(oauthStoreOf(state), provider).catch(() => void 0);
4316
+ const expires = typeof stored?.expires === "number" ? stored.expires : void 0;
4317
+ if (expires === void 0 || expires <= Date.now()) await publish();
4318
+ else publish();
4319
+ yield* next();
4320
+ })();
4321
+ }));
4322
+ }
4323
+ /**
4324
+ * Ask a provider package to discover its models, then let the host see them.
4325
+ *
4326
+ * Gateway discovery needs a credential, and a credential is not always there
4327
+ * when the provider registers: OAuth providers get theirs from a `/login` the
4328
+ * user runs later. So this is called at registration AND after a login — the
4329
+ * two moments a credential can appear — and it ends by re-announcing the
4330
+ * route, because DSH's directory observers (the model picker among them)
4331
+ * re-read on the route-set notification and nothing else.
4332
+ * @param name - the Pi provider id, which is also its DSH route name.
4333
+ * @param value - the provider config the package registered.
4334
+ * @returns how many models the route lists afterwards, or undefined when the
4335
+ * package has no discovery of its own.
4336
+ */
4337
+ async function discoverProviderModels(ctx, state, name, value) {
4338
+ const refreshModels = value.refreshModels;
4339
+ if (typeof refreshModels !== "function") return void 0;
4340
+ try {
4341
+ const apiKey = ((await resolvePiProviderAuth({
4342
+ providerId: name,
4343
+ providerConfig: value,
4344
+ store: oauthStoreOf(state)
4345
+ }).catch(() => void 0))?.auth)?.apiKey;
4346
+ await Promise.resolve(refreshModels.call(value, {
4347
+ stored: void 0,
4348
+ ...apiKey === void 0 ? {} : { credential: {
4349
+ type: "api_key",
4350
+ key: apiKey
4351
+ } },
4352
+ store: {
4353
+ read: async () => void 0,
4354
+ write: async () => {},
4355
+ delete: async () => {}
4356
+ },
4357
+ allowNetwork: true,
4358
+ signal: new AbortController().signal,
4359
+ publish: async (publication) => {
4360
+ publication.update?.();
4361
+ return true;
4362
+ }
4363
+ }));
4364
+ } catch (error) {
4365
+ logger(ctx).warn(`[pi2dsh] model catalog refresh for Pi provider ${JSON.stringify(name)} failed (its registry entries stay static): ${error instanceof Error ? error.message : String(error)}`);
4366
+ return;
4367
+ }
4368
+ state.modelCatalog?.refresh();
4369
+ state.providerRouteDisposers.get(name)?.reannounce?.();
4370
+ return await llmOf(ctx)?.listModels(name).then((models) => models.length).catch(() => void 0);
4371
+ }
3564
4372
  function registerLoginCommand(ctx, state) {
3565
4373
  registerCommand(ctx, state, {
3566
4374
  name: "login",
@@ -3570,22 +4378,55 @@ function registerLoginCommand(ctx, state) {
3570
4378
  const oauthProviders = [...state.providers.entries()].filter(([, config]) => providerSupportsOAuth(config)).map(([name]) => name);
3571
4379
  if (oauthProviders.length === 0) throw new Error("no registered Pi provider supports OAuth login");
3572
4380
  const ui = commandContext.ui;
3573
- let providerId = args.trim().split(/\s+/u)[0] ?? "";
3574
- if (providerId.length === 0) providerId = oauthProviders.length === 1 ? oauthProviders[0] : String(await ui.select("Log in to which provider?", oauthProviders) ?? "");
3575
- const config = state.providers.get(providerId);
3576
- if (config === void 0 || !providerSupportsOAuth(config)) throw new Error(`unknown OAuth provider ${JSON.stringify(providerId)}; available: ${oauthProviders.join(", ")}`);
4381
+ let answer = args.trim().split(/\s+/u)[0] ?? "";
4382
+ if (answer.length === 0) answer = oauthProviders.length === 1 ? oauthProviders[0] : String(await ui.select("Log in to which provider?", oauthProviders) ?? "");
4383
+ if (answer.length === 0) return "Login cancelled";
4384
+ const providerId = resolveOfferedChoice(answer, oauthProviders);
4385
+ const config = providerId === void 0 ? void 0 : state.providers.get(providerId);
4386
+ if (providerId === void 0 || config === void 0 || !providerSupportsOAuth(config)) throw new Error(`unknown OAuth provider ${JSON.stringify(answer)}; available: ${oauthProviders.join(", ")}`);
3577
4387
  const oauthName = config.oauth?.name ?? providerId;
3578
4388
  const commandSignal = commandContext.signal;
3579
- await loginPiProvider({
4389
+ const superseded = await supersedeActiveLogin(state);
4390
+ if (superseded !== void 0) ui.notify(`Cancelled the ${superseded} login that was still waiting.`);
4391
+ const active = {
4392
+ providerName: oauthName,
4393
+ controller: new AbortController(),
4394
+ finished: Promise.resolve(),
4395
+ published: []
4396
+ };
4397
+ if (commandSignal !== void 0) {
4398
+ if (commandSignal.aborted) active.controller.abort();
4399
+ else commandSignal.addEventListener("abort", () => active.controller.abort(), { once: true });
4400
+ }
4401
+ const attempt = loginPiProvider({
4402
+ open: openBrowser,
4403
+ shorten: (url) => {
4404
+ const path = publishAuthorization(url);
4405
+ const web = ctx.get("webServer");
4406
+ if (path === void 0 || web?.port === void 0) return void 0;
4407
+ active.published.push(path);
4408
+ return `http://${web.host === "0.0.0.0" || web.host === void 0 ? "127.0.0.1" : web.host}:${web.port}${path}`;
4409
+ },
3580
4410
  providerId,
3581
4411
  providerName: oauthName,
3582
4412
  providerConfig: config,
3583
4413
  store: oauthStoreOf(state),
3584
4414
  ui,
3585
- ...commandSignal !== void 0 ? { signal: commandSignal } : {}
4415
+ signal: active.controller.signal
3586
4416
  });
3587
- ui.notify(`Logged in to ${oauthName}; credential stored in ${oauthStoreOf(state).path}`);
3588
- return `Logged in to ${oauthName}`;
4417
+ active.finished = attempt.then(() => void 0, () => void 0);
4418
+ state.shared.activeLogin = active;
4419
+ try {
4420
+ await attempt;
4421
+ } finally {
4422
+ for (const path of active.published) revokeAuthorization(path);
4423
+ if (state.shared.activeLogin === active) state.shared.activeLogin = void 0;
4424
+ }
4425
+ const declared = await ensureLoggedInProviderRoute(ctx, state, providerId, config);
4426
+ const discovered = await discoverProviderModels(ctx, state, providerId, config) ?? (declared ? await llmOf(ctx)?.listModels(providerId).then((list) => list.length).catch(() => void 0) : void 0);
4427
+ const models = discovered === void 0 ? "" : `; ${discovered} models available`;
4428
+ ui.notify(`Logged in to ${oauthName}${models}`);
4429
+ return `Logged in to ${oauthName}${models}`;
3589
4430
  }
3590
4431
  });
3591
4432
  }
@@ -3641,6 +4482,7 @@ function registerTool(ctx, state, tool) {
3641
4482
  };
3642
4483
  const dispose = ctx.tools.register(definition);
3643
4484
  state.toolDisposers.set(tool.name, dispose);
4485
+ if (isKnownImageTool(state.packageName, tool.name)) state.shared.browserSurfaces?.registerImageTool(tool.name);
3644
4486
  }
3645
4487
  function unregisterTool(state, name) {
3646
4488
  const dispose = state.toolDisposers.get(name);
@@ -4011,6 +4853,7 @@ function createPiApi(ctx, state) {
4011
4853
  }
4012
4854
  const routeDisposer = registerPiProviderRoute({
4013
4855
  llm: llmOf(ctx),
4856
+ ctx,
4014
4857
  providerId: name,
4015
4858
  provider: value,
4016
4859
  host: {
@@ -4026,35 +4869,9 @@ function createPiApi(ctx, state) {
4026
4869
  if (routeDisposer !== void 0) {
4027
4870
  state.providerRouteDisposers.set(name, routeDisposer);
4028
4871
  state.modelCatalog?.refresh();
4029
- logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} registered as a native DSH llm route`);
4872
+ if (providerCarriesTransport(value)) logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} registered as a native DSH llm route`);
4030
4873
  } else if (!providerSupportsOAuth(value)) logger(ctx).info(`[pi2dsh] recorded Pi provider ${JSON.stringify(name)}; model calls stay on DSH llm adapters`);
4031
- const refreshModels = value.refreshModels;
4032
- if (typeof refreshModels === "function") (async () => {
4033
- const apiKey = ((await resolvePiProviderAuth({
4034
- providerId: name,
4035
- providerConfig: value,
4036
- store: oauthStoreOf(state)
4037
- }).catch(() => void 0))?.auth)?.apiKey;
4038
- await Promise.resolve(refreshModels.call(value, {
4039
- stored: void 0,
4040
- ...apiKey === void 0 ? {} : { credential: {
4041
- type: "api_key",
4042
- key: apiKey
4043
- } },
4044
- store: {
4045
- read: async () => void 0,
4046
- write: async () => {},
4047
- delete: async () => {}
4048
- },
4049
- allowNetwork: true,
4050
- signal: new AbortController().signal,
4051
- publish: async (publication) => {
4052
- publication.update?.();
4053
- return true;
4054
- }
4055
- }));
4056
- state.modelCatalog?.refresh();
4057
- })().catch((error) => logger(ctx).warn(`[pi2dsh] model catalog refresh for Pi provider ${JSON.stringify(name)} failed (its registry entries stay static): ${error instanceof Error ? error.message : String(error)}`));
4874
+ discoverProviderModels(ctx, state, name, value);
4058
4875
  },
4059
4876
  unregisterProvider(name) {
4060
4877
  state.providers.delete(name);
@@ -4265,6 +5082,20 @@ async function loadExtensions(rootDir, manifest, api, onExtensionError, onCapabi
4265
5082
  aliases[`${family}/pi-ai/compat`] = aiShim;
4266
5083
  aliases[`${family}/pi-ai/oauth`] = aiShim;
4267
5084
  aliases[`${family}/pi-ai/providers/all`] = aiShim;
5085
+ aliases[`${family}/pi-ai/bedrock-provider`] = aiShim;
5086
+ aliases[`${family}/pi-ai/bun-oauth`] = aiShim;
5087
+ for (const api of [
5088
+ "anthropic-messages",
5089
+ "openai-completions",
5090
+ "openai-responses",
5091
+ "openai-codex-responses",
5092
+ "azure-openai-responses",
5093
+ "google-generative-ai",
5094
+ "google-vertex",
5095
+ "mistral-conversations",
5096
+ "bedrock-converse-stream",
5097
+ "pi-messages"
5098
+ ]) aliases[`${family}/pi-ai/api/${api}`] = aiShim;
4268
5099
  }
4269
5100
  const require = createRequire(import.meta.url);
4270
5101
  for (const entry of [
@@ -4357,7 +5188,8 @@ async function applyPiPackage(ctx, options) {
4357
5188
  argMutations: /* @__PURE__ */ new WeakMap(),
4358
5189
  streamingTexts: /* @__PURE__ */ new Map(),
4359
5190
  lastLoggedModels: /* @__PURE__ */ new WeakMap(),
4360
- providerRouteDisposers: shared.providerRouteDisposers
5191
+ providerRouteDisposers: shared.providerRouteDisposers,
5192
+ publishedOAuthKeys: shared.publishedOAuthKeys
4361
5193
  };
4362
5194
  subscribeLifecycle(ctx, state);
4363
5195
  subscribeInterceptors(ctx, state);
@@ -4367,6 +5199,11 @@ async function applyPiPackage(ctx, options) {
4367
5199
  oauth: provider.auth.oauth
4368
5200
  });
4369
5201
  ensureLoginCommand(ctx, state);
5202
+ for (const [name, config] of [...state.providers]) try {
5203
+ await ensureLoggedInProviderRoute(ctx, state, name, config);
5204
+ } catch (error) {
5205
+ logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`);
5206
+ }
4370
5207
  if (options.manifest.skillDirs.length > 0) {
4371
5208
  if (ctx.get("skills") === void 0) logger(ctx).warn("[pi2dsh] migrated skills were not mounted because this DSH composition has no ctx.skills");
4372
5209
  else {
@@ -4397,14 +5234,69 @@ async function applyPiPackage(ctx, options) {
4397
5234
  }));
4398
5235
  ctx.effect(() => () => __setPiAiLlmBridge(void 0));
4399
5236
  }
4400
- const sidePanel = state.shared.sidePanel ??= new SidePanelRegistry();
4401
- if (state.shared.sidePanelRouted !== true) state.shared.sidePanelRouted = registerSidePanelRoute(ctx, sidePanel);
5237
+ const browserSurfaces = state.shared.browserSurfaces ??= new BrowserSurfaces();
5238
+ if (state.shared.browserSurfacesRouted !== true) state.shared.browserSurfacesRouted = registerBrowserSurfaceRoute(ctx, browserSurfaces);
5239
+ ctx.effect(() => browserSurfaces.trackCompletions(state.packageName ?? "pi", async (trigger, query) => {
5240
+ if (state.autocompleteProviders.length === 0) return [];
5241
+ let provider = { getSuggestions: async () => null };
5242
+ for (const factory of state.autocompleteProviders) {
5243
+ if (typeof factory !== "function") continue;
5244
+ provider = factory(provider) ?? provider;
5245
+ }
5246
+ const getSuggestions = provider.getSuggestions;
5247
+ if (typeof getSuggestions !== "function") return [];
5248
+ const line = `${trigger}${query}`;
5249
+ return ((await getSuggestions([line], 0, line.length, { signal: new AbortController().signal }))?.items ?? []).filter((item) => typeof item.value === "string").map((item) => ({
5250
+ value: String(item.value),
5251
+ label: String(item.label ?? item.value),
5252
+ ...typeof item.description === "string" ? { description: item.description } : {}
5253
+ }));
5254
+ }));
5255
+ ctx.effect(() => browserSurfaces.trackEntries(state.packageName ?? "pi", (sessionId) => {
5256
+ const rendered = [];
5257
+ const draw = (renderer, subject) => typeof renderer !== "function" ? void 0 : surfaceText(renderer(subject, {}, state.theme), state.theme);
5258
+ for (const entry of state.bridge.customEntries(sessionId)) {
5259
+ const text = draw(state.entryRenderers.get(entry.customType), {
5260
+ type: "custom",
5261
+ id: entry.id,
5262
+ customType: entry.customType,
5263
+ data: entry.data,
5264
+ timestamp: entry.timestamp
5265
+ });
5266
+ if (text !== void 0) rendered.push({
5267
+ id: entry.id,
5268
+ customType: entry.customType,
5269
+ text
5270
+ });
5271
+ }
5272
+ if (state.messageRenderers.size > 0) {
5273
+ const session = optionalService(ctx, "sessions")?.get(sessionId);
5274
+ const events = session === void 0 ? [] : eventsOf(session);
5275
+ for (const event of events) {
5276
+ const data = event.data ?? {};
5277
+ const customType = data.source?.piCustomType;
5278
+ if (typeof customType !== "string") continue;
5279
+ const text = draw(state.messageRenderers.get(customType), {
5280
+ role: "custom",
5281
+ customType,
5282
+ content: data.content,
5283
+ id: `dsh-${String(event.seq ?? "")}`
5284
+ });
5285
+ if (text !== void 0) rendered.push({
5286
+ id: `dsh-${String(event.seq ?? "")}`,
5287
+ customType,
5288
+ text
5289
+ });
5290
+ }
5291
+ }
5292
+ return rendered;
5293
+ }));
4402
5294
  __setSubagentSessionFactory(async (subagentOptions) => {
4403
5295
  const created = await createBridgedAgentSession(subagentHost(), subagentOptions);
4404
5296
  const parent = agentSession(currentAgent(state));
4405
5297
  const parentId = parent === void 0 ? "" : String(parent.id ?? "");
4406
5298
  if (parentId.length > 0) {
4407
- const dispose = sidePanel.track(parentId, {
5299
+ const dispose = browserSurfaces.track(parentId, {
4408
5300
  id: String(created.session.sessionId ?? "") || `pi2dsh-thread-${parentId}-${sidePanelSerial++}`,
4409
5301
  label: childLabel(subagentOptions.label, state.packageName),
4410
5302
  package: state.packageName,
@@ -4467,14 +5359,17 @@ async function applyPiPackage(ctx, options) {
4467
5359
  }
4468
5360
  const runtimeInternals = {
4469
5361
  compactionReason,
5362
+ resolveOfferedChoice,
4470
5363
  dshToPiContent,
4471
5364
  expandPrompt,
5365
+ isKnownImageTool,
4472
5366
  isSubagentOrigin,
4473
5367
  normalizeToolResult,
4474
5368
  splitArguments,
5369
+ supersedeActiveLogin,
4475
5370
  textBlocks
4476
5371
  };
4477
5372
  //#endregion
4478
5373
  export { runtimeInternals as i, normalizeToolSchema as n, registerVisionCompanions as r, applyPiPackage as t };
4479
5374
 
4480
- //# sourceMappingURL=runtime-B_Ig28og.mjs.map
5375
+ //# sourceMappingURL=runtime-B-SlR8mJ.mjs.map