@aiworker/web 1.22.2 → 1.23.1

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 (217) hide show
  1. package/bin/cli.js +7 -0
  2. package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
  3. package/dist/assets/DiffView-DC9RX3MF.js +4 -0
  4. package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
  5. package/dist/assets/FilesView-BFZkuu4O.js +13 -0
  6. package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
  7. package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
  8. package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
  9. package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
  10. package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
  11. package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
  12. package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
  13. package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
  14. package/dist/assets/PlanView-BmQUYT_z.js +5 -0
  15. package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
  16. package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
  17. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  18. package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
  19. package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
  20. package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
  21. package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
  22. package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
  23. package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
  24. package/dist/assets/de-DJsQr-p7.js +10 -0
  25. package/dist/assets/es-BuYMlA9g.js +16 -0
  26. package/dist/assets/fr-ePDay33C.js +14 -0
  27. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  28. package/dist/assets/index-BD8ILbxF.js +40 -0
  29. package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
  30. package/dist/assets/index-zhgg-SDz.css +1 -0
  31. package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
  32. package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
  33. package/dist/assets/ja-BzR9xOY_.js +16 -0
  34. package/dist/assets/ko-BEjHAm14.js +16 -0
  35. package/dist/assets/main-BydQ1tr_.js +930 -0
  36. package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
  37. package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
  38. package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
  39. package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
  40. package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
  41. package/dist/assets/pl-rQmpxNIF.js +16 -0
  42. package/dist/assets/pt-BR-CorbsSW-.js +16 -0
  43. package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
  44. package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
  45. package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
  46. package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
  47. package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
  48. package/dist/assets/surface-COKb77Ua.js +1 -0
  49. package/dist/assets/tr-CoAMXPtI.js +7 -0
  50. package/dist/assets/uk-YlAX0rRU.js +16 -0
  51. package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
  52. package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
  53. package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
  54. package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
  55. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
  56. package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
  57. package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
  58. package/dist/assets/vendor-zod-B6551dwG.js +39 -0
  59. package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
  60. package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
  61. package/dist/assets/zh-TW-CQKaCayr.js +16 -0
  62. package/dist/index.html +5 -43
  63. package/dist/mini-chat.html +4 -4
  64. package/dist/mobile.html +4 -4
  65. package/package.json +2 -3
  66. package/server/index.d.ts +15 -0
  67. package/server/index.js +48 -22
  68. package/server/lib/agent-memory/runtime.js +7 -2
  69. package/server/lib/agent-memory/runtime.test.js +22 -0
  70. package/server/lib/data-dir-migration.js +40 -0
  71. package/server/lib/data-dir-migration.test.js +59 -0
  72. package/server/lib/event-stream/DOCUMENTATION.md +3 -0
  73. package/server/lib/event-stream/global-hub.js +29 -4
  74. package/server/lib/event-stream/global-hub.test.js +22 -0
  75. package/server/lib/event-stream/global-ws-bridge.js +15 -18
  76. package/server/lib/event-stream/protocol.js +21 -7
  77. package/server/lib/event-stream/runtime.js +9 -6
  78. package/server/lib/event-stream/runtime.test.js +72 -12
  79. package/server/lib/fs/DOCUMENTATION.md +3 -0
  80. package/server/lib/fs/routes.js +31 -0
  81. package/server/lib/fs/routes.test.js +110 -1
  82. package/server/lib/git/DOCUMENTATION.md +39 -6
  83. package/server/lib/git/routes.js +45 -12
  84. package/server/lib/git/routes.test.js +78 -0
  85. package/server/lib/git/service.js +343 -202
  86. package/server/lib/git/service.test.js +496 -25
  87. package/server/lib/github/DOCUMENTATION.md +5 -0
  88. package/server/lib/github/routes.js +2 -2
  89. package/server/lib/inherited-env.js +16 -9
  90. package/server/lib/inherited-env.test.js +8 -9
  91. package/server/lib/message-queue/DOCUMENTATION.md +11 -1
  92. package/server/lib/message-queue/runtime.js +13 -0
  93. package/server/lib/message-queue/runtime.test.js +31 -1
  94. package/server/lib/network-defaults.js +23 -0
  95. package/server/lib/network-defaults.test.js +52 -0
  96. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  97. package/server/lib/notifications/emitter-runtime.js +4 -3
  98. package/server/lib/opencode/DOCUMENTATION.md +35 -5
  99. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  100. package/server/lib/opencode/env-runtime.js +5 -1
  101. package/server/lib/opencode/env-runtime.test.js +72 -0
  102. package/server/lib/opencode/feature-routes-runtime.js +17 -1
  103. package/server/lib/opencode/lifecycle.js +4 -3
  104. package/server/lib/opencode/lifecycle.test.js +29 -0
  105. package/server/lib/opencode/managed-plugin-config.js +1 -1
  106. package/server/lib/opencode/openchamber-routes.js +64 -4
  107. package/server/lib/opencode/openchamber-routes.test.js +128 -1
  108. package/server/lib/opencode/proxy.js +33 -0
  109. package/server/lib/opencode/routes.js +5 -3
  110. package/server/lib/opencode/settings-files.js +214 -0
  111. package/server/lib/opencode/settings-helpers.js +88 -41
  112. package/server/lib/opencode/settings-helpers.test.js +176 -1
  113. package/server/lib/opencode/settings-registry.json +750 -0
  114. package/server/lib/opencode/settings-runtime.js +175 -27
  115. package/server/lib/opencode/settings-runtime.test.js +321 -7
  116. package/server/lib/project-context/DOCUMENTATION.md +38 -4
  117. package/server/lib/project-context/routes.http.test.js +18 -0
  118. package/server/lib/project-context/routes.js +15 -0
  119. package/server/lib/project-context/runtime.js +256 -25
  120. package/server/lib/project-context/runtime.test.js +160 -3
  121. package/server/lib/projects/DOCUMENTATION.md +77 -0
  122. package/server/lib/projects/project-config.js +179 -17
  123. package/server/lib/projects/project-config.test.js +97 -0
  124. package/server/lib/projects/project-id.js +43 -0
  125. package/server/lib/projects/project-setup.js +409 -0
  126. package/server/lib/projects/project-setup.test.js +557 -0
  127. package/server/lib/projects/routes.js +59 -0
  128. package/server/lib/quota/DOCUMENTATION.md +31 -1
  129. package/server/lib/quota/providers/cline-pass.js +125 -0
  130. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  131. package/server/lib/quota/providers/index.js +7 -0
  132. package/server/lib/quota/providers/neuralwatt.js +3 -6
  133. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  134. package/server/lib/quota/providers/ollama-cloud.js +31 -0
  135. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  136. package/server/lib/quota/providers/opencode-go.js +1 -1
  137. package/server/lib/quota/providers/opencode-go.test.js +26 -1
  138. package/server/lib/quota/providers/openrouter.js +119 -24
  139. package/server/lib/quota/providers/openrouter.test.js +330 -0
  140. package/server/lib/relay/DOCUMENTATION.md +75 -0
  141. package/server/lib/relay/cross-compat.test.js +26 -0
  142. package/server/lib/relay/downstream-scheduler.js +154 -0
  143. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  144. package/server/lib/relay/e2ee.js +10 -8
  145. package/server/lib/relay/flow-control.test.js +177 -0
  146. package/server/lib/relay/host-client.js +44 -8
  147. package/server/lib/relay/host-client.test.js +14 -0
  148. package/server/lib/relay/tunnel-codec.js +9 -0
  149. package/server/lib/relay/tunnel-host.js +66 -17
  150. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  151. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  152. package/server/lib/scheduled-tasks/runtime.js +12 -2
  153. package/server/lib/scheduled-tasks/runtime.test.js +53 -0
  154. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  155. package/server/lib/session-assist/context.js +127 -0
  156. package/server/lib/session-assist/context.test.js +132 -0
  157. package/server/lib/session-assist/prompt.js +54 -0
  158. package/server/lib/session-assist/runtime.js +117 -267
  159. package/server/lib/session-assist/runtime.test.js +203 -0
  160. package/server/lib/session-goal/runtime.js +4 -9
  161. package/server/lib/small-model/DOCUMENTATION.md +3 -2
  162. package/server/lib/small-model/call.js +9 -4
  163. package/server/lib/small-model/call.test.js +29 -0
  164. package/server/lib/small-model/index.js +5 -10
  165. package/server/lib/small-model/runtime-providers.js +15 -7
  166. package/server/lib/small-model/runtime-providers.test.js +7 -1
  167. package/server/lib/terminal/DOCUMENTATION.md +4 -4
  168. package/server/lib/terminal/runtime.js +11 -6
  169. package/server/lib/terminal/runtime.test.js +20 -13
  170. package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
  171. package/server/lib/walkthrough/model-settings.js +8 -12
  172. package/server/lib/walkthrough/pr-source.test.js +21 -0
  173. package/server/lib/walkthrough/prompt.js +3 -1
  174. package/server/lib/walkthrough/pull-request.js +7 -4
  175. package/server/lib/walkthrough/pull-request.test.js +16 -0
  176. package/server/lib/walkthrough/routes.js +19 -0
  177. package/server/lib/walkthrough/routes.test.js +26 -0
  178. package/server/lib/walkthrough/sources.js +36 -4
  179. package/server/opencode-worktree-readiness.test.js +130 -0
  180. package/dist/assets/DiffView-hDBKesA_.js +0 -4
  181. package/dist/assets/FilesView-CdWzeOp9.js +0 -17
  182. package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
  183. package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
  184. package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
  185. package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
  186. package/dist/assets/PlanView-DyDt_nZ_.js +0 -5
  187. package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
  188. package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
  189. package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
  190. package/dist/assets/de-B5JaA3SA.js +0 -10
  191. package/dist/assets/es-EPIH88ii.js +0 -16
  192. package/dist/assets/fr-CAMfxUdZ.js +0 -14
  193. package/dist/assets/index-DF5kMP-i.js +0 -39
  194. package/dist/assets/index-Dl3xbAB_.css +0 -1
  195. package/dist/assets/ja-CifOE1VN.js +0 -16
  196. package/dist/assets/ko-BBDyAyYQ.js +0 -16
  197. package/dist/assets/main-MhSaOZoB.js +0 -930
  198. package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
  199. package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
  200. package/dist/assets/pl-DgEHYfFM.js +0 -16
  201. package/dist/assets/pt-BR-DuWHojUw.js +0 -16
  202. package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
  203. package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
  204. package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
  205. package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
  206. package/dist/assets/tr-D5m2yIWL.js +0 -7
  207. package/dist/assets/uk-D_q60oog.js +0 -16
  208. package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
  209. package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
  210. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  211. package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
  212. package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
  213. package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
  214. package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
  215. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  216. package/server/lib/mcp-reconnect/runtime.js +0 -122
  217. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
