mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
fix(client): a legacy profile with both featureValues and options keeps both
A legacy daemon keeps an unknown `options` field on a profile it stored (profiles are passthrough), for example after a downgrade, while its own app keeps editing `featureValues`. Reading such a profile took `options` and dropped `featureValues`, so a Fast setting there was lost and the next save replaced it. Both are merged on read with an explicit `options` key winning, the same rule the daemon applies when it folds `featureValues`. The draft form input of draftControlsRequest is a named type.
This commit is contained in:
@@ -118,6 +118,15 @@ export type DraftControlsRequest = Pick<
|
||||
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId" | "providerOptions"
|
||||
>;
|
||||
|
||||
/** What the draft's form has chosen; the options come from the model. */
|
||||
export interface DraftControlsForm {
|
||||
provider: string | null;
|
||||
cwd: string;
|
||||
modeId?: string | null;
|
||||
modelId?: string | null;
|
||||
thinkingOptionId?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The listing request for the draft's form, built from one snapshot of the model. The form
|
||||
* reaches a new provider before the model has observed it, and the model still holds the
|
||||
@@ -126,13 +135,7 @@ export type DraftControlsRequest = Pick<
|
||||
*/
|
||||
export function draftControlsRequest(
|
||||
state: DraftOptionsState,
|
||||
form: {
|
||||
provider: string | null;
|
||||
cwd: string;
|
||||
modeId?: string | null;
|
||||
modelId?: string | null;
|
||||
thinkingOptionId?: string | null;
|
||||
},
|
||||
form: DraftControlsForm,
|
||||
): DraftControlsRequest | null {
|
||||
if (!form.provider || !form.cwd || form.provider !== state.provider) return null;
|
||||
const providerOptions = ownDraftOptions(state);
|
||||
|
||||
@@ -203,11 +203,15 @@ export class LegacyProviderOptions {
|
||||
};
|
||||
}
|
||||
|
||||
/** A legacy daemon stores a profile's options under `featureValues`. */
|
||||
/**
|
||||
* A legacy daemon stores a profile's options under `featureValues`, and keeps an `options` field
|
||||
* a newer daemon left on it. Both are read, and an explicit `options` key wins, as it does on the
|
||||
* daemon (`foldFeatureValues`).
|
||||
*/
|
||||
function profileWithOptions(profile: AgentProfile): AgentProfile {
|
||||
if (profile.options || !profile.featureValues) return profile;
|
||||
if (!profile.featureValues) return profile;
|
||||
const { featureValues, ...rest } = profile;
|
||||
return { ...rest, options: featureValues };
|
||||
return { ...rest, options: { ...featureValues, ...profile.options } };
|
||||
}
|
||||
|
||||
function profileWithFeatureValues(profile: AgentProfile): AgentProfile {
|
||||
|
||||
@@ -9165,6 +9165,47 @@ describe("provider options across daemon versions", () => {
|
||||
expect(written).not.toMatchObject({ config: { agentProfiles: [{ options: {} }] } });
|
||||
});
|
||||
|
||||
test("a legacy daemon's profile with both featureValues and options keeps both when saved", async () => {
|
||||
// `options` survives on a legacy daemon's passthrough profile after a downgrade, while its
|
||||
// own app keeps editing `featureValues`.
|
||||
const { client, sentMessages, mock } = await connect({});
|
||||
const reading = client.getDaemonConfig();
|
||||
const [read] = sentMessages();
|
||||
mock.triggerMessage(
|
||||
wrapSessionMessage({
|
||||
type: "get_daemon_config_response",
|
||||
payload: {
|
||||
requestId: read.requestId,
|
||||
config: {
|
||||
mcp: { injectIntoAgents: true },
|
||||
agentProfiles: [
|
||||
{
|
||||
id: "p1",
|
||||
name: "Mixed",
|
||||
provider: "codex",
|
||||
featureValues: { service_tier: "fast", sandbox_mode: "workspace-write" },
|
||||
options: { sandbox_mode: "read-only" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const { config } = await reading;
|
||||
expect(config.agentProfiles?.[0]).toMatchObject({
|
||||
options: { service_tier: "fast", sandbox_mode: "read-only" },
|
||||
});
|
||||
expect(config.agentProfiles?.[0]).not.toHaveProperty("featureValues");
|
||||
|
||||
void client.patchDaemonConfig({ agentProfiles: config.agentProfiles }).catch(() => {});
|
||||
const written = sentMessages().find((message) => message.type === "set_daemon_config_request");
|
||||
expect(written).toMatchObject({
|
||||
config: {
|
||||
agentProfiles: [{ featureValues: { service_tier: "fast", sandbox_mode: "read-only" } }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("a legacy daemon applies a profile's options as feature values", async () => {
|
||||
const { client, sentMessages } = await connect({});
|
||||
void client.applyAgentConfig("agent-1", { options: { service_tier: "fast" } }).catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user