@vantran-se/openchamber-web 1.22.1 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +2 -3
  2. package/bin/cli.js +11 -1
  3. package/bin/cli.test.js +92 -0
  4. package/bin/lib/cli-startup.js +49 -0
  5. package/bin/lib/commands-startup.js +41 -6
  6. package/dist/assets/{CodeMirrorCommentWidgets-C53Q7pbX.js → CodeMirrorCommentWidgets-DFg2wnpo.js} +1 -1
  7. package/dist/assets/DiffView-3Dn4QHbu.js +4 -0
  8. package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +1 -0
  9. package/dist/assets/FilesView-BdTOpew9.js +17 -0
  10. package/dist/assets/GitView-Dqf9vHJh.js +6 -0
  11. package/dist/assets/{JsonTreeView-Bl0lp9Rs.js → JsonTreeView-BKdsjg5B.js} +1 -1
  12. package/dist/assets/LinearIssuesView-CmbeL7I6.js +1 -0
  13. package/dist/assets/MarkdownImageGallery-_5gEVr0X.js +1 -0
  14. package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +12 -0
  15. package/dist/assets/OnboardingScreen-DdP4_nuc.js +1 -0
  16. package/dist/assets/{PatchDiff-D6NXAfJk.js → PatchDiff-am3SAss2.js} +1 -1
  17. package/dist/assets/PierreDiffViewer-DCsydlKs.js +161 -0
  18. package/dist/assets/PlanView-BoJxdRZe.js +5 -0
  19. package/dist/assets/SettingsView-CcM8pF5O.js +28 -0
  20. package/dist/assets/{SettingsWindow-BiWDBRvb.js → SettingsWindow-DVTdDSol.js} +1 -1
  21. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  22. package/dist/assets/{ToolOutputDialog-BQexMQIm.js → ToolOutputDialog-BgfTbDZ4.js} +6 -6
  23. package/dist/assets/ToolPartDiffPreview-C7WLk5i9.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-Dpz2ra8h.js → VirtualizedFileDiff-DcdgkH-x.js} +1 -1
  25. package/dist/assets/WalkthroughView-BEVUDxuw.js +4 -0
  26. package/dist/assets/{computeVirtualFileMetrics-DtyozJdt.js → computeVirtualFileMetrics-C8byMnRh.js} +2 -2
  27. package/dist/assets/contextPanelEmbeddedChat-CirXEPJq.js +1 -0
  28. package/dist/assets/de-C8jV7J9y.js +10 -0
  29. package/dist/assets/es-BI6J1M5R.js +16 -0
  30. package/dist/assets/fr-D6oDFt7E.js +14 -0
  31. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  32. package/dist/assets/{index-DmhujXbT.js → index-BB7ryz8y.js} +1 -1
  33. package/dist/assets/index-BqhiCmYS.js +39 -0
  34. package/dist/assets/index-FqSOBz0J.css +1 -0
  35. package/dist/assets/{isDiffPlainText-XyulMHxx.js → isDiffPlainText-45ujMPGb.js} +1 -1
  36. package/dist/assets/{isFilePlainText-B19Ddps1.js → isFilePlainText-Cc4KFSYi.js} +1 -1
  37. package/dist/assets/ja-C_4i5EQu.js +16 -0
  38. package/dist/assets/ko-BFbOTpbq.js +16 -0
  39. package/dist/assets/{main-B2a_JfKZ.js → main-C9ETLKFc.js} +2 -2
  40. package/dist/assets/main-CJmb2dF8.js +930 -0
  41. package/dist/assets/markdownCore-DVuZpnli.js +5 -0
  42. package/dist/assets/{miniChat-B3CSU_Ps.js → miniChat-B4Ytoq2n.js} +2 -2
  43. package/dist/assets/{mobile-QNcFIVCW.js → mobile-BUT6-lo1.js} +2 -2
  44. package/dist/assets/patchFileDiff-8j1apICU.js +13 -0
  45. package/dist/assets/pl-COFvT56l.js +16 -0
  46. package/dist/assets/pt-BR-DtVOO2Lj.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +2 -0
  48. package/dist/assets/renderMobileApp-BNgHraq6.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +17 -0
  50. package/dist/assets/scheduledTasksApi-GC4epIYA.js +1 -0
  51. package/dist/assets/{shikiHighlight-DE-OwqGw.js → shikiHighlight-BJISeZsQ.js} +1 -1
  52. package/dist/assets/surface-BvRzS58U.js +1 -0
  53. package/dist/assets/tr-B8Q0Go2l.js +7 -0
  54. package/dist/assets/uk-Dk4a-Fn-.js +16 -0
  55. package/dist/assets/{useAppFontEffects-COAeIX-D.js → useAppFontEffects-DpFNefUA.js} +153 -179
  56. package/dist/assets/useGitComparison-Dfxaqd0s.js +1 -0
  57. package/dist/assets/useInlineCommentController-CscsE4t-.js +2 -0
  58. package/dist/assets/useRootScrollLock-BaCvuRlT.js +1 -0
  59. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
  60. package/dist/assets/{vendor-codemirror-language-data-BG3v1Ghq.js → vendor-codemirror-language-data-DzlSIu_W.js} +2 -2
  61. package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
  62. package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
  63. package/dist/assets/{web-components-BNMwa0qm.js → web-components-CdGfR2XJ.js} +1 -1
  64. package/dist/assets/zh-CN-B_BXxG1Q.js +16 -0
  65. package/dist/assets/zh-TW-DLrl1SPZ.js +16 -0
  66. package/dist/index.html +5 -43
  67. package/dist/mini-chat.html +4 -4
  68. package/dist/mobile.html +4 -4
  69. package/package.json +8 -5
  70. package/server/index.d.ts +15 -0
  71. package/server/index.js +82 -13
  72. package/server/lib/agent-tool/runtime.js +2 -19
  73. package/server/lib/changelog/update-notes.js +82 -0
  74. package/server/lib/changelog/update-notes.test.js +59 -0
  75. package/server/lib/data-dir-migration.js +40 -0
  76. package/server/lib/data-dir-migration.test.js +59 -0
  77. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  78. package/server/lib/dev-tunnel/runtime.js +4 -4
  79. package/server/lib/dev-tunnel/tunnel.test.js +20 -0
  80. package/server/lib/event-stream/runtime.test.js +12 -12
  81. package/server/lib/fs/DOCUMENTATION.md +5 -0
  82. package/server/lib/fs/routes.js +63 -21
  83. package/server/lib/fs/routes.test.js +187 -1
  84. package/server/lib/git/DOCUMENTATION.md +32 -6
  85. package/server/lib/git/routes.js +40 -4
  86. package/server/lib/git/service.js +291 -215
  87. package/server/lib/git/service.test.js +593 -25
  88. package/server/lib/inherited-env.js +16 -9
  89. package/server/lib/inherited-env.test.js +8 -9
  90. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  91. package/server/lib/mcp-reconnect/runtime.js +122 -0
  92. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  93. package/server/lib/message-queue/DOCUMENTATION.md +161 -0
  94. package/server/lib/message-queue/runtime.js +870 -0
  95. package/server/lib/message-queue/runtime.test.js +482 -0
  96. package/server/lib/network-defaults.js +23 -0
  97. package/server/lib/network-defaults.test.js +52 -0
  98. package/server/lib/openchamber-sessions/routes.js +102 -0
  99. package/server/lib/openchamber-sessions/routes.test.js +88 -0
  100. package/server/lib/opencode/DOCUMENTATION.md +30 -6
  101. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  102. package/server/lib/opencode/config-paths.test.js +116 -0
  103. package/server/lib/opencode/core-routes.js +1 -0
  104. package/server/lib/opencode/feature-routes-runtime.js +7 -0
  105. package/server/lib/opencode/input-history-scope.js +14 -0
  106. package/server/lib/opencode/managed-plugin-config.js +35 -0
  107. package/server/lib/opencode/openchamber-routes.js +64 -4
  108. package/server/lib/opencode/openchamber-routes.test.js +130 -3
  109. package/server/lib/opencode/plugins.js +2 -2
  110. package/server/lib/opencode/providers.js +44 -2
  111. package/server/lib/opencode/providers.test.js +131 -4
  112. package/server/lib/opencode/routes.js +9 -7
  113. package/server/lib/opencode/server-startup-runtime.js +1 -1
  114. package/server/lib/opencode/settings-files.js +214 -0
  115. package/server/lib/opencode/settings-helpers.js +113 -41
  116. package/server/lib/opencode/settings-helpers.test.js +342 -0
  117. package/server/lib/opencode/settings-registry.json +747 -0
  118. package/server/lib/opencode/settings-runtime.js +141 -20
  119. package/server/lib/opencode/settings-runtime.test.js +191 -4
  120. package/server/lib/opencode/shared.js +4 -1
  121. package/server/lib/opencode/shutdown-runtime.js +2 -0
  122. package/server/lib/opencode/skill-routes.js +2 -1
  123. package/server/lib/opencode/snippets.js +1 -2
  124. package/server/lib/package-manager.js +4 -29
  125. package/server/lib/package-manager.test.js +2 -2
  126. package/server/lib/project-context/DOCUMENTATION.md +25 -1
  127. package/server/lib/project-context/routes.http.test.js +18 -0
  128. package/server/lib/project-context/routes.js +15 -0
  129. package/server/lib/project-context/runtime.js +248 -23
  130. package/server/lib/project-context/runtime.test.js +106 -3
  131. package/server/lib/projects/DOCUMENTATION.md +76 -0
  132. package/server/lib/projects/project-config.js +142 -9
  133. package/server/lib/projects/project-id.js +17 -0
  134. package/server/lib/projects/project-setup.js +409 -0
  135. package/server/lib/projects/project-setup.test.js +557 -0
  136. package/server/lib/projects/routes.js +59 -0
  137. package/server/lib/quota/DOCUMENTATION.md +40 -2
  138. package/server/lib/quota/credentials/providers.js +4 -0
  139. package/server/lib/quota/credentials/store.js +1 -1
  140. package/server/lib/quota/credentials/store.test.js +4 -4
  141. package/server/lib/quota/index.js +1 -0
  142. package/server/lib/quota/providers/cline-pass.js +125 -0
  143. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  144. package/server/lib/quota/providers/exe-dev.js +75 -0
  145. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  146. package/server/lib/quota/providers/hyper.js +118 -0
  147. package/server/lib/quota/providers/hyper.test.js +126 -0
  148. package/server/lib/quota/providers/index.js +22 -0
  149. package/server/lib/quota/providers/neuralwatt.js +3 -6
  150. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  151. package/server/lib/quota/providers/ollama-cloud.js +31 -0
  152. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  153. package/server/lib/quota/providers/opencode-go.js +2 -1
  154. package/server/lib/quota/providers/opencode-go.test.js +31 -2
  155. package/server/lib/quota/providers/openrouter.js +119 -24
  156. package/server/lib/quota/providers/openrouter.test.js +330 -0
  157. package/server/lib/quota/routes.js +2 -0
  158. package/server/lib/quota/utils/auth.js +1 -1
  159. package/server/lib/relay/DOCUMENTATION.md +56 -1
  160. package/server/lib/relay/cross-compat.test.js +26 -0
  161. package/server/lib/relay/downstream-scheduler.js +154 -0
  162. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  163. package/server/lib/relay/e2ee.js +10 -8
  164. package/server/lib/relay/flow-control.test.js +177 -0
  165. package/server/lib/relay/host-client.js +37 -8
  166. package/server/lib/relay/host-client.test.js +9 -1
  167. package/server/lib/relay/tunnel-codec.js +9 -0
  168. package/server/lib/relay/tunnel-host.js +69 -18
  169. package/server/lib/relay/tunnel-host.test.js +9 -1
  170. package/server/lib/security/request-security.js +21 -6
  171. package/server/lib/security/request-security.test.js +42 -8
  172. package/server/lib/session-assist/runtime.js +7 -10
  173. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  174. package/server/lib/session-goal/create.js +3 -1
  175. package/server/lib/session-goal/runtime.js +81 -20
  176. package/server/lib/session-goal/runtime.test.js +426 -0
  177. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  178. package/server/lib/skills-catalog/install.js +3 -2
  179. package/server/lib/small-model/DOCUMENTATION.md +7 -2
  180. package/server/lib/small-model/call.js +17 -6
  181. package/server/lib/small-model/call.test.js +52 -0
  182. package/server/lib/small-model/index.js +7 -11
  183. package/server/lib/small-model/routes.js +2 -1
  184. package/server/lib/small-model/runtime-providers.js +15 -7
  185. package/server/lib/small-model/runtime-providers.test.js +7 -1
  186. package/server/lib/system-prompt/runtime.js +2 -21
  187. package/server/lib/terminal/DOCUMENTATION.md +14 -10
  188. package/server/lib/terminal/runtime.js +190 -31
  189. package/server/lib/terminal/runtime.test.js +571 -37
  190. package/server/lib/terminal/shells.js +17 -0
  191. package/server/lib/terminal/shells.test.js +32 -1
  192. package/server/lib/ui-auth/ui-auth.js +4 -1
  193. package/server/lib/ui-auth/ui-auth.test.js +16 -0
  194. package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
  195. package/server/lib/walkthrough/index.js +1 -0
  196. package/server/lib/walkthrough/model-settings.js +8 -12
  197. package/server/lib/walkthrough/prompt.js +3 -1
  198. package/server/lib/walkthrough/routes.test.js +26 -5
  199. package/server/lib/walkthrough/sources.js +23 -2
  200. package/dist/assets/CommitSection-X-f2RgYr.js +0 -1
  201. package/dist/assets/DiffView-Dh1ey2x9.js +0 -4
  202. package/dist/assets/FilesView-3ZYP72ki.js +0 -17
  203. package/dist/assets/GitView-CMuN7z2Z.js +0 -6
  204. package/dist/assets/LinearIssuesView-CQiUDU8b.js +0 -1
  205. package/dist/assets/MarkdownImageGallery-bA0PS-tn.js +0 -1
  206. package/dist/assets/MarkdownRendererImpl-BGQi2PLq.js +0 -12
  207. package/dist/assets/OnboardingScreen-D3kYJIb6.js +0 -1
  208. package/dist/assets/PierreDiffViewer-Ct2A13NO.js +0 -157
  209. package/dist/assets/PlanView-6AUjzNQn.js +0 -5
  210. package/dist/assets/SettingsView-CHI8fFaK.js +0 -28
  211. package/dist/assets/ToolPartDiffPreview-DPAATNn6.js +0 -8
  212. package/dist/assets/WalkthroughView-AX-u2pFg.js +0 -4
  213. package/dist/assets/contextPanelEmbeddedChat-C9EcRSDF.js +0 -1
  214. package/dist/assets/de-DqVn5vme.js +0 -9
  215. package/dist/assets/es-hj43Z53i.js +0 -15
  216. package/dist/assets/fr-BmkZt2mF.js +0 -13
  217. package/dist/assets/index-L_ncV-6H.css +0 -1
  218. package/dist/assets/index-qES19dlJ.js +0 -7
  219. package/dist/assets/ja-DioMIEGF.js +0 -15
  220. package/dist/assets/ko-BqPgaQJb.js +0 -15
  221. package/dist/assets/main-CYgSu6dx.js +0 -928
  222. package/dist/assets/markdownCore-CWRCqSH2.js +0 -3
  223. package/dist/assets/patchFileDiff-BpMQZnYu.js +0 -10
  224. package/dist/assets/pl-slULBF4R.js +0 -15
  225. package/dist/assets/pt-BR-BqH8fuCH.js +0 -15
  226. package/dist/assets/renderElectronMiniChatApp-D77bmLqo.js +0 -2
  227. package/dist/assets/renderMobileApp-BQ3Kdm-d.js +0 -25
  228. package/dist/assets/runtimeEndpointReset-CRGn1gVj.js +0 -16
  229. package/dist/assets/scheduledTasksApi-BSRCtP6T.js +0 -1
  230. package/dist/assets/tr-CjEbw1Aw.js +0 -6
  231. package/dist/assets/uk-BkkfxbOs.js +0 -15
  232. package/dist/assets/useInlineCommentController-JsBboQwt.js +0 -2
  233. package/dist/assets/useRootScrollLock-C0N9mCwz.js +0 -1
  234. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  235. package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
  236. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
  237. package/dist/assets/zh-CN-C6FMdVgv.js +0 -15
  238. package/dist/assets/zh-TW-CdcMjDxv.js +0 -15
