defaultValue: ColorMode.SYSTEM,
type: SettingsFieldType.SELECT,
section: SETTINGS_SECTION_SLUGS.GENERAL,
- options: COLOR_MODE_OPTIONS,
- sync: { serverKey: SETTINGS_KEYS.THEME, paramType: SyncableParameterType.STRING }
+ options: COLOR_MODE_OPTIONS
},
{
key: SETTINGS_KEYS.API_KEY,
help: 'The starting message that defines how model should behave.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- sync: {
- serverKey: SETTINGS_KEYS.SYSTEM_MESSAGE,
- paramType: SyncableParameterType.STRING
- }
+ section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
help: 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.',
defaultValue: 2500,
type: SettingsFieldType.INPUT,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- sync: {
- serverKey: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
- paramType: SyncableParameterType.NUMBER
- }
+ section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.SEND_ON_ENTER,
help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- sync: {
- serverKey: SETTINGS_KEYS.SEND_ON_ENTER,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL,
- isExperimental: true,
- sync: {
- serverKey: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
- paramType: SyncableParameterType.BOOLEAN
- }
+ isExperimental: true
},
{
key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
section: SETTINGS_SECTION_SLUGS.GENERAL,
- isExperimental: true,
- sync: {
- serverKey: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
- paramType: SyncableParameterType.BOOLEAN
- }
+ isExperimental: true
},
{
...TITLE_GENERATION_BASE,
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
label: 'Conversation title',
help: 'Choose how conversation titles are generated. The first non-empty line uses a fast deterministic rule; the LLM option uses a model-generated title from the first message exchange.',
- defaultValue: true,
- sync: {
- serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
- paramType: SyncableParameterType.BOOLEAN
- }
+ defaultValue: true
},
{
key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
type: SettingsFieldType.TEXTAREA,
section: SETTINGS_SECTION_SLUGS.GENERAL,
- dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
- sync: {
- serverKey: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
- paramType: SyncableParameterType.STRING
- }
+ dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM
},
{
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- sync: {
- serverKey: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.PDF_AS_IMAGE,
help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- sync: {
- serverKey: SETTINGS_KEYS.PDF_AS_IMAGE,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.GENERAL
},
{
key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
help: 'Images larger than this will be resized before sending to server. Set to 0 to disable.',
defaultValue: 0,
type: SettingsFieldType.INPUT,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- sync: {
- serverKey: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
- paramType: SyncableParameterType.NUMBER
- }
+ section: SETTINGS_SECTION_SLUGS.GENERAL
}
]
},
help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_AGENTIC_TURN_STATS,
help: 'Expand thought process by default when generating messages.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
help: 'Render user messages using markdown formatting in the chat.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN,
help: 'Render the reasoning/thinking block content as formatted Markdown instead of plain text.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
help: 'Always display code blocks at their full natural height, overriding any height limits.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION,
help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_MODEL_TAGS,
help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.',
defaultValue: true,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_MODEL_TAGS,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
},
{
key: SETTINGS_KEYS.SHOW_BUILD_VERSION,
help: 'Display the current build version in the bottom-right corner of the interface.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_BUILD_VERSION,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DISPLAY
}
]
},
help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- serverKey: SETTINGS_KEYS.BACKEND_SAMPLING,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.SAMPLING
}
]
},
defaultValue: 10,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.AGENTIC,
- isPositiveInteger: true,
- sync: {
- serverKey: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
- paramType: SyncableParameterType.NUMBER
- }
+ isPositiveInteger: true
},
{
key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.AGENTIC,
- isPositiveInteger: true,
- sync: {
- serverKey: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
- paramType: SyncableParameterType.NUMBER
- }
+ isPositiveInteger: true
}
]
},
help: 'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- sync: {
- serverKey: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- sync: {
- serverKey: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
help: 'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- sync: {
- serverKey: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
help: 'Expose a run_javascript tool to the model. Code runs in a Web Worker inside a sandboxed iframe with an opaque origin, isolated from the WebUI and its API, with a hard timeout.',
defaultValue: false,
type: SettingsFieldType.CHECKBOX,
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- sync: {
- serverKey: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
- paramType: SyncableParameterType.BOOLEAN
- }
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER
},
{
key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED,
help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.',
defaultValue: '',
type: SettingsFieldType.TEXTAREA,
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- sync: {
- serverKey: SETTINGS_KEYS.CUSTOM_CSS,
- paramType: SyncableParameterType.STRING
- }
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER
}
]
}
label: 'Show system message',
help: 'Display the system message at the top of each conversation.',
defaultValue: true,
- type: SettingsFieldType.CHECKBOX,
- sync: {
- serverKey: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE,
- paramType: SyncableParameterType.BOOLEAN
- }
+ type: SettingsFieldType.CHECKBOX
},
{
key: SETTINGS_KEYS.MCP_SERVERS,
label: 'MCP servers',
help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
defaultValue: '[]',
- type: SettingsFieldType.INPUT,
- sync: { serverKey: SETTINGS_KEYS.MCP_SERVERS, paramType: SyncableParameterType.STRING }
+ type: SettingsFieldType.INPUT
},
{
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
label: 'Generate title with LLM',
help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.',
defaultValue: false,
- type: SettingsFieldType.CHECKBOX,
- sync: {
- serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
- paramType: SyncableParameterType.BOOLEAN
- }
+ type: SettingsFieldType.CHECKBOX
}
// {
// key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED,
// defaultValue: false,
// type: SettingsFieldType.CHECKBOX,
// isExperimental: true,
- // sync: { serverKey: SETTINGS_KEYS.PY_INTERPRETER_ENABLED, paramType: SyncableParameterType.BOOLEAN }
+ //
// }
];
--- /dev/null
+import { beforeEach, describe, expect, it } from 'vitest';
+import { settingsStore, config } from '$lib/stores/settings.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
+import { ParameterSyncService } from '$lib/services/parameter-sync.service';
+import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-registry';
+import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import type { SettingsConfigType } from '$lib/types';
+
+type Primitive = string | number | boolean;
+
+const KEYS = Object.keys(SETTING_CONFIG_DEFAULT).filter(
+ (k) => ['string', 'number', 'boolean'].includes(typeof SETTING_CONFIG_DEFAULT[k]) && k !== 'theme'
+);
+
+function divergent(key: string, base: Primitive): Primitive {
+ if (typeof base === 'boolean') return !base;
+ if (typeof base === 'number') return base + 7;
+ return `user-${key}`;
+}
+
+function baselineFor(key: string, base: Primitive): Primitive {
+ if (typeof base === 'boolean') return !base;
+ if (typeof base === 'number') return base + 42;
+ return `admin-${key}`;
+}
+
+function mockProps(uiSettings: Record<string, Primitive>) {
+ Object.defineProperty(serverStore, 'props', {
+ configurable: true,
+ get: () =>
+ ({
+ default_generation_settings: { params: { temperature: 0.8 } },
+ ui_settings: uiSettings
+ }) as unknown as typeof serverStore.props
+ });
+}
+
+const setUser = (key: string, value: Primitive) =>
+ settingsStore.updateConfig(key as keyof SettingsConfigType, value as never);
+
+const current = (key: string) => (config() as Record<string, unknown>)[key];
+
+describe('registry-wide invariants', () => {
+ beforeEach(() => {
+ localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY);
+ });
+
+ it('I1: no load ever modifies a stored user value, for any key of any type', () => {
+ settingsStore.initialize();
+ const userValues: Record<string, Primitive> = {};
+ for (const key of KEYS) {
+ userValues[key] = divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
+ setUser(key, userValues[key]);
+ }
+
+ // simulated F5 + adverse admin baseline on every key, synced twice
+ settingsStore.initialize();
+ const adverse: Record<string, Primitive> = {};
+ for (const key of KEYS)
+ adverse[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
+ mockProps(adverse);
+ settingsStore.syncWithServerDefaults();
+ settingsStore.syncWithServerDefaults();
+
+ for (const key of KEYS) {
+ expect(current(key), key).toBe(userValues[key]);
+ }
+ });
+
+ it('first visit: the baseline applies for every key, false and 0 included', () => {
+ settingsStore.initialize();
+ const baseline: Record<string, Primitive> = {};
+ for (const key of KEYS)
+ baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
+ mockProps(baseline);
+
+ settingsStore.syncWithServerDefaults();
+
+ for (const key of KEYS) {
+ if (ParameterSyncService.canSyncParameter(key)) continue;
+ expect(current(key), key).toBe(baseline[key]);
+ }
+ });
+
+ it('I3: Reset returns every key to baseline when defined, factory default otherwise', () => {
+ settingsStore.initialize();
+ for (const key of KEYS) setUser(key, divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive));
+
+ const baseline: Record<string, Primitive> = {};
+ KEYS.filter((_, i) => i % 2 === 0).forEach((key) => {
+ baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
+ });
+ mockProps(baseline);
+
+ settingsStore.forceSyncWithServerDefaults();
+
+ for (const key of KEYS) {
+ if (key in baseline) {
+ expect(current(key), key).toBe(baseline[key]);
+ } else if (ParameterSyncService.canSyncParameter(key)) {
+ expect(current(key), key).toBe('');
+ } else {
+ expect(current(key), key).toBe(SETTING_CONFIG_DEFAULT[key]);
+ }
+ }
+ });
+});