fix(web): persist theme preference in settings

This commit is contained in:
Yichen Jiang
2026-08-06 20:27:31 +08:00
parent 6a32047e77
commit dd473870dd
30 changed files with 692 additions and 121 deletions

View File

@@ -308,8 +308,8 @@ describe('settings domain', () => {
// The settings seam is general: any plugin may register a namespace for
// its own configuration. The Web configuration plane remains opt-in, so a
// future internal plugin cannot become remotely configurable just by
// registering; permission and the product onboarding namespace are the
// non-model namespaces intentionally admitted by this surface.
// registering; permission, theme, and the product onboarding namespace
// are the non-model namespaces intentionally admitted by this surface.
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)
ctx.settings.register(settingsNamespace('some-other-plugin'), z.object({ secretPath: z.string() }))
@@ -318,15 +318,23 @@ describe('settings domain', () => {
}), {
base: { defaultPreset: 'read-only' },
})
ctx.settings.register(settingsNamespace('ui-theme'), z.object({
preference: z.union(['light', 'dark', 'system']).default('system'),
}))
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.settings.describe(request({})))
expect(value.namespaces.map(view => view.ns)).toEqual(['llm-deepseek', 'permission'])
expect(value.namespaces.map(view => view.ns)).toEqual(['llm-deepseek', 'permission', 'ui-theme'])
const permission = expectOk(await api.settings.mutate(request({
ns: 'permission',
ops: [{ op: 'set', path: ['defaultPreset'], value: 'workspace-write' }],
})))
expect(permission.value).toEqual({ defaultPreset: 'workspace-write' })
const theme = expectOk(await api.settings.mutate(request({
ns: 'ui-theme',
ops: [{ op: 'set', path: ['preference'], value: 'dark' }],
})))
expect(theme.value).toEqual({ preference: 'dark' })
for (const response of [
await api.settings.update(request({ ns: 'some-other-plugin', patch: { secretPath: '/etc/shadow' } })),
@@ -340,19 +348,29 @@ describe('settings domain', () => {
expect(ctx.settings.describe().find(d => String(d.ns) === 'some-other-plugin')?.value).toEqual({})
})
it('serves the product onboarding namespace without invalidating the model catalog', async () => {
it('serves product preference namespaces without invalidating the model catalog', async () => {
const ctx = await harness()
ctx.settings.register(settingsNamespace('ui-onboarding'), z.object({ welcomeNoticeVersion: z.string() }))
ctx.settings.register(settingsNamespace('ui-theme'), z.object({
preference: z.union(['light', 'dark', 'system']).default('system'),
}))
const api = createApiProxy(ctx, DEFAULTS)
expect(expectOk(await api.settings.describe(request({}))).namespaces.map(view => view.ns))
.toEqual(['ui-onboarding'])
const frames = await collectHost(api, ['host/settings-changed'], 1, async () => {
.toEqual(['ui-onboarding', 'ui-theme'])
const frames = await collectHost(api, ['host/settings-changed'], 2, async () => {
expectOk(await api.settings.mutate(request({
ns: 'ui-onboarding',
ops: [{ op: 'set', path: ['welcomeNoticeVersion'], value: 'v1' }],
})))
expectOk(await api.settings.mutate(request({
ns: 'ui-theme',
ops: [{ op: 'set', path: ['preference'], value: 'dark' }],
})))
})
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'ui-onboarding' }])
expect(frames).toEqual([
{ type: 'host/settings-changed', ns: 'ui-onboarding' },
{ type: 'host/settings-changed', ns: 'ui-theme' },
])
})
it('refuses even a model-provider namespace once its directory entry is gone', async () => {