fix(web): move the new-session chip when the default preset changes

The General row and the preset page already re-read on `settings/changed`, but
the hero chip did not — so changing the default and starting a session without
reloading composed the previous default. That is the one session the setting
claims to govern ("对此后新建的会话生效"), and the chip is what decides it.

A staged pick still wins: `load()` prefers the stage over the refreshed
fallback, so a refresh never overwrites a choice the user just made.
This commit is contained in:
Yichen Jiang
2026-08-07 03:20:49 +08:00
parent 0ab035c748
commit a7ab8b27c5
2 changed files with 56 additions and 2 deletions

View File

@@ -110,6 +110,15 @@ export function apply(ctx: ClientContext): void {
// and either way the chip's pick predates it — so the stage is applied
// when the session arrives, not when it was made.
const stop = scope.sessions.list.subscribe(() => { void seat.apply() })
// The chip opens on the deployment default, so a default changed from
// the settings surface moves it too — otherwise the screen that starts
// the next session keeps offering the previous default until a reload,
// which is exactly the session the setting claims to govern. A staged
// pick survives: `load()` prefers it over the refreshed fallback.
const settingsMoved = scope.on('settings/changed', (ns?: string) => {
if (ns !== undefined && ns !== AGENT_PRESET_SETTINGS_NS) return
void seat.load()
})
const chip = scope.slots.register({
name: 'conversation.hero.agentPreset',
locale: 'settings.agentPreset',
@@ -124,6 +133,7 @@ export function apply(ctx: ClientContext): void {
}, AgentPresetLabel)
return () => {
stop()
settingsMoved()
chip()
label()
}

View File

@@ -25,13 +25,32 @@ import type { AgentPresetSeatInjected } from '../src/client/AgentPresetSeat.tsx'
// the shipped Chinese copy, so they state the browser they assume.
usePinnedBrowserLanguages('zh-CN')
const ROSTER = {
const ROSTER_ONE = {
rpcId: 'r',
result: { ok: true as const, value: { presets: [{ id: 'standard', trust: 'system', isDefault: true }], authorable: true } },
}
/** The same roster with a second preset carrying the default. */
const ROSTER_MOVED = {
rpcId: 'r',
result: {
ok: true as const,
value: {
presets: [
{ id: 'standard', trust: 'system', isDefault: false },
{ id: 'minimal', trust: 'system', isDefault: true },
],
authorable: true,
},
},
}
async function bench() {
const ctx = new Context()
// The host's answer, mutable so a spec can move the default the way the
// settings surface does and watch who re-reads it.
let ROSTER: typeof ROSTER_ONE | typeof ROSTER_MOVED = ROSTER_ONE
const moveDefault = (): void => { ROSTER = ROSTER_MOVED }
await ctx.plugin(SlotsService).await()
const locale = new LocaleService(ctx)
ctx.provide('locale', locale)
@@ -56,7 +75,7 @@ async function bench() {
},
},
} as never)
return { ctx, slots: ctx.get('slots') as SlotsService, calls }
return { ctx, slots: ctx.get('slots') as SlotsService, calls, moveDefault }
}
function declareRoot(slots: SlotsService): () => void {
@@ -238,6 +257,31 @@ describe('ui-agent-preset apply', () => {
conversation()
})
it('moves the chip when the default changes on the settings surface', async () => {
const { ctx, slots, moveDefault } = await bench()
declareRoot(slots)
const conversation = declareConversation(slots)
ctx.provide('conversation', {} as never)
ctx.provide('sessions', sessionsDouble({ byId: {} }) as never)
await ctx.plugin({ inject: [...inject, 'conversation', 'sessions'], apply }).await()
const chip = slots.entries('conversation.hero.agentPreset')[0]!
const seat = (chip.inject as unknown as () => AgentPresetSeatInjected)()
await seat.load()
expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('standard')
// The chip opens on the deployment default, and the setting it comes from
// lives on another screen: without this the next session — the very one
// the setting governs — would be composed from the previous default until
// a reload.
moveDefault()
ctx.emit('settings/changed', 'agent-presets')
await vi.waitFor(() => {
expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('minimal')
})
conversation()
})
it('applies the staged choice to the blank session the flow lands on', async () => {
const { ctx, slots, calls } = await bench()
declareRoot(slots)