@@ -1,8 +1,19 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
1
6
  import { describe, expect, it } from 'vitest';
2
7
 
8
+ import {
9
+ DEFAULT_INPUT_HISTORY_LIMIT,
10
+ } from './input-history-scope.js';
3
11
  import { createSettingsHelpers } from './settings-helpers.js';
4
12
  import { createSettingsNormalizationRuntime } from './settings-normalization-runtime.js';
5
13
 
14
+ const testFilePath = fileURLToPath(import.meta.url);
15
+ const packagesWebDir = join(dirname(testFilePath), '..', '..', '..');
16
+
6
17
  const createTestHelpers = () => createSettingsHelpers({
7
18
  normalizePathForPersistence: (value) => value,
8
19
  normalizeDirectoryPath: (value) => value,
@@ -58,6 +69,79 @@ const createTestHelpersWithRealSanitizers = () => {
58
69
  };
59
70
 
60
71
  describe('settings helpers', () => {
72
+ it('round-trips telemetry opt-in with the hidden list and preserves it across unrelated writes', () => {
73
+ const helpers = createTestHelpers();
74
+ const legacy = helpers.sanitizeSettingsUpdate({ workStatusHiddenSections: [] });
75
+ expect(legacy.workStatusHiddenSectionsExplicit).toBeUndefined();
76
+ const changes = helpers.sanitizeSettingsUpdate({ workStatusHiddenSections: [], workStatusHiddenSectionsExplicit: true });
77
+ const saved = helpers.mergePersistedSettings(legacy, changes);
78
+ const reloaded = helpers.formatSettingsResponse(JSON.parse(JSON.stringify(saved)));
79
+ expect(reloaded.workStatusHiddenSections).toEqual([]);
80
+ expect(reloaded.workStatusHiddenSectionsExplicit).toBe(true);
81
+ const next = helpers.mergePersistedSettings(reloaded, helpers.sanitizeSettingsUpdate({ workStatusPanelEnabled: false }));
82
+ expect(helpers.formatSettingsResponse(next).workStatusHiddenSectionsExplicit).toBe(true);
83
+ expect(helpers.sanitizeSettingsUpdate({ workStatusHiddenSectionsExplicit: 'true' }).workStatusHiddenSectionsExplicit).toBeUndefined();
84
+ });
85
+ it('imports from the packed @openchamber/web tarball without escaping the published package', async () => {
86
+ const tempRoot = mkdtempSync(join(tmpdir(), 'settings-helpers-pack-'));
87
+ const packDir = join(tempRoot, 'pack');
88
+ const extractDir = join(tempRoot, 'extract');
89
+
90
+ try {
91
+ mkdirSync(packDir);
92
+ mkdirSync(extractDir);
93
+ execFileSync('npm', ['pack', '--silent', '--pack-destination', packDir], {
94
+ cwd: packagesWebDir,
95
+ stdio: 'pipe',
96
+ });
97
+
98
+ const tarballName = readdirSync(packDir).find((entry) => entry.endsWith('.tgz'));
99
+ expect(tarballName).toBeTruthy();
100
+
101
+ execFileSync('tar', ['-xzf', join(packDir, tarballName), '-C', extractDir], {
102
+ stdio: 'pipe',
103
+ });
104
+
105
+ const extractedModule = await import(
106
+ pathToFileURL(join(extractDir, 'package', 'server', 'lib', 'opencode', 'settings-helpers.js')).href
107
+ );
108
+
109
+ const helpers = extractedModule.createSettingsHelpers({
110
+ normalizePathForPersistence: (value) => value,
111
+ normalizeDirectoryPath: (value) => value,
112
+ normalizeTunnelBootstrapTtlMs: (value) => value,
113
+ normalizeTunnelSessionTtlMs: (value) => value,
114
+ normalizeTunnelProvider: (value) => value,
115
+ normalizeTunnelMode: (value) => value,
116
+ normalizeOptionalPath: (value) => value,
117
+ normalizeManagedRemoteTunnelHostname: (value) => value,
118
+ normalizeManagedRemoteTunnelPresets: () => undefined,
119
+ normalizeManagedRemoteTunnelPresetTokens: () => undefined,
120
+ sanitizeTypographySizesPartial: () => undefined,
121
+ normalizeStringArray: (input) => input,
122
+ sanitizeModelRefs: () => undefined,
123
+ sanitizeSkillCatalogs: () => undefined,
124
+ sanitizeProjects: () => undefined,
125
+ });
126
+
127
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'global' })).toEqual({
128
+ inputHistoryScope: 'global',
129
+ });
130
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'session' })).toEqual({
131
+ inputHistoryScope: 'session',
132
+ });
133
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 40 })).toEqual({
134
+ inputHistoryLimit: 40,
135
+ });
136
+ expect(helpers.formatSettingsResponse({})).toMatchObject({
137
+ inputHistoryScope: 'session',
138
+ inputHistoryLimit: DEFAULT_INPUT_HISTORY_LIMIT,
139
+ });
140
+ } finally {
141
+ rmSync(tempRoot, { recursive: true, force: true });
142
+ }
143
+ });
144
+
61
145
  it('accepts only booleans for draft starter visibility', () => {
62
146
  const helpers = createTestHelpers();
63
147
 
@@ -66,6 +150,14 @@ describe('settings helpers', () => {
66
150
  expect(helpers.sanitizeSettingsUpdate({ draftStartersVisible: 'false' })).toEqual({});
67
151
  });
68
152
 
153
+ it('sanitizes both Enter settings independently', () => {
154
+ const helpers = createTestHelpers();
155
+
156
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: true })).toEqual({ enterToSend: true });
157
+ expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: false })).toEqual({ enterToSendConfigured: false });
158
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: 'true', enterToSendConfigured: 1 })).toEqual({});
159
+ });
160
+
69
161
  it('sanitizes shared sidebar display preferences', () => {
70
162
  const helpers = createTestHelpers();
71
163
 
@@ -88,6 +180,15 @@ describe('settings helpers', () => {
88
180
  })).toEqual({});
89
181
  });
