@aiworker/web 1.22.2 → 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 (173) hide show
  1. package/bin/cli.js +7 -0
  2. package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-B1Gizi3a.js} +1 -1
  3. package/dist/assets/DiffView-DXjVsEOw.js +4 -0
  4. package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-aig6zJjg.js} +1 -1
  5. package/dist/assets/FilesView-CJdG0-wk.js +17 -0
  6. package/dist/assets/{GitView-VQQkYuUw.js → GitView-BXsS3jG6.js} +4 -4
  7. package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-D_hpXBnF.js} +1 -1
  8. package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-BoUiLQG1.js} +1 -1
  9. package/dist/assets/MarkdownImageGallery-B_B_a9KS.js +1 -0
  10. package/dist/assets/MarkdownRendererImpl-xgyyLiBM.js +12 -0
  11. package/dist/assets/OnboardingScreen-DIp84OWt.js +1 -0
  12. package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-8p6nLJaU.js} +1 -1
  13. package/dist/assets/PierreDiffViewer-DjC8nTk9.js +161 -0
  14. package/dist/assets/{PlanView-DyDt_nZ_.js → PlanView-CJTmOF6I.js} +2 -2
  15. package/dist/assets/SettingsView-DD-JqIJx.js +28 -0
  16. package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Cpb4hM6q.js} +1 -1
  17. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  18. package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-DfaKZXNU.js} +6 -6
  19. package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-DtZLzrSK.js} +2 -2
  20. package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-C8i9HpTB.js} +1 -1
  21. package/dist/assets/WalkthroughView-DeKMvZlL.js +4 -0
  22. package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BBI7ZwrC.js} +2 -2
  23. package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
  24. package/dist/assets/de-DCrsQ5zS.js +10 -0
  25. package/dist/assets/es-Dwv4YfH6.js +16 -0
  26. package/dist/assets/fr-BCWg4Cnx.js +14 -0
  27. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  28. package/dist/assets/index-B68h5S5B.css +1 -0
  29. package/dist/assets/index-DVwMarHI.js +39 -0
  30. package/dist/assets/{index-Bl3LSbjn.js → index-jXSK5RrB.js} +1 -1
  31. package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-B_x_rVon.js} +1 -1
  32. package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-DW_xMer1.js} +1 -1
  33. package/dist/assets/ja-Bruw3RYC.js +16 -0
  34. package/dist/assets/ko-idhXGF8j.js +16 -0
  35. package/dist/assets/main-BkyaQhJX.js +930 -0
  36. package/dist/assets/{main-DnBAoktx.js → main-CzdPuL0s.js} +2 -2
  37. package/dist/assets/markdownCore-Cn4Tyejo.js +5 -0
  38. package/dist/assets/{miniChat-BKyvLseN.js → miniChat-CCkqKOLp.js} +2 -2
  39. package/dist/assets/{mobile-DKjeFgMN.js → mobile-mvxTo055.js} +2 -2
  40. package/dist/assets/patchFileDiff-Csk4iNUi.js +13 -0
  41. package/dist/assets/pl-CGZWUqZJ.js +16 -0
  42. package/dist/assets/pt-BR-B8V7u7tA.js +16 -0
  43. package/dist/assets/renderElectronMiniChatApp-BxoyujA5.js +2 -0
  44. package/dist/assets/renderMobileApp-B48CsP4r.js +25 -0
  45. package/dist/assets/runtimeEndpointReset-Bg5gx18H.js +17 -0
  46. package/dist/assets/scheduledTasksApi-BW0Etzwz.js +1 -0
  47. package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-CRl0SUC9.js} +1 -1
  48. package/dist/assets/surface-oMvhsog3.js +1 -0
  49. package/dist/assets/tr-BK_KiO2V.js +7 -0
  50. package/dist/assets/uk-CG74xV8N.js +16 -0
  51. package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-CPKeOpUo.js} +160 -157
  52. package/dist/assets/useGitComparison-DH220WmO.js +1 -0
  53. package/dist/assets/useInlineCommentController-BiWz4AVb.js +2 -0
  54. package/dist/assets/useRootScrollLock-BS4-kr7P.js +1 -0
  55. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-DdV8vhHn.js} +1 -1
  56. package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Bg4AEeCr.js} +2 -2
  57. package/dist/assets/vendor-zod-BPAANqvH.js +39 -0
  58. package/dist/assets/{web-components-_mNEMjFY.js → web-components-z7hfnS8n.js} +1 -1
  59. package/dist/assets/zh-CN-T4AXofZI.js +16 -0
  60. package/dist/assets/zh-TW-Dte40sxp.js +16 -0
  61. package/dist/index.html +5 -43
  62. package/dist/mini-chat.html +4 -4
  63. package/dist/mobile.html +4 -4
  64. package/package.json +2 -3
  65. package/server/index.d.ts +15 -0
  66. package/server/index.js +45 -9
  67. package/server/lib/data-dir-migration.js +40 -0
  68. package/server/lib/data-dir-migration.test.js +59 -0
  69. package/server/lib/event-stream/runtime.test.js +12 -12
  70. package/server/lib/fs/DOCUMENTATION.md +3 -0
  71. package/server/lib/fs/routes.js +31 -0
  72. package/server/lib/fs/routes.test.js +110 -1
  73. package/server/lib/git/DOCUMENTATION.md +29 -5
  74. package/server/lib/git/routes.js +24 -4
  75. package/server/lib/git/service.js +184 -198
  76. package/server/lib/git/service.test.js +422 -25
  77. package/server/lib/inherited-env.js +16 -9
  78. package/server/lib/inherited-env.test.js +8 -9
  79. package/server/lib/message-queue/DOCUMENTATION.md +4 -1
  80. package/server/lib/network-defaults.js +23 -0
  81. package/server/lib/network-defaults.test.js +52 -0
  82. package/server/lib/opencode/DOCUMENTATION.md +17 -1
  83. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  84. package/server/lib/opencode/feature-routes-runtime.js +2 -0
  85. package/server/lib/opencode/openchamber-routes.js +64 -4
  86. package/server/lib/opencode/openchamber-routes.test.js +128 -1
  87. package/server/lib/opencode/routes.js +5 -3
  88. package/server/lib/opencode/settings-files.js +214 -0
  89. package/server/lib/opencode/settings-helpers.js +85 -41
  90. package/server/lib/opencode/settings-helpers.test.js +166 -1
  91. package/server/lib/opencode/settings-registry.json +747 -0
  92. package/server/lib/opencode/settings-runtime.js +139 -18
  93. package/server/lib/opencode/settings-runtime.test.js +163 -5
  94. package/server/lib/project-context/DOCUMENTATION.md +25 -1
  95. package/server/lib/project-context/routes.http.test.js +18 -0
  96. package/server/lib/project-context/routes.js +15 -0
  97. package/server/lib/project-context/runtime.js +248 -23
  98. package/server/lib/project-context/runtime.test.js +106 -3
  99. package/server/lib/projects/DOCUMENTATION.md +76 -0
  100. package/server/lib/projects/project-config.js +142 -9
  101. package/server/lib/projects/project-id.js +17 -0
  102. package/server/lib/projects/project-setup.js +409 -0
  103. package/server/lib/projects/project-setup.test.js +557 -0
  104. package/server/lib/projects/routes.js +59 -0
  105. package/server/lib/quota/DOCUMENTATION.md +31 -1
  106. package/server/lib/quota/providers/cline-pass.js +125 -0
  107. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  108. package/server/lib/quota/providers/index.js +7 -0
  109. package/server/lib/quota/providers/neuralwatt.js +3 -6
  110. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  111. package/server/lib/quota/providers/ollama-cloud.js +31 -0
  112. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  113. package/server/lib/quota/providers/opencode-go.js +1 -1
  114. package/server/lib/quota/providers/opencode-go.test.js +26 -1
  115. package/server/lib/quota/providers/openrouter.js +119 -24
  116. package/server/lib/quota/providers/openrouter.test.js +330 -0
  117. package/server/lib/relay/DOCUMENTATION.md +55 -0
  118. package/server/lib/relay/cross-compat.test.js +26 -0
  119. package/server/lib/relay/downstream-scheduler.js +154 -0
  120. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  121. package/server/lib/relay/e2ee.js +10 -8
  122. package/server/lib/relay/flow-control.test.js +177 -0
  123. package/server/lib/relay/host-client.js +37 -8
  124. package/server/lib/relay/tunnel-codec.js +9 -0
  125. package/server/lib/relay/tunnel-host.js +66 -17
  126. package/server/lib/session-assist/runtime.js +6 -10
  127. package/server/lib/session-goal/runtime.js +4 -9
  128. package/server/lib/small-model/DOCUMENTATION.md +3 -2
  129. package/server/lib/small-model/call.js +9 -4
  130. package/server/lib/small-model/call.test.js +29 -0
  131. package/server/lib/small-model/index.js +5 -10
  132. package/server/lib/small-model/runtime-providers.js +15 -7
  133. package/server/lib/small-model/runtime-providers.test.js +7 -1
  134. package/server/lib/terminal/DOCUMENTATION.md +4 -4
  135. package/server/lib/terminal/runtime.js +11 -6
  136. package/server/lib/terminal/runtime.test.js +20 -13
  137. package/server/lib/walkthrough/DOCUMENTATION.md +33 -14
  138. package/server/lib/walkthrough/model-settings.js +8 -12
  139. package/server/lib/walkthrough/prompt.js +3 -1
  140. package/server/lib/walkthrough/sources.js +23 -2
  141. package/dist/assets/DiffView-hDBKesA_.js +0 -4
  142. package/dist/assets/FilesView-CdWzeOp9.js +0 -17
  143. package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
  144. package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
  145. package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
  146. package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
  147. package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
  148. package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
  149. package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
  150. package/dist/assets/de-B5JaA3SA.js +0 -10
  151. package/dist/assets/es-EPIH88ii.js +0 -16
  152. package/dist/assets/fr-CAMfxUdZ.js +0 -14
  153. package/dist/assets/index-DF5kMP-i.js +0 -39
  154. package/dist/assets/index-Dl3xbAB_.css +0 -1
  155. package/dist/assets/ja-CifOE1VN.js +0 -16
  156. package/dist/assets/ko-BBDyAyYQ.js +0 -16
  157. package/dist/assets/main-MhSaOZoB.js +0 -930
  158. package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
  159. package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
  160. package/dist/assets/pl-DgEHYfFM.js +0 -16
  161. package/dist/assets/pt-BR-DuWHojUw.js +0 -16
  162. package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
  163. package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
  164. package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
  165. package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
  166. package/dist/assets/tr-D5m2yIWL.js +0 -7
  167. package/dist/assets/uk-D_q60oog.js +0 -16
  168. package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
  169. package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
  170. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  171. package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
  172. package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
  173. package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
