fix(app): a closed agent shows its stored options, and a provider switch asks about its own options

A closed agent's snapshot is projected from its stored record, which kept
controls and provider options but dropped them from the payload, so the
app drew no speed or other controls for it. The stored projection now
carries them, with the legacy features derived from the same source.

The draft read the form's provider and the model's options separately, so
for the render after a manual provider change it asked the new provider
about the previous provider's options. The request is now built from one
snapshot of the model and is absent until the model has observed the new
provider.
This commit is contained in:
Mohamed Boudra
2026-11-04 05:00:53 +01:00
parent b2a7596ffa
commit 6569b4f388
6 changed files with 167 additions and 24 deletions
@@ -397,6 +397,50 @@ describe("toAgentPayload", () => {
});
});
it("projects a closed agent's stored controls and options", () => {
const controls: AgentOptionControl[] = [
{
type: "select",
key: "service_tier",
label: "Speed",
defaultValue: "default",
choices: [
{ value: "default", label: "Normal" },
{ value: "priority", label: "Fast" },
],
},
];
const record = toStoredAgentRecord(
createManagedAgent({
provider: "codex",
lifecycle: "closed",
controls,
config: {
provider: "codex",
providerOptions: { service_tier: "priority", sandbox_mode: "read-only" },
},
}),
);
const payload = buildStoredAgentPayload(record, ["codex"]);
expect(payload.status).toBe("closed");
expect(payload.controls).toEqual(controls);
expect(payload.options).toEqual({ service_tier: "priority", sandbox_mode: "read-only" });
expect(payload.features).toEqual([
{
type: "select",
id: "service_tier",
label: "Speed",
value: "priority",
options: [
{ id: "default", label: "Normal", isDefault: true },
{ id: "priority", label: "Fast" },
],
},
]);
});
it("carries a stored lastError onto the payload", () => {
const record = toStoredAgentRecord(
createManagedAgent({ provider: "codex", config: { provider: "codex" }, lastError: "boom" }),
@@ -225,6 +225,21 @@ function visibilityFlags(agent: {
};
}
function buildStoredOptionControls(
record: StoredAgentRecord,
): Pick<AgentSnapshotPayload, "features" | "options" | "controls"> {
const controls = normalizeControls(record.controls);
const options = record.config?.providerOptions
? sanitizeEffectiveOptions(record.config.providerOptions)
: undefined;
return {
// COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01.
features: featuresFromControls({ controls, options }),
...(options ? { options } : {}),
controls,
};
}
export function buildStoredAgentPayload(
record: StoredAgentRecord,
validProviders: Iterable<AgentProvider>,
@@ -246,6 +261,7 @@ export function buildStoredAgentPayload(
const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null;
const runtimeInfo = buildStoredRuntimeInfo(record);
const optionControls = buildStoredOptionControls(record);
const providerAvailable = isStoredAgentProviderAvailable(record, validProviders);
const persistence = projectPersistenceHandleForWire(
buildStoredPersistenceHandle(record, validProviders),
@@ -270,6 +286,7 @@ export function buildStoredAgentPayload(
capabilities: defaultCapabilities,
currentModeId: record.lastModeId ?? null,
availableModes: [],
...optionControls,
pendingPermissions: [],
persistence,
title: record.title ?? null,
@@ -475,7 +492,7 @@ function normalizeControls(
return Array.isArray(controls) ? controls.map((control) => ({ ...control })) : [];
}
function sanitizeEffectiveOptions(options: ProviderOptions): ProviderOptions {
function sanitizeEffectiveOptions(options: Record<string, unknown>): ProviderOptions {
const sanitized = sanitizeOptionalJson(options);
return sanitized && isJsonObject(sanitized) ? sanitized : {};
}