@@ -1,4 +1,32 @@
1
+ import { createRequire } from 'node:module';
2
+
1
3
  import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
4
+
5
+ // Generated from packages/ui/src/lib/settings/registry.ts by
6
+ // `bun run settings-registry:generate`; `registry.test.ts` fails when stale.
7
+ // The server is plain ESM without a bundler, so the snapshot is read with
8
+ // `createRequire` (import attributes differ across the Node versions we run on).
9
+ const settingsRegistry = createRequire(import.meta.url)('./settings-registry.json');
10
+
11
+ /**
12
+ * Whether a client may persist this key through PUT /api/config/settings:
13
+ * it must be a registry key, not a server-computed flag, not a device field
14
+ * that only lives in the browser, and not one the desktop shell writes itself.
15
+ */
16
+ const isPersistableSettingsKey = (key) => {
17
+ const field = settingsRegistry.fields[key];
18
+ if (!field) return false;
19
+ if (field.computed || field.local) return false;
20
+ if (field.owner === 'desktop-shell') return false;
21
+ return true;
22
+ };
23
+
24
+ /** Keys accepted on write but never returned by a read. */
25
+ const SECRET_SETTINGS_KEYS = Object.freeze(
26
+ Object.entries(settingsRegistry.fields)
27
+ .filter(([, field]) => field.secret === true)
28
+ .map(([key]) => key),
29
+ );
2
30
  import {
3
31
  DEFAULT_INPUT_HISTORY_LIMIT,
4
32
  DEFAULT_INPUT_HISTORY_SCOPE,
@@ -18,7 +46,6 @@ export const createSettingsHelpers = (dependencies) => {
18
46
  normalizeManagedRemoteTunnelHostname,
19
47
  normalizeManagedRemoteTunnelPresets,
20
48
  normalizeManagedRemoteTunnelPresetTokens,
21
- sanitizeTypographySizesPartial,
22
49
  normalizeStringArray,
23
50
  sanitizeModelRefs,
24
51
  sanitizeSkillCatalogs,
@@ -203,6 +230,9 @@ export const createSettingsHelpers = (dependencies) => {
203
230
  ...new Set(candidate.workStatusHiddenSections.filter((entry) => typeof entry === 'string' && entry.length > 0)),
204
231
  ];
205
232
  }
233
+ if (typeof candidate.workStatusHiddenSectionsExplicit === 'boolean') {
234
+ result.workStatusHiddenSectionsExplicit = candidate.workStatusHiddenSectionsExplicit;
235
+ }
206
236
  if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
207
237
  result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
208
238
  }
