@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.5

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 (178) hide show
  1. package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-BBmQXYDq.js} +1 -1
  2. package/dist/assets/AssistantView-BG8hK4y9.js +3 -0
  3. package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-CN0qWNU5.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
  5. package/dist/assets/Icon-B9kZgYZn.js +1 -0
  6. package/dist/assets/{MarkdownRenderer-CJHSsWin.js → MarkdownRenderer-Ds61--LT.js} +2 -2
  7. package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-DOMmiOH0.js} +1 -1
  8. package/dist/assets/{MarkstreamRendererImpl-CSnbQMu3.js → MarkstreamRendererImpl-F7MGg5ER.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DcmVOjkw.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-C1Fc4ErB.js} +2 -2
  11. package/dist/assets/MobileSurface-BA7eS5Yd.js +2 -0
  12. package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-BoTnH6Yp.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BxItVnnO.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-B4e0QD05.js} +1 -1
  15. package/dist/assets/SettingsView-CjesWNLw.js +25 -0
  16. package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CReMbg-w.js} +1 -1
  17. package/dist/assets/TerminalView-C64-QYOG.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-CJu8DNrt.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-4MR0F7Jz.js → appThemeRegistry-CF4lIxNt.js} +1 -1
  20. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  21. package/dist/assets/{button-DKuz3L8l.js → button-CwXyux0a.js} +1 -1
  22. package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
  23. package/dist/assets/context-menu-DHrRifPE.js +1 -0
  24. package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-JGFqhlaM.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-DDlVKyOE.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  26. package/dist/assets/{diagram-D7T8HQgj.js → diagram-BwnPNKWt.js} +1 -1
  27. package/dist/assets/{dialog-DQYl3W_o.js → dialog-Dp0lQv5K.js} +1 -1
  28. package/dist/assets/es-DicJ8lKD.js +15 -0
  29. package/dist/assets/{fileMentionTouchSelection-BZ6ovBYH.js → fileMentionTouchSelection-DPZRxNa2.js} +3 -4
  30. package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DN6gbLhM.js} +1 -1
  31. package/dist/assets/fr-lhgVcBW_.js +13 -0
  32. package/dist/assets/{gitApi-CM42qUed.js → gitApi-D8R7ic-T.js} +3 -3
  33. package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BMztRu0c.js} +1 -1
  34. package/dist/assets/{input-CzQy8UhV.js → input-Biw8aNzR.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DdwCD4S2.js +1 -0
  36. package/dist/assets/ja-B9EZwOcw.js +15 -0
  37. package/dist/assets/ko-Dxk-uJRb.js +15 -0
  38. package/dist/assets/main-BhlwwJ2c.js +169 -0
  39. package/dist/assets/main-D2BzpnUt.css +1 -0
  40. package/dist/assets/main-DSPCkT74.js +2 -0
  41. package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-BOOrGCuG.js} +1 -1
  42. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  43. package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-CE9tk-Jz.js} +1 -1
  44. package/dist/assets/miniChat-Ckk9ABql.js +2 -0
  45. package/dist/assets/mobile-cLMRZqI9.js +2 -0
  46. package/dist/assets/multirun-DVnFRb1V.js +4 -0
  47. package/dist/assets/persistence-D0y7226-.js +3 -0
  48. package/dist/assets/pl-CFhwWLJj.js +15 -0
  49. package/dist/assets/platform-D_c-T5nO.js +6 -0
  50. package/dist/assets/projectMeta-CSQgHequ.js +3 -0
  51. package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
  52. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  53. package/dist/assets/renderElectronMiniChatApp-gGFkwoL_.js +2 -0
  54. package/dist/assets/renderMobileApp-B2w50kAc.js +28 -0
  55. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  56. package/dist/assets/{runtimeConfig-BIGBRGQI.js → runtimeConfig-BHH1hJDd.js} +1 -1
  57. package/dist/assets/runtimeEndpointReset-5uJ4sJk0.js +149 -0
  58. package/dist/assets/{sessionChangeSummary-eIbpSfrO.js → sessionChangeSummary-spf_atO8.js} +2 -2
  59. package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-BGjjwiCq.js} +1 -1
  60. package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
  61. package/dist/assets/src-BjEyU6if.css +2 -0
  62. package/dist/assets/store-DmowK-GO.js +15 -0
  63. package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-Dhqqp4xi.js} +1 -1
  64. package/dist/assets/textarea-QxsmcBGj.js +1 -0
  65. package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-CpMLl5_k.js} +1 -1
  66. package/dist/assets/uk-ChchBzy6.js +15 -0
  67. package/dist/assets/{url-CDHnjlsm.js → url-CGbJbiJq.js} +1 -1
  68. package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-B1h7ab_J.js} +55 -50
  69. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  70. package/dist/assets/useDesktopWindowControlsLayout-Cxsx84NO.js +1 -0
  71. package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-DsqhqsJR.js} +1 -1
  72. package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-Xw1vSkmC.js} +1 -1
  73. package/dist/assets/useMobileNavigationStore-B1axS4g-.js +414 -0
  74. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  75. package/dist/assets/useSessionAutoCleanup-B5FTG_qf.js +2 -0
  76. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  77. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  78. package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-DC_Bc-RV.js} +1 -1
  79. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  80. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  81. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  84. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  85. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  86. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  87. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  88. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  89. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  90. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  91. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-B8parU1Y.js} +1 -1
  92. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  93. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  94. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  95. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  96. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
  97. package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
  98. package/dist/assets/zh-TW-CZeTm7kd.js +15 -0
  99. package/dist/index.html +14 -14
  100. package/dist/mini-chat.html +14 -14
  101. package/dist/mobile.html +14 -14
  102. package/package.json +3 -2
  103. package/server/index.js +38 -7
  104. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  105. package/server/lib/browser-provider/contract.js +312 -0
  106. package/server/lib/browser-provider/host.js +421 -0
  107. package/server/lib/browser-provider/host.test.js +304 -0
  108. package/server/lib/browser-provider/routes.js +122 -0
  109. package/server/lib/browser-provider/routes.test.js +120 -0
  110. package/server/lib/browser-provider/selection-store.js +43 -0
  111. package/server/lib/browser-provider/selection-store.test.js +35 -0
  112. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  113. package/server/lib/event-stream/reasoning-projection.js +16 -1
  114. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  115. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  116. package/server/lib/notifications/runtime.js +128 -31
  117. package/server/lib/notifications/runtime.test.js +103 -0
  118. package/server/lib/notifications/template-runtime.js +130 -38
  119. package/server/lib/notifications/template-runtime.test.js +96 -0
  120. package/server/lib/opencode/DOCUMENTATION.md +8 -0
  121. package/server/lib/opencode/config-entity-routes.js +6 -1
  122. package/server/lib/opencode/config-entity-routes.test.js +25 -0
  123. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  124. package/server/lib/opencode/feature-routes-runtime.js +10 -0
  125. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  126. package/server/lib/opencode/proxy.js +6 -1
  127. package/server/lib/opencode/proxy.test.js +18 -3
  128. package/server/lib/opencode/routes.js +30 -0
  129. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  130. package/server/lib/opencode/upgrade-screen.js +299 -0
  131. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  132. package/server/lib/session-metadata/DOCUMENTATION.md +27 -17
  133. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  134. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  135. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  136. package/server/lib/session-metadata/routes.js +5 -1
  137. package/server/lib/session-metadata/session-archive.js +1 -1
  138. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  139. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  140. package/server/lib/session-metadata/session-projection.js +26 -2
  141. package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
  142. package/dist/assets/Icon-BiEQEcih.js +0 -1
  143. package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
  144. package/dist/assets/SettingsView-BRxELjow.js +0 -25
  145. package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
  146. package/dist/assets/collapsible-DXbDynN4.js +0 -3
  147. package/dist/assets/context-menu-CqolsLCm.js +0 -1
  148. package/dist/assets/es-CN3E_zXj.js +0 -15
  149. package/dist/assets/fr-9zZvQZD2.js +0 -13
  150. package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
  151. package/dist/assets/ja-Dnjk_82N.js +0 -15
  152. package/dist/assets/ko-BxCEE99C.js +0 -15
  153. package/dist/assets/main-BILgBI3B.js +0 -169
  154. package/dist/assets/main-BeveiW6F.js +0 -2
  155. package/dist/assets/main-qSeeJRW1.css +0 -1
  156. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  157. package/dist/assets/miniChat-BgrzM3u7.js +0 -2
  158. package/dist/assets/mobile-DtYoQE8C.js +0 -2
  159. package/dist/assets/multirun-jy8dEI96.js +0 -4
  160. package/dist/assets/persistence-C9LQpiOS.js +0 -3
  161. package/dist/assets/pl-CNNsWnPM.js +0 -15
  162. package/dist/assets/platform-BQghHRfp.js +0 -6
  163. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  164. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
  165. package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
  166. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  167. package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
  168. package/dist/assets/src-CLN7Z-zx.css +0 -2
  169. package/dist/assets/store-DLCiZaA6.js +0 -15
  170. package/dist/assets/textarea-CakKvqFw.js +0 -1
  171. package/dist/assets/uk-rwQrxeYM.js +0 -15
  172. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
  173. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
  174. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
  175. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  176. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  177. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  178. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
