@vantran-se/openchamber-web 2.0.3 → 2.0.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 (265) hide show
  1. package/README.md +20 -28
  2. package/bin/cli.js +3 -1
  3. package/bin/cli.test.js +13 -4
  4. package/bin/lib/DOCUMENTATION.md +4 -1
  5. package/bin/lib/commands-serve.js +9 -0
  6. package/dist/assets/{CodeMirrorCommentWidgets-CWhdh0wf.js → CodeMirrorCommentWidgets-DCAQTLMM.js} +1 -1
  7. package/dist/assets/DiffView-uzNpHBlV.js +4 -0
  8. package/dist/assets/FilesView-Bkq0sjNx.js +17 -0
  9. package/dist/assets/{GitView-DKIjpnu4.js → GitView-BmibZC7n.js} +4 -4
  10. package/dist/assets/{GuestAttachDialog-BIa9jCfL.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
  11. package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
  12. package/dist/assets/{JsonTreeView-CtI1Mqwo.js → JsonTreeView-BNT2yUVu.js} +1 -1
  13. package/dist/assets/{LinearIssuesView-BD2zLu-r.js → LinearIssuesView-D3Azksju.js} +1 -1
  14. package/dist/assets/MarkdownImageGallery-BGY6tuiK.js +1 -0
  15. package/dist/assets/MarkdownRendererImpl-DJmhQ37f.js +12 -0
  16. package/dist/assets/{OnboardingScreen-B8TxloXZ.js → OnboardingScreen-D_41H2qC.js} +1 -1
  17. package/dist/assets/{PatchDiff--Civs1xY.js → PatchDiff-C0RSqa9W.js} +1 -1
  18. package/dist/assets/{PierreDiffViewer-BlDmw3-A.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
  19. package/dist/assets/{PlanView-DyH2SyCP.js → PlanView-Bqc-xuDe.js} +4 -4
  20. package/dist/assets/PluginPane-DQ0k41Q-.js +3 -0
  21. package/dist/assets/SettingsView-BAiJU5hJ.js +30 -0
  22. package/dist/assets/{SettingsWindow-y9WpZqK6.js → SettingsWindow-DuC4ecLw.js} +1 -1
  23. package/dist/assets/{SubmoduleDiffSummary-DeFmhPFZ.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
  24. package/dist/assets/{ToolOutputDialog-8DmF7_PZ.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
  25. package/dist/assets/{ToolPartDiffPreview-Ca15DKE1.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
  26. package/dist/assets/{VirtualizedFileDiff-BfEBad09.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
  27. package/dist/assets/{WalkthroughView-Cj3K8Xz6.js → WalkthroughView-BJicp0RC.js} +2 -2
  28. package/dist/assets/{appThemeRegistry-DAdawJZn.js → appThemeRegistry-CiuyzfSW.js} +1 -1
  29. package/dist/assets/changesTree-CePHbFcj.js +1 -0
  30. package/dist/assets/commitAndPush-DTKb6IwT.js +1 -0
  31. package/dist/assets/{computeVirtualFileMetrics-RF-KjfUH.js → computeVirtualFileMetrics-kM_zJNMt.js} +2 -2
  32. package/dist/assets/de-BMnfXm-G.js +10 -0
  33. package/dist/assets/es-DKEvCCL6.js +16 -0
  34. package/dist/assets/fr-D9fgE2xE.js +14 -0
  35. package/dist/assets/index-DUke1nme.css +1 -0
  36. package/dist/assets/{index-CL7ZQ94-.js → index-DWB8bR_g.js} +1 -1
  37. package/dist/assets/index-Di6zUe5h.js +41 -0
  38. package/dist/assets/{isDiffPlainText-D2yJ9fO6.js → isDiffPlainText-DOoIsGqs.js} +1 -1
  39. package/dist/assets/{isFilePlainText-BwMEq1Ey.js → isFilePlainText-bt5VHvcd.js} +1 -1
  40. package/dist/assets/ja-BEXad-KW.js +16 -0
  41. package/dist/assets/ko-DGrQ0lEe.js +16 -0
  42. package/dist/assets/{main-quLSA7jc.js → main-BpLQbZC2.js} +2 -2
  43. package/dist/assets/main-CI9DtHJv.js +896 -0
  44. package/dist/assets/{markdown-shiki.worker-CahLvHIq.js → markdown-shiki.worker-Bg8SoAJJ.js} +8 -5
  45. package/dist/assets/markdownCore-DK2KK5RW.js +7 -0
  46. package/dist/assets/{miniChat-D-lJJibr.js → miniChat-CIWaR8wG.js} +2 -2
  47. package/dist/assets/{mobile-VTtphbW6.js → mobile-CnWKEYIP.js} +2 -2
  48. package/dist/assets/patchFileDiff-Cqn0C0az.js +13 -0
  49. package/dist/assets/pl-BlhkmR5b.js +16 -0
  50. package/dist/assets/pt-BR-Bs96Ew9I.js +16 -0
  51. package/dist/assets/{renderElectronMiniChatApp-DF7ch5eM.js → renderElectronMiniChatApp-D9t0-9nV.js} +2 -2
  52. package/dist/assets/renderMobileApp-CW7fVs_s.js +25 -0
  53. package/dist/assets/runtimeEndpointReset-D0PcCb3K.js +27 -0
  54. package/dist/assets/scheduledTasksApi-Dahv3ws0.js +1 -0
  55. package/dist/assets/{shikiHighlight-_HeCCHx1.js → shikiHighlight-l-MbaNS4.js} +1 -1
  56. package/dist/assets/{textMateThemeFromAppTheme-C95XdFGj.js → textMateThemeFromAppTheme-C-kGcOpD.js} +1 -1
  57. package/dist/assets/tr-CehUK1M9.js +7 -0
  58. package/dist/assets/uk-Bno5WGNz.js +16 -0
  59. package/dist/assets/{useAppFontEffects-ZM9ij19-.js → useAppFontEffects-DDjuwVqi.js} +160 -156
  60. package/dist/assets/{useInlineCommentController-CkfxMJPy.js → useInlineCommentController-Clg8EO3A.js} +2 -2
  61. package/dist/assets/{useRootScrollLock-BtiAC1fj.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
  62. package/dist/assets/{vendor-codemirror-language-data-U-lXzXNK.js → vendor-codemirror-language-data-oKF0MAwd.js} +2 -2
  63. package/dist/assets/vendor-opencode-client-DSZl7jSi.js +12 -0
  64. package/dist/assets/vendor-zod-BSoePkpP.js +39 -0
  65. package/dist/assets/{web-components-C583Pmfm.js → web-components-Bc1WPd4i.js} +1 -1
  66. package/dist/assets/zh-CN-DnAweIWo.js +16 -0
  67. package/dist/assets/zh-TW-BbBBK25p.js +16 -0
  68. package/dist/index.html +5 -5
  69. package/dist/mini-chat.html +5 -5
  70. package/dist/mobile.html +5 -5
  71. package/package.json +3 -3
  72. package/server/index.d.ts +2 -0
  73. package/server/index.js +196 -52
  74. package/server/lib/agent-tool/DOCUMENTATION.md +5 -0
  75. package/server/lib/agent-tool/runtime.js +89 -51
  76. package/server/lib/agent-tool/runtime.test.js +17 -0
  77. package/server/lib/browser-control/DOCUMENTATION.md +14 -2
  78. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  79. package/server/lib/event-stream/global-hub.js +45 -7
  80. package/server/lib/event-stream/global-hub.test.js +128 -2
  81. package/server/lib/event-stream/global-ws-bridge.js +3 -1
  82. package/server/lib/event-stream/rebind.test.js +1 -0
  83. package/server/lib/event-stream/runtime.js +1 -2
  84. package/server/lib/event-stream/upstream-reader.js +3 -2
  85. package/server/lib/git/DOCUMENTATION.md +1 -1
  86. package/server/lib/git/service.js +22 -0
  87. package/server/lib/git/service.test.js +8 -0
  88. package/server/lib/guests/DOCUMENTATION.md +4 -3
  89. package/server/lib/guests/catalog.js +19 -1
  90. package/server/lib/guests/catalog.test.js +22 -0
  91. package/server/lib/guests/html-styles.js +5 -3
  92. package/server/lib/guests/html-styles.test.js +3 -1
  93. package/server/lib/guests/routes.js +11 -3
  94. package/server/lib/guests/service.js +5 -0
  95. package/server/lib/guests/surface.js +55 -9
  96. package/server/lib/guests/surface.test.js +56 -6
  97. package/server/lib/notifications/DOCUMENTATION.md +10 -0
  98. package/server/lib/notifications/emit-route.js +118 -0
  99. package/server/lib/notifications/emit-route.test.js +112 -0
  100. package/server/lib/openchamber-control/actions.js +16 -2
  101. package/server/lib/openchamber-control/service.js +23 -0
  102. package/server/lib/openchamber-control/service.test.js +31 -0
  103. package/server/lib/openchamber-sessions/fork-inheritance.js +103 -0
  104. package/server/lib/openchamber-sessions/fork-inheritance.test.js +69 -0
  105. package/server/lib/openchamber-sessions/routes.js +11 -0
  106. package/server/lib/openchamber-sessions/routes.test.js +18 -0
  107. package/server/lib/opencode/DOCUMENTATION.md +23 -23
  108. package/server/lib/opencode/bootstrap-runtime.js +14 -1
  109. package/server/lib/opencode/compatibility.js +6 -3
  110. package/server/lib/opencode/compatibility.test.js +8 -0
  111. package/server/lib/opencode/config-entity-v2.test.js +27 -0
  112. package/server/lib/opencode/config-paths.test.js +12 -0
  113. package/server/lib/opencode/config-v2.d.ts +2 -0
  114. package/server/lib/opencode/config-v2.js +42 -0
  115. package/server/lib/opencode/config-v2.test.js +17 -0
  116. package/server/lib/opencode/core-routes.js +20 -4
  117. package/server/lib/opencode/core-routes.test.js +41 -0
  118. package/server/lib/opencode/env-runtime.js +1 -8
  119. package/server/lib/opencode/feature-routes-runtime.js +2 -1
  120. package/server/lib/opencode/lifecycle.js +156 -2
  121. package/server/lib/opencode/lifecycle.test.js +255 -1
  122. package/server/lib/opencode/managed-config-file.js +18 -6
  123. package/server/lib/opencode/managed-config-file.test.js +23 -7
  124. package/server/lib/opencode/mcp.js +7 -2
  125. package/server/lib/opencode/network-runtime.js +16 -7
  126. package/server/lib/opencode/network-runtime.test.js +29 -1
  127. package/server/lib/opencode/npm-registry-config.d.ts +1 -0
  128. package/server/lib/opencode/npm-registry-config.js +123 -0
  129. package/server/lib/opencode/npm-registry-config.test.js +167 -0
  130. package/server/lib/opencode/npm-registry.js +5 -6
  131. package/server/lib/opencode/npm-registry.test.js +30 -0
  132. package/server/lib/opencode/openchamber-routes.js +29 -11
  133. package/server/lib/opencode/openchamber-routes.test.js +18 -0
  134. package/server/lib/opencode/project-directory-runtime.js +8 -0
  135. package/server/lib/opencode/project-directory-runtime.test.js +13 -0
  136. package/server/lib/opencode/provider-source-route.test.js +59 -0
  137. package/server/lib/opencode/providers.js +3 -0
  138. package/server/lib/opencode/providers.test.js +62 -0
  139. package/server/lib/opencode/proxy.js +53 -16
  140. package/server/lib/opencode/routes-upgrade.test.js +1 -1
  141. package/server/lib/opencode/routes.js +36 -1
  142. package/server/lib/opencode/server-startup-runtime.js +25 -0
  143. package/server/lib/opencode/server-startup-runtime.test.js +67 -1
  144. package/server/lib/opencode/server-utils-runtime.js +5 -0
  145. package/server/lib/opencode/settings-helpers.js +10 -0
  146. package/server/lib/opencode/settings-helpers.test.js +2 -2
  147. package/server/lib/opencode/settings-registry.json +17 -0
  148. package/server/lib/opencode/settings-runtime.js +1 -0
  149. package/server/lib/opencode/shared-service-runtime.js +155 -0
  150. package/server/lib/opencode/shared-service-runtime.test.js +180 -0
  151. package/server/lib/opencode/shared.js +49 -4
  152. package/server/lib/opencode/shared.test.js +78 -1
  153. package/server/lib/opencode/shutdown-runtime.js +14 -5
  154. package/server/lib/opencode/shutdown-runtime.test.js +77 -2
  155. package/server/lib/opencode/skill-routes.js +28 -12
  156. package/server/lib/opencode/skill-routes.test.js +121 -1
  157. package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
  158. package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
  159. package/server/lib/opencode/upgrade-capability.js +0 -14
  160. package/server/lib/opencode/v1-migration-topup.js +21 -15
  161. package/server/lib/opencode/v1-migration-topup.test.js +14 -6
  162. package/server/lib/opencode/v2-install.d.ts +1 -0
  163. package/server/lib/opencode/v2-install.js +8 -7
  164. package/server/lib/opencode/v2-install.test.js +13 -4
  165. package/server/lib/opencode/watcher.js +3 -1
  166. package/server/lib/opencode/watcher.test.js +54 -0
  167. package/server/lib/opencode/websearch-config.js +13 -1
  168. package/server/lib/quota/providers/kimi.js +1 -1
  169. package/server/lib/realtime-proxy.js +5 -1
  170. package/server/lib/realtime-proxy.test.js +29 -0
  171. package/server/lib/relay/tunnel-host.js +4 -1
  172. package/server/lib/relay/tunnel-host.test.js +11 -0
  173. package/server/lib/session-assist/DOCUMENTATION.md +17 -5
  174. package/server/lib/session-assist/runtime.js +19 -2
  175. package/server/lib/session-assist/runtime.test.js +8 -0
  176. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  177. package/server/lib/small-model/DOCUMENTATION.md +11 -10
  178. package/server/lib/small-model/index.js +27 -32
  179. package/server/lib/small-model/index.test.js +28 -16
  180. package/server/lib/spaces/DOCUMENTATION.md +404 -32
  181. package/server/lib/spaces/code-in-bait.js +145 -6
  182. package/server/lib/spaces/code-in.js +10 -150
  183. package/server/lib/spaces/code-in.test.js +7 -83
  184. package/server/lib/spaces/code-out.js +1819 -0
  185. package/server/lib/spaces/code-out.test.js +3945 -0
  186. package/server/lib/spaces/code-transfer.js +173 -0
  187. package/server/lib/spaces/command-stream.test.js +181 -0
  188. package/server/lib/spaces/dispatcher.js +670 -0
  189. package/server/lib/spaces/dispatcher.test.js +582 -0
  190. package/server/lib/spaces/gatekeeper-channel.js +61 -7
  191. package/server/lib/spaces/gatekeeper-channel.test.js +43 -7
  192. package/server/lib/spaces/gatekeeper-program.cjs +68 -14
  193. package/server/lib/spaces/gatekeeper-program.test.js +118 -3
  194. package/server/lib/spaces/hardening.js +20 -4
  195. package/server/lib/spaces/hardening.test.js +18 -3
  196. package/server/lib/spaces/host-git.js +7 -4
  197. package/server/lib/spaces/host-git.test.js +6 -2
  198. package/server/lib/spaces/host.js +310 -0
  199. package/server/lib/spaces/host.test.js +273 -0
  200. package/server/lib/spaces/journey.js +552 -0
  201. package/server/lib/spaces/journey.test.js +589 -0
  202. package/server/lib/spaces/layout.js +42 -8
  203. package/server/lib/spaces/manager.js +4 -3
  204. package/server/lib/spaces/manager.test.js +6 -0
  205. package/server/lib/spaces/places/code-out.docker.live.test.js +412 -0
  206. package/server/lib/spaces/places/contract-suite.js +36 -0
  207. package/server/lib/spaces/places/dispatcher.docker.live.test.js +296 -0
  208. package/server/lib/spaces/places/docker-live-support.js +27 -4
  209. package/server/lib/spaces/places/docker.js +33 -5
  210. package/server/lib/spaces/places/docker.test.js +42 -3
  211. package/server/lib/spaces/places/escape-suite.js +118 -14
  212. package/server/lib/spaces/places/fake-docker.js +3 -1
  213. package/server/lib/spaces/places/grants.docker.live.test.js +197 -0
  214. package/server/lib/spaces/places/memory-place.js +24 -1
  215. package/server/lib/spaces/places/registry.js +1 -1
  216. package/server/lib/spaces/places/server.docker.live.test.js +69 -33
  217. package/server/lib/spaces/places/sessions.docker.live.test.js +222 -0
  218. package/server/lib/spaces/routes.js +216 -0
  219. package/server/lib/spaces/routes.test.js +213 -0
  220. package/server/lib/spaces/run-command.js +261 -8
  221. package/server/lib/spaces/run-command.test.js +193 -3
  222. package/server/lib/spaces/space-events.js +153 -0
  223. package/server/lib/spaces/space-events.test.js +160 -0
  224. package/server/lib/spaces/space-opencode.js +52 -0
  225. package/server/lib/spaces/space-opencode.test.js +41 -0
  226. package/server/lib/spaces/space-records.js +150 -0
  227. package/server/lib/spaces/space-records.test.js +113 -0
  228. package/server/lib/spaces/space-sessions.js +210 -0
  229. package/server/lib/spaces/space-sessions.test.js +161 -0
  230. package/server/lib/spaces/websocket.js +329 -0
  231. package/server/lib/spaces/websocket.test.js +345 -0
  232. package/server/lib/ui-auth/ui-auth.js +8 -2
  233. package/server/lib/ui-auth/ui-auth.test.js +29 -0
  234. package/server/opencode-proxy.test.js +271 -0
  235. package/server/proxy-headers.js +8 -2
  236. package/server/proxy-headers.test.js +20 -0
  237. package/dist/assets/DiffView-CX3wAX2S.js +0 -4
  238. package/dist/assets/FilesView-OL3CON3r.js +0 -17
  239. package/dist/assets/GuestSurfacePane-B_cqX5I5.js +0 -1
  240. package/dist/assets/MarkdownImageGallery-DFiTFZgj.js +0 -1
  241. package/dist/assets/MarkdownRendererImpl-DZm5sf1W.js +0 -12
  242. package/dist/assets/PluginPane-BibJit86.js +0 -3
  243. package/dist/assets/SettingsView-h-VpVO47.js +0 -29
  244. package/dist/assets/commitAndPush-BIPfOMXY.js +0 -1
  245. package/dist/assets/de-BqCNGROu.js +0 -10
  246. package/dist/assets/es-BTPpTRgN.js +0 -16
  247. package/dist/assets/fr-hxbzMdAg.js +0 -14
  248. package/dist/assets/index-CLcM8HKx.css +0 -1
  249. package/dist/assets/index-CvLgnuNI.js +0 -41
  250. package/dist/assets/ja-_zLwwYiy.js +0 -16
  251. package/dist/assets/ko-CFn3Dzhc.js +0 -16
  252. package/dist/assets/main-BLJaug9I.js +0 -896
  253. package/dist/assets/markdownCore-DS9FdwrI.js +0 -5
  254. package/dist/assets/patchFileDiff-D9iBcTEi.js +0 -13
  255. package/dist/assets/pl-DFHE7xac.js +0 -16
  256. package/dist/assets/pt-BR-CUjZF179.js +0 -16
  257. package/dist/assets/renderMobileApp-CNQhdXsi.js +0 -25
  258. package/dist/assets/runtimeEndpointReset-DeaERc0M.js +0 -27
  259. package/dist/assets/scheduledTasksApi-BD6Vx9pu.js +0 -1
  260. package/dist/assets/tr-BB6sA_zH.js +0 -7
  261. package/dist/assets/uk-CB4o8g-o.js +0 -16
  262. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +0 -12
  263. package/dist/assets/vendor-zod-Gea9uBlY.js +0 -39
  264. package/dist/assets/zh-CN-BRyDuWhc.js +0 -16
  265. package/dist/assets/zh-TW-BDGbWA1Q.js +0 -16
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+ import { registerOpenCodeRoutes } from './routes.js';
5
+
6
+ const createApp = (getProviderSources) => {
7
+ const app = express();
8
+ app.use(express.json());
9
+ registerOpenCodeRoutes(app, {
10
+ resolveProjectDirectory: vi.fn(async () => ({ directory: '/projects/app' })),
11
+ getProviderSources,
12
+ });
13
+ return app;
14
+ };
15
+
16
+ describe('GET /api/provider/:providerId/source', () => {
17
+ it('returns the stored config entry next to the layer sources', async () => {
18
+ const stored = {
19
+ name: 'Stored',
20
+ package: 'aisdk:@ai-sdk/openai-compatible',
21
+ env: ['STORED_KEY'],
22
+ settings: { baseURL: 'https://stored.example.com/v1' },
23
+ models: { m: { modelID: 'm', name: 'M' } },
24
+ };
25
+ const getProviderSources = vi.fn(() => ({
26
+ sources: {
27
+ auth: { exists: false },
28
+ user: { exists: false, path: '/user.json' },
29
+ project: { exists: true, path: '/projects/app/opencode.json' },
30
+ custom: { exists: false, path: null },
31
+ },
32
+ config: stored,
33
+ }));
34
+
35
+ const response = await request(createApp(getProviderSources))
36
+ .get('/api/provider/stored-llm/source?directory=/projects/app')
37
+ .expect(200);
38
+
39
+ expect(getProviderSources).toHaveBeenCalledWith('stored-llm', '/projects/app');
40
+ expect(response.body.config).toEqual(stored);
41
+ expect(response.body.sources.project.exists).toBe(true);
42
+ });
43
+
44
+ it('returns null config when no layer defines the provider', async () => {
45
+ const response = await request(createApp(() => ({
46
+ sources: {
47
+ auth: { exists: false },
48
+ user: { exists: false, path: '/user.json' },
49
+ project: { exists: false, path: null },
50
+ custom: { exists: false, path: null },
51
+ },
52
+ config: null,
53
+ })))
54
+ .get('/api/provider/other/source')
55
+ .expect(200);
56
+
57
+ expect(response.body.config).toBeNull();
58
+ });
59
+ });
@@ -11,6 +11,7 @@ import {
11
11
  deleteSectionEntry,
12
12
  toModelVariants,
13
13
  toProviderEntity,
14
+ readStoredProviderEntry,
14
15
  toProviderPackage,
15
16
  toNpmPackage,
16
17
  } from './config-v2.js';
@@ -43,6 +44,8 @@ function getProviderSources(providerId, workingDirectory) {
43
44
  project: { exists: providerExistsIn(projectConfig, providerId), path: paths.projectPath || null },
44
45
  custom: { exists: providerExistsIn(customConfig, providerId), path: paths.customPath },
45
46
  },
47
+ // Same precedence as the edit scope: custom > project > user.
48
+ config: readStoredProviderEntry([customConfig, projectConfig, userConfig], providerId),
46
49
  };
47
50
  }
48
51
 
@@ -436,6 +436,68 @@ describe('custom provider config persistence', () => {
436
436
  }
437
437
  });
438
438
 
439
+ test('getProviderSources returns the stored entry from a JSONC config, v2 shape, without the key', () => {
440
+ const configPath = path.join(projectDir, '.opencode', 'opencode.jsonc');
441
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
442
+ fs.writeFileSync(configPath, `{
443
+ // v1 spelling, still decoded by OpenCode 2
444
+ "provider": {
445
+ "stored-llm": {
446
+ "name": "Stored",
447
+ "npm": "@ai-sdk/openai-compatible",
448
+ "env": ["STORED_KEY"],
449
+ "options": { "baseURL": "https://stored.example.com/v1", "apiKey": "sk-secret", "timeout": 5 },
450
+ "models": { "m": { "name": "M", "variants": { "high": { "reasoningEffort": "high" } } } },
451
+ },
452
+ },
453
+ }`, 'utf8');
454
+
455
+ const { config } = getProviderSources('stored-llm', projectDir);
456
+ expect(config).toEqual({
457
+ name: 'Stored',
458
+ package: 'aisdk:@ai-sdk/openai-compatible',
459
+ env: ['STORED_KEY'],
460
+ settings: { baseURL: 'https://stored.example.com/v1', timeout: 5 },
461
+ models: { m: { name: 'M', variants: [{ id: 'high', settings: { reasoningEffort: 'high' } }] } },
462
+ });
463
+ expect(getProviderSources('missing-llm', projectDir).config).toBeNull();
464
+ });
465
+
466
+ test('saving the stored entry back keeps env, settings and model fields it does not edit', () => {
467
+ const providerId = 'keep-llm';
468
+ const configPath = path.join(projectDir, '.opencode', 'opencode.json');
469
+ writeJson(configPath, {
470
+ providers: {
471
+ [providerId]: {
472
+ name: 'Keep',
473
+ package: 'aisdk:@ai-sdk/openai-compatible',
474
+ env: ['KEEP_KEY'],
475
+ settings: { baseURL: 'https://keep.example.com/v1', timeout: 5 },
476
+ body: { store: false },
477
+ models: { m: { modelID: 'm', name: 'M', limit: { context: 1000 } } },
478
+ },
479
+ },
480
+ });
481
+
482
+ const { config } = getProviderSources(providerId, projectDir);
483
+ upsertProviderConfig(providerId, {
484
+ name: config.name,
485
+ package: config.package,
486
+ env: config.env,
487
+ settings: { baseURL: config.settings.baseURL },
488
+ models: { m: { name: 'M renamed' } },
489
+ }, projectDir, 'project', { hasStoredAuth: true });
490
+
491
+ expect(readJson(configPath).providers[providerId]).toEqual({
492
+ name: 'Keep',
493
+ package: 'aisdk:@ai-sdk/openai-compatible',
494
+ env: ['KEEP_KEY'],
495
+ settings: { baseURL: 'https://keep.example.com/v1', timeout: 5 },
496
+ body: { store: false },
497
+ models: { m: { modelID: 'm', name: 'M renamed', limit: { context: 1000 } } },
498
+ });
499
+ });
500
+
439
501
  test('custom-scope edit updates custom layer without creating a user entry', () => {
440
502
  const providerId = `custom-scope-${Date.now()}`;
441
503
  const customPath = path.join(projectDir, 'custom-opencode.json');
@@ -236,7 +236,7 @@ const SESSION_LIST_ALLOWED_FIELDS = [
236
236
  'fork',
237
237
  ];
238
238
 
239
- const sanitizeSessionListItem = (session) => {
239
+ export const sanitizeSessionListItem = (session) => {
240
240
  if (!session || typeof session !== 'object' || Array.isArray(session)) {
241
241
  return session;
242
242
  }
@@ -307,6 +307,11 @@ export const registerOpenCodeProxy = (app, deps) => {
307
307
  // migrated.
308
308
  getArchivedSessions = null,
309
309
  getStoredSessionMetadata = null,
310
+ // Isolated spaces, when the feature's switch is on: the merged session list, and the hub
311
+ // whose space events the global SSE stream carries beside the host's. Both absent means
312
+ // the host's own answers go out exactly as before spaces.
313
+ mergeSpaceSessionList = null,
314
+ spaceEventHub = null,
310
315
  } = deps;
311
316
 
312
317
  /**
@@ -477,18 +482,15 @@ export const registerOpenCodeProxy = (app, deps) => {
477
482
  const resolveProxyTarget = () => {
478
483
  const runtimeState = getRuntime();
479
484
 
480
- // `buildOpenCodeUrl` throws while the port is unknown, and the port is
481
- // nulled on several runtime paths (health-check failure, failed restart),
482
- // not just cold start. Checking first keeps a degraded OpenCode from
483
- // making every proxied request pay for a thrown-and-caught exception.
484
- if (runtimeState.openCodePort) {
485
- try {
486
- const resolved = normalizeProxyTarget(buildOpenCodeUrl('/', ''));
487
- if (resolved) {
488
- return resolved;
489
- }
490
- } catch {
485
+ // The shared-service endpoint can remain authoritative even when it has no
486
+ // explicit port (for example, HTTPS on its default port). Resolve it before
487
+ // consulting the legacy runtime snapshot.
488
+ try {
489
+ const resolved = normalizeProxyTarget(buildOpenCodeUrl('/', ''));
490
+ if (resolved) {
491
+ return resolved;
491
492
  }
493
+ } catch {
492
494
  }
493
495
 
494
496
  const externalBase = normalizeProxyTarget(runtimeState.openCodeBaseUrl);
@@ -550,6 +552,7 @@ export const registerOpenCodeProxy = (app, deps) => {
550
552
  let heartbeatTimer = null;
551
553
  let upstreamStallTimer = null;
552
554
  let didUpstreamStall = false;
555
+ let unsubscribeSpaceEvents = null;
553
556
  let writeQueue = Promise.resolve(true);
554
557
  const sseBoundary = createSseBoundaryTracker();
555
558
 
@@ -644,6 +647,26 @@ export const registerOpenCodeProxy = (app, deps) => {
644
647
  return writeQueue;
645
648
  };
646
649
 
650
+ // The events of isolated spaces ride the global stream too, one block each, written
651
+ // only between the upstream's own blocks so a block of the host's is never cut.
652
+ // A directory in the query or in the header scopes the stream to the host's one directory.
653
+ const isGlobalStream = !new URL(requestUrl, 'http://localhost').searchParams.get('directory') && !req.get('x-opencode-directory');
654
+ const pendingSpaceBlocks = [];
655
+ const flushSpaceBlocks = async () => {
656
+ while (pendingSpaceBlocks.length > 0 && sseBoundary.isAtBoundary() && !abortController.signal.aborted) {
657
+ const canContinue = await enqueueSseWrite(pendingSpaceBlocks.shift());
658
+ if (!canContinue) return false;
659
+ }
660
+ return true;
661
+ };
662
+ if (spaceEventHub && isGlobalStream) {
663
+ unsubscribeSpaceEvents = spaceEventHub.subscribeEvent((event) => {
664
+ if (event.spaceId === null) return;
665
+ pendingSpaceBlocks.push(`data: ${JSON.stringify(event.payload)}\n\n`);
666
+ void flushSpaceBlocks();
667
+ }, { spaces: true });
668
+ }
669
+
647
670
  scheduleHeartbeat();
648
671
  resetUpstreamStallTimer();
649
672
 
@@ -660,6 +683,9 @@ export const registerOpenCodeProxy = (app, deps) => {
660
683
  if (!canContinue) {
661
684
  break;
662
685
  }
686
+ if (!await flushSpaceBlocks()) {
687
+ break;
688
+ }
663
689
  }
664
690
  }
665
691
 
@@ -679,6 +705,7 @@ export const registerOpenCodeProxy = (app, deps) => {
679
705
  res.end();
680
706
  }
681
707
  } finally {
708
+ unsubscribeSpaceEvents?.();
682
709
  if (heartbeatTimer) {
683
710
  clearTimeout(heartbeatTimer);
684
711
  heartbeatTimer = null;
@@ -763,7 +790,13 @@ export const registerOpenCodeProxy = (app, deps) => {
763
790
  }
764
791
 
765
792
  res.setHeader('content-type', result.contentType);
766
- res.json(await overlayOwnedStateOnList(sanitizeSessionListPayload(result.payload)));
793
+ const hostList = await overlayOwnedStateOnList(sanitizeSessionListPayload(result.payload));
794
+ // The first page of the global list carries every space's sessions after the host's; a
795
+ // later page, and a list scoped to one directory, are the host's alone.
796
+ const listQuery = new URL(upstreamPath, 'http://localhost').searchParams;
797
+ const scopedToDirectory = Boolean(listQuery.get('directory') || req.get('x-opencode-directory'));
798
+ const wantsSpaces = typeof mergeSpaceSessionList === 'function' && !listQuery.get('cursor') && !scopedToDirectory;
799
+ res.json(wantsSpaces ? await mergeSpaceSessionList(hostList) : hostList);
767
800
  } catch (error) {
768
801
  if (isAbortError(error)) {
769
802
  return;
@@ -973,10 +1006,14 @@ export const registerOpenCodeProxy = (app, deps) => {
973
1006
  router: () => resolveProxyTarget(),
974
1007
  on: {
975
1008
  proxyReq: (proxyReq, req) => {
976
- // Inject OpenCode auth headers
1009
+ // The proxy middleware starts with browser headers. Never let an
1010
+ // OpenChamber client credential reach OpenCode, even when the active
1011
+ // OpenCode connection itself has no authorization header.
1012
+ proxyReq.removeHeader('authorization');
977
1013
  const authHeaders = getOpenCodeAuthHeaders();
978
- if (authHeaders.Authorization) {
979
- proxyReq.setHeader('Authorization', authHeaders.Authorization);
1014
+ for (const [key, value] of Object.entries(authHeaders)) {
1015
+ proxyReq.removeHeader(key);
1016
+ proxyReq.setHeader(key, value);
980
1017
  }
981
1018
 
982
1019
  if (req.headers?.['x-opencode-directory-encoding'] === 'uri') {
@@ -86,7 +86,7 @@ describe('OpenCode upgrade routes', () => {
86
86
  expect(globalThis.fetch).not.toHaveBeenCalled();
87
87
  });
88
88
 
89
- it.each(['external', 'unavailable', 'windows-arm64-workaround'])('rejects %s on the server', async (reason) => {
89
+ it.each(['external', 'unavailable'])('rejects %s on the server', async (reason) => {
90
90
  const { app, dependencies } = createApp({ getOpenCodeUpgradeCapability: () => ({ supported: false, reason }) });
91
91
  await request(app).post('/api/opencode/upgrade').send({}).expect(409);
92
92
  expect(dependencies.upgradeOpenCodeCli).not.toHaveBeenCalled();
@@ -10,7 +10,7 @@ import { getClaudeCliAuthStatus } from './claude-cli-auth.js';
10
10
  import { OPENCODE_CONFIG_DIR } from './shared.js';
11
11
  import { settingsSurfaceOf } from './settings-files.js';
12
12
  import { parseWebSearchSelection } from './config-v2.js';
13
- import { getWebSearchSource, setWebSearchSelection } from './websearch-config.js';
13
+ import { getWebSearchSource, setWarmingEnabled, setWebSearchSelection } from './websearch-config.js';
14
14
 
15
15
  export const registerOpenCodeRoutes = (app, dependencies) => {
16
16
  const {
@@ -287,6 +287,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
287
287
  return res.json({
288
288
  providerId,
289
289
  sources: sources.sources,
290
+ config: sources.config,
290
291
  });
291
292
  } catch (error) {
292
293
  console.error('Failed to get provider sources:', error);
@@ -379,6 +380,22 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
379
380
  }
380
381
  });
381
382
 
383
+ // Session warming (`warming` in OpenCode config), written like the web
384
+ // search choice above. Settings reads the effective value from OpenCode.
385
+ app.put('/api/config/warming', (req, res) => {
386
+ const enabled = req.body?.enabled;
387
+ if (typeof enabled !== 'boolean') {
388
+ return res.status(400).json({ error: 'enabled must be a boolean' });
389
+ }
390
+ try {
391
+ const result = setWarmingEnabled(enabled);
392
+ return res.json({ success: true, changed: result.changed });
393
+ } catch (error) {
394
+ console.error('Failed to save session warming:', error);
395
+ return res.status(500).json({ error: error.message || 'Failed to save session warming' });
396
+ }
397
+ });
398
+
382
399
  app.delete('/api/provider/:providerId/auth', async (req, res) => {
383
400
  try {
384
401
  const { providerId } = req.params;
@@ -528,6 +545,24 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
528
545
  return res.status(413).json({ error: `Content exceeds maximum size of ${MAX_BEHAVIOR_PROMPT_SIZE} bytes` });
529
546
  }
530
547
 
548
+ // `expectedContent` is what the editor loaded (null: no file). A file
549
+ // changed on disk since then is not overwritten with the stale copy.
550
+ if (req.body && Object.prototype.hasOwnProperty.call(req.body, 'expectedContent')) {
551
+ const expected = req.body.expectedContent;
552
+ let current = null;
553
+ try {
554
+ current = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
555
+ } catch (error) {
556
+ if (error?.code !== 'ENOENT') throw error;
557
+ }
558
+ if (current !== expected) {
559
+ return res.status(409).json({
560
+ error: 'AGENTS.md changed on disk since it was loaded',
561
+ code: 'AGENTS_MD_CONFLICT',
562
+ });
563
+ }
564
+ }
565
+
531
566
  // Ensure parent directory exists
532
567
  const parentDir = path.dirname(AGENTS_MD_PATH);
533
568
  try {
@@ -31,6 +31,7 @@ export const createServerStartupRuntime = (dependencies) => {
31
31
  bindHost,
32
32
  startupTunnelRequest,
33
33
  onTunnelReady,
34
+ afterListening,
34
35
  }) => {
35
36
  let activePort = port;
36
37
 
@@ -45,6 +46,30 @@ export const createServerStartupRuntime = (dependencies) => {
45
46
  try {
46
47
  const addressInfo = server.address();
47
48
  activePort = typeof addressInfo === 'object' && addressInfo ? addressInfo.port : port;
49
+ try {
50
+ await afterListening?.({ activePort, bindHost });
51
+ } catch (error) {
52
+ if (typeof process.send === 'function' && process.connected) {
53
+ const startupError = {
54
+ type: 'openchamber:error',
55
+ code: error?.code || 'WEB_STARTUP_ERROR',
56
+ message: error instanceof Error ? error.message : String(error),
57
+ };
58
+ if (error?.url) startupError.url = error.url;
59
+ await new Promise((resolveStartupError) => {
60
+ try {
61
+ process.send(startupError, () => resolveStartupError());
62
+ } catch {
63
+ resolveStartupError();
64
+ }
65
+ });
66
+ }
67
+ await new Promise((resolveClose) => {
68
+ server.close(() => resolveClose());
69
+ server.closeAllConnections?.();
70
+ });
71
+ throw error;
72
+ }
48
73
 
49
74
  if (typeof process.send === 'function') {
50
75
  if (!process.connected) {
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from 'bun:test';
1
+ import { describe, expect, test, mock } from 'bun:test';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import { createServerStartupRuntime } from './server-startup-runtime.js';
4
4
 
@@ -7,6 +7,72 @@ import { createServerStartupRuntime } from './server-startup-runtime.js';
7
7
  * on a single uncaught exception turned every stray socket error into "the
8
8
  * instance is unreachable until restarted". Only a sustained storm shuts down.
9
9
  */
10
+ describe('server startup readiness', () => {
11
+ test('reports daemon ready only after listener-dependent startup finishes', async () => {
12
+ const order = [];
13
+ const fakeProcess = {
14
+ connected: true,
15
+ send: mock((message, callback) => {
16
+ order.push(message.type);
17
+ callback();
18
+ }),
19
+ };
20
+ const server = new EventEmitter();
21
+ server.address = () => ({ port: 3901 });
22
+ server.listen = (_port, _host, callback) => callback();
23
+ const runtime = createServerStartupRuntime({
24
+ process: fakeProcess,
25
+ crypto: {},
26
+ server,
27
+ readSettingsFromDiskMigrated: async () => ({}),
28
+ tunnelAuthController: {},
29
+ });
30
+
31
+ await runtime.startListeningAndMaybeTunnel({
32
+ port: 0,
33
+ bindHost: '127.0.0.1',
34
+ afterListening: async () => order.push('prepared'),
35
+ });
36
+
37
+ expect(order).toEqual(['prepared', 'openchamber:ready']);
38
+ });
39
+
40
+ test('closes the listener and reports startup failure when listener-dependent startup fails', async () => {
41
+ const sent = [];
42
+ const fakeProcess = {
43
+ connected: true,
44
+ send: mock((message, callback) => {
45
+ sent.push(message);
46
+ callback();
47
+ }),
48
+ };
49
+ const server = new EventEmitter();
50
+ server.address = () => ({ port: 3901 });
51
+ server.listen = (_port, _host, callback) => callback();
52
+ server.close = mock((callback) => callback());
53
+ server.closeAllConnections = mock(() => {});
54
+ const runtime = createServerStartupRuntime({ process: fakeProcess, crypto: {}, server });
55
+ const startupError = Object.assign(new Error('shared service rejected'), {
56
+ code: 'OPENCODE_INCOMPATIBLE',
57
+ });
58
+
59
+ await expect(runtime.startListeningAndMaybeTunnel({
60
+ port: 0,
61
+ bindHost: '127.0.0.1',
62
+ afterListening: async () => { throw startupError; },
63
+ })).rejects.toBe(startupError);
64
+
65
+ expect(server.close).toHaveBeenCalledTimes(1);
66
+ expect(server.closeAllConnections).toHaveBeenCalledTimes(1);
67
+ expect(sent).toEqual([expect.objectContaining({
68
+ type: 'openchamber:error',
69
+ code: 'OPENCODE_INCOMPATIBLE',
70
+ })]);
71
+ expect(sent).not.toContainEqual(expect.objectContaining({ type: 'openchamber:ready' }));
72
+ });
73
+
74
+ });
75
+
10
76
  describe('uncaught exception policy', () => {
11
77
  const setup = () => {
12
78
  const fakeProcess = new EventEmitter();
@@ -23,6 +23,8 @@ export const createServerUtilsRuntime = (dependencies) => {
23
23
  clearLastOpenCodeError,
24
24
  getLoginShellPath,
25
25
  getArchivedSessions = null,
26
+ getMergeSpaceSessionList = null,
27
+ getSpaceEventHub = null,
26
28
  getStoredSessionMetadata = null,
27
29
  } = dependencies;
28
30
 
@@ -221,6 +223,9 @@ export const createServerUtilsRuntime = (dependencies) => {
221
223
  getUiNotificationClients,
222
224
  getArchivedSessions,
223
225
  getStoredSessionMetadata,
226
+ // Read when the proxy is set up, after `main` decided whether the spaces host exists.
227
+ mergeSpaceSessionList: typeof getMergeSpaceSessionList === 'function' ? getMergeSpaceSessionList() : null,
228
+ spaceEventHub: typeof getSpaceEventHub === 'function' ? getSpaceEventHub() : null,
224
229
  });
225
230
  };
226
231
 
@@ -65,6 +65,7 @@ export const createSettingsHelpers = (dependencies) => {
65
65
  const SIDEBAR_PROJECT_DISPLAY_MODE_VALUES = new Set(['all', 'single']);
66
66
  const SIDEBAR_VIEW_MODE_VALUES = new Set(['projects', 'timeline']);
67
67
  const SIDEBAR_PROJECT_SORT_ORDER_VALUES = new Set(['manual', 'a-z', 'z-a', 'date-added', 'recent']);
68
+ const SIDEBAR_WORKTREE_SORT_ORDER_VALUES = new Set(['recent', 'manual', 'a-z']);
68
69
  const HIDDEN_MODELS_MAX = 1024;
69
70
  const RECENT_EFFORTS_MAX_KEYS = 128;
70
71
  const RECENT_EFFORTS_MAX_VARIANTS_PER_KEY = 5;
@@ -302,6 +303,9 @@ export const createSettingsHelpers = (dependencies) => {
302
303
  if (SIDEBAR_PROJECT_SORT_ORDER_VALUES.has(candidate.sidebarProjectSortOrder)) {
303
304
  result.sidebarProjectSortOrder = candidate.sidebarProjectSortOrder;
304
305
  }
306
+ if (SIDEBAR_WORKTREE_SORT_ORDER_VALUES.has(candidate.sidebarWorktreeSortOrder)) {
307
+ result.sidebarWorktreeSortOrder = candidate.sidebarWorktreeSortOrder;
308
+ }
305
309
  if (typeof candidate.sidebarShowRecentSection === 'boolean') {
306
310
  result.sidebarShowRecentSection = candidate.sidebarShowRecentSection;
307
311
  }
@@ -607,6 +611,12 @@ export const createSettingsHelpers = (dependencies) => {
607
611
  if (typeof candidate.agentMemoryToolEnabled === 'boolean') {
608
612
  result.agentMemoryToolEnabled = candidate.agentMemoryToolEnabled;
609
613
  }
614
+ if (typeof candidate.agentNotifyToolEnabled === 'boolean') {
615
+ result.agentNotifyToolEnabled = candidate.agentNotifyToolEnabled;
616
+ }
617
+ if (typeof candidate.isolatedSpacesEnabled === 'boolean') {
618
+ result.isolatedSpacesEnabled = candidate.isolatedSpacesEnabled;
619
+ }
610
620
  if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') {
611
621
  const version = candidate.openCodeUpdateToastDismissedVersion.trim();
612
622
  result.openCodeUpdateToastDismissedVersion = version.slice(0, VERSION_STRING_MAX_LENGTH);
@@ -764,13 +764,13 @@ describe('settings registry gate', () => {
764
764
  desktopLanAccessEnabled: true, desktopKeepAwakeEnabled: true, desktopMinimizeToTrayEnabled: true, desktopMacMenuBarEnabled: true,
765
765
  desktopUiPassword: 'secret', githubClientId: 'client', githubScopes: 'repo', skillCatalogs: [{ id: 'c', label: 'C', source: 'https://x' }],
766
766
  defaultGitIdentityId: 'global', permissionAutoAccept: { sessions: { s: true }, revision: 1 },
767
- agentControlToolEnabled: true, agentWebToolEnabled: true, browserProvider: 'builtin', agentMemoryToolEnabled: true, openCodeUpdateToastDismissedVersion: '1.0.0',
767
+ agentControlToolEnabled: true, agentWebToolEnabled: true, browserProvider: 'builtin', agentMemoryToolEnabled: true, agentNotifyToolEnabled: true, isolatedSpacesEnabled: true, openCodeUpdateToastDismissedVersion: '1.0.0',
768
768
  autoDeleteEnabled: true, autoDeleteAfterDays: 30, sessionRetentionOnlyArchived: false, sessionRetentionAction: 'archive', terminalShell: 'zsh', terminalLoginShells: ['zsh'],
769
769
  openInAppId: 'vscode', dictationEnabled: true, sttProvider: 'local', sttServerUrl: 'http://localhost:8001/v1', sttModel: 'm', sttLocalModel: 'm', sttLanguage: 'en',
770
770
  tunnelProvider: 'cloudflare', tunnelMode: 'quick', tunnelBootstrapTtlMs: 600000, tunnelSessionTtlMs: 86400000, managedLocalTunnelConfigPath: '/tmp/x',
771
771
  managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
772
772
  managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
773
- sidebarProjectDisplayMode: 'all', sidebarViewMode: 'timeline', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
773
+ sidebarProjectDisplayMode: 'all', sidebarViewMode: 'timeline', sidebarProjectSortOrder: 'manual', sidebarWorktreeSortOrder: 'recent', sidebarShowRecentSection: true,
774
774
  workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true, workStatusSectionOrder: ['mcp', 'session'],
775
775
  showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
776
776
  chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
@@ -123,9 +123,15 @@
123
123
  "browserProvider": {
124
124
  "scope": "instance"
125
125
  },
126
+ "agentNotifyToolEnabled": {
127
+ "scope": "instance"
128
+ },
126
129
  "agentMemoryToolEnabled": {
127
130
  "scope": "instance"
128
131
  },
132
+ "isolatedSpacesEnabled": {
133
+ "scope": "instance"
134
+ },
129
135
  "agentMemoryFeatureAvailable": {
130
136
  "scope": "instance",
131
137
  "computed": true
@@ -222,6 +228,9 @@
222
228
  "sidebarProjectSortOrder": {
223
229
  "scope": "profile"
224
230
  },
231
+ "sidebarWorktreeSortOrder": {
232
+ "scope": "profile"
233
+ },
225
234
  "sidebarShowRecentSection": {
226
235
  "scope": "profile"
227
236
  },
@@ -674,6 +683,14 @@
674
683
  "scope": "device",
675
684
  "local": true
676
685
  },
686
+ "diffFileListMode": {
687
+ "scope": "device",
688
+ "local": true
689
+ },
690
+ "diffFileTreeWidth": {
691
+ "scope": "device",
692
+ "local": true
693
+ },
677
694
  "linearIssueListStatus": {
678
695
  "scope": "device",
679
696
  "local": true
@@ -45,6 +45,7 @@ const MANAGED_PLUGIN_SETTINGS_KEYS = new Set([
45
45
  'agentControlToolEnabled',
46
46
  'agentWebToolEnabled',
47
47
  'agentMemoryToolEnabled',
48
+ 'agentNotifyToolEnabled',
48
49
  ]);
49
50
 
50
51
  export const createSettingsRuntime = (deps) => {