@@ -313,9 +343,6 @@ export const createSettingsHelpers = (dependencies) => {
313
343
  if (typeof candidate.monoFont === 'string' && candidate.monoFont.length > 0) {
314
344
  result.monoFont = candidate.monoFont;
315
345
  }
316
- if (typeof candidate.markdownDisplayMode === 'string' && candidate.markdownDisplayMode.length > 0) {
317
- result.markdownDisplayMode = candidate.markdownDisplayMode;
318
- }
319
346
  if (typeof candidate.githubClientId === 'string') {
320
347
  const trimmed = candidate.githubClientId.trim();
321
348
  if (trimmed.length > 0) {
@@ -331,6 +358,45 @@ export const createSettingsHelpers = (dependencies) => {
331
358
  if (typeof candidate.showReasoningTraces === 'boolean') {
332
359
  result.showReasoningTraces = candidate.showReasoningTraces;
333
360
  }
361
+ if (typeof candidate.streamingAutoFollowEnabled === 'boolean') {
362
+ result.streamingAutoFollowEnabled = candidate.streamingAutoFollowEnabled;
363
+ }
364
+ if (typeof candidate.codeBlockLineWrap === 'boolean') {
365
+ result.codeBlockLineWrap = candidate.codeBlockLineWrap;
366
+ }
367
+ if (typeof candidate.autoSaveEnabled === 'boolean') {
368
+ result.autoSaveEnabled = candidate.autoSaveEnabled;
369
+ }
370
+ if (typeof candidate.diffWrapLines === 'boolean') {
371
+ result.diffWrapLines = candidate.diffWrapLines;
372
+ }
373
+ if (typeof candidate.persistChatDraft === 'boolean') {
374
+ result.persistChatDraft = candidate.persistChatDraft;
375
+ }
376
+ if (typeof candidate.allowPromptingSubagentSessions === 'boolean') {
377
+ result.allowPromptingSubagentSessions = candidate.allowPromptingSubagentSessions;
378
+ }
379
+ if (typeof candidate.showOpenCodeRestartConfirm === 'boolean') {
380
+ result.showOpenCodeRestartConfirm = candidate.showOpenCodeRestartConfirm;
381
+ }
382
+ if (typeof candidate.sessionTabsEnabled === 'boolean') {
383
+ result.sessionTabsEnabled = candidate.sessionTabsEnabled;
384
+ }
385
+ if (typeof candidate.largeTextPasteBehavior === 'string') {
386
+ const mode = candidate.largeTextPasteBehavior.trim();
387
+ if (mode === 'ask' || mode === 'attach' || mode === 'inline') {
388
+ result.largeTextPasteBehavior = mode;
389
+ }
390
+ }
391
+ if (typeof candidate.fileEditorKeymap === 'string') {
392
+ const mode = candidate.fileEditorKeymap.trim();
393
+ if (mode === 'default' || mode === 'vim') {
394
+ result.fileEditorKeymap = mode;
395
+ }
396
+ }
397
+ if (Array.isArray(candidate.providerOrder)) {
398
+ result.providerOrder = normalizeStringArray(candidate.providerOrder);
399
+ }
334
400
  if (typeof candidate.sessionRecapEnabled === 'boolean') {
335
401
  result.sessionRecapEnabled = candidate.sessionRecapEnabled;
336
402
  }
@@ -407,6 +473,9 @@ export const createSettingsHelpers = (dependencies) => {
407
473
  if (candidate.sessionRetentionAction === 'archive' || candidate.sessionRetentionAction === 'delete') {
408
474
  result.sessionRetentionAction = candidate.sessionRetentionAction;
409
475
  }
476
+ if (typeof candidate.sessionRetentionOnlyArchived === 'boolean') {
477
+ result.sessionRetentionOnlyArchived = candidate.sessionRetentionOnlyArchived;
478
+ }
410
479
  if (candidate.tunnelBootstrapTtlMs === null) {
411
480
  result.tunnelBootstrapTtlMs = null;
412
481
  } else if (typeof candidate.tunnelBootstrapTtlMs === 'number' && Number.isFinite(candidate.tunnelBootstrapTtlMs)) {
@@ -452,11 +521,6 @@ export const createSettingsHelpers = (dependencies) => {
452
521
  result.managedRemoteTunnelSelectedPresetId = id || undefined;
453
522
  }
454
523
 
455
- const typography = sanitizeTypographySizesPartial(candidate.typographySizes);
456
- if (typography) {
457
- result.typographySizes = typography;
458
- }
459
-
460
524
  if (typeof candidate.defaultModel === 'string') {
461
525
  const trimmed = candidate.defaultModel.trim();
462
526
  result.defaultModel = trimmed.length > 0 ? trimmed : undefined;
@@ -502,14 +566,6 @@ export const createSettingsHelpers = (dependencies) => {
502
566
  const trimmed = candidate.zenModel.trim();
503
567
  result.zenModel = trimmed.length > 0 ? trimmed : undefined;
504
568
  }
505
- if (typeof candidate.gitProviderId === 'string') {
506
- const trimmed = candidate.gitProviderId.trim();
507
- result.gitProviderId = trimmed.length > 0 ? trimmed : undefined;
508
- }
509
- if (typeof candidate.gitModelId === 'string') {
510
- const trimmed = candidate.gitModelId.trim();
511
- result.gitModelId = trimmed.length > 0 ? trimmed : undefined;
512
- }
513
569
  if (typeof candidate.pwaAppName === 'string') {
514
570
  result.pwaAppName = normalizePwaAppName(candidate.pwaAppName, undefined);
515
571
  }
@@ -522,12 +578,6 @@ export const createSettingsHelpers = (dependencies) => {
522
578
  result.mobileKeyboardMode = mode;
523
579
  }
524
580
  }
525
- if (typeof candidate.toolCallExpansion === 'string') {
526
- const mode = candidate.toolCallExpansion.trim();
527
- if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed' || mode === 'changes') {
528
- result.toolCallExpansion = mode;
529
- }
530
- }
531
581
  if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
532
582
  result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
533
583
  }
@@ -619,9 +669,6 @@ export const createSettingsHelpers = (dependencies) => {
619
669
  if (typeof candidate.promptNavigatorEnabled === 'boolean') {
620
670
  result.promptNavigatorEnabled = candidate.promptNavigatorEnabled;
621
671
  }
622
- if (typeof candidate.expandedEditorToolbar === 'boolean') {
623
- result.expandedEditorToolbar = candidate.expandedEditorToolbar;
624
- }
625
672
  if (typeof candidate.wideChatLayoutEnabled === 'boolean') {
626
673
  result.wideChatLayoutEnabled = candidate.wideChatLayoutEnabled;
627
674
  }
@@ -724,11 +771,6 @@ export const createSettingsHelpers = (dependencies) => {
724
771
  }
725
772
  }
726
773
 
727
- // Message limit — single setting for fetch / trim / Load More chunk
728
- if (typeof candidate.messageLimit === 'number' && Number.isFinite(candidate.messageLimit)) {
729
- result.messageLimit = Math.max(10, Math.min(500, Math.round(candidate.messageLimit)));
730
- }
731
-
732
774
  const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
733
775
  if (skillCatalogs) {
734
776
  result.skillCatalogs = skillCatalogs;
@@ -912,6 +954,16 @@ export const createSettingsHelpers = (dependencies) => {
912
954
  }
913
955
  }
914
956
 
957
+ // The registry is the last word on what a client may persist: a key the
958
+ // code above still names but the registry no longer lists is dropped here,
959
+ // so the two cannot drift apart silently (settings-helpers.test.js checks
960
+ // the other direction).
961
+ for (const key of Object.keys(result)) {
962
+ if (!isPersistableSettingsKey(key)) {
963
+ delete result[key];
964
+ }
965
+ }
966
+
915
967
  return result;
916
968
  };
917
969
 
@@ -922,13 +974,6 @@ export const createSettingsHelpers = (dependencies) => {
922
974
  ? current.securityScopedBookmarks
923
975
  : [];
924
976
 
925
- const nextTypographySizes = changes.typographySizes
926
- ? {
927
- ...(current.typographySizes || {}),
928
- ...changes.typographySizes
929
- }
930
- : current.typographySizes;
931
-
932
977
  const next = {
933
978
  ...current,
934
979
  ...changes,
@@ -937,7 +982,6 @@ export const createSettingsHelpers = (dependencies) => {
937
982
  baseBookmarks.filter((entry) => typeof entry === 'string' && entry.length > 0)
938
983
  )
939
984
  ),
940
- typographySizes: nextTypographySizes
941
985
  };
942
986
 
943
987
  return next;
@@ -945,9 +989,12 @@ export const createSettingsHelpers = (dependencies) => {
945
989
 
946
990
  const formatSettingsResponse = (settings) => {
947
991
  const sanitized = sanitizeSettingsUpdate(settings);
948
- delete sanitized.managedRemoteTunnelToken;
992
+ for (const key of SECRET_SETTINGS_KEYS) {
993
+ delete sanitized[key];
994
+ }
949
995
  const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
950
996
  const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
997
+ const hasDesktopUiPassword = typeof settings?.desktopUiPassword === 'string' && settings.desktopUiPassword.trim().length > 0;
951
998
  const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
952
999
  const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
953
1000
  const mobileKeyboardMode = normalizeMobileKeyboardMode(settings?.mobileKeyboardMode, 'native');
@@ -957,6 +1004,7 @@ export const createSettingsHelpers = (dependencies) => {
957
1004
  return {
958
1005
  ...sanitized,
959
1006
  hasManagedRemoteTunnelToken,
1007
+ hasDesktopUiPassword,
960
1008
  // Tells the client whether agent memory exists in this build at all, so
961
1009
  // its settings row and panel tab can be absent rather than merely off.
962
1010
  agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
@@ -967,7 +1015,6 @@ export const createSettingsHelpers = (dependencies) => {
967
1015
  inputHistoryLimit,
968
1016
  securityScopedBookmarks: bookmarks,
969
1017
  pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
970
- typographySizes: sanitizeTypographySizesPartial(settings.typographySizes),
971
1018
  ...(process.env.AIWORKER_RUNTIME === 'desktop'
972
1019
  ? {
973
1020
  desktopLanAccessActive: process.env.AIWORKER_DESKTOP_LAN_ACCESS_ACTIVE === 'true',
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -69,6 +69,19 @@ const createTestHelpersWithRealSanitizers = () => {
69
69
  };
70
70
 
71
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
+ });
72
85
  it('imports from the packed @aiworker/web tarball without escaping the published package', async () => {
73
86
  const tempRoot = mkdtempSync(join(tmpdir(), 'settings-helpers-pack-'));
74
87
  const packDir = join(tempRoot, 'pack');
@@ -677,6 +690,14 @@ describe('settings helpers', () => {
677
690
  });
678
691
 
679
692
  describe('session retention settings persistence', () => {
693
+ it('round-trips archived-only retention and rejects non-boolean values', () => {
694
+ const helpers = createTestHelpersWithRealSanitizers();
695
+ for (const sessionRetentionOnlyArchived of [true, false]) {
696
+ expect(helpers.sanitizeSettingsUpdate({ sessionRetentionOnlyArchived })).toEqual({ sessionRetentionOnlyArchived });
697
+ }
698
+ expect(helpers.sanitizeSettingsUpdate({ sessionRetentionOnlyArchived: 'true' })).toEqual({});
699
+ expect(helpers.sanitizeSettingsUpdate({ sessionRetentionOnlyArchived: null })).toEqual({});
700
+ });
680
701
  it('round-trips sessionRetentionAction archive and delete through the sanitizer', () => {
681
702
  const helpers = createTestHelpersWithRealSanitizers();
682
703
 
@@ -701,6 +722,7 @@ describe('settings helpers', () => {
701
722
  autoDeleteEnabled: true,
702
723
  autoDeleteAfterDays: 60,
703
724
  sessionRetentionAction: 'delete',
725
+ sessionRetentionOnlyArchived: true,
704
726
  };
705
727
 
706
728
  const sanitized = helpers.sanitizeSettingsUpdate(payload);
@@ -708,6 +730,159 @@ describe('settings helpers', () => {
708
730
  expect(sanitized.autoDeleteEnabled).toBe(true);
709
731
  expect(sanitized.autoDeleteAfterDays).toBe(60);
710
732
  expect(sanitized.sessionRetentionAction).toBe('delete');
733
+ expect(sanitized.sessionRetentionOnlyArchived).toBe(true);
734
+ });
735
+ });
736
+ });
737
+
738
+ describe('settings registry gate', () => {
739
+ const registryPath = join(dirname(testFilePath), 'settings-registry.json');
740
+ const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
741
+ const persistableKeys = Object.entries(registry.fields)
742
+ .filter(([, field]) => !field.computed && !field.local && field.owner !== 'desktop-shell')
743
+ .map(([key]) => key);
744
+
745
+ // One valid value per persistable registry key. The test below fails when a
746
+ // key is added to the registry without a line here, and when the sanitizer
747
+ // stops accepting a key the registry still lists — that is the drift the
748
+ // registry exists to end.
749
+ const validValues = {
750
+ themeId: 'openchamber-dark', useSystemTheme: true, themeVariant: 'dark', lightThemeId: 'openchamber-light', darkThemeId: 'openchamber-dark',
751
+ splashBgLight: '#fff', splashFgLight: '#000', splashBgDark: '#000', splashFgDark: '#fff',
752
+ lastDirectory: '/home/testuser/project', homeDirectory: '/home/testuser', opencodeBinary: '/usr/local/bin/opencode',
753
+ projects: [{ id: 'p', path: '/home/testuser/project' }], activeProjectId: 'p',
754
+ securityScopedBookmarks: ['bookmark'], pinnedDirectories: ['/home/testuser/project'],
755
+ desktopLanAccessEnabled: true, desktopKeepAwakeEnabled: true, desktopMinimizeToTrayEnabled: true, desktopMacMenuBarEnabled: true,
756
+ desktopUiPassword: 'secret', githubClientId: 'client', githubScopes: 'repo', skillCatalogs: [{ id: 'c', label: 'C', source: 'https://x' }],
757
+ defaultGitIdentityId: 'global', permissionAutoAccept: { sessions: { s: true }, revision: 1 },
758
+ agentControlToolEnabled: true, agentWebToolEnabled: true, agentMemoryToolEnabled: true, openCodeUpdateToastDismissedVersion: '1.0.0',
759
+ autoDeleteEnabled: true, autoDeleteAfterDays: 30, sessionRetentionOnlyArchived: false, sessionRetentionAction: 'archive', terminalShell: 'zsh', terminalLoginShells: ['zsh'],
760
+ openInAppId: 'vscode', dictationEnabled: true, sttProvider: 'local', sttServerUrl: 'http://localhost:8001/v1', sttModel: 'm', sttLocalModel: 'm', sttLanguage: 'en',
761
+ tunnelProvider: 'cloudflare', tunnelMode: 'quick', tunnelBootstrapTtlMs: 600000, tunnelSessionTtlMs: 86400000, managedLocalTunnelConfigPath: '/tmp/x',
762
+ managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
763
+ managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
764
+ sidebarProjectDisplayMode: 'all', sidebarSessionGroupingMode: 'flat', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
765
+ workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true,
766
+ showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
767
+ chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
768
+ stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
769
+ codeBlockLineWrap: true, showTurnChangedFiles: true, showExpandedBashTools: true, showExpandedEditTools: true, toolJsonViewMode: 'raw',
770
+ timeFormatPreference: '24h', weekStartPreference: 'monday', messageStreamTransport: 'ws', diffLayoutPreference: 'inline', diffWrapLines: true,
771
+ gitChangesViewMode: 'tree', gitmojiEnabled: true, defaultFileViewerPreview: true, directoryShowHidden: true, filesViewShowGitignored: true,
772
+ fileEditorKeymap: 'vim', autoSaveEnabled: true, autoCreateWorktree: true, sessionTabsEnabled: true, showOpenCodeRestartConfirm: true,
773
+ allowPromptingSubagentSessions: true, inputSpellcheckEnabled: true, enterToSend: true, enterToSendConfigured: true, persistChatDraft: true,
774
+ largeTextPasteBehavior: 'attach', followUpBehavior: 'steer', queueModeEnabled: true, inputHistoryScope: 'global', inputHistoryLimit: 40,
775
+ draftStarters: [{ type: 'command', name: 'plan-feature' }], draftStartersVisible: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
776
+ fontSize: 100, terminalFontSize: 14, editorFontSize: 14, uiFont: 'inter', monoFont: 'jetbrains-mono', padding: 100, cornerRadius: 8,
777
+ shortcutOverrides: { 'chat.send': 'mod+enter' },
778
+ defaultModel: 'anthropic/claude', defaultVariant: 'high', defaultAgent: 'build', smallModelUseDefault: false, smallModelOverride: 'anthropic/haiku',
779
+ walkthroughModelOverride: 'anthropic/claude', zenModel: 'zen/model',
780
+ favoriteModels: [{ providerID: 'anthropic', modelID: 'claude' }], hiddenModels: [{ providerID: 'openai', modelID: 'gpt' }], collapsedModelProviders: ['openai'],
781
+ recentModels: [{ providerID: 'anthropic', modelID: 'claude' }], recentAgents: ['build'], recentEfforts: { 'anthropic/claude': ['high'] }, providerOrder: ['anthropic'],
782
+ sessionRecapEnabled: true, sessionSuggestionEnabled: true, sessionGoalEnabled: true, sessionGoalDefaultBudgetEnabled: true, sessionGoalDefaultBudget: 5,
783
+ summarizeLastMessage: true, summaryThreshold: 100, summaryLength: 50, maxLastMessageLength: 200, showDeletionDialog: true,
784
+ nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnSubtasks: true, notifyOnCompletion: true, notifyOnError: true, notifyOnQuestion: true,
785
+ notificationTemplates: { completion: { title: 't', message: 'm' } }, showOpenCodeUpdateNotifications: true, reportUsage: true,
786
+ usageDisplayMode: 'usage', usageDropdownProviders: ['anthropic'], usageSelectedModels: { anthropic: ['claude'] }, usageCollapsedFamilies: { anthropic: ['f'] },
787
+ usageExpandedFamilies: { anthropic: ['f'] }, usageModelGroups: { anthropic: { customGroups: [{ id: 'g', label: 'G', models: ['claude'], order: 0 }] } },
788
+ globalBehaviorPrompt: 'Be brief.', responseStyleEnabled: true, responseStylePreset: 'concise', responseStyleCustomInstructions: 'x', optimizeSystemPrompt: true,
789
+ pwaAppName: 'AiWorker', pwaOrientation: 'portrait', mobileKeyboardMode: 'native', desktopWindowControlsPosition: 'left', desktopWindowControlsStyle: 'classic',
790
+ inputBarOffset: 10,
791
+ };
792
+
793
+ it('accepts a valid value for every persistable registry key (no server-side drift)', () => {
794
+ // The shared test helpers stub the injected list sanitizers to `undefined`
795
+ // (they are covered by their own suites); here they must pass values through
796
+ // so a key is judged by the sanitizer's own branch, not by a stub.
797
+ const helpers = createSettingsHelpers({
798
+ normalizePathForPersistence: (value) => value,
799
+ normalizeDirectoryPath: (value) => value,
800
+ normalizeTunnelBootstrapTtlMs: (value) => value,
801
+ normalizeTunnelSessionTtlMs: (value) => value,
802
+ normalizeTunnelProvider: (value) => value,
803
+ normalizeTunnelMode: (value) => value,
804
+ normalizeOptionalPath: (value) => value,
805
+ normalizeManagedRemoteTunnelHostname: (value) => value,
806
+ normalizeManagedRemoteTunnelPresets: (value) => value,
807
+ normalizeManagedRemoteTunnelPresetTokens: (value) => value,
808
+ normalizeStringArray: (input) => input,
809
+ sanitizeModelRefs: (value) => value,
810
+ sanitizeSkillCatalogs: (value) => value,
811
+ sanitizeProjects: (value) => value,
812
+ });
813
+ const missingFixture = persistableKeys.filter((key) => !(key in validValues));
814
+ expect(missingFixture).toEqual([]);
815
+
816
+ const rejected = persistableKeys.filter((key) => {
817
+ const result = helpers.sanitizeSettingsUpdate({ [key]: validValues[key] });
818
+ // `queueModeEnabled` is absorbed into `followUpBehavior` on purpose.
819
+ const landedAs = key === 'queueModeEnabled' ? 'followUpBehavior' : key;
820
+ return result[landedAs] === undefined;
821
+ });
822
+ expect(rejected).toEqual([]);
823
+ });
824
+
825
+ it('drops keys the registry does not list, computed flags, and desktop-shell-owned keys', () => {
826
+ const helpers = createTestHelpers();
827
+ expect(helpers.sanitizeSettingsUpdate({
828
+ markdownDisplayMode: 'raw',
829
+ toolCallExpansion: 'collapsed',
830
+ expandedEditorToolbar: true,
831
+ typographySizes: { base: 14 },
832
+ gitProviderId: 'anthropic',
833
+ gitModelId: 'claude',
834
+ messageLimit: 200,
835
+ agentMemoryFeatureAvailable: true,
836
+ desktopHosts: [],
837
+ notARealKey: 1,
838
+ })).toEqual({});
839
+ });
840
+
841
+ it('never returns secret keys from a formatted response', () => {
842
+ const helpers = createTestHelpers();
843
+ const secretKeys = Object.entries(registry.fields).filter(([, field]) => field.secret).map(([key]) => key);
844
+ expect(secretKeys).toContain('managedRemoteTunnelToken');
845
+ expect(secretKeys).toContain('desktopUiPassword');
846
+ expect(secretKeys).toContain('managedRemoteTunnelPresetTokens');
847
+ const response = helpers.formatSettingsResponse({
848
+ managedRemoteTunnelToken: 'token',
849
+ desktopUiPassword: 'pw',
850
+ managedRemoteTunnelPresetTokens: { a: 'tok' },
851
+ themeId: 'x',
711
852
  });
853
+ for (const key of secretKeys) {
854
+ expect(response).not.toHaveProperty(key);
855
+ }
856
+ expect(response.hasManagedRemoteTunnelToken).toBe(true);
857
+ expect(response.hasDesktopUiPassword).toBe(true);
858
+ expect(helpers.formatSettingsResponse({ desktopUiPassword: '' }).hasDesktopUiPassword).toBe(false);
859
+ });
860
+
861
+ it('accepts the newly shared profile fields', () => {
862
+ const helpers = createTestHelpersWithRealSanitizers();
863
+ expect(helpers.sanitizeSettingsUpdate({
864
+ providerOrder: ['b', 'a', 'a'],
865
+ diffWrapLines: true,
866
+ persistChatDraft: false,
867
+ largeTextPasteBehavior: 'inline',
868
+ fileEditorKeymap: 'vim',
869
+ allowPromptingSubagentSessions: true,
870
+ showOpenCodeRestartConfirm: false,
871
+ codeBlockLineWrap: true,
872
+ streamingAutoFollowEnabled: false,
873
+ autoSaveEnabled: false,
874
+ })).toEqual({
875
+ providerOrder: ['b', 'a'],
876
+ diffWrapLines: true,
877
+ persistChatDraft: false,
878
+ largeTextPasteBehavior: 'inline',
879
+ fileEditorKeymap: 'vim',
880
+ allowPromptingSubagentSessions: true,
881
+ showOpenCodeRestartConfirm: false,
882
+ codeBlockLineWrap: true,
883
+ streamingAutoFollowEnabled: false,
884
+ autoSaveEnabled: false,
885
+ });
886
+ expect(helpers.sanitizeSettingsUpdate({ largeTextPasteBehavior: 'maybe', fileEditorKeymap: 'emacs' })).toEqual({});
712
887
  });
713
888
  });