@@ -0,0 +1,421 @@
1
+ /**
2
+ * In-process host for extensions that answer agent `browser.*` actions.
3
+ *
4
+ * There is no guest process manager here. An installed extension registers a
5
+ * declaration plus either an `answer` function or a loopback `endpoint`. The
6
+ * host does not drive the in-app browser pane. No selected provider is an
7
+ * explicit refusal, never an empty success and never a hidden builtin run.
8
+ */
9
+
10
+ import crypto from 'node:crypto';
11
+
12
+ import {
13
+ BROWSER_PROVIDER_ACTION_TIMEOUT_MS,
14
+ BROWSER_PROVIDER_OPEN_TIMEOUT_MS,
15
+ BROWSER_PROVIDER_RESPONSE_MAX,
16
+ BrowserProviderError,
17
+ extensionDeclaresBrowser,
18
+ normalizeBrowserAction,
19
+ parseBrowserProviderResult,
20
+ readBrowserProviderContext,
21
+ readBrowserProviderId,
22
+ resolveBrowserProviderEndpoint,
23
+ toPublicBrowserProvider,
24
+ } from './contract.js';
25
+
26
+ const catalogUnavailable = () => new BrowserProviderError(
27
+ 'OpenChamber could not read the browser provider catalog, so the action was not run. Try again.',
28
+ { code: 'CATALOG_UNAVAILABLE', status: 503 },
29
+ );
30
+
31
+ const notSent = (message, code, status) => new BrowserProviderError(
32
+ `${message} Nothing was changed.`,
33
+ { code, status },
34
+ );
35
+
36
+ /**
37
+ * @param {{
38
+ * readInstalled?: (records: object[]) => Promise<object[]> | object[],
39
+ * createId?: () => string,
40
+ * fetchImpl?: typeof fetch,
41
+ * selectionStore?: { read: () => Promise<string | null>, write: (id: string) => Promise<void> } | null,
42
+ * }} [options]
43
+ */
44
+ export const createBrowserProviderHost = ({
45
+ readInstalled = (records) => records,
46
+ createId = () => crypto.randomUUID(),
47
+ fetchImpl = globalThis.fetch,
48
+ selectionStore: initialSelectionStore = null,
49
+ } = {}) => {
50
+ /** @type {Map<string, object>} */
51
+ const installed = new Map();
52
+ /** @type {string | null} */
53
+ let selectedId = null;
54
+ let selectionStore = initialSelectionStore;
55
+ /** @type {Promise<void> | null} */
56
+ let selectionLoad = null;
57
+
58
+ const loadRecords = async () => {
59
+ let records;
60
+ try {
61
+ records = await readInstalled([...installed.values()]);
62
+ } catch {
63
+ throw catalogUnavailable();
64
+ }
65
+ if (!Array.isArray(records)) throw catalogUnavailable();
66
+ return records;
67
+ };
68
+
69
+ const ensureSelectionLoaded = async () => {
70
+ if (!selectionStore) return;
71
+ if (!selectionLoad) {
72
+ const store = selectionStore;
73
+ selectionLoad = Promise.resolve().then(() => store.read()).then((saved) => {
74
+ if (selectedId === null) {
75
+ const id = readBrowserProviderId(saved);
76
+ if (id) selectedId = id;
77
+ }
78
+ }).catch((error) => {
79
+ selectionLoad = null;
80
+ throw error;
81
+ });
82
+ }
83
+ try {
84
+ await selectionLoad;
85
+ } catch {
86
+ throw catalogUnavailable();
87
+ }
88
+ };
89
+
90
+ const list = async () => {
91
+ const records = await loadRecords();
92
+ const providers = [];
93
+ for (const record of records) {
94
+ const pub = toPublicBrowserProvider(record);
95
+ if (pub) providers.push(pub);
96
+ }
97
+ return providers;
98
+ };
99
+
100
+ const findServing = async (id) => {
101
+ const records = await loadRecords();
102
+ for (const record of records) {
103
+ const pub = toPublicBrowserProvider(record);
104
+ if (pub && pub.id === id) return { public: pub, record };
105
+ }
106
+ return null;
107
+ };
108
+
109
+ return {
110
+ /**
111
+ * Register an installed extension. A declaration that does not provide
112
+ * browser is rejected; it is not stored and it is not reported as success.
113
+ *
114
+ * @param {object} declaration
115
+ */
116
+ install(declaration) {
117
+ if (!extensionDeclaresBrowser(declaration)) {
118
+ throw new BrowserProviderError(
119
+ 'This extension does not declare that it provides browser, so it was not registered.',
120
+ { code: 'INVALID_DECLARATION', status: 400 },
121
+ );
122
+ }
123
+ if (declaration?.enabled === false) {
124
+ throw new BrowserProviderError(
125
+ 'This extension is not enabled, so it was not registered as a browser provider.',
126
+ { code: 'INVALID_DECLARATION', status: 400 },
127
+ );
128
+ }
129
+ const pub = toPublicBrowserProvider({ ...declaration, enabled: true });
130
+ if (!pub) {
131
+ const endpoint = declaration?.endpoint;
132
+ if (typeof endpoint === 'string' && !resolveBrowserProviderEndpoint(endpoint)) {
133
+ throw new BrowserProviderError(
134
+ 'A browser provider endpoint must be a loopback http(s) URL with no credentials. Nothing was registered.',
135
+ { code: 'INVALID_DECLARATION', status: 400 },
136
+ );
137
+ }
138
+ throw new BrowserProviderError(
139
+ 'This extension declares browser but cannot answer actions, so it was not registered.',
140
+ { code: 'INVALID_DECLARATION', status: 400 },
141
+ );
142
+ }
143
+ const endpoint = typeof declaration.endpoint === 'string'
144
+ ? resolveBrowserProviderEndpoint(declaration.endpoint)
145
+ : null;
146
+ installed.set(pub.id, {
147
+ id: pub.id,
148
+ name: pub.name,
149
+ enabled: true,
150
+ service: { provides: ['browser'] },
151
+ answer: typeof declaration.answer === 'function' ? declaration.answer : null,
152
+ endpoint,
153
+ });
154
+ return pub;
155
+ },
156
+
157
+ list,
158
+
159
+ attachSelectionStore(store) {
160
+ selectionStore = store;
161
+ selectionLoad = null;
162
+ },
163
+
164
+ selectedId() {
165
+ return selectedId;
166
+ },
167
+
168
+ /**
169
+ * Catalog the settings page reads. `selectedId` is null unless that id is
170
+ * in `providers`, so a saved choice for a missing extension is not shown
171
+ * as a connected browser.
172
+ */
173
+ async catalog() {
174
+ await ensureSelectionLoaded();
175
+ const providers = await list();
176
+ const visible = providers.some((provider) => provider.id === selectedId) ? selectedId : null;
177
+ return { providers, selectedId: visible };
178
+ },
179
+
180
+ /**
181
+ * Choose which installed provider answers later agent actions.
182
+ * An unknown or invalid id does not change the choice. The choice is not
183
+ * cleared by this call; a missing extension stays saved but is not listed
184
+ * as connected until it is registered again.
185
+ *
186
+ * @param {string} providerId
187
+ */
188
+ async select(providerId) {
189
+ await ensureSelectionLoaded();
190
+ const id = readBrowserProviderId(providerId);
191
+ if (!id) {
192
+ throw new BrowserProviderError(
193
+ 'id must be a browser provider id. The selection was not changed.',
194
+ { code: 'INVALID_SELECTION', status: 400 },
195
+ );
196
+ }
197
+ const match = await findServing(id);
198
+ if (!match) {
199
+ throw new BrowserProviderError(
200
+ `No browser provider "${id}" is installed, so the selection was not changed.`,
201
+ { code: 'PROVIDER_NOT_FOUND', status: 404 },
202
+ );
203
+ }
204
+ if (selectionStore) {
205
+ try {
206
+ await selectionStore.write(match.public.id);
207
+ } catch {
208
+ throw new BrowserProviderError(
209
+ 'The browser provider selection could not be saved, so it was not changed.',
210
+ { code: 'SELECTION_UNAVAILABLE', status: 503 },
211
+ );
212
+ }
213
+ }
214
+ selectedId = match.public.id;
215
+ return selectedId;
216
+ },
217
+
218
+ /**
219
+ * Send one agent browser action to the selected provider and return that
220
+ * provider's data. No provider, a catalog read failure, or an unusable
221
+ * answer rejects. This never calls the in-app browser pane.
222
+ *
223
+ * @param {{
224
+ * action: unknown,
225
+ * parameters?: unknown,
226
+ * context?: unknown,
227
+ * providerId?: unknown,
228
+ * signal?: AbortSignal,
229
+ * timeoutMs?: number,
230
+ * }} input
231
+ */
232
+ async dispatchAgentBrowserAction(input = {}) {
233
+ const normalized = normalizeBrowserAction(input.action, input.parameters);
234
+ if (input.signal?.aborted) {
235
+ throw notSent('The action was cancelled before it was sent.', 'CANCELLED', 499);
236
+ }
237
+ await ensureSelectionLoaded();
238
+
239
+ const explicitId = readBrowserProviderId(input.providerId);
240
+ const providerId = explicitId || selectedId;
241
+ if (!providerId) {
242
+ throw notSent(
243
+ 'No browser provider is selected, so the action was not run.',
244
+ 'NO_PROVIDER',
245
+ 409,
246
+ );
247
+ }
248
+
249
+ const match = await findServing(providerId);
250
+ if (!match) {
251
+ throw notSent(
252
+ `Browser provider "${providerId}" is not available, so the action was not run.`,
253
+ 'PROVIDER_NOT_FOUND',
254
+ 404,
255
+ );
256
+ }
257
+
258
+ const request = {
259
+ requestId: createId(),
260
+ action: normalized.action,
261
+ parameters: normalized.parameters,
262
+ context: readBrowserProviderContext(input.context),
263
+ };
264
+ const timeoutMs = input.timeoutMs
265
+ ?? (normalized.action === 'browser.open'
266
+ ? BROWSER_PROVIDER_OPEN_TIMEOUT_MS
267
+ : BROWSER_PROVIDER_ACTION_TIMEOUT_MS);
268
+ const raw = await askProvider(match.record, request, {
269
+ timeoutMs,
270
+ signal: input.signal,
271
+ fetchImpl,
272
+ providerName: match.public.name,
273
+ });
274
+ const parsed = parseBrowserProviderResult(raw);
275
+ if (!parsed) {
276
+ throw new BrowserProviderError(
277
+ `The browser provider "${match.public.name}" answered something that is not a browser result. Nothing is known about the page.`,
278
+ { code: 'UNKNOWN_RESULT', status: 502 },
279
+ );
280
+ }
281
+ if (!parsed.ok) {
282
+ throw new BrowserProviderError(parsed.error, { code: 'PROVIDER_REJECTED', status: 400 });
283
+ }
284
+ return { providerId: match.public.id, data: parsed.data };
285
+ },
286
+ };
287
+ };
288
+
289
+ let singleton = null;
290
+
291
+ /** Process host used by routes. Tests construct their own host and do not call this. */
292
+ export const getBrowserProviderHost = () => {
293
+ if (!singleton) singleton = createBrowserProviderHost();
294
+ return singleton;
295
+ };
296
+
297
+ const connectionNeverStarted = (error) => {
298
+ const code = error?.cause?.code || error?.code;
299
+ return code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EHOSTUNREACH' || code === 'EADDRNOTAVAIL';
300
+ };
301
+
302
+ const askProvider = async (record, request, { timeoutMs, signal, fetchImpl, providerName }) => {
303
+ const body = JSON.stringify(request);
304
+ if (typeof record.answer === 'function') {
305
+ return callAnswer(record.answer, request, { timeoutMs, signal, providerName });
306
+ }
307
+ if (record.endpoint) {
308
+ return postEndpoint(record.endpoint, body, { timeoutMs, signal, fetchImpl, providerName });
309
+ }
310
+ throw notSent(
311
+ `The browser provider "${providerName}" cannot be reached, so the action was not run.`,
312
+ 'PROVIDER_UNAVAILABLE',
313
+ 503,
314
+ );
315
+ };
316
+
317
+ const lostAnswer = (providerName) => new BrowserProviderError(
318
+ `The browser provider "${providerName}" was sent this action but no answer came back, so it may or may not have run; read the page before repeating it.`,
319
+ { code: 'REQUEST_FAILED', status: 504 },
320
+ );
321
+
322
+ const callAnswer = async (answer, request, { timeoutMs, signal, providerName }) => {
323
+ let timer;
324
+ let onAbort;
325
+ try {
326
+ return await new Promise((resolve, reject) => {
327
+ timer = setTimeout(() => {
328
+ reject(Object.assign(new Error('timeout'), { code: 'TIMEOUT' }));
329
+ }, timeoutMs);
330
+ if (signal) {
331
+ onAbort = () => reject(Object.assign(new Error('aborted'), { code: 'ABORT' }));
332
+ if (signal.aborted) {
333
+ onAbort();
334
+ return;
335
+ }
336
+ signal.addEventListener('abort', onAbort, { once: true });
337
+ }
338
+ Promise.resolve().then(() => answer(request)).then(resolve, reject);
339
+ });
340
+ } catch (error) {
341
+ if (error instanceof BrowserProviderError) throw error;
342
+ throw lostAnswer(providerName);
343
+ } finally {
344
+ clearTimeout(timer);
345
+ if (signal && onAbort) signal.removeEventListener('abort', onAbort);
346
+ }
347
+ };
348
+
349
+ const postEndpoint = async (endpoint, body, { timeoutMs, signal, fetchImpl, providerName }) => {
350
+ const url = resolveBrowserProviderEndpoint(endpoint);
351
+ if (!url) {
352
+ throw notSent(
353
+ `The browser provider "${providerName}" cannot be reached, so the action was not run.`,
354
+ 'PROVIDER_UNAVAILABLE',
355
+ 503,
356
+ );
357
+ }
358
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
359
+ const signals = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
360
+ let response;
361
+ try {
362
+ response = await fetchImpl(url, {
363
+ method: 'POST',
364
+ redirect: 'error',
365
+ headers: {
366
+ 'content-type': 'application/json',
367
+ accept: 'application/json',
368
+ },
369
+ body,
370
+ signal: signals,
371
+ });
372
+ } catch (error) {
373
+ if (connectionNeverStarted(error)) {
374
+ throw notSent(
375
+ `The browser provider "${providerName}" could not be reached, so the action was not run.`,
376
+ 'PROVIDER_UNAVAILABLE',
377
+ 503,
378
+ );
379
+ }
380
+ throw new BrowserProviderError(
381
+ `The browser provider "${providerName}" was sent this action but no answer came back, so it may or may not have run; read the page before repeating it.`,
382
+ { code: 'REQUEST_FAILED', status: 504 },
383
+ );
384
+ }
385
+ if (response.status !== 200) {
386
+ throw new BrowserProviderError(
387
+ `The browser provider "${providerName}" answered HTTP ${response.status} instead of a result. Nothing is known about the page.`,
388
+ { code: 'UNKNOWN_RESULT', status: 502 },
389
+ );
390
+ }
391
+ const contentLength = Number(response.headers.get('content-length'));
392
+ if (Number.isFinite(contentLength) && contentLength > BROWSER_PROVIDER_RESPONSE_MAX) {
393
+ throw new BrowserProviderError(
394
+ `The browser provider "${providerName}" answered something that is not a browser result. Nothing is known about the page.`,
395
+ { code: 'UNKNOWN_RESULT', status: 502 },
396
+ );
397
+ }
398
+ let text;
399
+ try {
400
+ text = await response.text();
401
+ } catch {
402
+ throw new BrowserProviderError(
403
+ `The browser provider "${providerName}" was sent this action but no answer came back, so it may or may not have run; read the page before repeating it.`,
404
+ { code: 'REQUEST_FAILED', status: 504 },
405
+ );
406
+ }
407
+ if (text.length > BROWSER_PROVIDER_RESPONSE_MAX) {
408
+ throw new BrowserProviderError(
409
+ `The browser provider "${providerName}" answered something that is not a browser result. Nothing is known about the page.`,
410
+ { code: 'UNKNOWN_RESULT', status: 502 },
411
+ );
412
+ }
413
+ try {
414
+ return JSON.parse(text);
415
+ } catch {
416
+ throw new BrowserProviderError(
417
+ `The browser provider "${providerName}" answered something that is not a browser result. Nothing is known about the page.`,
418
+ { code: 'UNKNOWN_RESULT', status: 502 },
419
+ );
420
+ }
421
+ };