@@ -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
  }
@@ -452,11 +518,6 @@ export const createSettingsHelpers = (dependencies) => {
452
518
  result.managedRemoteTunnelSelectedPresetId = id || undefined;
453
519
  }
454
520
 
455
- const typography = sanitizeTypographySizesPartial(candidate.typographySizes);
456
- if (typography) {
457
- result.typographySizes = typography;
458
- }
459
-
460
521
  if (typeof candidate.defaultModel === 'string') {
461
522
  const trimmed = candidate.defaultModel.trim();
462
523
  result.defaultModel = trimmed.length > 0 ? trimmed : undefined;
@@ -502,14 +563,6 @@ export const createSettingsHelpers = (dependencies) => {
502
563
  const trimmed = candidate.zenModel.trim();
503
564
  result.zenModel = trimmed.length > 0 ? trimmed : undefined;
504
565
  }
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
566
  if (typeof candidate.pwaAppName === 'string') {
514
567
  result.pwaAppName = normalizePwaAppName(candidate.pwaAppName, undefined);
515
568
  }
@@ -522,12 +575,6 @@ export const createSettingsHelpers = (dependencies) => {
522
575
  result.mobileKeyboardMode = mode;
523
576
  }
524
577
  }
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
578
  if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
532
579
  result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
533
580
  }