90
182
 
183
+ it('persists valid tool JSON view modes', () => {
184
+ const helpers = createTestHelpers();
185
+
186
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'summary' })).toEqual({ toolJsonViewMode: 'summary' });
187
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'formatted' })).toEqual({ toolJsonViewMode: 'formatted' });
188
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'raw' })).toEqual({ toolJsonViewMode: 'raw' });
189
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'unknown' })).toEqual({});
190
+ });
191
+
91
192
  it('accepts only booleans for wide chat layout', () => {
92
193
  const helpers = createTestHelpers();
93
194
 
@@ -134,6 +235,74 @@ describe('settings helpers', () => {
134
235
  expect(helpers.sanitizeSettingsUpdate({ messageStreamTransport: 'websocket' })).toEqual({});
135
236
  });
136
237
 
238
+ it('accepts inputHistoryScope as a persisted shared setting', () => {
239
+ const helpers = createTestHelpers();
240
+
241
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'global' })).toEqual({
242
+ inputHistoryScope: 'global',
243
+ });
244
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'session' })).toEqual({
245
+ inputHistoryScope: 'session',
246
+ });
247
+ });
248
+
249
+ it('accepts valid inputHistoryLimit values as a persisted shared setting', () => {
250
+ const helpers = createTestHelpers();
251
+
252
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 1 })).toEqual({
253
+ inputHistoryLimit: 1,
254
+ });
255
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 40 })).toEqual({
256
+ inputHistoryLimit: 40,
257
+ });
258
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 100 })).toEqual({
259
+ inputHistoryLimit: 100,
260
+ });
261
+ });
262
+
263
+ it('rejects invalid inputHistoryLimit values', () => {
264
+ const helpers = createTestHelpers();
265
+
266
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 0 })).toEqual({});
267
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 101 })).toEqual({});
268
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 1.5 })).toEqual({});
269
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: '40' })).toEqual({});
270
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: Number.NaN })).toEqual({});
271
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: Number.POSITIVE_INFINITY })).toEqual({});
272
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: Number.NEGATIVE_INFINITY })).toEqual({});
273
+ });
274
+
275
+ it('rejects invalid inputHistoryScope values', () => {
276
+ const helpers = createTestHelpers();
277
+
278
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'workspace' })).toEqual({});
279
+ });
280
+
281
+ it('defaults inputHistoryScope to session in formatted settings responses', () => {
282
+ const helpers = createTestHelpers();
283
+
284
+ expect(helpers.formatSettingsResponse({ inputHistoryScope: 'global' })).toMatchObject({
285
+ inputHistoryScope: 'global',
286
+ });
287
+ expect(helpers.formatSettingsResponse({})).toMatchObject({
288
+ inputHistoryScope: 'session',
289
+ });
290
+ });
291
+
292
+ it('defaults missing inputHistoryLimit to 40 in formatted settings responses and preserves valid values', () => {
293
+ const helpers = createTestHelpers();
294
+
295
+ expect(helpers.formatSettingsResponse({})).toMatchObject({
296
+ inputHistoryLimit: DEFAULT_INPUT_HISTORY_LIMIT,
297
+ });
298
+ expect(helpers.formatSettingsResponse({ inputHistoryLimit: 1 })).toMatchObject({
299
+ inputHistoryLimit: 1,
300
+ });
301
+ expect(helpers.formatSettingsResponse({ inputHistoryLimit: 100 })).toMatchObject({
302
+ inputHistoryLimit: 100,
303
+ });
304
+ });
305
+
137
306
  it('sanitizes the persisted terminal shell', () => {
138
307
  const helpers = createTestHelpers();
139
308
 
@@ -316,6 +485,27 @@ describe('settings helpers', () => {
316
485
  });
317
486
  });
