@openchambery/web 2.0.0-beta.4 → 2.0.0-beta.6

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 (172) hide show
  1. package/dist/assets/{AgentAvatar-Q1SmFRx2.js → AgentAvatar-BBmQXYDq.js} +1 -1
  2. package/dist/assets/AssistantView-BpvbpZBC.js +3 -0
  3. package/dist/assets/{DiagramView-BW92schO.js → DiagramView-UwKDr8HS.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-BZmwl-zE.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
  5. package/dist/assets/Icon-B9kZgYZn.js +1 -0
  6. package/dist/assets/{MarkdownRenderer-Bv05iDZZ.js → MarkdownRenderer-C8tDsO5F.js} +2 -2
  7. package/dist/assets/{MarkdownRendererImpl-C81xqYib.js → MarkdownRendererImpl-DRZ1iTbg.js} +1 -1
  8. package/dist/assets/MarkstreamRendererImpl-fU6uhDTr.js +3 -0
  9. package/dist/assets/{MobileDetailNavigation-BMgIu7zc.js → MobileDetailNavigation-MXUDe0nG.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-BOXGNHmC.js → MobileShareBridge-C7x4jOGb.js} +2 -2
  11. package/dist/assets/MobileSurface-BQSmXYJ_.js +2 -0
  12. package/dist/assets/{MultiRunWindow-B5At2Z95.js → MultiRunWindow-ClfxxgP5.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-bsPLgz4r.js → OnboardingScreen-BxItVnnO.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-4wp4migh.js → OverlayScrollbar-B4e0QD05.js} +1 -1
  15. package/dist/assets/SettingsView-DtWgey4y.js +25 -0
  16. package/dist/assets/{SettingsWindow-CMtX2bMl.js → SettingsWindow-Du8y1EI8.js} +1 -1
  17. package/dist/assets/TerminalView-DL9qkQDj.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-uH1ZZDSS.js → ToolOutputDialog-BXawDmmf.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-DK7q2juD.js → appThemeRegistry-DsSahEsq.js} +1 -1
  20. package/dist/assets/{button-CcNJUxUd.js → button-CwXyux0a.js} +1 -1
  21. package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
  22. package/dist/assets/context-menu-DHrRifPE.js +1 -0
  23. package/dist/assets/{desktopHosts-C6HrP0jq.js → desktopHosts-JGFqhlaM.js} +1 -1
  24. package/dist/assets/{desktopRelayRestore-DdVBp45U.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  25. package/dist/assets/{diagram-CxTs9hCQ.js → diagram-BwnPNKWt.js} +1 -1
  26. package/dist/assets/{dialog-KMVL9QKF.js → dialog-Dp0lQv5K.js} +1 -1
  27. package/dist/assets/es-DicJ8lKD.js +15 -0
  28. package/dist/assets/fileMentionTouchSelection-PZQrywzZ.js +3 -0
  29. package/dist/assets/{fileQueries-CjdBRy8y.js → fileQueries-DN6gbLhM.js} +1 -1
  30. package/dist/assets/fr-lhgVcBW_.js +13 -0
  31. package/dist/assets/{gitApi-yuWg9BN4.js → gitApi-C1oGENfw.js} +4 -4
  32. package/dist/assets/{imageSource-9vQBq3zp.js → imageSource-BMztRu0c.js} +1 -1
  33. package/dist/assets/{input-CrIvk3HZ.js → input-Biw8aNzR.js} +1 -1
  34. package/dist/assets/insertionBoundaries-BmQh-tke.js +1 -0
  35. package/dist/assets/ja-B9EZwOcw.js +15 -0
  36. package/dist/assets/ko-Dxk-uJRb.js +15 -0
  37. package/dist/assets/main-BnyE5H4i.js +169 -0
  38. package/dist/assets/main-D2BzpnUt.css +1 -0
  39. package/dist/assets/main-DOgXHO44.js +2 -0
  40. package/dist/assets/{markdownHeightCache-BmXVY_gc.js → markdownHeightCache-BOOrGCuG.js} +1 -1
  41. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  42. package/dist/assets/{mcpQueries-C7jknHV8.js → mcpQueries-D9Dyzcve.js} +1 -1
  43. package/dist/assets/miniChat-C7FecHIX.js +2 -0
  44. package/dist/assets/mobile-_zrfbam4.js +2 -0
  45. package/dist/assets/multirun-BLFbv4I9.js +4 -0
  46. package/dist/assets/persistence-D0y7226-.js +3 -0
  47. package/dist/assets/pl-CFhwWLJj.js +15 -0
  48. package/dist/assets/platform-D_c-T5nO.js +6 -0
  49. package/dist/assets/projectMeta-BCdCRuYS.js +3 -0
  50. package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
  51. package/dist/assets/renderElectronMiniChatApp-QQq7dj-0.js +2 -0
  52. package/dist/assets/renderMobileApp-BVIkeTKi.js +28 -0
  53. package/dist/assets/{runtimeConfig-BEANkvOd.js → runtimeConfig-S9D4fVI5.js} +1 -1
  54. package/dist/assets/runtimeEndpointReset-B6eE14f9.js +149 -0
  55. package/dist/assets/sessionChangeSummary-DLo3Iv_g.js +2 -0
  56. package/dist/assets/{sessionLookup-34XFmP9C.js → sessionLookup-BXXTw54z.js} +1 -1
  57. package/dist/assets/{sortable-tabs-strip-DyEV0Heg.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
  58. package/dist/assets/src-lS1keDVV.css +2 -0
  59. package/dist/assets/store-DmowK-GO.js +15 -0
  60. package/dist/assets/{terminalApi-CXaBEoa_.js → terminalApi-Dhqqp4xi.js} +1 -1
  61. package/dist/assets/{textarea-BBOlRI9O.js → textarea-QxsmcBGj.js} +1 -1
  62. package/dist/assets/{tooltip-CPI4fr1a.js → tooltip-CpMLl5_k.js} +1 -1
  63. package/dist/assets/uk-ChchBzy6.js +15 -0
  64. package/dist/assets/{url-DRDQXjyX.js → url-CGbJbiJq.js} +1 -1
  65. package/dist/assets/{useAppFontEffects-CiDe42I5.js → useAppFontEffects-BDs0O2qK.js} +56 -51
  66. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  67. package/dist/assets/useDesktopWindowControlsLayout-ojoBEglc.js +1 -0
  68. package/dist/assets/{useEffectiveDirectory-C_CouL5v.js → useEffectiveDirectory-B_nHyDMJ.js} +1 -1
  69. package/dist/assets/{useFontPreferences-B0oUZAiB.js → useFontPreferences-Xw1vSkmC.js} +1 -1
  70. package/dist/assets/useMobileNavigationStore-D_96D661.js +414 -0
  71. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  72. package/dist/assets/useSessionAutoCleanup-Dw34f-EW.js +2 -0
  73. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  74. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  75. package/dist/assets/{useWorkerHighlightedLines-CPNYz6Wf.js → useWorkerHighlightedLines-CNlfP7nA.js} +1 -1
  76. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  77. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  78. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  79. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  80. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  81. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  82. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  83. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  84. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  85. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  86. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  87. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  88. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  89. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  90. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  91. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  92. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  93. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
  94. package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
  95. package/dist/assets/zh-TW-CZeTm7kd.js +15 -0
  96. package/dist/index.html +13 -13
  97. package/dist/mini-chat.html +13 -13
  98. package/dist/mobile.html +13 -13
  99. package/package.json +3 -2
  100. package/server/index.js +38 -7
  101. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  102. package/server/lib/browser-provider/contract.js +312 -0
  103. package/server/lib/browser-provider/host.js +421 -0
  104. package/server/lib/browser-provider/host.test.js +304 -0
  105. package/server/lib/browser-provider/routes.js +122 -0
  106. package/server/lib/browser-provider/routes.test.js +120 -0
  107. package/server/lib/browser-provider/selection-store.js +43 -0
  108. package/server/lib/browser-provider/selection-store.test.js +35 -0
  109. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  110. package/server/lib/notifications/runtime.js +110 -26
  111. package/server/lib/notifications/runtime.test.js +92 -0
  112. package/server/lib/notifications/template-runtime.js +130 -38
  113. package/server/lib/notifications/template-runtime.test.js +96 -0
  114. package/server/lib/opencode/DOCUMENTATION.md +4 -0
  115. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  116. package/server/lib/opencode/feature-routes-runtime.js +10 -0
  117. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  118. package/server/lib/opencode/proxy.js +3 -1
  119. package/server/lib/opencode/proxy.test.js +3 -3
  120. package/server/lib/opencode/routes.js +30 -0
  121. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  122. package/server/lib/opencode/upgrade-screen.js +299 -0
  123. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  124. package/server/lib/session-metadata/DOCUMENTATION.md +27 -17
  125. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  126. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  127. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  128. package/server/lib/session-metadata/routes.js +5 -1
  129. package/server/lib/session-metadata/session-archive.js +1 -1
  130. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  131. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  132. package/server/lib/session-metadata/session-projection.js +26 -2
  133. package/server/lib/session-title/runtime.js +2 -2
  134. package/dist/assets/AssistantView-CwXkiCAd.js +0 -3
  135. package/dist/assets/Icon-AQPvxMiU.js +0 -1
  136. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +0 -3
  137. package/dist/assets/MobileSurface-xuc_z0_G.js +0 -2
  138. package/dist/assets/SettingsView-CJ6tcH5_.js +0 -25
  139. package/dist/assets/TerminalView-Dvr-ysTS.js +0 -8
  140. package/dist/assets/collapsible-ZmZ6rLWs.js +0 -3
  141. package/dist/assets/context-menu-B2LXJgXL.js +0 -1
  142. package/dist/assets/es-CN3E_zXj.js +0 -15
  143. package/dist/assets/fileMentionTouchSelection-7RIwlpkB.js +0 -4
  144. package/dist/assets/fr-9zZvQZD2.js +0 -13
  145. package/dist/assets/insertionBoundaries-BiUCSkx-.js +0 -1
  146. package/dist/assets/ja-Dnjk_82N.js +0 -15
  147. package/dist/assets/ko-BxCEE99C.js +0 -15
  148. package/dist/assets/main-C9mIBo77.js +0 -2
  149. package/dist/assets/main-DiL6w6KF.js +0 -169
  150. package/dist/assets/main-qSeeJRW1.css +0 -1
  151. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  152. package/dist/assets/miniChat-CwtUaElU.js +0 -2
  153. package/dist/assets/mobile-DB-6--ws.js +0 -2
  154. package/dist/assets/multirun-TAsxFI8e.js +0 -4
  155. package/dist/assets/persistence-56kq8Z4V.js +0 -3
  156. package/dist/assets/pl-CNNsWnPM.js +0 -15
  157. package/dist/assets/platform-C13fMVEs.js +0 -6
  158. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  159. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +0 -2
  160. package/dist/assets/renderMobileApp-CGMMqbsO.js +0 -28
  161. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +0 -147
  162. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +0 -2
  163. package/dist/assets/src-ubCWXZhR.css +0 -2
  164. package/dist/assets/store-DLCiZaA6.js +0 -15
  165. package/dist/assets/uk-rwQrxeYM.js +0 -15
  166. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +0 -1
  167. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +0 -414
  168. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +0 -2
  169. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  170. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  171. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  172. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
@@ -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
@@ -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
  }
@@ -518,7 +518,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
518
518
  return app;
519
519
  };