@@ -619,9 +666,6 @@ export const createSettingsHelpers = (dependencies) => {
619
666
  if (typeof candidate.promptNavigatorEnabled === 'boolean') {
620
667
  result.promptNavigatorEnabled = candidate.promptNavigatorEnabled;
621
668
  }
622
- if (typeof candidate.expandedEditorToolbar === 'boolean') {
623
- result.expandedEditorToolbar = candidate.expandedEditorToolbar;
624
- }
625
669
  if (typeof candidate.wideChatLayoutEnabled === 'boolean') {
626
670
  result.wideChatLayoutEnabled = candidate.wideChatLayoutEnabled;
627
671
  }
@@ -724,11 +768,6 @@ export const createSettingsHelpers = (dependencies) => {
724
768
  }
725
769
  }
726
770
 
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
771
  const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
733
772
  if (skillCatalogs) {
734
773
  result.skillCatalogs = skillCatalogs;
@@ -912,6 +951,16 @@ export const createSettingsHelpers = (dependencies) => {
912
951
  }
913
952
  }
914
953
 
954
+ // The registry is the last word on what a client may persist: a key the
955
+ // code above still names but the registry no longer lists is dropped here,
956
+ // so the two cannot drift apart silently (settings-helpers.test.js checks
957
+ // the other direction).
958
+ for (const key of Object.keys(result)) {
959
+ if (!isPersistableSettingsKey(key)) {
960
+ delete result[key];
961
+ }
962
+ }
963
+
915
964
  return result;