318
487
 
488
+ it('accepts and rejects invalid Enter-to-send preference values', () => {
489
+ const helpers = createTestHelpers();
490
+
491
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: true, enterToSendConfigured: true })).toEqual({
492
+ enterToSend: true,
493
+ enterToSendConfigured: true,
494
+ });
495
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: false, enterToSendConfigured: true })).toEqual({
496
+ enterToSend: false,
497
+ enterToSendConfigured: true,
498
+ });
499
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: false, enterToSendConfigured: false })).toEqual({
500
+ enterToSend: false,
501
+ enterToSendConfigured: false,
502
+ });
503
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: 'true' })).toEqual({});
504
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: 1 })).toEqual({});
505
+ expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: 'true' })).toEqual({});
506
+ expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: 1 })).toEqual({});
507
+ });
508
+
319
509
  it('accepts dismissed OpenCode update toast version as a persisted shared setting', () => {
320
510
  const helpers = createTestHelpers();
321
511
 
@@ -534,3 +724,155 @@ describe('settings helpers', () => {
534
724
  });
535
725
  });
536
726
  });
727
+
728
+ describe('settings registry gate', () => {
729
+ const registryPath = join(dirname(testFilePath), 'settings-registry.json');
730
+ const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
731
+ const persistableKeys = Object.entries(registry.fields)
732
+ .filter(([, field]) => !field.computed && !field.local && field.owner !== 'desktop-shell')
733
+ .map(([key]) => key);
734
+
735
+ // One valid value per persistable registry key. The test below fails when a
736
+ // key is added to the registry without a line here, and when the sanitizer
737
+ // stops accepting a key the registry still lists — that is the drift the
738
+ // registry exists to end.
739
+ const validValues = {
740
+ themeId: 'openchamber-dark', useSystemTheme: true, themeVariant: 'dark', lightThemeId: 'openchamber-light', darkThemeId: 'openchamber-dark',
741
+ splashBgLight: '#fff', splashFgLight: '#000', splashBgDark: '#000', splashFgDark: '#fff',
742
+ lastDirectory: '/home/testuser/project', homeDirectory: '/home/testuser', opencodeBinary: '/usr/local/bin/opencode',
743
+ projects: [{ id: 'p', path: '/home/testuser/project' }], activeProjectId: 'p',
744
+ securityScopedBookmarks: ['bookmark'], pinnedDirectories: ['/home/testuser/project'],
745
+ desktopLanAccessEnabled: true, desktopKeepAwakeEnabled: true, desktopMinimizeToTrayEnabled: true, desktopMacMenuBarEnabled: true,
746
+ desktopUiPassword: 'secret', githubClientId: 'client', githubScopes: 'repo', skillCatalogs: [{ id: 'c', label: 'C', source: 'https://x' }],
747
+ defaultGitIdentityId: 'global', permissionAutoAccept: { sessions: { s: true }, revision: 1 },
748
+ agentControlToolEnabled: true, agentWebToolEnabled: true, agentMemoryToolEnabled: true, openCodeUpdateToastDismissedVersion: '1.0.0',
749
+ autoDeleteEnabled: true, autoDeleteAfterDays: 30, sessionRetentionAction: 'archive', terminalShell: 'zsh', terminalLoginShells: ['zsh'],
750
+ openInAppId: 'vscode', dictationEnabled: true, sttProvider: 'local', sttServerUrl: 'http://localhost:8001/v1', sttModel: 'm', sttLocalModel: 'm', sttLanguage: 'en',
751
+ tunnelProvider: 'cloudflare', tunnelMode: 'quick', tunnelBootstrapTtlMs: 600000, tunnelSessionTtlMs: 86400000, managedLocalTunnelConfigPath: '/tmp/x',
752
+ managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
753
+ managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
754
+ sidebarProjectDisplayMode: 'all', sidebarSessionGroupingMode: 'flat', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
755
+ workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true,
756
+ showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
757
+ chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
758
+ stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
759
+ codeBlockLineWrap: true, showTurnChangedFiles: true, showExpandedBashTools: true, showExpandedEditTools: true, toolJsonViewMode: 'raw',
760
+ timeFormatPreference: '24h', weekStartPreference: 'monday', messageStreamTransport: 'ws', diffLayoutPreference: 'inline', diffWrapLines: true,
761
+ gitChangesViewMode: 'tree', gitmojiEnabled: true, defaultFileViewerPreview: true, directoryShowHidden: true, filesViewShowGitignored: true,
762
+ fileEditorKeymap: 'vim', autoSaveEnabled: true, autoCreateWorktree: true, sessionTabsEnabled: true, showOpenCodeRestartConfirm: true,
763
+ allowPromptingSubagentSessions: true, inputSpellcheckEnabled: true, enterToSend: true, enterToSendConfigured: true, persistChatDraft: true,
764
+ largeTextPasteBehavior: 'attach', followUpBehavior: 'steer', queueModeEnabled: true, inputHistoryScope: 'global', inputHistoryLimit: 40,
765
+ draftStarters: [{ type: 'command', name: 'plan-feature' }], draftStartersVisible: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
766
+ fontSize: 100, terminalFontSize: 14, editorFontSize: 14, uiFont: 'inter', monoFont: 'jetbrains-mono', padding: 100, cornerRadius: 8,
767
+ shortcutOverrides: { 'chat.send': 'mod+enter' },
768
+ defaultModel: 'anthropic/claude', defaultVariant: 'high', defaultAgent: 'build', smallModelUseDefault: false, smallModelOverride: 'anthropic/haiku',
769
+ walkthroughModelOverride: 'anthropic/claude', zenModel: 'zen/model',
770
+ favoriteModels: [{ providerID: 'anthropic', modelID: 'claude' }], hiddenModels: [{ providerID: 'openai', modelID: 'gpt' }], collapsedModelProviders: ['openai'],
771
+ recentModels: [{ providerID: 'anthropic', modelID: 'claude' }], recentAgents: ['build'], recentEfforts: { 'anthropic/claude': ['high'] }, providerOrder: ['anthropic'],
772
+ sessionRecapEnabled: true, sessionSuggestionEnabled: true, sessionGoalEnabled: true, sessionGoalDefaultBudgetEnabled: true, sessionGoalDefaultBudget: 5,
773
+ summarizeLastMessage: true, summaryThreshold: 100, summaryLength: 50, maxLastMessageLength: 200, showDeletionDialog: true,
774
+ nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnSubtasks: true, notifyOnCompletion: true, notifyOnError: true, notifyOnQuestion: true,
775
+ notificationTemplates: { completion: { title: 't', message: 'm' } }, showOpenCodeUpdateNotifications: true, reportUsage: true,
776
+ usageDisplayMode: 'usage', usageDropdownProviders: ['anthropic'], usageSelectedModels: { anthropic: ['claude'] }, usageCollapsedFamilies: { anthropic: ['f'] },
777
+ usageExpandedFamilies: { anthropic: ['f'] }, usageModelGroups: { anthropic: { customGroups: [{ id: 'g', label: 'G', models: ['claude'], order: 0 }] } },
778
+ globalBehaviorPrompt: 'Be brief.', responseStyleEnabled: true, responseStylePreset: 'concise', responseStyleCustomInstructions: 'x', optimizeSystemPrompt: true,
779
+ pwaAppName: 'OpenChamber', pwaOrientation: 'portrait', mobileKeyboardMode: 'native', desktopWindowControlsPosition: 'left', desktopWindowControlsStyle: 'classic',
780
+ inputBarOffset: 10,
781
+ };
782
+
783
+ it('accepts a valid value for every persistable registry key (no server-side drift)', () => {
784
+ // The shared test helpers stub the injected list sanitizers to `undefined`
785
+ // (they are covered by their own suites); here they must pass values through
786
+ // so a key is judged by the sanitizer's own branch, not by a stub.
787
+ const helpers = createSettingsHelpers({
788
+ normalizePathForPersistence: (value) => value,
789
+ normalizeDirectoryPath: (value) => value,
790
+ normalizeTunnelBootstrapTtlMs: (value) => value,
791
+ normalizeTunnelSessionTtlMs: (value) => value,
792
+ normalizeTunnelProvider: (value) => value,
793
+ normalizeTunnelMode: (value) => value,
794
+ normalizeOptionalPath: (value) => value,
795
+ normalizeManagedRemoteTunnelHostname: (value) => value,
796
+ normalizeManagedRemoteTunnelPresets: (value) => value,
797
+ normalizeManagedRemoteTunnelPresetTokens: (value) => value,
798
+ normalizeStringArray: (input) => input,
799
+ sanitizeModelRefs: (value) => value,
800
+ sanitizeSkillCatalogs: (value) => value,
801
+ sanitizeProjects: (value) => value,
802
+ });
803
+ const missingFixture = persistableKeys.filter((key) => !(key in validValues));
804
+ expect(missingFixture).toEqual([]);
805
+
806
+ const rejected = persistableKeys.filter((key) => {
807
+ const result = helpers.sanitizeSettingsUpdate({ [key]: validValues[key] });
808
+ // `queueModeEnabled` is absorbed into `followUpBehavior` on purpose.
809
+ const landedAs = key === 'queueModeEnabled' ? 'followUpBehavior' : key;
810
+ return result[landedAs] === undefined;
811
+ });
812
+ expect(rejected).toEqual([]);
813
+ });
814
+
815
+ it('drops keys the registry does not list, computed flags, and desktop-shell-owned keys', () => {
816
+ const helpers = createTestHelpers();
817
+ expect(helpers.sanitizeSettingsUpdate({
818
+ markdownDisplayMode: 'raw',
819
+ toolCallExpansion: 'collapsed',
820
+ expandedEditorToolbar: true,
821
+ typographySizes: { base: 14 },
822
+ gitProviderId: 'anthropic',
823
+ gitModelId: 'claude',
824
+ messageLimit: 200,
825
+ agentMemoryFeatureAvailable: true,
826
+ desktopHosts: [],
827
+ notARealKey: 1,
828
+ })).toEqual({});
829
+ });
830
+
831
+ it('never returns secret keys from a formatted response', () => {
832
+ const helpers = createTestHelpers();
833
+ const secretKeys = Object.entries(registry.fields).filter(([, field]) => field.secret).map(([key]) => key);
834
+ expect(secretKeys).toContain('managedRemoteTunnelToken');
835
+ expect(secretKeys).toContain('desktopUiPassword');
836
+ expect(secretKeys).toContain('managedRemoteTunnelPresetTokens');
837
+ const response = helpers.formatSettingsResponse({
838
+ managedRemoteTunnelToken: 'token',
839
+ desktopUiPassword: 'pw',
840
+ managedRemoteTunnelPresetTokens: { a: 'tok' },
841
+ themeId: 'x',
842
+ });
843
+ for (const key of secretKeys) {
844
+ expect(response).not.toHaveProperty(key);
845
+ }
846
+ expect(response.hasManagedRemoteTunnelToken).toBe(true);
847
+ expect(response.hasDesktopUiPassword).toBe(true);
848
+ expect(helpers.formatSettingsResponse({ desktopUiPassword: '' }).hasDesktopUiPassword).toBe(false);
849
+ });
850
+
851
+ it('accepts the newly shared profile fields', () => {
852
+ const helpers = createTestHelpersWithRealSanitizers();
853
+ expect(helpers.sanitizeSettingsUpdate({
854
+ providerOrder: ['b', 'a', 'a'],
855
+ diffWrapLines: true,
856
+ persistChatDraft: false,
857
+ largeTextPasteBehavior: 'inline',
858
+ fileEditorKeymap: 'vim',
859
+ allowPromptingSubagentSessions: true,
860
+ showOpenCodeRestartConfirm: false,
861
+ codeBlockLineWrap: true,
862
+ streamingAutoFollowEnabled: false,
863
+ autoSaveEnabled: false,
864
+ })).toEqual({
865
+ providerOrder: ['b', 'a'],
866
+ diffWrapLines: true,
867
+ persistChatDraft: false,
868
+ largeTextPasteBehavior: 'inline',
869
+ fileEditorKeymap: 'vim',
870
+ allowPromptingSubagentSessions: true,
871
+ showOpenCodeRestartConfirm: false,
872
+ codeBlockLineWrap: true,
873
+ streamingAutoFollowEnabled: false,
874
+ autoSaveEnabled: false,
875
+ });
876
+ expect(helpers.sanitizeSettingsUpdate({ largeTextPasteBehavior: 'maybe', fileEditorKeymap: 'emacs' })).toEqual({});
877
+ });
878
+ });