fix: keep each mode's provider across output type switches

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-21 18:36:40 +02:00
co-authored by Claude Opus 5
parent fda24bfa1b
commit 323ad63ee5
3 changed files with 39 additions and 26 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
anonymous usage-stats payload. It answers "does anyone use this, and which variant do they pick"
without any identifying data leaving the instance.
It currently carries 57 registered actions across nineteen features (`ai_session`, `ai_chat`,
It currently carries 58 registered actions across nineteen features (`ai_session`, `ai_chat`,
`ai_fix`, `ai_agent`, `ai_agent_eval`, `app_sandbox`, `datatable`, `flow_editor`, `flow_run`,
`flow_step`, `home`, `run_form`, `debugger`, `trigger`, `command_script`, `hub_script`,
`usage_meter`, `sso_groups_claim`, `cloud_trial_offer`). Nearly all of the
@@ -66,18 +66,27 @@
}
}
// A kind the mode does not offer cannot run, and switching the output type leaves the old kind
// behind. Its resource, model and reasoning effort belong to it, so they go with it rather than
// surfacing only as a failed run.
// Each mode offers kinds the other does not, so switching the output type swaps the provider: the
// one the step held is set aside and comes back with its mode, and a mode not seen yet starts
// empty rather than on a kind it cannot run.
type PickerMode = 'chat' | 'decision'
const heldByMode: Partial<Record<PickerMode, ProviderConfig>> = {}
let shownMode: PickerMode = untrack(() => (decision ? 'decision' : 'chat'))
$effect(() => {
const offered = offeredProviders
const mode: PickerMode = decision ? 'decision' : 'chat'
untrack(() => {
if (value?.kind && !offered.includes(value.kind)) {
value.kind = decision ? 'typesafe' : 'openai'
value.resource = ''
value.model = ''
value.reasoning_effort = undefined
if (mode === shownMode || !value) return
heldByMode[shownMode] = $state.snapshot(value) as ProviderConfig
const next = heldByMode[mode] ?? {
kind: mode === 'decision' ? 'typesafe' : 'openai',
resource: '',
model: ''
}
value.kind = next.kind
value.resource = next.resource
value.model = next.model
value.reasoning_effort = next.reasoning_effort
shownMode = mode
})
})
@@ -215,22 +224,26 @@
<!-- No auto-select: this picker spans every provider type, so a single candidate means "the
only AI resource in the workspace" rather than "the only one of the kind this agent uses".
Taking it would redefine the agent's provider and drop its model, on open and unasked. -->
<ResourcePicker
bind:value={
() => resourceValueToPath(value?.resource),
(v) => {
if (value) {
value.resource = pathToResourceValue(v) ?? ''
<!-- Remounted per mode: the picker empties its value whenever the types it offers change,
which would drop the resource the switch above just restored. -->
{#key decision}
<ResourcePicker
bind:value={
() => resourceValueToPath(value?.resource),
(v) => {
if (value) {
value.resource = pathToResourceValue(v) ?? ''
}
}
}
}
resourceType={providerResourceTypes}
{disabled}
{workspace}
placeholder="Select an AI provider resource"
selectFirst={false}
onValueChange={onResourcePicked}
/>
resourceType={providerResourceTypes}
{disabled}
{workspace}
placeholder="Select an AI provider resource"
selectFirst={false}
onValueChange={onResourcePicked}
/>
{/key}
</div>
<div class="flex flex-col gap-1">
@@ -81,8 +81,8 @@
if ((mod.value as { type?: string })?.type !== 'aiagent') return all
const transforms = (mod.value as { input_transforms?: Record<string, unknown> })
?.input_transforms
// A step's own agent holds its output type; a linked one takes it from the agent, and until
// that has loaded no field is dropped for it.
// A step's own agent holds its output type; a linked one takes it from the agent, and reads as
// text until that has loaded.
const outputTransform = transforms?.output_type as
| { type?: string; value?: unknown }
| undefined