Show the picked session-option value and let the provider report correct it

The pill showed a "not confirmed" second tooltip line for any value we had sent
but not yet seen reported back. Nothing acts on it, and for the PTY lane it was
permanent — that transport has no report channel. The pill now shows the picked
value immediately and the provider's per-turn report corrects it when the two
disagree; a newer local write still outranks a report that precedes it.

`dispatched` stays as a provenance member rather than collapsing into `applied`:
it is produced independently by the PTY lane, and it is where the `confirmed`
wire field lands, which would otherwise be unobservable.

Effort keeps its readback and its rejection path. That matters more now, not
less: with the hedge gone the rejection is the only user-visible failure signal
on this surface, so a spurious one would be the loudest bug here. Skipping the
readback for an effort the settings response structurally cannot echo is what
prevents it — the response carries the persisted level, so reading it back for a
session-scoped value would report the level underneath and fail a valid write.
This commit is contained in:
Merge Sim
2026-09-03 23:15:58 -07:00
parent 4baa29b346
commit bbdc2ee178
11 changed files with 71 additions and 74 deletions
@@ -6,6 +6,7 @@ import {
} from '../../shared/structured-agent-session-options'
import { CLAUDE_SESSION_OPTION_CATALOG } from '../../shared/agent-session-option-catalog-claude-codex'
import type { AgentSessionOptionsResult } from '../../shared/agent-session-wire'
import type { SessionOptionDescriptor } from '../../shared/native-chat-session-options'
import { setClaudeStructuredOption } from './claude-structured-options'
import type { ClaudeSession } from './claude-structured-session-state'
import { PROVIDER_SESSION_ID, acquired, fakeClaude } from './claude-structured-session-test-support'
@@ -26,19 +27,28 @@ function initFrame(model: string): Record<string, unknown> {
}
}
/** Source the pill reads: 'dispatched' is the one that renders the hedge. */
function modelSource(result: AgentSessionOptionsResult): string | undefined {
function modelPill(result: AgentSessionOptionsResult): SessionOptionDescriptor | undefined {
const state = applyStructuredAgentSessionOptions(
createStructuredAgentSessionOptionState('claude'),
CLAUDE_SESSION_OPTION_CATALOG,
result
)
return structuredAgentSessionOptionSnapshot(state).find((d) => d.category === 'model')
?.valueSource
}
/** Provenance the record keeps. Nothing renders it — the pill shows the value
* either way, and a report that disagrees is what corrects it. */
function modelSource(result: AgentSessionOptionsResult): string | undefined {
return modelPill(result)?.valueSource
}
function modelValue(result: AgentSessionOptionsResult): string | undefined {
const kind = modelPill(result)?.kind
return kind?.type === 'select' ? kind.currentValue : undefined
}
describe('structured option confirmation reaches the pill', () => {
it('leaves a just-set model hedged until a turn reports it', async () => {
it('shows a just-set model before any turn reports it', async () => {
const claude = fakeClaude({
initModel: 'claude-sonnet-5',
routes: { list_models: () => CATALOG }
@@ -51,7 +61,7 @@ describe('structured option confirmation reaches the pill', () => {
expect(modelSource(result)).toBe('dispatched')
})
it('clears the hedge once the provider reports the model back', async () => {
it('marks the model reported once the provider names it back', async () => {
const claude = fakeClaude({
initModel: 'claude-sonnet-5',
routes: { list_models: () => CATALOG }
@@ -65,7 +75,7 @@ describe('structured option confirmation reaches the pill', () => {
expect(modelSource(result)).toBe('reported')
})
it('keeps an effort the readback could not take hedged', async () => {
it('records an effort the readback could not take without confirming it', async () => {
const claude = fakeClaude({
initModel: 'claude-sonnet-5',
settings: { applied: {}, effective: {}, sources: {} },
@@ -95,14 +105,48 @@ describe('structured option confirmation reaches the pill', () => {
})
it('treats a host that reports no confirmation as unconfirmed', () => {
// Wire compatibility: an older host omits `confirmed` entirely; the client must
// keep hedging rather than read the absence as a provider report.
expect(
modelSource({
models: [{ id: 'haiku', label: 'Haiku', isDefault: false, efforts: [] }],
current: { model: 'haiku' }
})
).toBe('dispatched')
// Wire compatibility: an older host omits `confirmed` entirely. Absence must
// read as unconfirmed provenance, and the pill still shows the host's value.
const result = {
models: [{ id: 'haiku', label: 'Haiku', isDefault: false, efforts: [] }],
current: { model: 'haiku' }
}
expect(modelSource(result)).toBe('dispatched')
expect(modelValue(result)).toBe('haiku')
})
})
describe('the provider report corrects the pill', () => {
it('moves the pill to the model the turn actually ran', async () => {
const claude = fakeClaude({
initModel: 'claude-sonnet-5',
routes: { list_models: () => CATALOG }
})
const adapter = await acquired(claude)
await adapter.setOption({ sessionId: 'session-1', key: 'model', value: 'haiku', fence: 7 })
expect(modelValue(await adapter.readOptions({ sessionId: 'session-1', fence: 7 }))).toBe(
'haiku'
)
claude.connections[0]!.handlers.onMessage?.(initFrame('claude-sonnet-5'))
const corrected = await adapter.readOptions({ sessionId: 'session-1', fence: 7 })
expect(modelValue(corrected)).toBe('sonnet')
expect(corrected.current.confirmed).toContain('model')
})
it('lets a newer write outrank the report it precedes', async () => {
const claude = fakeClaude({
initModel: 'claude-sonnet-5',
routes: { list_models: () => CATALOG }
})
const adapter = await acquired(claude)
claude.connections[0]!.handlers.onMessage?.(initFrame('claude-sonnet-5'))
await adapter.setOption({ sessionId: 'session-1', key: 'model', value: 'haiku', fence: 7 })
const result = await adapter.readOptions({ sessionId: 'session-1', fence: 7 })
expect(modelValue(result)).toBe('haiku')
expect(result.current.confirmed ?? []).not.toContain('model')
})
})
@@ -342,7 +342,9 @@ describe('NativeChatSessionOptionPickers', () => {
expect(screen.queryByRole('button', { name: /^Effort/ })).toBeNull()
})
it('shows the unconfirmed hint for dispatched values', () => {
// The pill shows the picked value straight away; a provider report is what
// corrects it, so an unsent-yet-unreported value gets no separate hedge line.
it('tooltips a dispatched value with the category alone', () => {
render(
<NativeChatSessionOptionPickers
surface={surface}
@@ -350,7 +352,8 @@ describe('NativeChatSessionOptionPickers', () => {
isWorking={false}
/>
)
expect(screen.getByText('Sent to the agent — not confirmed')).not.toBeNull()
expect(screen.getByText('Model')).not.toBeNull()
expect(screen.queryByText(/not confirmed/)).toBeNull()
})
it('renders agent-picker routes as one action instead of radio choices', async () => {
@@ -463,26 +466,7 @@ describe('NativeChatSessionOptionPickers', () => {
await waitFor(() => expect(setOption).toHaveBeenCalledWith('thinking', false))
})
it('does not show unconfirmed for applied flip-only booleans', () => {
render(
<NativeChatSessionOptionPickers
surface={surface}
snapshot={[
model(),
{
...fast,
kind: { type: 'boolean', currentValue: true },
// Why: flip-only tracks as applied — never a healable dispatched state.
valueSource: 'applied'
}
]}
isWorking={false}
/>
)
expect(screen.queryByText('Sent to the agent — not confirmed')).toBeNull()
})
it('shows unconfirmed for confirmable dispatched booleans', () => {
it('tooltips a dispatched option pill with the category alone', () => {
render(
<NativeChatSessionOptionPickers
surface={surface}
@@ -500,6 +484,7 @@ describe('NativeChatSessionOptionPickers', () => {
isWorking={false}
/>
)
expect(screen.getByText('Sent to the agent — not confirmed')).not.toBeNull()
expect(screen.getAllByText('Thinking').length).toBeGreaterThan(0)
expect(screen.queryByText(/not confirmed/)).toBeNull()
})
})
@@ -37,32 +37,11 @@ export type NativeChatSessionOptionPickersProps = {
pickerRequest?: NativeChatOptionPickerRequest | null
}
function PickerTooltipContent(props: {
label: string
disabledReason?: string | null
dispatched: boolean
}): React.JSX.Element {
return (
<div className="space-y-0.5">
<div>{props.disabledReason ?? props.label}</div>
{props.dispatched ? (
<div>
{translate(
'components.native-chat.composer.sentNotConfirmed',
'Sent to the agent — not confirmed'
)}
</div>
) : null}
</div>
)
}
function PickerTrigger(props: {
label: string
tooltipLabel: string
disabled: boolean
disabledReason?: string | null
dispatched: boolean
}): React.JSX.Element {
// Why: value-only visible text must still include the category in the
// accessible name (WCAG 2.5.3 Label in Name / voice control).
@@ -90,11 +69,7 @@ function PickerTrigger(props: {
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
<PickerTooltipContent
label={props.tooltipLabel}
disabledReason={props.disabledReason}
dispatched={props.dispatched}
/>
{props.disabledReason ?? props.tooltipLabel}
</TooltipContent>
</Tooltip>
)
@@ -250,7 +225,6 @@ function NativeChatSessionOptionPickersInner({
tooltipLabel={optionsTooltip}
disabled={isWorking || pendingId !== null}
disabledReason={optionsReason}
dispatched={options.some((descriptor) => descriptor.valueSource === 'dispatched')}
/>
<DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-60">
{options.map((descriptor, index) => {
@@ -283,7 +257,6 @@ function NativeChatSessionOptionPickersInner({
tooltipLabel={modelTooltip}
disabled={isWorking || pendingId !== null}
disabledReason={modelReason}
dispatched={model.valueSource === 'dispatched'}
/>
<DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-64">
{modelReason && !model.settable ? (
-1
View File
@@ -16905,7 +16905,6 @@
"toggleOption": "Toggle {{value0}}",
"pillAccessibleName": "{{value0}} {{value1}}",
"valueUnknown": "Current value unknown — pick On or Off",
"sentNotConfirmed": "Sent to the agent — not confirmed",
"setWhenSessionStarts": "Set when the session starts.",
"availableAfterSessionStarts": "Available after the session starts.",
"optionUpdateFailed": "Could not update option",
-1
View File
@@ -14589,7 +14589,6 @@
"toggleOption": "Alternar {{value0}}",
"pillAccessibleName": "{{value0}} {{value1}}",
"valueUnknown": "Valor actual desconocido — elige Activado o Desactivado",
"sentNotConfirmed": "Enviado al agente — sin confirmar",
"setWhenSessionStarts": "Configúralo al iniciar la sesión.",
"availableAfterSessionStarts": "Disponible después de iniciar la sesión.",
"optionUpdateFailed": "No se pudo actualizar la opción",
-1
View File
@@ -16021,7 +16021,6 @@
"toggleOption": "Basculer {{value0}}",
"pillAccessibleName": "{{value0}} {{value1}}",
"valueUnknown": "Valeur actuelle inconnue — choisissez Activé ou Désactivé",
"sentNotConfirmed": "Envoyé à l'agent — non confirmé",
"setWhenSessionStarts": "Défini au démarrage de la session.",
"availableAfterSessionStarts": "Disponible après le démarrage de la session.",
"optionUpdateFailed": "Impossible de mettre à jour l'option",
-1
View File
@@ -14589,7 +14589,6 @@
"toggleOption": "{{value0}}を切り替える",
"pillAccessibleName": "{{value0}} {{value1}}",
"valueUnknown": "現在の値が不明です — オンまたはオフを選んでください",
"sentNotConfirmed": "Agent に送信済み — 未確認",
"setWhenSessionStarts": "セッション開始時に設定してください。",
"availableAfterSessionStarts": "セッション開始後に利用できます。",
"optionUpdateFailed": "オプションを更新できませんでした",
-1
View File
@@ -14702,7 +14702,6 @@
"toggleOption": "{{value0}} 전환",
"pillAccessibleName": "{{value0}} {{value1}}",
"valueUnknown": "현재 값을 알 수 없음 — 켜짐 또는 꺼짐을 선택하세요",
"sentNotConfirmed": "에이전트에 전송됨 — 확인되지 않음",
"setWhenSessionStarts": "세션을 시작할 때 설정하세요.",
"availableAfterSessionStarts": "세션이 시작된 후 사용할 수 있습니다.",
"optionUpdateFailed": "옵션을 업데이트하지 못했습니다",
-1
View File
@@ -14645,7 +14645,6 @@
"toggleOption": "切换{{value0}}",
"pillAccessibleName": "{{value0}} {{value1}}",
"valueUnknown": "当前值未知 — 请选择开或关",
"sentNotConfirmed": "已发送给智能体 — 尚未确认",
"setWhenSessionStarts": "请在会话开始时设置。",
"availableAfterSessionStarts": "会话开始后可用。",
"optionUpdateFailed": "无法更新选项",
@@ -53,8 +53,7 @@ describe('native chat locale copy', () => {
'sessionOptions',
'chooseInAgentPicker',
'toggleOption',
'valueUnknown',
'sentNotConfirmed'
'valueUnknown'
] as const) {
expect(composer[key].trim()).not.toBe('')
expect(composer[key]).not.toBe(englishComposer[key])
+3 -1
View File
@@ -7,7 +7,9 @@ export type SessionOptionSelectChoice = {
}
/** `default` is the catalog's own value shown before anything is observed —
* truthful to display, but never evidence about a running agent. */
* truthful to display, but never evidence about a running agent. `dispatched`
* is provenance only: the pill shows a sent value the same as a reported one,
* and a later report that disagrees is what corrects it. */
export type SessionOptionValueSource = 'applied' | 'dispatched' | 'reported' | 'default' | 'unknown'
/** Closed set of reasons an option is not settable in the current mode. A key