merge: resolve v1.23.0 upstream conflicts, preserve custom git provider config

Resolved conflicts in 8 files by taking upstream refactored code:
- desktop.ts: re-export DesktopSettings from registry
- openchamberConfig.ts: simplified project setup client
- persistence.ts: registry-derived settings, add git provider hydration
- search.ts: upstream search entries + git provider entries
- useConfigStore.ts: loadDesktopSettings() path
- settings-helpers.js: add gitProviderId/gitModelId/gitProviders sanitization
- DOCUMENTATION.md: upstream walkthrough docs
- vite.config.ts: upstream SW glob patterns

Custom fork additions preserved:
- gitProviderId, gitModelId, gitProviders fields in settings registry
- Git provider domain store hydration in persistence.ts
- Git provider search entries in search.ts
- Git provider sanitization in settings-helpers.js
This commit is contained in:
2026-09-10 10:11:50 +00:00
559 changed files with 36139 additions and 9087 deletions
@@ -1,5 +1,33 @@
import { sanitizeGitProviders } from '../git-providers/config.js';
import { createRequire } from 'node:module';
import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
import { sanitizeGitProviders } from '../git-providers/config.js';
// Generated from packages/ui/src/lib/settings/registry.ts by
// `bun run settings-registry:generate`; `registry.test.ts` fails when stale.
// The server is plain ESM without a bundler, so the snapshot is read with
// `createRequire` (import attributes differ across the Node versions we run on).
const settingsRegistry = createRequire(import.meta.url)('./settings-registry.json');
/**
* Whether a client may persist this key through PUT /api/config/settings:
* it must be a registry key, not a server-computed flag, not a device field
* that only lives in the browser, and not one the desktop shell writes itself.
*/
const isPersistableSettingsKey = (key) => {
const field = settingsRegistry.fields[key];
if (!field) return false;
if (field.computed || field.local) return false;
if (field.owner === 'desktop-shell') return false;
return true;
};
/** Keys accepted on write but never returned by a read. */
const SECRET_SETTINGS_KEYS = Object.freeze(
Object.entries(settingsRegistry.fields)
.filter(([, field]) => field.secret === true)
.map(([key]) => key),
);
import {
DEFAULT_INPUT_HISTORY_LIMIT,
DEFAULT_INPUT_HISTORY_SCOPE,
@@ -19,7 +47,6 @@ export const createSettingsHelpers = (dependencies) => {
normalizeManagedRemoteTunnelHostname,
normalizeManagedRemoteTunnelPresets,
normalizeManagedRemoteTunnelPresetTokens,
sanitizeTypographySizesPartial,
normalizeStringArray,
sanitizeModelRefs,
sanitizeSkillCatalogs,
@@ -204,6 +231,9 @@ export const createSettingsHelpers = (dependencies) => {
...new Set(candidate.workStatusHiddenSections.filter((entry) => typeof entry === 'string' && entry.length > 0)),
];
}
if (typeof candidate.workStatusHiddenSectionsExplicit === 'boolean') {
result.workStatusHiddenSectionsExplicit = candidate.workStatusHiddenSectionsExplicit;
}
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
}
@@ -314,9 +344,6 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.monoFont === 'string' && candidate.monoFont.length > 0) {
result.monoFont = candidate.monoFont;
}
if (typeof candidate.markdownDisplayMode === 'string' && candidate.markdownDisplayMode.length > 0) {
result.markdownDisplayMode = candidate.markdownDisplayMode;
}
if (typeof candidate.githubClientId === 'string') {
const trimmed = candidate.githubClientId.trim();
if (trimmed.length > 0) {
@@ -332,6 +359,45 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces;
}
if (typeof candidate.streamingAutoFollowEnabled === 'boolean') {
result.streamingAutoFollowEnabled = candidate.streamingAutoFollowEnabled;
}
if (typeof candidate.codeBlockLineWrap === 'boolean') {
result.codeBlockLineWrap = candidate.codeBlockLineWrap;
}
if (typeof candidate.autoSaveEnabled === 'boolean') {
result.autoSaveEnabled = candidate.autoSaveEnabled;
}
if (typeof candidate.diffWrapLines === 'boolean') {
result.diffWrapLines = candidate.diffWrapLines;
}
if (typeof candidate.persistChatDraft === 'boolean') {
result.persistChatDraft = candidate.persistChatDraft;
}
if (typeof candidate.allowPromptingSubagentSessions === 'boolean') {
result.allowPromptingSubagentSessions = candidate.allowPromptingSubagentSessions;
}
if (typeof candidate.showOpenCodeRestartConfirm === 'boolean') {
result.showOpenCodeRestartConfirm = candidate.showOpenCodeRestartConfirm;
}
if (typeof candidate.sessionTabsEnabled === 'boolean') {
result.sessionTabsEnabled = candidate.sessionTabsEnabled;
}
if (typeof candidate.largeTextPasteBehavior === 'string') {
const mode = candidate.largeTextPasteBehavior.trim();
if (mode === 'ask' || mode === 'attach' || mode === 'inline') {
result.largeTextPasteBehavior = mode;
}
}
if (typeof candidate.fileEditorKeymap === 'string') {
const mode = candidate.fileEditorKeymap.trim();
if (mode === 'default' || mode === 'vim') {
result.fileEditorKeymap = mode;
}
}
if (Array.isArray(candidate.providerOrder)) {
result.providerOrder = normalizeStringArray(candidate.providerOrder);
}
if (typeof candidate.sessionRecapEnabled === 'boolean') {
result.sessionRecapEnabled = candidate.sessionRecapEnabled;
}
@@ -453,11 +519,6 @@ export const createSettingsHelpers = (dependencies) => {
result.managedRemoteTunnelSelectedPresetId = id || undefined;
}
const typography = sanitizeTypographySizesPartial(candidate.typographySizes);
if (typography) {
result.typographySizes = typography;
}
if (typeof candidate.defaultModel === 'string') {
const trimmed = candidate.defaultModel.trim();
result.defaultModel = trimmed.length > 0 ? trimmed : undefined;
@@ -527,12 +588,6 @@ export const createSettingsHelpers = (dependencies) => {
result.mobileKeyboardMode = mode;
}
}
if (typeof candidate.toolCallExpansion === 'string') {
const mode = candidate.toolCallExpansion.trim();
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed' || mode === 'changes') {
result.toolCallExpansion = mode;
}
}
if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
}
@@ -624,9 +679,6 @@ export const createSettingsHelpers = (dependencies) => {
if (typeof candidate.promptNavigatorEnabled === 'boolean') {
result.promptNavigatorEnabled = candidate.promptNavigatorEnabled;
}
if (typeof candidate.expandedEditorToolbar === 'boolean') {
result.expandedEditorToolbar = candidate.expandedEditorToolbar;
}
if (typeof candidate.wideChatLayoutEnabled === 'boolean') {
result.wideChatLayoutEnabled = candidate.wideChatLayoutEnabled;
}
@@ -729,11 +781,6 @@ export const createSettingsHelpers = (dependencies) => {
}
}
// Message limit — single setting for fetch / trim / Load More chunk
if (typeof candidate.messageLimit === 'number' && Number.isFinite(candidate.messageLimit)) {
result.messageLimit = Math.max(10, Math.min(500, Math.round(candidate.messageLimit)));
}
const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
if (skillCatalogs) {
result.skillCatalogs = skillCatalogs;
@@ -917,6 +964,16 @@ export const createSettingsHelpers = (dependencies) => {
}
}
// The registry is the last word on what a client may persist: a key the
// code above still names but the registry no longer lists is dropped here,
// so the two cannot drift apart silently (settings-helpers.test.js checks
// the other direction).
for (const key of Object.keys(result)) {
if (!isPersistableSettingsKey(key)) {
delete result[key];
}
}
return result;
};
@@ -927,13 +984,6 @@ export const createSettingsHelpers = (dependencies) => {
? current.securityScopedBookmarks
: [];
const nextTypographySizes = changes.typographySizes
? {
...(current.typographySizes || {}),
...changes.typographySizes
}
: current.typographySizes;
const next = {
...current,
...changes,
@@ -942,7 +992,6 @@ export const createSettingsHelpers = (dependencies) => {
baseBookmarks.filter((entry) => typeof entry === 'string' && entry.length > 0)
)
),
typographySizes: nextTypographySizes
};
return next;
@@ -950,9 +999,12 @@ export const createSettingsHelpers = (dependencies) => {
const formatSettingsResponse = (settings) => {
const sanitized = sanitizeSettingsUpdate(settings);
delete sanitized.managedRemoteTunnelToken;
for (const key of SECRET_SETTINGS_KEYS) {
delete sanitized[key];
}
const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
const hasDesktopUiPassword = typeof settings?.desktopUiPassword === 'string' && settings.desktopUiPassword.trim().length > 0;
const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
const mobileKeyboardMode = normalizeMobileKeyboardMode(settings?.mobileKeyboardMode, 'native');
@@ -962,6 +1014,7 @@ export const createSettingsHelpers = (dependencies) => {
return {
...sanitized,
hasManagedRemoteTunnelToken,
hasDesktopUiPassword,
// Tells the client whether agent memory exists in this build at all, so
// its settings row and panel tab can be absent rather than merely off.
agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
@@ -972,7 +1025,6 @@ export const createSettingsHelpers = (dependencies) => {
inputHistoryLimit,
securityScopedBookmarks: bookmarks,
pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
typographySizes: sanitizeTypographySizesPartial(settings.typographySizes),
...(process.env.OPENCHAMBER_RUNTIME === 'desktop'
? {
desktopLanAccessActive: process.env.OPENCHAMBER_DESKTOP_LAN_ACCESS_ACTIVE === 'true',