fix(app): drafts and profile forms send every option layer to the daemon

A cold create against a daemon without providerOptionControls routed control
options (Claude fast_mode, Codex service_tier) as providerOptions, which that
daemon does not read for them. The client now lists the provider's features
before the first create or workspace create that carries options, so callers
never prime a cache.

Draft options moved into a plain model. Applying a profile replaces provider
and options in one step, so the form's provider change that follows no longer
clears a cross-provider profile's non-control options; choosing a provider by
hand still starts over. The model sends its complete options (seed, last-used
values once the provider says it still draws them, edits) to the daemon and
the controls draw only the answer, so a remembered legacy fast is drawn as
what it runs as. A toggle shows over the last answer until its own arrives.
The profile form does the same: its edits go through resolution too.
This commit is contained in:
Mohamed Boudra
2026-11-01 23:06:46 +01:00
parent ddae0a4c36
commit ea8c9e7dc4
13 changed files with 523 additions and 199 deletions
+10 -5
View File
@@ -72,14 +72,19 @@ or without a control.
`applyOptions` result like any other value. The `featureValues` to `options` fold is separate
and provider-agnostic; it stays at the protocol and legacy edges.
- **Clients draw from the daemon's answer.** The app reads `controls` and `options` from the
agent snapshot for a running or closed agent. A draft and a profile form ask
`provider.controls.list.request` with their `providerOptions` and draw what comes back, so an
alias such as a legacy Fast profile's `service_tier: "fast"` is resolved once, by the daemon.
A draft adds only what the user changed and each provider's last-used control values; a
agent snapshot for a running or closed agent. A draft and a profile form send everything they
would create with (seed, the provider's last-used control values once the provider has said it
still draws them, and the user's edits) in `provider.controls.list.request` and draw only what
comes back, so an alias such as a legacy Fast profile's `service_tier: "fast"` is resolved once,
by the daemon. A toggle shows over the last answer until its own answer arrives. The draft's
layers live in a plain model (`agent-controls/draft-options-model.ts`): applying a profile
replaces provider and options in one step, while choosing a provider by hand starts over. A
profile saves its stored options plus the form's edits, so options no control draws survive an
edit. A daemon without `server_info.features.providerOptionControls` sends `features` and
`featureValues`; `packages/client/src/compat/legacy-provider-options.ts` is the one place that
turns those into `controls` and `options` and back, so nothing in the app branches on it.
turns those into `controls` and `options` and back, so nothing in the app branches on it. Its
first create for a provider lists that provider's features to learn which options go out as
`featureValues`; callers do not prime anything.
- **Provider-initiated changes.** A setting the provider changes on its own is an
`options_changed` event. The daemon stores and forwards it and does not push the old value back.