feat(web): pick a session's agent preset from the composer
A seat in the composer tool row, left of the model select, showing the preset THIS session runs — its own recorded one, not the deployment default, because a resumed session runs what it was created with. `SessionSummary` carries `agentPreset` for that, alongside `cwd` and `origin`. The switch exists only while the conversation has not started. After the first turn the seat becomes a plain label rather than a disabled menu: a greyed control reads as "temporarily unavailable", when in fact the choice is gone for good. The seat never asks in that state, and the host refuses independently with `agent-preset-locked`, so a stale client cannot slip one past it. A rejected switch restores the previous value and surfaces the host's message rather than leaving the seat showing something the session is not running.
This commit is contained in:
@@ -10,6 +10,7 @@ import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import {
|
||||
AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController,
|
||||
} from '../src/client/settings-store.ts'
|
||||
import { AgentPresetSeatController } from '../src/client/seat-store.ts'
|
||||
|
||||
interface Recorded { ns: string; patch: unknown }
|
||||
|
||||
@@ -120,3 +121,100 @@ describe('the agent-preset settings controller', () => {
|
||||
expect(state.error).toBe('host down')
|
||||
})
|
||||
})
|
||||
|
||||
describe('the composer seat controller', () => {
|
||||
/** A seat over a fixed session summary. */
|
||||
function seat(
|
||||
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
|
||||
summary: { blank: boolean; agentPreset?: string } | undefined,
|
||||
options: { writes?: Recorded[]; failSelect?: string } = {},
|
||||
): AgentPresetSeatController {
|
||||
const api = {
|
||||
agentPresets: {
|
||||
list: () => Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { presets } } }),
|
||||
select: (payload: { agentPreset: string }) => {
|
||||
options.writes?.push({ ns: 'select', patch: payload.agentPreset })
|
||||
return Promise.resolve(options.failSelect === undefined
|
||||
? { rpcId: 'r', result: { ok: true as const, value: { agentPreset: payload.agentPreset } } }
|
||||
: { rpcId: 'r', result: { ok: false as const, error: { code: 'agent-preset-locked', message: options.failSelect, details: {} } } })
|
||||
},
|
||||
},
|
||||
} as unknown as IApiClient
|
||||
return new AgentPresetSeatController(api, 's1' as never, () => summary)
|
||||
}
|
||||
|
||||
const ROSTER: { id: string; trust: 'system' | 'user'; isDefault: boolean }[] = [
|
||||
{ id: 'standard', trust: 'system', isDefault: true },
|
||||
{ id: 'core-web', trust: 'system', isDefault: false },
|
||||
]
|
||||
|
||||
it('shows what the session runs, not the deployment default', async () => {
|
||||
const controller = seat(ROSTER, { blank: true, agentPreset: 'core-web' })
|
||||
|
||||
await controller.load()
|
||||
|
||||
// A resumed session runs what it was created with; showing `standard`
|
||||
// because it is the current default would be a lie about this session.
|
||||
expect(controller.store.getSnapshot().current).toBe('core-web')
|
||||
expect(controller.store.getSnapshot().switchable).toBe(true)
|
||||
})
|
||||
|
||||
it('falls back to the roster default when the session records none', async () => {
|
||||
const controller = seat(ROSTER, { blank: true })
|
||||
|
||||
await controller.load()
|
||||
|
||||
expect(controller.store.getSnapshot().current).toBe('standard')
|
||||
})
|
||||
|
||||
it('is not switchable once the conversation has started', async () => {
|
||||
const controller = seat(ROSTER, { blank: false, agentPreset: 'standard' })
|
||||
|
||||
await controller.load()
|
||||
|
||||
expect(controller.store.getSnapshot().switchable).toBe(false)
|
||||
})
|
||||
|
||||
it('refuses to switch a session that already started', async () => {
|
||||
const writes: Recorded[] = []
|
||||
const controller = seat(ROSTER, { blank: false, agentPreset: 'standard' }, { writes })
|
||||
await controller.load()
|
||||
|
||||
await controller.select('core-web')
|
||||
|
||||
// The host enforces the same rule; the seat simply never asks.
|
||||
expect(writes).toEqual([])
|
||||
expect(controller.store.getSnapshot().current).toBe('standard')
|
||||
})
|
||||
|
||||
it('switches a blank session and keeps the host\'s answer', async () => {
|
||||
const writes: Recorded[] = []
|
||||
const controller = seat(ROSTER, { blank: true, agentPreset: 'standard' }, { writes })
|
||||
await controller.load()
|
||||
|
||||
await controller.select('core-web')
|
||||
|
||||
expect(writes).toEqual([{ ns: 'select', patch: 'core-web' }])
|
||||
expect(controller.store.getSnapshot().current).toBe('core-web')
|
||||
})
|
||||
|
||||
it('restores the previous value when the host rejects the switch', async () => {
|
||||
const controller = seat(ROSTER, { blank: true, agentPreset: 'standard' }, { failSelect: 'already started' })
|
||||
await controller.load()
|
||||
|
||||
await controller.select('core-web')
|
||||
|
||||
const state = controller.store.getSnapshot()
|
||||
expect(state.current).toBe('standard')
|
||||
expect(state.error).toBe('already started')
|
||||
})
|
||||
|
||||
it('reports no options when the session is unknown to the list yet', async () => {
|
||||
const controller = seat([], undefined)
|
||||
|
||||
await controller.load()
|
||||
|
||||
expect(controller.store.getSnapshot().options).toEqual([])
|
||||
expect(controller.store.getSnapshot().switchable).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user