916
965
  };
917
966
 
@@ -922,13 +971,6 @@ export const createSettingsHelpers = (dependencies) => {
922
971
  ? current.securityScopedBookmarks
923
972
  : [];
924
973
 
925
- const nextTypographySizes = changes.typographySizes
926
- ? {
927
- ...(current.typographySizes || {}),
928
- ...changes.typographySizes
929
- }
930
- : current.typographySizes;
931
-
932
974
  const next = {
933
975
  ...current,
934
976
  ...changes,
@@ -937,7 +979,6 @@ export const createSettingsHelpers = (dependencies) => {
937
979
  baseBookmarks.filter((entry) => typeof entry === 'string' && entry.length > 0)
938
980
  )
939
981
  ),
940
- typographySizes: nextTypographySizes
941
982
  };
942
983
 
943
984
  return next;
@@ -945,9 +986,12 @@ export const createSettingsHelpers = (dependencies) => {
945
986
 
946
987
  const formatSettingsResponse = (settings) => {
947
988
  const sanitized = sanitizeSettingsUpdate(settings);
948
- delete sanitized.managedRemoteTunnelToken;
989
+ for (const key of SECRET_SETTINGS_KEYS) {
990
+ delete sanitized[key];
991
+ }
949
992
  const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
950
993
  const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
994
+ const hasDesktopUiPassword = typeof settings?.desktopUiPassword === 'string' && settings.desktopUiPassword.trim().length > 0;
951
995
  const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
952
996
  const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
953
997
  const mobileKeyboardMode = normalizeMobileKeyboardMode(settings?.mobileKeyboardMode, 'native');
@@ -957,6 +1001,7 @@ export const createSettingsHelpers = (dependencies) => {
957
1001
  return {
958
1002
  ...sanitized,
959
1003
  hasManagedRemoteTunnelToken,
1004
+ hasDesktopUiPassword,
960
1005
  // Tells the client whether agent memory exists in this build at all, so
961
1006
  // its settings row and panel tab can be absent rather than merely off.
962
1007
  agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
@@ -967,7 +1012,6 @@ export const createSettingsHelpers = (dependencies) => {
967
1012
  inputHistoryLimit,
968
1013
  securityScopedBookmarks: bookmarks,
969
1014
  pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
970
- typographySizes: sanitizeTypographySizesPartial(settings.typographySizes),
971
1015
  ...(process.env.AIWORKER_RUNTIME === 'desktop'
972
1016
  ? {
973
1017
  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');
@@ -711,3 +724,155 @@ describe('settings helpers', () => {
711
724
  });
712
725
  });
713
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: 'AiWorker', 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
+ });