Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
4cf7bed
🧭 feat: Purpose-Built AI Providers Editor (Wave 2)
danny-avila Oct 20, 2026
3fb0ac0
🛠️ feat: MCP Servers Editor (Wave 2)
danny-avila Oct 20, 2026
b35ef73
🔌 feat: Custom Endpoints List and Editor Drawer (Wave 2)
danny-avila Oct 20, 2026
6c84a38
🧩 feat: Model Specs Editor (Wave 2)
danny-avila Oct 20, 2026
843d799
✨ feat: Features Tab Editors (Wave 2)
danny-avila Oct 20, 2026
2455ccf
🗂️ feat: Files & Storage and System Editors (Wave 2)
danny-avila Oct 20, 2026
995c1b3
🐛 fix: Keep Pending Edits Reachable When Showing Set Values Only
danny-avila Oct 22, 2026
a897480
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
0585188
Merge branch 'feat/config-w2-1-providers' into feat/config-w2-2-endpo…
danny-avila Oct 22, 2026
f2a3c5b
Merge branch 'feat/config-w2-2-endpoints' into feat/config-w2-3-specs
danny-avila Oct 22, 2026
4ebfc09
🐛 fix: Harden MCP Server Auth, Rename, Duplicate and Transport Edits
danny-avila Oct 22, 2026
9b8f6da
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
020ad7d
🐛 fix: Restore Saved Memory Agent IDs and Read Memory State Correctly
danny-avila Oct 22, 2026
ae3dac8
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
348b6f2
🐛 fix: Accurate Storage, Registration, Credits, Skill Sync and Rate L…
danny-avila Oct 22, 2026
b9d1d02
🔒 fix: Mask Secret Values in Collapsed Provider Summaries
danny-avila Oct 22, 2026
9aea2b8
Merge branch 'feat/config-w2-1-providers' into feat/config-w2-2-endpo…
danny-avila Oct 22, 2026
ecbfe5d
🐛 fix: Restore Undone Endpoint Deletions in Their Original Order
danny-avila Oct 22, 2026
e8e08ac
🐛 fix: Keep Pending Inherited Endpoint Overrides Listed Under Set Val…
danny-avila Oct 22, 2026
1030105
🐛 fix: Store Complete Built-In Parameter Definitions
danny-avila Oct 22, 2026
de1e539
🐛 fix: Insert Header Placeholders Into Header Values Only
danny-avila Oct 22, 2026
0f404a7
Merge branch 'feat/config-w2-2-endpoints' into feat/config-w2-3-specs
danny-avila Oct 22, 2026
3d5460b
🐛 fix: Keep Spec Order When One Save Adds and Removes Specs
danny-avila Oct 22, 2026
16abc8a
🐛 fix: Keep a New Spec's Required Fields Under Set Values Only
danny-avila Oct 22, 2026
c09f17f
🐛 fix: Block Saving Blank or Space-Padded Duplicate Spec Names
danny-avila Oct 22, 2026
3e04456
🐛 fix: Index Only the Prompt Fields a Spec Editor Shows
danny-avila Oct 22, 2026
ebdab4c
🐛 fix: Compare a Spec Rename With the Name It Was Loaded With
danny-avila Oct 22, 2026
5a1c35d
🐛 fix: Don't Inherit Spec Fields Through a Replaced Spec List
danny-avila Oct 22, 2026
e7d8998
🐛 fix: Clear the Agent ID When a Spec Leaves the Agents Endpoint
danny-avila Oct 22, 2026
821bf2a
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
e03a2e5
🔒 fix: Require Stored MCP Secrets Again on Rename and Drop Rename Rec…
danny-avila Oct 22, 2026
5b5be53
🐛 fix: Keep an Overridden Inherited MCP Server Visible Under Set Valu…
danny-avila Oct 22, 2026
a3fbfd8
🐛 fix: Pick MCP Copy Names Free in Any Letter Case
danny-avila Oct 22, 2026
5955f12
🔒 fix: Show Stored MCP Secrets as Their Masked Preview With Replace
danny-avila Oct 22, 2026
c7d1b88
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
418417b
🐛 fix: Keep a Stored Memory Agent ID When Switching Back Before Saving
danny-avila Oct 22, 2026
6b58e7f
🐛 fix: Forget the Chosen Memory Agent Definition When the Edit Sessio…
danny-avila Oct 22, 2026
2401894
🐛 fix: Show Every Setting of a Web Search Service in Use, Pending One…
danny-avila Oct 22, 2026
31a367b
🐛 fix: Keep Feature Groups With Pending Edits Under Set Values Only
danny-avila Oct 22, 2026
76c81de
🐛 fix: Keep a Newly Edited Memory Agent Under Set Values Only
danny-avila Oct 22, 2026
e4aea2a
🐛 fix: Drop a Pending Domain List When Its Last Domain Is Removed
danny-avila Oct 22, 2026
67b42b3
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
355e53f
🐛 fix: Keep Pending Upload Limits Under Set Values Only
danny-avila Oct 22, 2026
e622304
🐛 fix: Read Rate Limits From librechat.yaml and List Unplaced Schema …
danny-avila Oct 22, 2026
abda3ec
🐛 fix: Show Startup-Only Registration Settings From librechat.yaml
danny-avila Oct 22, 2026
962b3ea
🐛 fix: Render Storage File Types the Fixed List Does Not Know
danny-avila Oct 22, 2026
c1e3573
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
666288f
🎨 style: Format MCP Secret Preview and Rename Validation Code
danny-avila Oct 22, 2026
1247f3e
🎨 style: Format Web Search Service Tests
danny-avila Oct 22, 2026
dad24c5
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
1aca69b
🐛 fix: Keep Configured Azure Groups Visible When Showing Set Values Only
danny-avila Oct 22, 2026
eb3ff9a
Merge branch 'feat/config-w2-1-providers' into feat/config-w2-2-endpo…
danny-avila Oct 22, 2026
28238c1
Merge branch 'feat/config-w2-2-endpoints' into feat/config-w2-3-specs
danny-avila Oct 22, 2026
5888feb
🐛 fix: Resolve Swapped and Chained Entry Renames as One Batch
danny-avila Oct 22, 2026
1c9f458
🐛 fix: Keep Card State With a Spec When the List Is Reordered
danny-avila Oct 22, 2026
626b7ae
🐛 fix: Align the Spec Editor's Model, Parameter and Unknown Rows With…
danny-avila Oct 22, 2026
8ec1a05
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
4914645
🐛 fix: Ask a Renamed MCP Server Only for Secrets Its Authentication S…
danny-avila Oct 22, 2026
193ab79
🐛 fix: Clear MCP Delete Undo State When the Edit Session Ends
danny-avila Oct 22, 2026
56b3109
🐛 fix: Keep a Renamed MCP Server's Field Edits When Its Rename Is Rev…
danny-avila Oct 22, 2026
dede842
🎨 style: Format the Spec Editor's Parameter Imports
danny-avila Oct 22, 2026
6e47384
Merge branch 'feat/config-w2-3-specs' into feat/config-w2-4-mcp
danny-avila Oct 22, 2026
b0e21b0
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
7a09cb8
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
e279e7e
🐛 fix: Keep Storage Backends the Panel Does Not Know
danny-avila Oct 22, 2026
c7ab348
🐛 fix: List Only Set Rate Limiters When Showing Set Values Only
danny-avila Oct 22, 2026
dba753c
🐛 fix: List Unknown GitHub Skill Sync Settings in the GitHub Group
danny-avila Oct 22, 2026
ddc823b
🐛 fix: List Unknown Keys of librechat.yaml-Only Objects in Their Group
danny-avila Oct 22, 2026
adcc44b
🐛 fix: Confirm an Empty MIME List Even When It Matches the List Beneath
danny-avila Oct 22, 2026
bf90e66
🐛 fix: Remove a Setting Whole When a Reverted Rename Had Dropped It
danny-avila Oct 22, 2026
a283353
Merge branch 'feat/config-w2-4-mcp' into feat/config-w2-5-features
danny-avila Oct 22, 2026
11caaa1
Merge branch 'feat/config-w2-5-features' into feat/config-w2-6-filesys
danny-avila Oct 22, 2026
6641a00
Merge main (previous level's squash 28e8c6a) into feat/config-w2-6-fi…
danny-avila Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion e2e/config-shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ test.describe('Configuration shell: profile scope and guard', () => {
await expect(guard).toBeVisible();
await guard.getByRole('button', { name: 'Cancel' }).click();
await expect(page).toHaveURL(/configuration/);
await expect(page.locator('#balance-startBalance')).toHaveValue('424242');
await expect(page.locator('#balance-startBalance')).toHaveValue('424,242');
await page.locator('aside a[href*="/access"]').first().click();
await page.getByRole('dialog').getByRole('button', { name: 'Discard changes' }).click();
await expect(page).toHaveURL(/access/);
Expand Down
2 changes: 1 addition & 1 deletion e2e/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test.describe('Configuration: Enter never edits another field', () => {
await page.getByRole('button', { name: /^Review & save$/ }).click();
const dialog = page.getByRole('dialog');
await expect(dialog).toContainText('Save 1 change to the base configuration?');
await expect(dialog).toContainText('Start balance');
await expect(dialog).toContainText('Starting credits');
await dialog.getByRole('button', { name: 'Cancel' }).click();
await page.getByRole('button', { name: 'Discard' }).click();
});
Expand Down
102 changes: 102 additions & 0 deletions e2e/files-system.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { test, expect } from '@playwright/test';
import type { Page } from '@playwright/test';
import { LIVE, LIVE_SKIP_REASON, openConfigTab } from './live';

/**
* Files & storage and System journeys (WS14). They need a live LibreChat
* backend: run with E2E_LIVE=1 and E2E_BASE_URL. Each journey discards or
* reverts what it changes.
*/
const unsavedBar = (page: Page) => page.getByRole('region', { name: 'Unsaved changes' });

async function discard(page: Page) {
await unsavedBar(page).getByRole('button', { name: 'Discard' }).click();
await expect(unsavedBar(page)).toHaveCount(0);
}

test.describe('Files & storage and System editors', () => {
test.skip(!LIVE, LIVE_SKIP_REASON);
test.describe.configure({ mode: 'serial', timeout: 120_000 });

test('switching the storage backend asks first and names what it needs', async ({ page }) => {
await openConfigTab(page, 'files');
const backend = page.getByLabel('Storage backend', { exact: true });
const before = await backend.inputValue();
await backend.selectOption('s3');
const dialog = page.getByRole('dialog', { name: 'Switch file storage to Amazon S3?' });
await expect(dialog).toContainText('AWS_BUCKET_NAME');
await expect(dialog).toContainText('Nothing is moved');
await dialog.getByRole('button', { name: 'Cancel' }).click();
await expect(backend).toHaveValue(before);
await expect(unsavedBar(page)).toHaveCount(0);

await backend.selectOption('s3');
await page.getByRole('button', { name: 'Switch to Amazon S3' }).click();
await expect(backend).toHaveValue('s3');
await discard(page);
});

test('a MIME list starts from the list in effect, so adding a type extends it', async ({
page,
}) => {
await openConfigTab(page, 'files');
await page.locator('button.settings-trigger', { hasText: 'OCR-eligible MIME types' }).click();
const list = page.locator('#fileConfig-ocr-supportedMimeTypes');
await expect(list).toContainText("Uses LibreChat's default list (8 patterns).");
await list.getByRole('button', { name: 'Customize' }).click();
await list.getByRole('button', { name: 'Add pattern' }).click();
await page.getByRole('textbox', { name: 'New MIME type pattern' }).fill('.*');
await page.keyboard.press('Enter');
await expect(page.getByRole('dialog', { name: 'Match every file type?' })).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();

await list.getByRole('button', { name: 'Add pattern' }).click();
await page.getByRole('textbox', { name: 'New MIME type pattern' }).fill('^text\\/rtf$');
await page.keyboard.press('Enter');
await expect(list).toContainText('Custom list (9 patterns)');
await list.getByRole('textbox', { name: 'Test a MIME type' }).fill('text/rtf');
await expect(list).toContainText('text/rtf files are sent to OCR');
await discard(page);
});

test('an endpoint key outside the ones LibreChat looks up is refused', async ({ page }) => {
await openConfigTab(page, 'files');
const group = page.locator('[id="section-fileConfig.endpoints"]');
await group.getByRole('button', { name: 'Add endpoint' }).first().click();
const key = group.getByRole('combobox', { name: 'Key' });
await key.fill('Agents');
await key.press('Enter');
await expect(group.getByRole('alert')).toContainText('Did you mean agents?');
await group.getByRole('button', { name: 'Cancel' }).click();
await expect(unsavedBar(page)).toHaveCount(0);
});

test('emptying the allowed sign-up domains asks first', async ({ page }) => {
await openConfigTab(page, 'system');
const chips = page.locator('#registration-allowedDomains li');
const count = await chips.count();
test.skip(count === 0, 'librechat.yaml sets no allowed domains');
for (let i = 0; i < count; i++) {
await chips.first().getByRole('button').click();
}
await expect(page.getByRole('dialog', { name: 'Let anyone sign up?' })).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
await discard(page);
});

test('starting credits read separators, and rate limits are a read-only table', async ({
page,
}) => {
await openConfigTab(page, 'system');
const start = page.getByLabel('Starting credits', { exact: true });
await start.fill('2,500,000');
await start.press('Tab');
await expect(start).toHaveValue('2,500,000');
await expect(page.getByText('Worth about $2.50 of model usage')).toBeVisible();
await discard(page);

const table = page.getByRole('table', { name: 'Rate limits' });
await expect(table.getByRole('row', { name: /File uploads/ })).toContainText('requests /');
await expect(table.getByRole('spinbutton')).toHaveCount(0);
});
});
6 changes: 3 additions & 3 deletions e2e/import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ test('imports a snippet into Base, previewing exactly its keys', async ({ page }
await page.getByRole('radio', { name: /^Base configuration/ }).click();
const { dialog, apply } = await previewAndApply(page, /into Base configuration$/);
await expect(dialog.getByRole('checkbox')).toHaveCount(1);
await expect(dialog.getByText('Start balance')).toBeVisible();
await expect(dialog.getByText('Starting credits')).toBeVisible();
await expect(dialog.getByText(/Prompts: managed with role permissions/)).toBeVisible();
await apply.click();
await expect(
page.getByText(/Imported 1 value into the base configuration/).first(),
).toBeVisible();
await expect(await startBalanceIn(page)).toHaveValue(String(value));
await expect(await startBalanceIn(page)).toHaveValue(value.toLocaleString('en-US'));
});

test('imports into an existing profile, comparing with its own values', async ({ page }) => {
Expand All @@ -73,7 +73,7 @@ test('imports into an existing profile, comparing with its own values', async ({
const { apply } = await previewAndApply(page, /into Role: USER$/);
await apply.click();
await expect(page.getByText(/Imported 1 value into Role: USER/).first()).toBeVisible();
await expect(await startBalanceIn(page, 'role:USER')).toHaveValue(String(value));
await expect(await startBalanceIn(page, 'role:USER')).toHaveValue(value.toLocaleString('en-US'));
});

test('imports into a role that has no profile yet', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion e2e/profiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test.describe('Configuration: profile lifecycle', () => {
await page.getByRole('option', { name: new RegExp(`^${name}`) }).click();
expect(dialogs).toEqual([]);
await expect(unsavedBar(page)).toHaveCount(1);
await expect(startBalance).toHaveValue('4321');
await expect(startBalance).toHaveValue('4,321');

await openSelector(page);
const trash = page.getByRole('option', { name: new RegExp(`^Delete profile ${name}`) });
Expand Down
74 changes: 60 additions & 14 deletions src/components/configuration/ConfigTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { useMemo, useContext } from 'react';
import { Button } from '@clickhouse/click-ui';
import type { KeyboardEvent, ReactNode } from 'react';
import type * as t from '@/types';
import {
SECTION_RENDERERS,
READ_ONLY_DETAIL_KEYS,
SECTION_GROUP_RENDERERS,
UNFOLDED_READ_ONLY_SECTIONS,
SELF_CONTAINED_SECTION_RENDERERS,
} from './sections';
import {
countSection,
hasPendingValueIn,
countSectionOverrides,
countEntrySectionOverrides,
} from './counts';
import { SECTION_RENDERERS, SELF_CONTAINED_SECTION_RENDERERS } from './sections';
import { ConfigValuesProvider } from '@/contexts/ConfigValuesContext';
import { RevealTargetContext } from '@/contexts/RevealTargetContext';
import { FieldRenderer, SingleFieldRenderer } from './renderer';
Expand All @@ -17,7 +23,7 @@ import { deriveFieldValue, isSectionDisabled } from '@/utils';
import { ReadOnlySection } from './ReadOnlySection';
import { UnknownSettings } from './UnknownSettings';
import { ConfigSection } from './ConfigSection';
import { isPathInSection } from './configMeta';
import { SECTION_META, isPathInSection } from './configMeta';
import { CodeField } from './fields/CodeField';
import { InfoBanner } from './InfoBanner';
import { ScopeGate } from './ScopeGate';
Expand Down Expand Up @@ -238,18 +244,17 @@ export function ConfigTabContent({
);
}

const renderSectionContent = (section: t.ConfigSectionConfig) => {
const isSectionReadOnly = (section: t.ConfigSectionConfig): boolean =>
section.scopeGate != null ||
section.readOnlyReason != null ||
isSectionDisabled(!!fieldsDisabled, sectionPermissions, section.schemaKey ?? section.id);

const buildFieldRendererProps = (section: t.ConfigSectionConfig): t.FieldRendererProps => {
const dataKey = section.schemaKey ?? section.id;
const sectionValue = configValues?.[dataKey];
const sectionDisabled =
section.scopeGate != null ||
section.readOnlyReason != null ||
isSectionDisabled(!!fieldsDisabled, sectionPermissions, dataKey);
const CustomSectionRenderer =
section.fields.length > 0 ? SECTION_RENDERERS[section.id] : undefined;
const fieldRendererProps: t.FieldRendererProps = {
const sectionDisabled = isSectionReadOnly(section);
return {
fields: section.fields,
parentValue: sectionValue,
parentValue: configValues?.[dataKey],
parentPath: dataKey,
getValue,
onChange: onFieldChange,
Expand Down Expand Up @@ -277,6 +282,15 @@ export function ConfigTabContent({
onUnhideField: sectionDisabled ? undefined : onUnhideField,
onRenameEntry,
};
};

const renderSectionContent = (section: t.ConfigSectionConfig) => {
const dataKey = section.schemaKey ?? section.id;
const sectionValue = configValues?.[dataKey];
const sectionDisabled = isSectionReadOnly(section);
const CustomSectionRenderer =
section.fields.length > 0 ? SECTION_RENDERERS[section.id] : undefined;
const fieldRendererProps = buildFieldRendererProps(section);
const content = (
<>
{section.bannerText && !section.readOnlyReason && !section.scopeGate && (
Expand Down Expand Up @@ -346,12 +360,18 @@ export function ConfigTabContent({
);
}
if (!section.readOnlyReason) return content;
const isEmpty = countsById[section.id]?.configured === 0;
/** A single-field section is its row: nothing to fold. */
const isEmpty =
countsById[section.id]?.configured === 0 &&
!UNFOLDED_READ_ONLY_SECTIONS.has(section.id) &&
!isInlineSection(section);
const detailKey = READ_ONLY_DETAIL_KEYS[section.id];
const reveal =
revealTarget != null && (revealTarget === dataKey || revealTarget.startsWith(`${dataKey}.`));
return (
<ReadOnlySection
reason={section.readOnlyReason}
detail={detailKey ? localize(detailKey) : undefined}
isEmpty={isEmpty && !filterPaths}
reveal={reveal}
>
Expand All @@ -369,9 +389,21 @@ export function ConfigTabContent({
type SectionGroup =
| { kind: 'inline'; section: t.ConfigSectionConfig }
| { kind: 'accordion'; sections: t.ConfigSectionConfig[] }
| { kind: 'flat'; section: t.ConfigSectionConfig };
| { kind: 'flat'; section: t.ConfigSectionConfig }
| { kind: 'group'; key: string; sections: t.ConfigSectionConfig[] };

const groups = visibleSections.reduce<SectionGroup[]>((acc, section) => {
/** Sections sharing a `SectionMeta.group` with a group renderer render as one card (e.g. Storage). */
const groupKey = SECTION_META[section.id]?.group;
if (groupKey && SECTION_GROUP_RENDERERS[groupKey]) {
const existing = acc.find(
(g): g is Extract<SectionGroup, { kind: 'group' }> =>
g.kind === 'group' && g.key === groupKey,
);
if (existing) existing.sections.push(section);
else acc.push({ kind: 'group', key: groupKey, sections: [section] });
return acc;
}
if (SELF_CONTAINED_SECTION_RENDERERS.has(section.id)) {
acc.push({ kind: 'flat', section });
return acc;
Expand Down Expand Up @@ -400,6 +432,20 @@ export function ConfigTabContent({
>
<button type="submit" disabled hidden aria-hidden="true" tabIndex={-1} />
{groups.map((group) => {
if (group.kind === 'group') {
const GroupRenderer = SECTION_GROUP_RENDERERS[group.key];
if (!GroupRenderer) return null;
return (
<GroupRenderer
key={group.key}
sections={group.sections}
propsFor={buildFieldRendererProps}
renderSection={renderSectionContent}
counts={countsById}
showConfiguredOnly={showConfiguredOnly}
/>
);
}
if (group.kind === 'flat') {
return (
<div key={group.section.id} id={`section-${group.section.id}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,16 @@ describe('ImportYamlDialog wizard', () => {
);
await toPreview();
const dialog = screen.getByRole('dialog');
expect(within(dialog).getByText('Start balance')).toBeInTheDocument();
expect(within(dialog).getByText('Starting credits')).toBeInTheDocument();
expect(within(dialog).getByText('1,000,000 (librechat.yaml)')).toBeInTheDocument();
expect(within(dialog).queryByText('Enabled')).toBeNull();
fireEvent.click(
within(dialog).getByRole('button', { name: /Show 1 setting that is already set/ }),
);
expect(within(dialog).getByText(/Already set to this value/)).toBeInTheDocument();
expect(within(dialog).getByRole('checkbox', { name: 'Include Enabled' })).not.toBeChecked();
expect(
within(dialog).getByRole('checkbox', { name: 'Include Track balances' }),
).not.toBeChecked();
expect(within(dialog).getByText(/2 keys won't be imported/)).toBeInTheDocument();
expect(within(dialog).getByText(/managed with role permissions/)).toBeInTheDocument();
expect(within(dialog).getByText(/"us.anthropic.claude"/)).toBeInTheDocument();
Expand Down
20 changes: 11 additions & 9 deletions src/components/configuration/configMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ export const SECTION_META: Record<string, t.SectionMeta> = {
},

fileStrategy: {
titleKey: 'com_config_section_file_strategy',
descriptionKey: 'com_config_section_file_strategy_desc',
titleKey: 'com_config_storage_title',
descriptionKey: 'com_config_storage_desc',
tab: 'files',
order: 10,
group: 'storage',
},
fileStrategies: {
titleKey: 'com_config_section_fileStrategies',
descriptionKey: 'com_config_section_fileStrategies_desc',
titleKey: 'com_config_storage_per_type',
descriptionKey: 'com_config_storage_per_type_desc',
tab: 'files',
order: 20,
group: 'storage',
Expand All @@ -136,7 +136,7 @@ export const SECTION_META: Record<string, t.SectionMeta> = {
},
fileConfig: {
titleKey: 'com_config_section_file_config',
descriptionKey: 'com_config_section_file_config_desc',
descriptionKey: 'com_config_files_config_desc',
tab: 'files',
order: 40,
},
Expand Down Expand Up @@ -185,20 +185,22 @@ export const SECTION_META: Record<string, t.SectionMeta> = {
order: 10,
},
balance: {
titleKey: 'com_config_section_balance',
descriptionKey: 'com_config_section_balance_desc',
titleKey: 'com_config_credits_title',
descriptionKey: 'com_config_credits_desc',
tab: 'system',
order: 20,
group: 'credits',
},
transactions: {
titleKey: 'com_config_section_transactions',
descriptionKey: 'com_config_section_transactions_desc',
tab: 'system',
order: 30,
group: 'credits',
},
permissions: {
titleKey: 'com_config_section_permissions',
descriptionKey: 'com_config_section_permissions_desc',
titleKey: 'com_config_system_permissions_title',
descriptionKey: 'com_config_system_permissions_desc',
tab: 'system',
order: 40,
},
Expand Down
6 changes: 3 additions & 3 deletions src/components/configuration/controls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,11 @@ describe('getFieldOptions / coerceEnumValue', () => {

it('uses brand and format labels', () => {
expect(labels('fileStrategy')).toEqual([
'Local',
'Firebase',
'Local filesystem',
'Firebase Storage',
'Amazon S3',
'Azure Blob Storage',
'Amazon CloudFront',
'Amazon CloudFront (S3)',
]);
expect(labels('imageOutputType')).toEqual(['PNG', 'WebP', 'JPEG']);
expect(labels('ocr.strategy')).toContain('Vertex AI Mistral OCR');
Expand Down
2 changes: 2 additions & 0 deletions src/components/configuration/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export function canRoundTrip(
case 'array':
case 'multi-enum':
case 'domain-list':
case 'mime-list':
return isStringList(value);
case 'model-list':
return Array.isArray(value) && value.every(isModelItem);
Expand Down Expand Up @@ -365,6 +366,7 @@ export function fallbackControl(
return 'select';
case 'multi-enum':
case 'domain-list':
case 'mime-list':
return 'array';
case 'model-list':
return value == null || isStringList(value) ? 'array' : 'code';
Expand Down
Loading
Loading