520
520
 
521
- 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 () => {
522
522
  globalThis.fetch = vi.fn(async (url) => {
523
523
  const path = String(url).split('?')[0];
524
524
  if (path.endsWith('/api/session') || path.endsWith('/session')) {
@@ -566,7 +566,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
566
566
  id: 'ses_1',
567
567
  metadata: {
568
568
  fromOpenCode: true,
569
- shared: 'ours',
569
+ shared: 'theirs',
570
570
  openchamber: { goal: { status: 'active' } },
571
571
  },
572
572
  });
@@ -578,7 +578,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
578
578
  id: 'ses_1',
579
579
  metadata: {
580
580
  fromOpenCode: true,
581
- shared: 'ours',
581
+ shared: 'theirs',
582
582
  openchamber: { goal: { status: 'active' } },
583
583
  },
584
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
+ }>;
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Startup upgrade screen for OpenCode below 2.0.15.
3
+ *
4
+ * This module does not own process lifecycle. It classifies the version the
5
+ * existing startup path already detected, and installs through the owned-cache
6
+ * installer (`installPinnedOpenCode2Cli`) plus the caller's restart hook.
7
+ * Success is a verified running version, never a download that has not started.
8
+ */
9
+
10
+ import {
11
+ PINNED_OPENCODE2_VERSION,
12
+ isAcceptableOpenCode2HealthVersion,
13
+ isOpenCode1xVersion,
14
+ isOpenCode2VersionAtLeast,
15
+ npmPackageForOpenCode2,
16
+ } from './opencode2-pin.js';
17
+
18
+ /** Oldest OpenCode this OpenChamber build will enter a session against. */
19
+ export const REQUIRED_OPENCODE_VERSION = '2.0.15';
20
+
21
+ const normalizeVersion = (value) => {
22
+ if (typeof value !== 'string') return null;
23
+ const trimmed = value.trim().replace(/^v/i, '');
24
+ return trimmed || null;
25
+ };
26
+
27
+ /**
28
+ * Install target is the pin when the pin already satisfies the screen, otherwise
29
+ * the screen minimum. A pin still at 2.0.12 must not be what this screen installs.
30
+ * @param {string} [pin]
31
+ */
32
+ export function resolveUpgradeScreenTarget(pin = PINNED_OPENCODE2_VERSION) {
33
+ if (isOpenCode2VersionAtLeast(pin, REQUIRED_OPENCODE_VERSION)) return pin;
34
+ return REQUIRED_OPENCODE_VERSION;
35
+ }
36
+
37
+ /** 2.x at or above the screen minimum. A future major is not this contract. */
38
+ function isUpgradeScreenSatisfied(version) {
39
+ const normalized = normalizeVersion(version);
40
+ if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
41
+ return isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
42
+ }
43
+
44
+ function isOlderThanUpgradeScreenMinimum(version) {
45
+ const normalized = normalizeVersion(version);
46
+ if (!normalized) return false;
47
+ if (isOpenCode1xVersion(normalized)) return true;
48
+ if (!isAcceptableOpenCode2HealthVersion(normalized)) return false;
49
+ return !isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
50
+ }
51
+
52
+ /**
53
+ * @param {string | null | undefined} ownership
54
+ * @returns {'managed' | 'external' | 'bundled'}
55
+ */
56
+ function classifyUpgradeScreenInstallation(ownership) {
57
+ if (ownership === 'external-serve') return 'external';
58
+ if (ownership === 'bundled') return 'bundled';
59
+ return 'managed';
60
+ }
61
+
62
+ export function platformSupportsUpgradeScreenInstall(platform = process.platform, arch = process.arch) {
63
+ try {
64
+ npmPackageForOpenCode2(platform, arch);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ const pickDetectedVersion = (serveVersion, cliVersion) => {
72
+ const serve = normalizeVersion(serveVersion);
73
+ const cli = normalizeVersion(cliVersion);
74
+ if (serve && !isUpgradeScreenSatisfied(serve)) return serve;
75
+ if (serve) return serve;
76
+ return cli;
77
+ };
78
+
79
+ /**
80
+ * @param {{
81
+ * version?: string | null,
82
+ * installation?: 'managed' | 'external' | 'bundled',
83
+ * platformCanInstall?: boolean,
84
+ * guidance?: string | null,
85
+ * pin?: string,
86
+ * }} input
87
+ */
88
+ export function describeUpgradeScreen(input = {}) {
89
+ const version = normalizeVersion(input.version);
90
+ const installation = input.installation === 'external' || input.installation === 'bundled'
91
+ ? input.installation
92
+ : 'managed';
93
+ const state = version == null
94
+ ? 'unavailable'
95
+ : isUpgradeScreenSatisfied(version)
96
+ ? 'compatible'
97
+ : 'incompatible';
98
+ const older = version != null && isOlderThanUpgradeScreenMinimum(version);
99
+ // External processes are not replaced. Bundled and managed locals can receive
100
+ // an owned-cache install; that does not overwrite the user's other binaries.
101
+ const canInstall = state === 'incompatible'
102
+ && older
103
+ && installation !== 'external'
104
+ && input.platformCanInstall === true;
105
+ const guidance = canInstall
106
+ ? null
107
+ : (typeof input.guidance === 'string' && input.guidance.trim() ? input.guidance.trim() : null);
108
+ return {
109
+ state,
110
+ version,
111
+ installation,
112
+ minimumVersion: REQUIRED_OPENCODE_VERSION,
113
+ targetVersion: resolveUpgradeScreenTarget(input.pin),
114
+ canInstall,
115
+ guidance,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * A running serve below the minimum, or a missing version, is not an upgrade.
121
+ * @param {{ targetVersion?: string | null, serveVersion?: string | null }} input
122
+ */
123
+ export function evaluateUpgradeScreenInstallResult(input = {}) {
124
+ const targetVersion = resolveUpgradeScreenTarget(input.targetVersion || REQUIRED_OPENCODE_VERSION);
125
+ const version = normalizeVersion(input.serveVersion);
126
+ if (!version || !isUpgradeScreenSatisfied(version) || !isOpenCode2VersionAtLeast(version, targetVersion)) {
127
+ const found = version || 'unknown';
128
+ return {
129
+ ok: false,
130
+ upgraded: false,
131
+ version,
132
+ targetVersion,
133
+ errorCode: 'UPGRADE_SCREEN_NOT_VERIFIED',
134
+ error: `OpenCode is still ${found}. OpenChamber requires ${REQUIRED_OPENCODE_VERSION} or newer.`,
135
+ };
136
+ }
137
+ return {
138
+ ok: true,
139
+ upgraded: true,
140
+ version,
141
+ targetVersion,
142
+ error: null,
143
+ errorCode: null,
144
+ };
145
+ }
146
+
147
+ async function readUpgradeScreenStatus(deps) {
148
+ const ownership = typeof deps.resolveOwnership === 'function'
149
+ ? await deps.resolveOwnership()
150
+ : { ownership: 'unknown', guidance: null };
151
+ const installation = classifyUpgradeScreenInstallation(ownership?.ownership);
152
+ const serveVersion = typeof deps.readServeVersion === 'function'
153
+ ? await Promise.resolve(deps.readServeVersion()).catch(() => null)
154
+ : null;
155
+ const cliVersion = typeof deps.readCliVersion === 'function'
156
+ ? await Promise.resolve(deps.readCliVersion()).catch(() => null)
157
+ : null;
158
+ return describeUpgradeScreen({
159
+ version: pickDetectedVersion(serveVersion, cliVersion),
160
+ installation,
161
+ platformCanInstall: typeof deps.platformCanInstall === 'boolean'
162
+ ? deps.platformCanInstall
163
+ : platformSupportsUpgradeScreenInstall(deps.platform, deps.arch),
164
+ guidance: ownership?.guidance ?? null,
165
+ pin: deps.pin,
166
+ });
167
+ }
168
+
169
+ const failure = (message, code, status = 500) => {
170
+ const error = new Error(message);
171
+ error.code = code;
172
+ error.status = status;
173
+ return error;
174
+ };
175
+
176
+ /**
177
+ * Install the screen target into the owned cache and restart through the
178
+ * caller's existing lifecycle hooks. Restores the previous binary selection
179
+ * when verification fails. Never returns upgraded:true for a failed install.
180
+ * @param {object} deps
181
+ */
182
+ export async function installRequiredOpenCode(deps) {
183
+ const status = await readUpgradeScreenStatus(deps);
184
+ if (status.state !== 'incompatible' || status.canInstall !== true) {
185
+ throw failure(
186
+ status.guidance || 'Automatic OpenCode installation is unavailable for this runtime.',
187
+ 'UPGRADE_SCREEN_UNAVAILABLE',
188
+ 409,
189
+ );
190
+ }
191
+ const target = status.targetVersion;
192
+ const previousBinary = typeof deps.getResolvedBinary === 'function' ? deps.getResolvedBinary() : null;
193
+ const previousSource = typeof deps.getResolvedBinarySource === 'function' ? deps.getResolvedBinarySource() : null;
194
+ const previousSetting = typeof deps.readConfiguredBinary === 'function'
195
+ ? await deps.readConfiguredBinary()
196
+ : undefined;
197
+ let persisted = false;
198
+ try {
199
+ if (typeof deps.install !== 'function') {
200
+ throw failure('OpenCode install is not configured.', 'UPGRADE_SCREEN_INSTALL_UNAVAILABLE', 500);
201
+ }
202
+ const installedPath = await deps.install({ version: target });
203
+ const diskVersion = typeof deps.readBinaryVersion === 'function'
204
+ ? await Promise.resolve(deps.readBinaryVersion(installedPath))
205
+ : null;
206
+ if (diskVersion !== target) {
207
+ throw failure(
208
+ `Installed OpenCode version mismatch: expected ${target}, got ${diskVersion || 'unknown'}`,
209
+ 'UPGRADE_SCREEN_BINARY_MISMATCH',
210
+ 500,
211
+ );
212
+ }
213
+ if (typeof deps.persistBinary === 'function') {
214
+ await deps.persistBinary(installedPath);
215
+ persisted = true;
216
+ }
217
+ if (typeof deps.forceBinary === 'function') {
218
+ deps.forceBinary(installedPath, 'installed');
219
+ }
220
+ if (typeof deps.restart === 'function') {
221
+ const shared = typeof deps.isSharedService === 'function' && deps.isSharedService() === true;
222
+ await deps.restart(shared ? { binaryPath: installedPath } : undefined);
223
+ }
224
+ if (typeof deps.waitReady === 'function') {
225
+ await deps.waitReady();
226
+ }
227
+ const serveVersion = typeof deps.readServeVersion === 'function'
228
+ ? await deps.readServeVersion()
229
+ : null;
230
+ const verification = evaluateUpgradeScreenInstallResult({ targetVersion: target, serveVersion });
231
+ if (!verification.ok) {
232
+ throw failure(verification.error, verification.errorCode, 500);
233
+ }
234
+ return verification;
235
+ } catch (error) {
236
+ if (persisted && typeof deps.persistBinary === 'function') {
237
+ try {
238
+ await deps.persistBinary(typeof previousSetting === 'string' ? previousSetting : '');
239
+ } catch {
240
+ // Keep the install error. A failed restore must not become success.
241
+ }
242
+ }
243
+ if (previousBinary && typeof deps.forceBinary === 'function') {
244
+ try {
245
+ deps.forceBinary(previousBinary, previousSource || 'discovered');
246
+ } catch {
247
+ // Same: report the install failure, not a restored-success.
248
+ }
249
+ }
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ export function registerUpgradeScreenRoutes(app, deps) {
255
+ app.get('/api/opencode/compatibility', async (_req, res) => {
256
+ try {
257
+ res.json(await readUpgradeScreenStatus(deps));
258
+ } catch (error) {
259
+ res.status(503).json({
260
+ error: error instanceof Error ? error.message : 'Could not check OpenCode compatibility',
261
+ });
262
+ }
263
+ });
264
+
265
+ let inFlight = null;
266
+ app.post('/api/opencode/install-required', async (_req, res) => {
267
+ try {
268
+ if (!inFlight) {
269
+ inFlight = installRequiredOpenCode(deps).finally(() => {
270
+ inFlight = null;
271
+ });
272
+ }
273
+ const result = await inFlight;
274
+ if (!result || result.ok !== true || result.upgraded !== true || !result.version) {
275
+ return res.status(500).json({
276
+ success: false,
277
+ upgraded: false,
278
+ error: result?.error || 'OpenCode installation did not verify a new version',
279
+ errorCode: result?.errorCode || 'UPGRADE_SCREEN_NOT_VERIFIED',
280
+ version: result?.version ?? null,
281
+ });
282
+ }
283
+ return res.json({
284
+ success: true,
285
+ upgraded: true,
286
+ version: result.version,
287
+ targetVersion: result.targetVersion,
288
+ });
289
+ } catch (error) {
290
+ const status = Number.isInteger(error?.status) ? error.status : 500;
291
+ return res.status(status).json({
292
+ success: false,
293
+ upgraded: false,
294
+ error: error instanceof Error ? error.message : 'OpenCode installation failed',
295
+ errorCode: error?.code || 'UPGRADE_SCREEN_FAILED',
296
+ });
297
+ }
298
+ });
299
+ }