@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
@@ -1,5 +1,75 @@
1
+ import { projectSessionMessageRecords } from '../session-turn-pages/session-message-projection.js';
1
2
  import { summarizeText as summarizeSharedText } from '../text/summarization.js';
2
3
 
4
+ const trimmedText = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
5
+
6
+ const isSessionRecord = (value) => {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
8
+ return typeof value.id === 'string'
9
+ || typeof value.sessionID === 'string'
10
+ || typeof value.title === 'string'
11
+ || typeof value.parentID === 'string'
12
+ || (value.metadata && typeof value.metadata === 'object');
13
+ };
14
+
15
+ /**
16
+ * OpenCode 2 `GET /api/session/:id` returns `{ data: Session }`.
17
+ * Legacy responses and tests still return the session object itself.
18
+ * @param {unknown} payload
19
+ * @returns {object | null}
20
+ */
21
+ export const readNotificationSessionRecord = (payload) => {
22
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
23
+ if (isSessionRecord(payload.data)) return payload.data;
24
+ return isSessionRecord(payload) ? payload : null;
25
+ };
26
+
27
+ const TITLE_EVENT_TYPES = new Set(['session.created', 'session.updated', 'session.renamed']);
28
+
29
+ /**
30
+ * Title carried by a session lifecycle event.
31
+ * v2 puts it on `data.title` (`session.created` / `session.renamed`).
32
+ * Legacy events put it on `properties.info.title`.
33
+ * @param {object | null | undefined} payload
34
+ * @returns {{ sessionId: string, title: string } | null}
35
+ */
36
+ export const readSessionTitleFromEvent = (payload) => {
37
+ if (!payload || typeof payload !== 'object' || !TITLE_EVENT_TYPES.has(payload.type)) return null;
38
+ const bodies = [payload.properties, payload.data].filter((body) => body && typeof body === 'object' && !Array.isArray(body));
39
+ for (const body of bodies) {
40
+ const info = body.info && typeof body.info === 'object' ? body.info : null;
41
+ const title = trimmedText(info?.title) || trimmedText(body.title);
42
+ const sessionId = trimmedText(info?.id)
43
+ || trimmedText(info?.sessionID)
44
+ || trimmedText(body.sessionID)
45
+ || trimmedText(body.id);
46
+ if (title && sessionId) return { sessionId, title };
47
+ }
48
+ return null;
49
+ };
50
+
51
+ const readNotificationDirectory = (payload) => {
52
+ const properties = payload?.properties && typeof payload.properties === 'object' ? payload.properties : {};
53
+ const data = payload?.data && typeof payload.data === 'object' && !Array.isArray(payload.data) ? payload.data : {};
54
+ const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
55
+ const dataInfo = data.info && typeof data.info === 'object' ? data.info : {};
56
+ const candidates = [
57
+ properties.directory,
58
+ info.directory,
59
+ info.location?.directory,
60
+ payload?.location?.directory,
61
+ data.directory,
62
+ data.location?.directory,
63
+ dataInfo.directory,
64
+ dataInfo.location?.directory,
65
+ ];
66
+ for (const value of candidates) {
67
+ const directory = trimmedText(value);
68
+ if (directory) return directory;
69
+ }
70
+ return '';
71
+ };
72
+
3
73
  export const createNotificationTemplateRuntime = (deps) => {
4
74
  const {
5
75
  readSettingsFromDisk,
@@ -133,37 +203,45 @@ export const createNotificationTemplateRuntime = (deps) => {
133
203
  return '';
134
204
  };
135
205
 
136
- const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS) => {
206
+ const fetchLastAssistantMessageText = async (sessionId, messageId, maxLength = NOTIFICATION_BODY_MAX_CHARS, directory) => {
137
207
  if (!sessionId) return '';
138
208
 
139
209
  try {
140
- const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
141
- const response = await fetch(`${url}?limit=5`, {
210
+ const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
211
+ const params = new URLSearchParams({ limit: '5', order: 'desc' });
212
+ const scopedDirectory = trimmedText(directory);
213
+ if (scopedDirectory) params.set('directory', scopedDirectory);
214
+ const response = await fetch(`${base}?${params.toString()}`, {
142
215
  method: 'GET',
143
216
  headers: {
144
217
  Accept: 'application/json',
145
- ...getOpenCodeAuthHeaders(),
218
+ ...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
146
219
  },
147
220
  signal: AbortSignal.timeout(3000),
148
221
  });
149
222
 
150
223
  if (!response.ok) return '';
151
224
 
152
- const messages = await response.json().catch(() => null);
153
- if (!Array.isArray(messages)) return '';
225
+ const payload = await response.json().catch(() => null);
226
+ // Legacy lists are a chronological array. OpenCode 2 returns `{ data }`
227
+ // and `order=desc` puts the newest turn first.
228
+ const newestFirst = !Array.isArray(payload);
229
+ const records = Array.isArray(payload)
230
+ ? payload
231
+ : (Array.isArray(payload?.data) ? payload.data : null);
232
+ if (!records) return '';
233
+ const projected = projectSessionMessageRecords(records);
234
+ const messages = newestFirst ? projected : [...projected].reverse();
154
235
 
155
236
  let target = null;
156
237
  if (messageId) {
157
238
  target = messages.find((message) => message?.info?.id === messageId && message?.info?.role === 'assistant');
158
239
  }
159
240
  if (!target) {
160
- for (let i = messages.length - 1; i >= 0; i -= 1) {
161
- const message = messages[i];
162
- if (message?.info?.role === 'assistant' && message?.info?.finish === 'stop') {
163
- target = message;
164
- break;
165
- }
166
- }
241
+ target = messages.find((message) => (
242
+ message?.info?.role === 'assistant'
243
+ && extractTextFromParts(message.parts, maxLength).length > 0
244
+ ));
167
245
  }
168
246
 
169
247
  if (!target || !Array.isArray(target.parts)) return '';
@@ -175,9 +253,9 @@ export const createNotificationTemplateRuntime = (deps) => {
175
253
  };
176
254
 
177
255
  const cacheSessionTitle = (sessionId, title) => {
178
- if (typeof sessionId === 'string' && sessionId.length > 0 && typeof title === 'string' && title.length > 0) {
179
- sessionTitleCache.set(sessionId, title);
180
- }
256
+ const id = trimmedText(sessionId);
257
+ const name = trimmedText(title);
258
+ if (id && name) sessionTitleCache.set(id, name);
181
259
  };
182
260
 
183
261
  const getCachedSessionTitle = (sessionId) => {
@@ -185,15 +263,12 @@ export const createNotificationTemplateRuntime = (deps) => {
185
263
  };
186
264
 
187
265
  const maybeCacheSessionInfoFromEvent = (payload) => {
188
- if (!payload || typeof payload !== 'object') return;
189
- const type = payload.type;
190
- if (type !== 'session.updated' && type !== 'session.created') return;
191
- const info = payload.properties?.info;
192
- if (!info || typeof info !== 'object') return;
193
- cacheSessionTitle(info.id, info.title);
266
+ const titled = readSessionTitleFromEvent(payload);
267
+ if (!titled) return;
268
+ cacheSessionTitle(titled.sessionId, titled.title);
194
269
  };
195
270
 
196
- const fetchSessionInfo = async (sessionId) => {
271
+ const fetchSessionInfo = async (sessionId, directory) => {
197
272
  if (!sessionId) return null;
198
273
 
199
274
  const cached = sessionInfoCache.get(sessionId);
@@ -202,20 +277,24 @@ export const createNotificationTemplateRuntime = (deps) => {
202
277
  }
203
278
 
204
279
  try {
205
- const url = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
280
+ const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
281
+ const url = directory ? `${base}?directory=${encodeURIComponent(directory)}` : base;
206
282
  const response = await fetch(url, {
207
283
  method: 'GET',
208
- headers: { Accept: 'application/json' },
284
+ headers: {
285
+ Accept: 'application/json',
286
+ ...(typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {}),
287
+ },
209
288
  signal: AbortSignal.timeout(2000),
210
289
  });
211
290
  if (!response.ok) {
212
291
  console.warn(`[Notification] fetchSessionInfo: ${response.status} for session ${sessionId}`);
213
292
  return null;
214
293
  }
215
- const data = await response.json().catch(() => null);
216
- if (data && typeof data === 'object') {
217
- sessionInfoCache.set(sessionId, { data, at: Date.now() });
218
- return data;
294
+ const session = readNotificationSessionRecord(await response.json().catch(() => null));
295
+ if (session) {
296
+ sessionInfoCache.set(sessionId, { data: session, at: Date.now() });
297
+ return session;
219
298
  }
220
299
  return null;
221
300
  } catch (error) {
@@ -227,7 +306,11 @@ export const createNotificationTemplateRuntime = (deps) => {
227
306
  const buildTemplateVariables = async (payload, sessionId) => {
228
307
  const info = payload?.properties?.info || {};
229
308
 
230
- let sessionTitle = payload?.properties?.sessionTitle || payload?.properties?.session?.title || (typeof info.sessionTitle === 'string' ? info.sessionTitle : '') || '';
309
+ const fromEvent = readSessionTitleFromEvent(payload);
310
+ let sessionTitle = trimmedText(payload?.properties?.sessionTitle)
311
+ || trimmedText(payload?.properties?.session?.title)
312
+ || trimmedText(info.sessionTitle)
313
+ || (fromEvent && (!sessionId || fromEvent.sessionId === sessionId) ? fromEvent.title : '');
231
314
 
232
315
  if (!sessionTitle && sessionId) {
233
316
  const cached = getCachedSessionTitle(sessionId);
@@ -238,25 +321,34 @@ export const createNotificationTemplateRuntime = (deps) => {
238
321
 
239
322
  let sessionInfo = null;
240
323
  if (!sessionTitle && sessionId) {
241
- sessionInfo = await fetchSessionInfo(sessionId);
242
- if (sessionInfo && typeof sessionInfo.title === 'string') {
243
- sessionTitle = sessionInfo.title;
324
+ sessionInfo = await fetchSessionInfo(sessionId, readNotificationDirectory(payload));
325
+ const fetchedTitle = trimmedText(sessionInfo?.title);
326
+ if (fetchedTitle) {
327
+ sessionTitle = fetchedTitle;
244
328
  cacheSessionTitle(sessionId, sessionTitle);
245
329
  }
246
330
  }
247
331
 
332
+ const eventAgent = trimmedText(info.agent) || trimmedText(info.mode);
333
+ const eventModel = trimmedText(info.modelID)
334
+ || trimmedText(info.model?.modelID)
335
+ || trimmedText(info.model?.id);
336
+ if ((!eventAgent || !eventModel) && sessionId && !sessionInfo) {
337
+ sessionInfo = await fetchSessionInfo(sessionId, readNotificationDirectory(payload));
338
+ }
339
+
248
340
  const agentName = (() => {
249
- const mode = typeof info.agent === 'string' && info.agent.trim().length > 0
250
- ? info.agent.trim()
251
- : (typeof info.mode === 'string' ? info.mode.trim() : '');
341
+ const mode = eventAgent || trimmedText(sessionInfo?.agent) || trimmedText(sessionInfo?.mode);
252
342
  if (!mode) return 'Agent';
253
343
  return mode.split(/[-_\s]+/).filter(Boolean)
254
344
  .map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join(' ');
255
345
  })();
256
346
 
257
347
  const modelName = (() => {
258
- const raw = typeof info.modelID === 'string' ? info.modelID.trim()
259
- : (typeof info.model?.modelID === 'string' ? info.model.modelID.trim() : '');
348
+ const raw = eventModel
349
+ || trimmedText(sessionInfo?.model?.modelID)
350
+ || trimmedText(sessionInfo?.model?.id)
351
+ || trimmedText(sessionInfo?.modelID);
260
352
  if (!raw) return 'Assistant';
261
353
  return raw.split(/[-_]+/).filter(Boolean)
262
354
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
@@ -82,3 +82,99 @@ describe('notification template message extraction', () => {
82
82
  await expect(runtime.fetchLastAssistantMessageText('session-1', 'msg-1')).resolves.toBe('final answer');
83
83
  });
84
84
  });
85
+
86
+ describe('notification session titles', () => {
87
+ afterEach(() => {
88
+ globalThis.fetch = originalFetch;
89
+ });
90
+
91
+ it('reads the OpenCode 2 session title from the data envelope', async () => {
92
+ const runtime = createNotificationTemplateRuntime({
93
+ readSettingsFromDisk: async () => ({}),
94
+ buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
95
+ getOpenCodeAuthHeaders: () => ({ authorization: 'Basic test' }),
96
+ resolveGitBinaryForSpawn: () => 'git',
97
+ });
98
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
99
+ data: { id: 'ses_1', title: '发布说明' },
100
+ })));
101
+
102
+ const variables = await runtime.buildTemplateVariables({
103
+ type: 'message.updated',
104
+ location: { directory: '/repo' },
105
+ properties: { info: { sessionID: 'ses_1', role: 'assistant', finish: 'stop' } },
106
+ }, 'ses_1');
107
+
108
+ expect(variables.session_name).toBe('发布说明');
109
+ expect(globalThis.fetch).toHaveBeenCalledWith(
110
+ 'http://opencode.test/session/ses_1?directory=%2Frepo',
111
+ expect.objectContaining({
112
+ headers: expect.objectContaining({ authorization: 'Basic test' }),
113
+ }),
114
+ );
115
+ });
116
+
117
+ it('keeps a title learned from session.renamed when the session fetch fails', async () => {
118
+ const runtime = createRuntime();
119
+ globalThis.fetch = vi.fn(async () => new Response(null, { status: 401 }));
120
+ runtime.maybeCacheSessionInfoFromEvent({
121
+ type: 'session.renamed',
122
+ data: { sessionID: 'ses_1', title: '修通知标题' },
123
+ });
124
+
125
+ const variables = await runtime.buildTemplateVariables({
126
+ type: 'session.execution.succeeded',
127
+ data: { sessionID: 'ses_1' },
128
+ }, 'ses_1');
129
+
130
+ expect(variables.session_name).toBe('修通知标题');
131
+ });
132
+
133
+ it('reads the latest assistant text from an OpenCode 2 message page', async () => {
134
+ const runtime = createRuntime();
135
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
136
+ data: [
137
+ {
138
+ id: 'msg_new',
139
+ sessionID: 'ses_1',
140
+ type: 'assistant',
141
+ agent: 'build',
142
+ model: { id: 'gpt-5', providerID: 'openai' },
143
+ content: [
144
+ { type: 'reasoning', text: 'private' },
145
+ { type: 'text', text: '发布说明已写好' },
146
+ ],
147
+ time: { created: 2 },
148
+ },
149
+ {
150
+ id: 'msg_old',
151
+ sessionID: 'ses_1',
152
+ type: 'assistant',
153
+ content: [{ type: 'text', text: 'older' }],
154
+ finish: 'stop',
155
+ time: { created: 1 },
156
+ },
157
+ ],
158
+ })));
159
+
160
+ await expect(runtime.fetchLastAssistantMessageText('ses_1')).resolves.toBe('发布说明已写好');
161
+ });
162
+
163
+ it('fills agent and model names from the OpenCode 2 session record', async () => {
164
+ const runtime = createRuntime();
165
+ globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({
166
+ data: { id: 'ses_1', title: '发布说明', agent: 'build', model: { id: 'gpt-5', providerID: 'openai' } },
167
+ })));
168
+
169
+ const variables = await runtime.buildTemplateVariables({
170
+ type: 'session.execution.succeeded',
171
+ data: { sessionID: 'ses_1' },
172
+ }, 'ses_1');
173
+
174
+ expect(variables).toMatchObject({
175
+ session_name: '发布说明',
176
+ agent_name: 'Build',
177
+ model_name: 'Gpt 5',
178
+ });
179
+ });
180
+ });
@@ -15,6 +15,7 @@ This module provides OpenCode server integration utilities for the web server ru
15
15
  - `packages/web/server/lib/opencode/managed-capabilities-runtime.js`: managed-child scheduled-task resources, config injection, rotating bridge identity, and bridge authorization.
16
16
  - `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
17
17
  - `packages/web/server/lib/opencode/ensure-cli.js`: detect local `opencode` version and install the pinned official V2 into the OpenChamber data dir when missing or too old. Never uses a packaged/bundled binary.
18
+ - `packages/web/server/lib/opencode/upgrade-screen.js`: startup gate for a detected OpenCode below `2.0.15`. It does not own process lifecycle. `GET /api/opencode/compatibility` classifies the version already detected by startup. `POST /api/opencode/install-required` installs `max(pin, 2.0.15)` through `installPinnedOpenCode2Cli`, points the managed binary at that cache, and restarts through the existing lifecycle hooks. Success requires the running serve to report `2.0.15` or newer. External serves are not replaced. Failure returns `upgraded: false` and the error; it does not claim the previous process was restored.
18
19
  - `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
19
20
  - `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
20
21
  - `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
@@ -30,6 +31,7 @@ This module provides OpenCode server integration utilities for the web server ru
30
31
  - `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring. The SPA fallback sends `index.html` relative to `root`, so deep links keep working when the install lives under a dot directory.
31
32
  - `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
32
33
  - `packages/web/server/lib/conversations/`: combined create-session-and-prompt orchestration (OpenChamber-owned, registered before generic proxy).
34
+ - `packages/web/server/lib/browser-provider/`: host for an installed extension that declares `browser`. Lists reachable providers, sends one agent browser action to the selected provider, and refuses when none is selected. Does not drive the in-app browser pane. See `browser-provider/DOCUMENTATION.md`.
33
35
  - `packages/web/server/lib/session-turn-pages/`: turn-window session message pagination (`GET /api/openchamber/sessions/:sessionID/messages`), anchor reconcile (`GET .../messages/reconcile`), Changes L2/L3 (`GET .../changes`), and exact message L1 projection (`GET /api/session/:sessionID/message/:messageID`); loops official OpenCode `session.messages` for turn pages and head→anchor gap recovery with continuation/budgets/`resetRequired`; all registered before generic proxy. L1 first-packet / SSE / exact-message paths keep `summary.diffs` as a slim `{ file, status?, additions, deletions }` list plus additive `diffCount`/`hasDiffs` (patch bodies stay on L3). See `session-turn-pages/DOCUMENTATION.md`.
34
36
  - `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
35
37
  - `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: active loopback-port wiring shared by pairing/LAN URLs and the private relay host.
@@ -87,6 +89,8 @@ This module provides OpenCode server integration utilities for the web server ru
87
89
  - `PUT /api/behavior/agents-md`
88
90
  - `POST /api/opencode/upgrade` (ticket 12): installs the pinned/target `@opencode/cli` into the **owned cache** at `~/.config/openchamber/opencode-cli/<version>/`. Managed owned-cache processes restart on that binary; official shared services use that exact binary's `service restart`, then adopt its registration again. Success requires a fresh running-serve version probe and runtime contract verification. Arbitrary external servers, private processes using global CLI, and private bundled binaries return HTTP 409; user installs are never overwritten. Concurrent upgrades are single-flight (`operation` state). Active sessions require `confirmActiveTasks`/`force`. Failure preserves cached binaries, restores the previous binary selection, and reports the failed phase; it does not claim the old process was restored. Shared-service recovery can subsequently re-ensure the registered service.
89
91
  - `GET /api/opencode/upgrade-status`: returns `currentVersion`/`serveVersion`/`cliVersion`, `targetVersion`/`latestVersion` (pin), `supplySource`, `ownership`, `canManage`, `management`, `guidance`, `contract`, and `operation`. Official shared services report `ownership: 'shared-service'`, `processOwnership: 'shared'`, and `canManage: true`. `available` is true only when `canManage` and target > current; unmanaged global/external runtimes report `available: null` plus manual guidance.
92
+ - `GET /api/opencode/compatibility`: startup upgrade-screen status. `state` is `compatible` at OpenCode `2.0.15` or newer, `incompatible` when a detected version is older or not 2.x, and `unavailable` when no version could be read. `canInstall` is true only for a local (managed or bundled) runtime this platform can install into the owned cache. External serves are reported and not replaced.
93
+ - `POST /api/opencode/install-required`: installs the upgrade-screen target (`2.0.15`, or the pin when the pin is newer) into the owned cache and restarts through the existing lifecycle. `upgraded: true` only after the running serve verifies at or above that target. Failure is `upgraded: false` plus `error`. Concurrent installs are single-flight.
90
94
  - `GET /api/opencode/contract` (ticket 11): runtime contract admission for the **running serve** (not health alone). Dimensions: reachable / authenticated / protocolCompatible / executionAllowed / capabilities / migration / CLI↔serve mismatch. Floor `2.0.12` (acceptable OpenCode 2.x at or above); below-min 2.x and 1.x block execution with upgrade guidance; no upper version cap.
91
95
  - `GET /api/opencode/health`: probes managed OpenCode `GET /api/info` (Basic auth, 4s timeout). `healthy` is reachability/version shape via `evaluateOpenCodeHealthBody` (admits official 2.x `ServerInfo` and classic `{ healthy: true, version }`; rejects 1.x / missing / `healthy: false`). Response also includes `executionAllowed`, `protocolCompatible`, and `contract` so clients do not treat health success as full execution semantics. Upstream non-OK responses pass through status with `{ healthy: false, error, contract }`; transport failures return HTTP 503.
92
96
  - `GET /api/opencode/version`: serve + CLI versions, mismatch flag, and contract snapshot
@@ -305,6 +309,10 @@ configuration reload uses official SDK `location.reload`. The historical
305
309
  `/api/config/reload` host/bridge endpoint remains reserved for existing process
306
310
  startup/recovery/binary-change callers, not ordinary Settings mutations.
307
311
 
312
+ Command catalog SDK fetch forwarding preserves the generated request options,
313
+ including authentication headers, and combines caller cancellation with its
314
+ eight-second request deadline.
315
+
308
316
  `config-hot-reload.integration.test.js` is opt-in via
309
317
  `OPENCHAMBER_TEST_OPENCODE_BINARY=<absolute-v2-cli>`. It starts a separate loopback
310
318
  server with isolated HOME/XDG/config/database, checks global command changes,
@@ -531,7 +531,12 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
531
531
  const client = createOpenCodeClient({
532
532
  baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
533
533
  headers: getOpenCodeAuthHeaders(),
534
- fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
534
+ fetch: (request, init) => fetch(request, {
535
+ ...init,
536
+ signal: init?.signal
537
+ ? AbortSignal.any([init.signal, AbortSignal.timeout(8_000)])
538
+ : AbortSignal.timeout(8_000),
539
+ }),
535
540
  });
536
541
  const response = await client.command.list({ location: { directory } });
537
542
  if (!Array.isArray(response?.data)) {
@@ -69,6 +69,31 @@ describe('V2 configuration persistence', () => {
69
69
  });
70
70
 
71
71
  describe('config entity command metadata route', () => {
72
+ it('preserves real SDK authentication and request options when fetching the catalog', async () => {
73
+ const { OpenCode: realOpenCode } = await vi.importActual('@opencode/client');
74
+ createOpencodeClient.mockImplementation(realOpenCode.make);
75
+ const upstream = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
76
+ if (new Headers(init?.headers).get('Authorization') !== 'Basic example') {
77
+ return new Response('Unauthorized', { status: 401 });
78
+ }
79
+ return Response.json({ data: [{ name: 'review', description: 'Review changes' }] });
80
+ });
81
+ try {
82
+ const app = express();
83
+ app.use(express.json());
84
+ registerConfigEntityRoutes(app, createDependencies(() => ({ md: { exists: false }, json: { exists: false } })));
85
+ const response = await request(app).post('/api/config/commands/metadata').send({ catalog: true });
86
+ expect(response.status).toBe(200);
87
+ expect(response.body.commands).toEqual([expect.objectContaining({ name: 'review' })]);
88
+ const [url, init] = upstream.mock.calls[0];
89
+ expect(new URL(url).pathname).toBe('/api/command');
90
+ expect(init.method).toBe('GET');
91
+ expect(init.signal).toBeInstanceOf(AbortSignal);
92
+ } finally {
93
+ upstream.mockRestore();
94
+ }
95
+ });
96
+
72
97
  it('returns metadata for many commands through one request', async () => {
73
98
  const app = express();
74
99
  app.use(express.json());
@@ -0,0 +1,8 @@
1
+ export function readOpenCode2BinaryVersion(binaryPath: string): string;
2
+
3
+ export function installPinnedOpenCode2Cli(options?: {
4
+ version?: string;
5
+ platform?: string;
6
+ arch?: string;
7
+ dataDir?: string;
8
+ }): Promise<string>;
@@ -19,6 +19,9 @@ import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';
19
19
  import { registerAssistantRoutes } from '../assistants/routes.js';
20
20
  import { registerLlmRoutes } from '../llm/routes.js';
21
21
  import { registerMessageQueueRoutes } from '../message-queue/routes.js';
22
+ import { getBrowserProviderHost } from '../browser-provider/host.js';
23
+ import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';
24
+ import { createBrowserProviderSelectionStore } from '../browser-provider/selection-store.js';
22
25
  import { registerSkillRoutes } from './skill-routes.js';
23
26
  import { registerPluginRoutes } from './plugin-routes.js';
24
27
  import { getNpmInfo, clearCache as clearNpmCache } from './npm-registry.js';
@@ -513,6 +516,13 @@ export const createFeatureRoutesRuntime = (dependencies) => {
513
516
  openchamberUserConfigRoot,
514
517
  openchamberDataDir,
515
518
  });
519
+ const browserProviderHost = getBrowserProviderHost();
520
+ if (typeof openchamberDataDir === 'string' && openchamberDataDir) {
521
+ browserProviderHost.attachSelectionStore(createBrowserProviderSelectionStore(
522
+ path.join(openchamberDataDir, 'browser-provider-selection.json'),
523
+ ));
524
+ }
525
+ registerBrowserProviderRoutes(app, { host: browserProviderHost });
516
526
  };
517
527
 
518
528
  return {
@@ -54,6 +54,13 @@ describe('feature routes runtime composition', () => {
54
54
  expect(source).toMatch(/registerScheduledTaskRoutes\(app, \{[\s\S]*runHistoryStore,/);
55
55
  });
56
56
 
57
+ it('registers browser provider routes on the shared host before proxy composition', async () => {
58
+ const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
59
+ expect(source).toContain("import { registerBrowserProviderRoutes } from '../browser-provider/routes.js';");
60
+ expect(source).toContain('registerBrowserProviderRoutes(app, { host: browserProviderHost });');
61
+ expect(source).toContain('createBrowserProviderSelectionStore(');
62
+ });
63
+
57
64
  it('registers message queue routes with the injected service before proxy composition', async () => {
58
65
  const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
59
66
  expect(source).toContain("import { registerMessageQueueRoutes } from '../message-queue/routes.js';");
@@ -957,7 +957,9 @@ export const registerOpenCodeProxy = (app, deps) => {
957
957
  req.path === '/opencode/version' ||
958
958
  req.path === '/opencode/contract' ||
959
959
  req.path === '/opencode/upgrade-status' ||
960
- req.path === '/opencode/upgrade'
960
+ req.path === '/opencode/upgrade' ||
961
+ req.path === '/opencode/compatibility' ||
962
+ req.path === '/opencode/install-required'
961
963
  ) {
962
964
  return next();
963
965
  }
@@ -1207,6 +1209,9 @@ export const registerOpenCodeProxy = (app, deps) => {
1207
1209
  app.get('/api/session/:sessionID/message', (req, res, next) => {
1208
1210
  return forwardSessionMessagesListRequest(req, res, next);
1209
1211
  });
1212
+ app.get('/api/session/:sessionID/context', (req, res, next) => {
1213
+ return forwardSessionMessagesListRequest(req, res, next);
1214
+ });
1210
1215
 
1211
1216
  app.get('/api/global/event', forwardSseRequest);
1212
1217
  app.get('/api/event', forwardSseRequest);
@@ -460,6 +460,21 @@ describe('registerOpenCodeProxy reasoning projection routes', () => {
460
460
  expect(JSON.stringify(res.body)).not.toContain('hidden');
461
461
  });
462
462
 
463
+ it.each(['message', 'context'])('filters native v2 %s before outbound transport', async (surface) => {
464
+ const upstreamUrls = [];
465
+ globalThis.fetch = vi.fn(async (url) => {
466
+ upstreamUrls.push(String(url));
467
+ return new Response(JSON.stringify({ data: [{ id: 'm', type: 'assistant', content: [
468
+ { type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
469
+ ] }], cursor: { next: 'older' } }), { headers: { 'content-type': 'application/json' } });
470
+ });
471
+ const res = await request(mountProxy()).get(`/api/session/ses_1/${surface}?includeReasoning=false`);
472
+ expect(res.status).toBe(200);
473
+ expect(res.body.data[0].content).toEqual([{ type: 'text', text: 'visible' }]);
474
+ expect(res.body.cursor.next).toBe('older');
475
+ expect(upstreamUrls[0]).not.toContain('includeReasoning');
476
+ });
477
+
463
478
  it('keeps reasoning parts on session.messages list when includeReasoning is omitted', async () => {
464
479
  globalThis.fetch = vi.fn(async () => ({
465
480
  ok: true,
@@ -503,7 +518,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
503
518
  return app;
504
519
  };
505
520
 
506
- it('merges Host store metadata onto session list and detail (store keys win)', async () => {
521
+ it('merges side-store gaps onto the OpenCode record without letting the side row win conflicts', async () => {
507
522
  globalThis.fetch = vi.fn(async (url) => {
508
523
  const path = String(url).split('?')[0];
509
524
  if (path.endsWith('/api/session') || path.endsWith('/session')) {
@@ -551,7 +566,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
551
566
  id: 'ses_1',
552
567
  metadata: {
553
568
  fromOpenCode: true,
554
- shared: 'ours',
569
+ shared: 'theirs',
555
570
  openchamber: { goal: { status: 'active' } },
556
571
  },
557
572
  });
@@ -563,7 +578,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
563
578
  id: 'ses_1',
564
579
  metadata: {
565
580
  fromOpenCode: true,
566
- shared: 'ours',
581
+ shared: 'theirs',
567
582
  openchamber: { goal: { status: 'active' } },
568
583
  },
569
584
  });
@@ -11,6 +11,7 @@ import {
11
11
  readOpenCode2BinaryVersion,
12
12
  resolveOpenChamberDataDir,
13
13
  } from './ensure-cli.js';
14
+ import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
14
15
  import { evaluateRuntimeContract } from './runtime-contract.js';
15
16
  import {
16
17
  buildUpgradeStatusSnapshot,
@@ -253,6 +254,35 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
253
254
  return fallback;
254
255
  };
255
256
 
257
+ registerUpgradeScreenRoutes(app, {
258
+ resolveOwnership: resolveOwnershipContext,
259
+ readServeVersion: async () => {
260
+ const probe = await readOpenCodeCurrentVersion().catch(() => ({ currentVersion: null }));
261
+ return probe.currentVersion || null;
262
+ },
263
+ readCliVersion: () => {
264
+ const fromRuntime = typeof getOpenCodeCliVersion === 'function' ? getOpenCodeCliVersion() : null;
265
+ if (fromRuntime) return fromRuntime;
266
+ const binaryPath = typeof getResolvedOpenCodeBinary === 'function' ? getResolvedOpenCodeBinary() : null;
267
+ return binaryPath ? readOpenCode2BinaryVersion(binaryPath) : null;
268
+ },
269
+ readConfiguredBinary: async () => {
270
+ const settings = await readSettingsFromDiskMigrated();
271
+ return typeof settings?.opencodeBinary === 'string' ? settings.opencodeBinary : null;
272
+ },
273
+ persistBinary: async (binaryPath) => {
274
+ await persistSettings({ opencodeBinary: binaryPath });
275
+ },
276
+ getResolvedBinary: getResolvedOpenCodeBinary,
277
+ getResolvedBinarySource: getResolvedOpenCodeBinarySource,
278
+ forceBinary: typeof forceResolvedOpenCodeBinary === 'function' ? forceResolvedOpenCodeBinary : null,
279
+ restart: typeof restartOpenCode === 'function' ? restartOpenCode : null,
280
+ waitReady: typeof waitForOpenCodeReady === 'function' ? waitForOpenCodeReady : null,
281
+ isSharedService: () => getIsSharedOpenCodeService() === true,
282
+ install: (options) => installPinnedOpenCode2Cli({ ...options, dataDir: resolveDataDir() }),
283
+ readBinaryVersion: readOpenCode2BinaryVersion,
284
+ });
285
+
256
286
  app.get('/api/config/settings', async (req, res) => {
257
287
  try {
258
288
  const settings = await readSettingsFromDiskMigrated();
@@ -0,0 +1,30 @@
1
+ export const REQUIRED_OPENCODE_VERSION: string;
2
+
3
+ export type UpgradeScreen = {
4
+ state: 'unavailable' | 'compatible' | 'incompatible';
5
+ version: string | null;
6
+ installation: 'managed' | 'external' | 'bundled';
7
+ minimumVersion: string;
8
+ targetVersion: string;
9
+ canInstall: boolean;
10
+ guidance: string | null;
11
+ };
12
+
13
+ export function describeUpgradeScreen(input?: {
14
+ version?: string | null;
15
+ installation?: 'managed' | 'external' | 'bundled';
16
+ platformCanInstall?: boolean;
17
+ guidance?: string | null;
18
+ pin?: string;
19
+ }): UpgradeScreen;
20
+
21
+ export function platformSupportsUpgradeScreenInstall(platform?: string, arch?: string): boolean;
22
+
23
+ export function installRequiredOpenCode(deps: object): Promise<{
24
+ ok: true;
25
+ upgraded: true;
26
+ version: string;
27
+ targetVersion: string;
28
+ error: null;
29
+ errorCode: null;
30
+ }>;