Add DeepSeek TUI to the ACP catalog (#1096)
* Consolidate provider selection handling * List DeepSeek TUI in docs and website
This commit is contained in:
@@ -1,967 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveAgentForm,
|
||||
resolveFormState,
|
||||
resolveThinkingOptionId,
|
||||
mergeSelectedComposerPreferences,
|
||||
combineInitialValues,
|
||||
buildProviderDefinitionMap,
|
||||
buildProviderDefinitionMapForStatuses,
|
||||
resolveDefaultModel,
|
||||
INITIAL_USER_MODIFIED,
|
||||
type AgentFormReducerState,
|
||||
type UserModifiedFields,
|
||||
} from "./resolve-agent-form";
|
||||
import { buildProviderDefinitions } from "@/utils/provider-definitions";
|
||||
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
import type {
|
||||
AgentModelDefinition,
|
||||
AgentProvider,
|
||||
ProviderSnapshotEntry,
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
|
||||
const TEST_CODEX_DEFINITION: AgentProviderDefinition = {
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
description: "Codex test provider",
|
||||
defaultModeId: "auto",
|
||||
modes: [
|
||||
{ id: "auto", label: "Auto", icon: "ShieldAlert", colorTier: "moderate" },
|
||||
{ id: "full-access", label: "Full Access", icon: "ShieldAlert", colorTier: "dangerous" },
|
||||
],
|
||||
};
|
||||
|
||||
const TEST_CLAUDE_DEFINITION: AgentProviderDefinition = {
|
||||
id: "claude",
|
||||
label: "Claude",
|
||||
description: "Claude test provider",
|
||||
defaultModeId: "default",
|
||||
modes: [
|
||||
{ id: "default", label: "Always Ask", icon: "ShieldCheck", colorTier: "safe" },
|
||||
{ id: "acceptEdits", label: "Accept File Edits", icon: "ShieldAlert", colorTier: "moderate" },
|
||||
{ id: "plan", label: "Plan Mode", icon: "ShieldCheck", colorTier: "planning" },
|
||||
{ id: "bypassPermissions", label: "Bypass", icon: "ShieldAlert", colorTier: "dangerous" },
|
||||
],
|
||||
};
|
||||
|
||||
const CODEX_MODELS: AgentModelDefinition[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
id: "gpt-5.3-codex",
|
||||
label: "gpt-5.3-codex",
|
||||
isDefault: true,
|
||||
defaultThinkingOptionId: "xhigh",
|
||||
thinkingOptions: [
|
||||
{ id: "low", label: "low" },
|
||||
{ id: "xhigh", label: "xhigh", isDefault: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function makeProviderMap(
|
||||
...definitions: AgentProviderDefinition[]
|
||||
): Map<AgentProvider, AgentProviderDefinition> {
|
||||
return new Map(definitions.map((d) => [d.id, d]));
|
||||
}
|
||||
|
||||
const codexProviderMap = makeProviderMap(TEST_CODEX_DEFINITION);
|
||||
const claudeProviderMap = makeProviderMap(TEST_CLAUDE_DEFINITION);
|
||||
const bothProviderMap = makeProviderMap(TEST_CODEX_DEFINITION, TEST_CLAUDE_DEFINITION);
|
||||
|
||||
function makeState(
|
||||
overrides: Partial<AgentFormReducerState["form"]> = {},
|
||||
modified: Partial<UserModifiedFields> = {},
|
||||
): AgentFormReducerState {
|
||||
return {
|
||||
form: {
|
||||
serverId: null,
|
||||
provider: null,
|
||||
modeId: "",
|
||||
model: "",
|
||||
thinkingOptionId: "",
|
||||
workingDir: "",
|
||||
...overrides,
|
||||
},
|
||||
userModified: { ...INITIAL_USER_MODIFIED, ...modified },
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveDefaultModel", () => {
|
||||
it("returns null for empty or null input", () => {
|
||||
expect(resolveDefaultModel(null)).toBeNull();
|
||||
expect(resolveDefaultModel([])).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the model marked isDefault", () => {
|
||||
const models: AgentModelDefinition[] = [
|
||||
{ provider: "codex", id: "a", label: "A", isDefault: false },
|
||||
{ provider: "codex", id: "b", label: "B", isDefault: true },
|
||||
];
|
||||
expect(resolveDefaultModel(models)?.id).toBe("b");
|
||||
});
|
||||
|
||||
it("falls back to the first model when none is marked default", () => {
|
||||
const models: AgentModelDefinition[] = [
|
||||
{ provider: "codex", id: "a", label: "A", isDefault: false },
|
||||
{ provider: "codex", id: "b", label: "B", isDefault: false },
|
||||
];
|
||||
expect(resolveDefaultModel(models)?.id).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveThinkingOptionId", () => {
|
||||
it("returns empty string when model has no thinking options", () => {
|
||||
const modelsWithoutThinking: AgentModelDefinition[] = [
|
||||
{ provider: "claude", id: "claude-sonnet-4-6", label: "Sonnet 4.6", isDefault: true },
|
||||
];
|
||||
expect(
|
||||
resolveThinkingOptionId({
|
||||
availableModels: modelsWithoutThinking,
|
||||
modelId: "claude-sonnet-4-6",
|
||||
requestedThinkingOptionId: "",
|
||||
}),
|
||||
).toBe("");
|
||||
});
|
||||
|
||||
it("returns the requested option when it is valid", () => {
|
||||
expect(
|
||||
resolveThinkingOptionId({
|
||||
availableModels: CODEX_MODELS,
|
||||
modelId: "gpt-5.3-codex",
|
||||
requestedThinkingOptionId: "low",
|
||||
}),
|
||||
).toBe("low");
|
||||
});
|
||||
|
||||
it("falls back to defaultThinkingOptionId when requested option is invalid", () => {
|
||||
expect(
|
||||
resolveThinkingOptionId({
|
||||
availableModels: CODEX_MODELS,
|
||||
modelId: "gpt-5.3-codex",
|
||||
requestedThinkingOptionId: "invalid",
|
||||
}),
|
||||
).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("falls back to first option when no default and requested is invalid", () => {
|
||||
const modelsNoDefault: AgentModelDefinition[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
id: "m",
|
||||
label: "M",
|
||||
isDefault: true,
|
||||
thinkingOptions: [
|
||||
{ id: "low", label: "Low" },
|
||||
{ id: "high", label: "High" },
|
||||
],
|
||||
},
|
||||
];
|
||||
expect(
|
||||
resolveThinkingOptionId({
|
||||
availableModels: modelsNoDefault,
|
||||
modelId: "m",
|
||||
requestedThinkingOptionId: "",
|
||||
}),
|
||||
).toBe("low");
|
||||
});
|
||||
});
|
||||
|
||||
describe("combineInitialValues", () => {
|
||||
it("returns undefined when no initial values and no initial server id", () => {
|
||||
expect(combineInitialValues(undefined, null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not inject a null serverId override when initialValues are present but serverId is absent", () => {
|
||||
const combined = combineInitialValues({}, null);
|
||||
expect(combined).toEqual({});
|
||||
expect(Object.prototype.hasOwnProperty.call(combined, "serverId")).toBe(false);
|
||||
});
|
||||
|
||||
it("injects serverId from options when provided", () => {
|
||||
expect(combineInitialValues({}, "daemon-1")).toEqual({ serverId: "daemon-1" });
|
||||
});
|
||||
|
||||
it("keeps other initial values without forcing serverId", () => {
|
||||
const combined = combineInitialValues({ workingDir: "/repo" }, null);
|
||||
expect(combined).toEqual({ workingDir: "/repo" });
|
||||
expect(Object.prototype.hasOwnProperty.call(combined, "serverId")).toBe(false);
|
||||
});
|
||||
|
||||
it("respects an explicit serverId override (including null) over initialServerId", () => {
|
||||
expect(combineInitialValues({ serverId: null }, "daemon-1")).toEqual({ serverId: null });
|
||||
expect(combineInitialValues({ serverId: "daemon-2" }, "daemon-1")).toEqual({
|
||||
serverId: "daemon-2",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeSelectedComposerPreferences", () => {
|
||||
it("stores the selected model for the selected provider", () => {
|
||||
expect(
|
||||
mergeSelectedComposerPreferences({
|
||||
preferences: {},
|
||||
provider: "codex",
|
||||
updates: { model: "gpt-5.4" },
|
||||
}),
|
||||
).toEqual({
|
||||
provider: "codex",
|
||||
providerPreferences: { codex: { model: "gpt-5.4" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves existing provider preferences when the selected model changes", () => {
|
||||
expect(
|
||||
mergeSelectedComposerPreferences({
|
||||
preferences: {
|
||||
provider: "claude",
|
||||
providerPreferences: {
|
||||
codex: {
|
||||
mode: "full-access",
|
||||
thinkingByModel: { "gpt-5.4-mini": "medium" },
|
||||
featureValues: { fast_mode: true },
|
||||
},
|
||||
claude: { model: "claude-sonnet-4-6" },
|
||||
},
|
||||
favoriteModels: [{ provider: "codex", modelId: "gpt-5.4-mini" }],
|
||||
},
|
||||
provider: "codex",
|
||||
updates: { model: "gpt-5.4" },
|
||||
}),
|
||||
).toEqual({
|
||||
provider: "codex",
|
||||
providerPreferences: {
|
||||
codex: {
|
||||
model: "gpt-5.4",
|
||||
mode: "full-access",
|
||||
thinkingByModel: { "gpt-5.4-mini": "medium" },
|
||||
featureValues: { fast_mode: true },
|
||||
},
|
||||
claude: { model: "claude-sonnet-4-6" },
|
||||
},
|
||||
favoriteModels: [{ provider: "codex", modelId: "gpt-5.4-mini" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("stores mode and thinking preferences without dropping the selected model", () => {
|
||||
expect(
|
||||
mergeSelectedComposerPreferences({
|
||||
preferences: {
|
||||
provider: "codex",
|
||||
providerPreferences: {
|
||||
codex: {
|
||||
model: "gpt-5.4",
|
||||
mode: "auto",
|
||||
thinkingByModel: { "gpt-5.4-mini": "low" },
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "codex",
|
||||
updates: {
|
||||
mode: "full-access",
|
||||
thinkingByModel: { "gpt-5.4": "xhigh" },
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
provider: "codex",
|
||||
providerPreferences: {
|
||||
codex: {
|
||||
model: "gpt-5.4",
|
||||
mode: "full-access",
|
||||
thinkingByModel: { "gpt-5.4-mini": "low", "gpt-5.4": "xhigh" },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildProviderDefinitions", () => {
|
||||
it("returns empty array when snapshot data is unavailable", () => {
|
||||
expect(buildProviderDefinitions(undefined)).toEqual([]);
|
||||
expect(buildProviderDefinitions([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("builds provider definitions from snapshot metadata", () => {
|
||||
const entries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "zai",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
label: "ZAI",
|
||||
description: "Claude with ZAI config",
|
||||
defaultModeId: "default",
|
||||
modes: [
|
||||
{
|
||||
id: "default",
|
||||
label: "Default",
|
||||
description: "Safe mode",
|
||||
icon: "ShieldCheck",
|
||||
colorTier: "safe",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(buildProviderDefinitions(entries)).toEqual([
|
||||
{
|
||||
id: "zai",
|
||||
label: "ZAI",
|
||||
description: "Claude with ZAI config",
|
||||
defaultModeId: "default",
|
||||
modes: [
|
||||
{
|
||||
id: "default",
|
||||
label: "Default",
|
||||
description: "Safe mode",
|
||||
icon: "ShieldCheck",
|
||||
colorTier: "safe",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveFormState", () => {
|
||||
it("keeps provider, mode, and model unset on first open without preferences or explicit values", () => {
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{},
|
||||
null,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState().form,
|
||||
|
||||
bothProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.provider).toBeNull();
|
||||
expect(resolved.modeId).toBe("");
|
||||
expect(resolved.model).toBe("");
|
||||
expect(resolved.thinkingOptionId).toBe("");
|
||||
});
|
||||
|
||||
it("does not auto-select a model on fresh drafts without preferences", () => {
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "codex" },
|
||||
CODEX_MODELS,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
codexProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.model).toBe("");
|
||||
expect(resolved.thinkingOptionId).toBe("");
|
||||
});
|
||||
|
||||
it("auto-selects the model's default thinking option when model is preferred but thinking is not", () => {
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "codex", providerPreferences: { codex: { model: "gpt-5.3-codex" } } },
|
||||
CODEX_MODELS,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
codexProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.model).toBe("gpt-5.3-codex");
|
||||
expect(resolved.thinkingOptionId).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("falls back to model default when saved thinking preference is invalid", () => {
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "codex", providerPreferences: { codex: { model: "gpt-5.3-codex" } } },
|
||||
CODEX_MODELS,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
codexProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.thinkingOptionId).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("normalizes legacy model id 'default' from initial values to the provider default model", () => {
|
||||
const resolved = resolveFormState(
|
||||
{ model: "default" },
|
||||
{ provider: "codex" },
|
||||
CODEX_MODELS,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
codexProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.model).toBe("gpt-5.3-codex");
|
||||
});
|
||||
|
||||
it("keeps an explicit initial thinking option when it is valid", () => {
|
||||
const resolved = resolveFormState(
|
||||
{ model: "gpt-5.3-codex", thinkingOptionId: "low" },
|
||||
{ provider: "codex" },
|
||||
CODEX_MODELS,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
codexProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.model).toBe("gpt-5.3-codex");
|
||||
expect(resolved.thinkingOptionId).toBe("low");
|
||||
});
|
||||
|
||||
it("falls back to the first thinking option when model exposes options without a provider default", () => {
|
||||
const claudeWithThinking: AgentModelDefinition[] = [
|
||||
{
|
||||
provider: "claude",
|
||||
id: "default",
|
||||
label: "Default (Sonnet 4.6)",
|
||||
isDefault: true,
|
||||
thinkingOptions: [
|
||||
{ id: "low", label: "Low" },
|
||||
{ id: "medium", label: "Medium" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "claude", providerPreferences: { claude: { model: "default" } } },
|
||||
claudeWithThinking,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "claude" }).form,
|
||||
|
||||
claudeProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.model).toBe("default");
|
||||
expect(resolved.thinkingOptionId).toBe("low");
|
||||
});
|
||||
|
||||
it("clears an invalid provider instead of falling back to the first allowed provider", () => {
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "codex" },
|
||||
null,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
claudeProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.provider).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves a user-selected provider and model while that provider is loading during refresh", () => {
|
||||
const loadingEntries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
status: "loading",
|
||||
enabled: true,
|
||||
label: TEST_CODEX_DEFINITION.label,
|
||||
description: TEST_CODEX_DEFINITION.description,
|
||||
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
|
||||
modes: TEST_CODEX_DEFINITION.modes,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
label: TEST_CLAUDE_DEFINITION.label,
|
||||
description: TEST_CLAUDE_DEFINITION.description,
|
||||
defaultModeId: TEST_CLAUDE_DEFINITION.defaultModeId,
|
||||
modes: TEST_CLAUDE_DEFINITION.modes,
|
||||
models: [{ provider: "claude", id: "default", label: "Default", isDefault: true }],
|
||||
},
|
||||
];
|
||||
const providerDefinitions = buildProviderDefinitions(loadingEntries);
|
||||
const resolvableProviderMap = buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries: loadingEntries,
|
||||
providerDefinitions,
|
||||
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]),
|
||||
});
|
||||
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{},
|
||||
null,
|
||||
{
|
||||
serverId: false,
|
||||
provider: true,
|
||||
modeId: true,
|
||||
model: true,
|
||||
thinkingOptionId: true,
|
||||
workingDir: false,
|
||||
},
|
||||
makeState({
|
||||
provider: "codex",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.3-codex",
|
||||
thinkingOptionId: "xhigh",
|
||||
}).form,
|
||||
|
||||
resolvableProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.provider).toBe("codex");
|
||||
expect(resolved.modeId).toBe("full-access");
|
||||
expect(resolved.model).toBe("gpt-5.3-codex");
|
||||
expect(resolved.thinkingOptionId).toBe("xhigh");
|
||||
});
|
||||
|
||||
it("ignores disabled ready providers when resolving selectable defaults", () => {
|
||||
const entries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
label: TEST_CODEX_DEFINITION.label,
|
||||
description: TEST_CODEX_DEFINITION.description,
|
||||
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
|
||||
modes: TEST_CODEX_DEFINITION.modes,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
status: "ready",
|
||||
enabled: false,
|
||||
label: TEST_CLAUDE_DEFINITION.label,
|
||||
description: TEST_CLAUDE_DEFINITION.description,
|
||||
defaultModeId: TEST_CLAUDE_DEFINITION.defaultModeId,
|
||||
modes: TEST_CLAUDE_DEFINITION.modes,
|
||||
},
|
||||
];
|
||||
const providerDefinitions = buildProviderDefinitions(entries);
|
||||
const selectableProviderMap = buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries: entries,
|
||||
providerDefinitions,
|
||||
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready"]),
|
||||
});
|
||||
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "claude" },
|
||||
null,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
selectableProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.provider).toBe("codex");
|
||||
expect(resolved.modeId).toBe("auto");
|
||||
});
|
||||
|
||||
it("excludes disabled providers from the selectable provider map without removing them from snapshot definitions", () => {
|
||||
const entries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
label: TEST_CODEX_DEFINITION.label,
|
||||
description: TEST_CODEX_DEFINITION.description,
|
||||
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
|
||||
modes: TEST_CODEX_DEFINITION.modes,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
status: "ready",
|
||||
enabled: false,
|
||||
label: TEST_CLAUDE_DEFINITION.label,
|
||||
description: TEST_CLAUDE_DEFINITION.description,
|
||||
defaultModeId: TEST_CLAUDE_DEFINITION.defaultModeId,
|
||||
modes: TEST_CLAUDE_DEFINITION.modes,
|
||||
},
|
||||
];
|
||||
const providerDefinitions = buildProviderDefinitions(entries);
|
||||
|
||||
const selectableProviderMap = buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries: entries,
|
||||
providerDefinitions,
|
||||
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready"]),
|
||||
});
|
||||
|
||||
expect([...selectableProviderMap.keys()]).toEqual(["codex"]);
|
||||
expect(providerDefinitions.map((d) => d.id)).toEqual(["codex", "claude"]);
|
||||
});
|
||||
|
||||
it("clears a user-selected provider when the refreshed snapshot marks it unavailable", () => {
|
||||
const unavailableEntries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
status: "unavailable",
|
||||
enabled: true,
|
||||
label: TEST_CODEX_DEFINITION.label,
|
||||
description: TEST_CODEX_DEFINITION.description,
|
||||
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
|
||||
modes: TEST_CODEX_DEFINITION.modes,
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
label: TEST_CLAUDE_DEFINITION.label,
|
||||
description: TEST_CLAUDE_DEFINITION.description,
|
||||
defaultModeId: TEST_CLAUDE_DEFINITION.defaultModeId,
|
||||
modes: TEST_CLAUDE_DEFINITION.modes,
|
||||
models: [{ provider: "claude", id: "default", label: "Default", isDefault: true }],
|
||||
},
|
||||
];
|
||||
const providerDefinitions = buildProviderDefinitions(unavailableEntries);
|
||||
const resolvableProviderMap = buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries: unavailableEntries,
|
||||
providerDefinitions,
|
||||
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]),
|
||||
});
|
||||
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{},
|
||||
null,
|
||||
{ ...INITIAL_USER_MODIFIED, provider: true },
|
||||
makeState({
|
||||
provider: "codex",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.3-codex",
|
||||
thinkingOptionId: "xhigh",
|
||||
}).form,
|
||||
|
||||
resolvableProviderMap,
|
||||
);
|
||||
|
||||
expect(resolved.provider).toBeNull();
|
||||
expect(resolved.modeId).toBe("");
|
||||
expect(resolved.model).toBe("");
|
||||
expect(resolved.thinkingOptionId).toBe("");
|
||||
});
|
||||
|
||||
it("does not force fallback provider when allowed provider map is empty", () => {
|
||||
const resolved = resolveFormState(
|
||||
undefined,
|
||||
{ provider: "codex" },
|
||||
null,
|
||||
INITIAL_USER_MODIFIED,
|
||||
makeState({ provider: "codex" }).form,
|
||||
|
||||
new Map<AgentProvider, AgentProviderDefinition>(),
|
||||
);
|
||||
|
||||
expect(resolved.provider).toBe("codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAgentForm", () => {
|
||||
describe("RESOLVE", () => {
|
||||
it("applies resolved provider and mode when no user modifications", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "RESOLVE",
|
||||
initialValues: undefined,
|
||||
preferences: { provider: "codex" },
|
||||
availableModels: null,
|
||||
|
||||
allowedProviderMap: codexProviderMap,
|
||||
});
|
||||
|
||||
expect(next.form.provider).toBe("codex");
|
||||
expect(next.form.modeId).toBe("auto");
|
||||
});
|
||||
|
||||
it("returns the same state reference when nothing changed", () => {
|
||||
const state = makeState({ provider: "codex", modeId: "auto" });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "RESOLVE",
|
||||
initialValues: undefined,
|
||||
preferences: { provider: "codex" },
|
||||
availableModels: null,
|
||||
|
||||
allowedProviderMap: codexProviderMap,
|
||||
});
|
||||
|
||||
expect(next).toBe(state);
|
||||
});
|
||||
|
||||
it("does not override user-modified provider", () => {
|
||||
const state = makeState({ provider: "codex", modeId: "auto" }, { provider: true });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "RESOLVE",
|
||||
initialValues: undefined,
|
||||
preferences: { provider: "claude" },
|
||||
availableModels: null,
|
||||
|
||||
allowedProviderMap: bothProviderMap,
|
||||
});
|
||||
|
||||
expect(next.form.provider).toBe("codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_SERVER_ID", () => {
|
||||
it("updates serverId without marking it user-modified", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, { type: "SET_SERVER_ID", value: "host-1" });
|
||||
|
||||
expect(next.form.serverId).toBe("host-1");
|
||||
expect(next.userModified.serverId).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_SERVER_ID_FROM_USER", () => {
|
||||
it("updates serverId and marks it user-modified", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, { type: "SET_SERVER_ID_FROM_USER", value: "host-2" });
|
||||
|
||||
expect(next.form.serverId).toBe("host-2");
|
||||
expect(next.userModified.serverId).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_PROVIDER_FROM_USER", () => {
|
||||
it("switches provider, picks preferred model and mode, marks provider modified", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_PROVIDER_FROM_USER",
|
||||
provider: "codex",
|
||||
providerModels: CODEX_MODELS,
|
||||
providerDef: TEST_CODEX_DEFINITION,
|
||||
providerPrefs: { model: "gpt-5.3-codex", mode: "full-access" },
|
||||
});
|
||||
|
||||
expect(next.form.provider).toBe("codex");
|
||||
expect(next.form.model).toBe("gpt-5.3-codex");
|
||||
expect(next.form.modeId).toBe("full-access");
|
||||
expect(next.userModified.provider).toBe(true);
|
||||
expect(next.userModified.model).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to provider defaults when no prefs", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_PROVIDER_FROM_USER",
|
||||
provider: "codex",
|
||||
providerModels: CODEX_MODELS,
|
||||
providerDef: TEST_CODEX_DEFINITION,
|
||||
providerPrefs: undefined,
|
||||
});
|
||||
|
||||
expect(next.form.modeId).toBe("auto");
|
||||
expect(next.form.model).toBe("gpt-5.3-codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_PROVIDER_AND_MODEL_FROM_USER", () => {
|
||||
it("sets provider, model, and default mode; marks both modified", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_PROVIDER_AND_MODEL_FROM_USER",
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.3-codex",
|
||||
providerDef: TEST_CODEX_DEFINITION,
|
||||
providerModels: CODEX_MODELS,
|
||||
});
|
||||
|
||||
expect(next.form.provider).toBe("codex");
|
||||
expect(next.form.model).toBe("gpt-5.3-codex");
|
||||
expect(next.form.modeId).toBe("auto");
|
||||
expect(next.userModified.provider).toBe(true);
|
||||
expect(next.userModified.model).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to provider default model when modelId is empty", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_PROVIDER_AND_MODEL_FROM_USER",
|
||||
provider: "codex",
|
||||
modelId: "",
|
||||
providerDef: TEST_CODEX_DEFINITION,
|
||||
providerModels: CODEX_MODELS,
|
||||
});
|
||||
|
||||
expect(next.form.model).toBe("gpt-5.3-codex");
|
||||
});
|
||||
|
||||
it("selects default thinking option for the chosen model", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_PROVIDER_AND_MODEL_FROM_USER",
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.3-codex",
|
||||
providerDef: TEST_CODEX_DEFINITION,
|
||||
providerModels: CODEX_MODELS,
|
||||
});
|
||||
|
||||
expect(next.form.thinkingOptionId).toBe("xhigh");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_MODE_FROM_USER", () => {
|
||||
it("updates modeId and marks it modified", () => {
|
||||
const state = makeState({ provider: "codex", modeId: "auto" });
|
||||
const next = resolveAgentForm(state, { type: "SET_MODE_FROM_USER", modeId: "full-access" });
|
||||
|
||||
expect(next.form.modeId).toBe("full-access");
|
||||
expect(next.userModified.modeId).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_MODEL_FROM_USER", () => {
|
||||
it("updates model and resets thinking to model default when thinking is not user-modified", () => {
|
||||
const state = makeState({ provider: "codex", model: "", thinkingOptionId: "" });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_MODEL_FROM_USER",
|
||||
modelId: "gpt-5.3-codex",
|
||||
availableModels: CODEX_MODELS,
|
||||
});
|
||||
|
||||
expect(next.form.model).toBe("gpt-5.3-codex");
|
||||
expect(next.form.thinkingOptionId).toBe("xhigh");
|
||||
expect(next.userModified.model).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves user-chosen thinking option when switching to same model", () => {
|
||||
const state = makeState(
|
||||
{ provider: "codex", model: "gpt-5.3-codex", thinkingOptionId: "low" },
|
||||
{ thinkingOptionId: true },
|
||||
);
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_MODEL_FROM_USER",
|
||||
modelId: "gpt-5.3-codex",
|
||||
availableModels: CODEX_MODELS,
|
||||
});
|
||||
|
||||
expect(next.form.thinkingOptionId).toBe("low");
|
||||
});
|
||||
|
||||
it("falls back to provider default model when modelId is blank", () => {
|
||||
const state = makeState({ provider: "codex" });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_MODEL_FROM_USER",
|
||||
modelId: " ",
|
||||
availableModels: CODEX_MODELS,
|
||||
});
|
||||
|
||||
expect(next.form.model).toBe("gpt-5.3-codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_THINKING_OPTION_FROM_USER", () => {
|
||||
it("updates thinkingOptionId and marks it modified", () => {
|
||||
const state = makeState({ thinkingOptionId: "xhigh" });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_THINKING_OPTION_FROM_USER",
|
||||
thinkingOptionId: "low",
|
||||
});
|
||||
|
||||
expect(next.form.thinkingOptionId).toBe("low");
|
||||
expect(next.userModified.thinkingOptionId).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_WORKING_DIR", () => {
|
||||
it("updates workingDir without marking it modified", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, { type: "SET_WORKING_DIR", value: "/home/user/proj" });
|
||||
|
||||
expect(next.form.workingDir).toBe("/home/user/proj");
|
||||
expect(next.userModified.workingDir).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_WORKING_DIR_FROM_USER", () => {
|
||||
it("updates workingDir and marks it modified", () => {
|
||||
const state = makeState();
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "SET_WORKING_DIR_FROM_USER",
|
||||
value: "/home/user/proj",
|
||||
});
|
||||
|
||||
expect(next.form.workingDir).toBe("/home/user/proj");
|
||||
expect(next.userModified.workingDir).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AUTO_SELECT_SERVER", () => {
|
||||
it("sets serverId when currently null", () => {
|
||||
const state = makeState({ serverId: null });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "AUTO_SELECT_SERVER",
|
||||
candidateServerId: "host-1",
|
||||
});
|
||||
|
||||
expect(next.form.serverId).toBe("host-1");
|
||||
});
|
||||
|
||||
it("does not override an already-set serverId", () => {
|
||||
const state = makeState({ serverId: "existing" });
|
||||
const next = resolveAgentForm(state, {
|
||||
type: "AUTO_SELECT_SERVER",
|
||||
candidateServerId: "host-1",
|
||||
});
|
||||
|
||||
expect(next).toBe(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RESET", () => {
|
||||
it("resets userModified flags while keeping form state", () => {
|
||||
const state = makeState(
|
||||
{ provider: "codex", modeId: "full-access", model: "gpt-5.3-codex" },
|
||||
{ provider: true, modeId: true, model: true },
|
||||
);
|
||||
const next = resolveAgentForm(state, { type: "RESET" });
|
||||
|
||||
expect(next.userModified).toEqual(INITIAL_USER_MODIFIED);
|
||||
expect(next.form).toEqual(state.form);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildProviderDefinitionMap", () => {
|
||||
it("builds a map from provider id to definition", () => {
|
||||
const map = buildProviderDefinitionMap([TEST_CODEX_DEFINITION, TEST_CLAUDE_DEFINITION]);
|
||||
expect(map.get("codex")).toBe(TEST_CODEX_DEFINITION);
|
||||
expect(map.get("claude")).toBe(TEST_CLAUDE_DEFINITION);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildProviderDefinitionMapForStatuses", () => {
|
||||
it("returns all definitions when no snapshot entries", () => {
|
||||
const map = buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries: undefined,
|
||||
providerDefinitions: [TEST_CODEX_DEFINITION],
|
||||
statuses: new Set(["ready"]),
|
||||
});
|
||||
expect([...map.keys()]).toEqual(["codex"]);
|
||||
});
|
||||
|
||||
it("filters to only matching-status enabled providers", () => {
|
||||
const entries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "codex",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
label: "Codex",
|
||||
description: "",
|
||||
defaultModeId: "auto",
|
||||
modes: [],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
status: "loading",
|
||||
enabled: true,
|
||||
label: "Claude",
|
||||
description: "",
|
||||
defaultModeId: "default",
|
||||
modes: [],
|
||||
},
|
||||
];
|
||||
const map = buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries: entries,
|
||||
providerDefinitions: [TEST_CODEX_DEFINITION, TEST_CLAUDE_DEFINITION],
|
||||
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready"]),
|
||||
});
|
||||
|
||||
expect([...map.keys()]).toEqual(["codex"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,559 +0,0 @@
|
||||
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
import type {
|
||||
AgentModelDefinition,
|
||||
AgentProvider,
|
||||
ProviderSnapshotEntry,
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
import {
|
||||
mergeProviderPreferences,
|
||||
type FormPreferences,
|
||||
type ProviderPreferences,
|
||||
} from "./use-form-preferences";
|
||||
|
||||
export interface FormInitialValues {
|
||||
serverId?: string | null;
|
||||
provider?: AgentProvider;
|
||||
modeId?: string | null;
|
||||
model?: string | null;
|
||||
thinkingOptionId?: string | null;
|
||||
workingDir?: string;
|
||||
}
|
||||
|
||||
export interface FormState {
|
||||
serverId: string | null;
|
||||
provider: AgentProvider | null;
|
||||
modeId: string;
|
||||
model: string;
|
||||
thinkingOptionId: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
export interface UserModifiedFields {
|
||||
serverId: boolean;
|
||||
provider: boolean;
|
||||
modeId: boolean;
|
||||
model: boolean;
|
||||
thinkingOptionId: boolean;
|
||||
workingDir: boolean;
|
||||
}
|
||||
|
||||
export interface AgentFormReducerState {
|
||||
form: FormState;
|
||||
userModified: UserModifiedFields;
|
||||
}
|
||||
|
||||
export const INITIAL_USER_MODIFIED: UserModifiedFields = {
|
||||
serverId: false,
|
||||
provider: false,
|
||||
modeId: false,
|
||||
model: false,
|
||||
thinkingOptionId: false,
|
||||
workingDir: false,
|
||||
};
|
||||
|
||||
type ProviderPrefs = NonNullable<FormPreferences["providerPreferences"]>[AgentProvider];
|
||||
|
||||
export const RESOLVABLE_PROVIDER_STATUSES = new Set<ProviderSnapshotEntry["status"]>([
|
||||
"ready",
|
||||
"loading",
|
||||
]);
|
||||
export const SELECTABLE_PROVIDER_STATUSES = new Set<ProviderSnapshotEntry["status"]>(["ready"]);
|
||||
|
||||
export type AgentFormAction =
|
||||
| {
|
||||
type: "RESOLVE";
|
||||
initialValues: FormInitialValues | undefined;
|
||||
preferences: FormPreferences | null;
|
||||
availableModels: AgentModelDefinition[] | null;
|
||||
allowedProviderMap: Map<AgentProvider, AgentProviderDefinition>;
|
||||
}
|
||||
| { type: "SET_SERVER_ID"; value: string | null }
|
||||
| { type: "SET_SERVER_ID_FROM_USER"; value: string | null }
|
||||
| {
|
||||
type: "SET_PROVIDER_FROM_USER";
|
||||
provider: AgentProvider;
|
||||
providerModels: AgentModelDefinition[] | null;
|
||||
providerDef: AgentProviderDefinition | undefined;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
}
|
||||
| {
|
||||
type: "SET_PROVIDER_AND_MODEL_FROM_USER";
|
||||
provider: AgentProvider;
|
||||
modelId: string;
|
||||
providerDef: AgentProviderDefinition | undefined;
|
||||
providerModels: AgentModelDefinition[] | null;
|
||||
}
|
||||
| { type: "SET_MODE_FROM_USER"; modeId: string }
|
||||
| {
|
||||
type: "SET_MODEL_FROM_USER";
|
||||
modelId: string;
|
||||
availableModels: AgentModelDefinition[] | null;
|
||||
}
|
||||
| { type: "SET_THINKING_OPTION_FROM_USER"; thinkingOptionId: string }
|
||||
| { type: "SET_WORKING_DIR"; value: string }
|
||||
| { type: "SET_WORKING_DIR_FROM_USER"; value: string }
|
||||
| { type: "AUTO_SELECT_SERVER"; candidateServerId: string }
|
||||
| { type: "RESET" };
|
||||
|
||||
export function normalizeSelectedModelId(modelId: string | null | undefined): string {
|
||||
return typeof modelId === "string" ? modelId.trim() : "";
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(
|
||||
availableModels: AgentModelDefinition[] | null,
|
||||
): AgentModelDefinition | null {
|
||||
if (!availableModels || availableModels.length === 0) return null;
|
||||
return availableModels.find((model) => model.isDefault) ?? availableModels[0] ?? null;
|
||||
}
|
||||
|
||||
export function resolveDefaultModelId(availableModels: AgentModelDefinition[] | null): string {
|
||||
return resolveDefaultModel(availableModels)?.id ?? "";
|
||||
}
|
||||
|
||||
export function resolveEffectiveModel(
|
||||
availableModels: AgentModelDefinition[] | null,
|
||||
modelId: string,
|
||||
): AgentModelDefinition | null {
|
||||
if (!availableModels || availableModels.length === 0) return null;
|
||||
const normalizedModelId = modelId.trim();
|
||||
if (!normalizedModelId) return null;
|
||||
return (
|
||||
availableModels.find((model) => model.id === normalizedModelId) ??
|
||||
resolveDefaultModel(availableModels)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveThinkingOptionId(args: {
|
||||
availableModels: AgentModelDefinition[] | null;
|
||||
modelId: string;
|
||||
requestedThinkingOptionId: string;
|
||||
}): string {
|
||||
const effectiveModel = resolveEffectiveModel(args.availableModels, args.modelId);
|
||||
const thinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
||||
if (thinkingOptions.length === 0) return "";
|
||||
|
||||
const normalizedThinkingOptionId = args.requestedThinkingOptionId.trim();
|
||||
if (
|
||||
normalizedThinkingOptionId &&
|
||||
thinkingOptions.some((option) => option.id === normalizedThinkingOptionId)
|
||||
) {
|
||||
return normalizedThinkingOptionId;
|
||||
}
|
||||
|
||||
return effectiveModel?.defaultThinkingOptionId ?? thinkingOptions[0]?.id ?? "";
|
||||
}
|
||||
|
||||
export function mergeSelectedComposerPreferences(args: {
|
||||
preferences: FormPreferences;
|
||||
provider: AgentProvider;
|
||||
updates: Partial<ProviderPreferences>;
|
||||
}): FormPreferences {
|
||||
return mergeProviderPreferences({
|
||||
preferences: args.preferences,
|
||||
provider: args.provider,
|
||||
updates: args.updates,
|
||||
});
|
||||
}
|
||||
|
||||
export function combineInitialValues(
|
||||
initialValues: FormInitialValues | undefined,
|
||||
initialServerId: string | null,
|
||||
): FormInitialValues | undefined {
|
||||
const hasExplicitServerId = initialValues?.serverId !== undefined;
|
||||
const serverIdFromOptions = initialServerId === null ? undefined : initialServerId;
|
||||
|
||||
if (!initialValues && !hasExplicitServerId && serverIdFromOptions === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasExplicitServerId) {
|
||||
return { ...initialValues, serverId: initialValues?.serverId };
|
||||
}
|
||||
|
||||
if (serverIdFromOptions !== undefined) {
|
||||
return { ...initialValues, serverId: serverIdFromOptions };
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
export function hasFormStateChanged(prev: FormState, next: FormState): boolean {
|
||||
return (
|
||||
prev.serverId !== next.serverId ||
|
||||
prev.provider !== next.provider ||
|
||||
prev.modeId !== next.modeId ||
|
||||
prev.model !== next.model ||
|
||||
prev.thinkingOptionId !== next.thinkingOptionId ||
|
||||
prev.workingDir !== next.workingDir
|
||||
);
|
||||
}
|
||||
|
||||
export function buildProviderDefinitionMap(
|
||||
providerDefinitions: AgentProviderDefinition[],
|
||||
): Map<AgentProvider, AgentProviderDefinition> {
|
||||
return new Map<AgentProvider, AgentProviderDefinition>(
|
||||
providerDefinitions.map((definition) => [definition.id, definition]),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildProviderDefinitionMapForStatuses(args: {
|
||||
snapshotEntries: ProviderSnapshotEntry[] | undefined;
|
||||
providerDefinitions: AgentProviderDefinition[];
|
||||
statuses: ReadonlySet<ProviderSnapshotEntry["status"]>;
|
||||
}): Map<AgentProvider, AgentProviderDefinition> {
|
||||
if (!args.snapshotEntries?.length) {
|
||||
return buildProviderDefinitionMap(args.providerDefinitions);
|
||||
}
|
||||
|
||||
const matchingProviders = new Set(
|
||||
args.snapshotEntries
|
||||
.filter((entry) => args.statuses.has(entry.status) && entry.enabled)
|
||||
.map((entry) => entry.provider),
|
||||
);
|
||||
|
||||
return buildProviderDefinitionMap(
|
||||
args.providerDefinitions.filter((definition) => matchingProviders.has(definition.id)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveProvider(input: {
|
||||
currentProvider: AgentProvider | null;
|
||||
userModified: boolean;
|
||||
initialValues: FormInitialValues | undefined;
|
||||
preferences: FormPreferences | null;
|
||||
allowedProviderMap: Map<AgentProvider, AgentProviderDefinition>;
|
||||
}): AgentProvider | null {
|
||||
const { currentProvider, userModified, initialValues, preferences, allowedProviderMap } = input;
|
||||
if (userModified) {
|
||||
if (
|
||||
currentProvider &&
|
||||
allowedProviderMap.size > 0 &&
|
||||
!allowedProviderMap.has(currentProvider)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return currentProvider;
|
||||
}
|
||||
if (initialValues?.provider && allowedProviderMap.has(initialValues.provider)) {
|
||||
return initialValues.provider;
|
||||
}
|
||||
if (preferences?.provider && allowedProviderMap.has(preferences.provider)) {
|
||||
return preferences.provider;
|
||||
}
|
||||
if (currentProvider && allowedProviderMap.size > 0 && !allowedProviderMap.has(currentProvider)) {
|
||||
return null;
|
||||
}
|
||||
return currentProvider;
|
||||
}
|
||||
|
||||
function resolveModeId(input: {
|
||||
provider: AgentProvider | null;
|
||||
userModified: boolean;
|
||||
currentModeId: string;
|
||||
initialValues: FormInitialValues | undefined;
|
||||
providerDef: AgentProviderDefinition | undefined;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
}): string {
|
||||
const { provider, userModified, currentModeId, initialValues, providerDef, providerPrefs } =
|
||||
input;
|
||||
if (userModified) return currentModeId;
|
||||
if (!provider) return "";
|
||||
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
|
||||
if (
|
||||
typeof initialValues?.modeId === "string" &&
|
||||
initialValues.modeId.length > 0 &&
|
||||
validModeIds.includes(initialValues.modeId)
|
||||
) {
|
||||
return initialValues.modeId;
|
||||
}
|
||||
if (providerPrefs?.mode && validModeIds.includes(providerPrefs.mode)) {
|
||||
return providerPrefs.mode;
|
||||
}
|
||||
return providerDef?.defaultModeId ?? validModeIds[0] ?? "";
|
||||
}
|
||||
|
||||
function resolveModelField(input: {
|
||||
provider: AgentProvider | null;
|
||||
userModified: boolean;
|
||||
currentModel: string;
|
||||
initialValues: FormInitialValues | undefined;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
availableModels: AgentModelDefinition[] | null;
|
||||
}): string {
|
||||
const { provider, userModified, currentModel, initialValues, providerPrefs, availableModels } =
|
||||
input;
|
||||
if (userModified) return currentModel;
|
||||
if (!provider) return "";
|
||||
const isValidModel = (m: string) => availableModels?.some((am) => am.id === m) ?? false;
|
||||
const initialModel = normalizeSelectedModelId(initialValues?.model);
|
||||
const preferredModel = normalizeSelectedModelId(providerPrefs?.model);
|
||||
const defaultModelId = resolveDefaultModelId(availableModels);
|
||||
if (initialModel) {
|
||||
return !availableModels || isValidModel(initialModel) ? initialModel : defaultModelId;
|
||||
}
|
||||
if (preferredModel) {
|
||||
return !availableModels || isValidModel(preferredModel) ? preferredModel : defaultModelId;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function resolveThinkingOption(input: {
|
||||
provider: AgentProvider | null;
|
||||
userModified: boolean;
|
||||
currentThinkingOptionId: string;
|
||||
modelId: string;
|
||||
initialValues: FormInitialValues | undefined;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
}): string {
|
||||
const { provider, userModified, currentThinkingOptionId, modelId, initialValues, providerPrefs } =
|
||||
input;
|
||||
if (!provider) return "";
|
||||
if (userModified) return currentThinkingOptionId;
|
||||
const initialThinkingOptionId =
|
||||
typeof initialValues?.thinkingOptionId === "string"
|
||||
? initialValues.thinkingOptionId.trim()
|
||||
: "";
|
||||
const effectiveModelId = modelId.trim();
|
||||
const preferredThinking = effectiveModelId
|
||||
? (providerPrefs?.thinkingByModel?.[effectiveModelId]?.trim() ?? "")
|
||||
: "";
|
||||
if (initialThinkingOptionId.length > 0) return initialThinkingOptionId;
|
||||
if (preferredThinking.length > 0) return preferredThinking;
|
||||
return "";
|
||||
}
|
||||
|
||||
export function resolveFormState(
|
||||
initialValues: FormInitialValues | undefined,
|
||||
preferences: FormPreferences | null,
|
||||
availableModels: AgentModelDefinition[] | null,
|
||||
userModified: UserModifiedFields,
|
||||
currentState: FormState,
|
||||
allowedProviderMap: Map<AgentProvider, AgentProviderDefinition>,
|
||||
): FormState {
|
||||
const result = { ...currentState };
|
||||
|
||||
result.provider = resolveProvider({
|
||||
currentProvider: result.provider,
|
||||
userModified: userModified.provider,
|
||||
initialValues,
|
||||
preferences,
|
||||
allowedProviderMap,
|
||||
});
|
||||
|
||||
const providerDef = result.provider ? allowedProviderMap.get(result.provider) : undefined;
|
||||
const providerPrefs = result.provider
|
||||
? preferences?.providerPreferences?.[result.provider]
|
||||
: undefined;
|
||||
|
||||
result.modeId = resolveModeId({
|
||||
provider: result.provider,
|
||||
userModified: userModified.modeId,
|
||||
currentModeId: result.modeId,
|
||||
initialValues,
|
||||
providerDef,
|
||||
providerPrefs,
|
||||
});
|
||||
|
||||
result.model = resolveModelField({
|
||||
provider: result.provider,
|
||||
userModified: userModified.model,
|
||||
currentModel: result.model,
|
||||
initialValues,
|
||||
providerPrefs,
|
||||
availableModels,
|
||||
});
|
||||
|
||||
result.thinkingOptionId = resolveThinkingOption({
|
||||
provider: result.provider,
|
||||
userModified: userModified.thinkingOptionId,
|
||||
currentThinkingOptionId: result.thinkingOptionId,
|
||||
modelId: result.model,
|
||||
initialValues,
|
||||
providerPrefs,
|
||||
});
|
||||
|
||||
if (result.provider && availableModels) {
|
||||
result.thinkingOptionId = resolveThinkingOptionId({
|
||||
availableModels,
|
||||
modelId: result.model,
|
||||
requestedThinkingOptionId: result.thinkingOptionId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!userModified.serverId && initialValues?.serverId !== undefined) {
|
||||
result.serverId = initialValues.serverId;
|
||||
}
|
||||
|
||||
if (!userModified.workingDir && initialValues?.workingDir !== undefined) {
|
||||
result.workingDir = initialValues.workingDir;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function pickNextModelForProvider(input: {
|
||||
providerModels: AgentModelDefinition[] | null;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
}): string {
|
||||
const { providerModels, providerPrefs } = input;
|
||||
const isValidModel = (m: string) => providerModels?.some((am) => am.id === m) ?? false;
|
||||
const preferredModel = normalizeSelectedModelId(providerPrefs?.model);
|
||||
const defaultModelId = resolveDefaultModelId(providerModels);
|
||||
if (preferredModel && (!providerModels || isValidModel(preferredModel))) {
|
||||
return preferredModel;
|
||||
}
|
||||
return defaultModelId;
|
||||
}
|
||||
|
||||
function pickNextModeForProvider(input: {
|
||||
providerDef: AgentProviderDefinition | undefined;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
}): string {
|
||||
const { providerDef, providerPrefs } = input;
|
||||
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
|
||||
if (providerPrefs?.mode && validModeIds.includes(providerPrefs.mode)) {
|
||||
return providerPrefs.mode;
|
||||
}
|
||||
return providerDef?.defaultModeId ?? "";
|
||||
}
|
||||
|
||||
function pickNextThinkingOptionForProvider(input: {
|
||||
providerModels: AgentModelDefinition[] | null;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
modelId: string;
|
||||
}): string {
|
||||
const { providerModels, providerPrefs, modelId } = input;
|
||||
const preferredThinking = modelId
|
||||
? (providerPrefs?.thinkingByModel?.[modelId]?.trim() ?? "")
|
||||
: "";
|
||||
return resolveThinkingOptionId({
|
||||
availableModels: providerModels,
|
||||
modelId,
|
||||
requestedThinkingOptionId: preferredThinking,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveAgentForm(
|
||||
state: AgentFormReducerState,
|
||||
action: AgentFormAction,
|
||||
): AgentFormReducerState {
|
||||
switch (action.type) {
|
||||
case "RESOLVE": {
|
||||
const resolved = resolveFormState(
|
||||
action.initialValues,
|
||||
action.preferences,
|
||||
action.availableModels,
|
||||
state.userModified,
|
||||
state.form,
|
||||
action.allowedProviderMap,
|
||||
);
|
||||
if (!hasFormStateChanged(state.form, resolved)) return state;
|
||||
return { ...state, form: resolved };
|
||||
}
|
||||
|
||||
case "SET_SERVER_ID":
|
||||
return { ...state, form: { ...state.form, serverId: action.value } };
|
||||
|
||||
case "SET_SERVER_ID_FROM_USER":
|
||||
return {
|
||||
form: { ...state.form, serverId: action.value },
|
||||
userModified: { ...state.userModified, serverId: true },
|
||||
};
|
||||
|
||||
case "SET_PROVIDER_FROM_USER": {
|
||||
const nextModelId = pickNextModelForProvider({
|
||||
providerModels: action.providerModels,
|
||||
providerPrefs: action.providerPrefs,
|
||||
});
|
||||
const nextModeId = pickNextModeForProvider({
|
||||
providerDef: action.providerDef,
|
||||
providerPrefs: action.providerPrefs,
|
||||
});
|
||||
const nextThinkingOptionId = pickNextThinkingOptionForProvider({
|
||||
providerModels: action.providerModels,
|
||||
providerPrefs: action.providerPrefs,
|
||||
modelId: nextModelId,
|
||||
});
|
||||
return {
|
||||
form: {
|
||||
...state.form,
|
||||
provider: action.provider,
|
||||
modeId: nextModeId,
|
||||
model: nextModelId,
|
||||
thinkingOptionId: nextThinkingOptionId,
|
||||
},
|
||||
userModified: { ...state.userModified, provider: true },
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_PROVIDER_AND_MODEL_FROM_USER": {
|
||||
const normalizedModelId = normalizeSelectedModelId(action.modelId);
|
||||
const nextModelId = normalizedModelId || resolveDefaultModelId(action.providerModels);
|
||||
const nextThinkingOptionId = resolveThinkingOptionId({
|
||||
availableModels: action.providerModels,
|
||||
modelId: nextModelId,
|
||||
requestedThinkingOptionId: "",
|
||||
});
|
||||
return {
|
||||
form: {
|
||||
...state.form,
|
||||
provider: action.provider,
|
||||
model: nextModelId,
|
||||
modeId: action.providerDef?.defaultModeId ?? "",
|
||||
thinkingOptionId: nextThinkingOptionId,
|
||||
},
|
||||
userModified: { ...state.userModified, provider: true, model: true },
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_MODE_FROM_USER":
|
||||
return {
|
||||
form: { ...state.form, modeId: action.modeId },
|
||||
userModified: { ...state.userModified, modeId: true },
|
||||
};
|
||||
|
||||
case "SET_MODEL_FROM_USER": {
|
||||
const normalizedModelId = normalizeSelectedModelId(action.modelId);
|
||||
const nextModelId = normalizedModelId || resolveDefaultModelId(action.availableModels);
|
||||
const nextThinkingOptionId = resolveThinkingOptionId({
|
||||
availableModels: action.availableModels,
|
||||
modelId: nextModelId,
|
||||
requestedThinkingOptionId: state.userModified.thinkingOptionId
|
||||
? state.form.thinkingOptionId
|
||||
: "",
|
||||
});
|
||||
return {
|
||||
form: {
|
||||
...state.form,
|
||||
model: nextModelId,
|
||||
thinkingOptionId: nextThinkingOptionId,
|
||||
},
|
||||
userModified: { ...state.userModified, model: true },
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_THINKING_OPTION_FROM_USER":
|
||||
return {
|
||||
form: { ...state.form, thinkingOptionId: action.thinkingOptionId },
|
||||
userModified: { ...state.userModified, thinkingOptionId: true },
|
||||
};
|
||||
|
||||
case "SET_WORKING_DIR":
|
||||
return { ...state, form: { ...state.form, workingDir: action.value } };
|
||||
|
||||
case "SET_WORKING_DIR_FROM_USER":
|
||||
return {
|
||||
form: { ...state.form, workingDir: action.value },
|
||||
userModified: { ...state.userModified, workingDir: true },
|
||||
};
|
||||
|
||||
case "AUTO_SELECT_SERVER":
|
||||
if (state.form.serverId) return state;
|
||||
return { ...state, form: { ...state.form, serverId: action.candidateServerId } };
|
||||
|
||||
case "RESET":
|
||||
return { ...state, userModified: INITIAL_USER_MODIFIED };
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import type {
|
||||
import { useHosts } from "@/runtime/host-runtime";
|
||||
import { buildProviderDefinitions } from "@/utils/provider-definitions";
|
||||
import {
|
||||
buildSelectableModelSelectorProviders,
|
||||
type ModelSelectorProvider,
|
||||
} from "@/components/combined-model-selector.utils";
|
||||
buildSelectableProviderSelectorProviders,
|
||||
type ProviderSelectorProvider,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
import { useProvidersSnapshot } from "./use-providers-snapshot";
|
||||
import {
|
||||
useFormPreferences,
|
||||
@@ -32,9 +32,9 @@ import {
|
||||
SELECTABLE_PROVIDER_STATUSES,
|
||||
type FormInitialValues,
|
||||
type FormState,
|
||||
} from "./resolve-agent-form";
|
||||
} from "@/provider-selection/resolve-agent-form";
|
||||
|
||||
export type { FormInitialValues } from "./resolve-agent-form";
|
||||
export type { FormInitialValues } from "@/provider-selection/resolve-agent-form";
|
||||
|
||||
export interface UseAgentFormStateOptions {
|
||||
initialServerId?: string | null;
|
||||
@@ -67,7 +67,7 @@ export interface UseAgentFormStateResult {
|
||||
modeOptions: AgentMode[];
|
||||
availableModels: AgentModelDefinition[];
|
||||
allProviderModels: Map<string, AgentModelDefinition[]>;
|
||||
modelSelectorProviders: ModelSelectorProvider[];
|
||||
modelSelectorProviders: ProviderSelectorProvider[];
|
||||
isAllModelsLoading: boolean;
|
||||
availableThinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
|
||||
isModelLoading: boolean;
|
||||
@@ -243,7 +243,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
[snapshotEntries],
|
||||
);
|
||||
const snapshotModelSelectorProviders = useMemo(
|
||||
() => buildSelectableModelSelectorProviders(snapshotEntries),
|
||||
() => buildSelectableProviderSelectorProviders(snapshotEntries),
|
||||
[snapshotEntries],
|
||||
);
|
||||
const snapshotSelectedEntry = useMemo(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { UserComposerAttachment } from "@/attachments/types";
|
||||
import type { DraftAgentStatusBarProps } from "@/components/agent-status-bar";
|
||||
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
|
||||
import type { UseAgentFormStateResult } from "@/hooks/use-agent-form-state";
|
||||
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types";
|
||||
|
||||
export interface DraftKeyContext {
|
||||
selectedServerId: string | null;
|
||||
@@ -20,56 +18,6 @@ export function resolveDraftKey(input: {
|
||||
return input.draftKey;
|
||||
}
|
||||
|
||||
export function resolveEffectiveComposerModelId(input: {
|
||||
selectedModel: string;
|
||||
availableModels: AgentModelDefinition[];
|
||||
}): string {
|
||||
return input.selectedModel.trim();
|
||||
}
|
||||
|
||||
export function resolveEffectiveComposerThinkingOptionId(input: {
|
||||
selectedThinkingOptionId: string;
|
||||
availableModels: AgentModelDefinition[];
|
||||
effectiveModelId: string;
|
||||
}): string {
|
||||
const selectedThinkingOptionId = input.selectedThinkingOptionId.trim();
|
||||
if (selectedThinkingOptionId) {
|
||||
return selectedThinkingOptionId;
|
||||
}
|
||||
|
||||
const selectedModelDefinition =
|
||||
input.availableModels.find((model) => model.id === input.effectiveModelId) ?? null;
|
||||
return selectedModelDefinition?.defaultThinkingOptionId ?? "";
|
||||
}
|
||||
|
||||
export function buildDraftComposerCommandConfig(input: {
|
||||
provider: AgentProvider | null;
|
||||
cwd: string;
|
||||
modeOptions: DraftAgentStatusBarProps["modeOptions"];
|
||||
selectedMode: string;
|
||||
effectiveModelId: string;
|
||||
effectiveThinkingOptionId: string;
|
||||
featureValues?: Record<string, unknown>;
|
||||
}): DraftCommandConfig | undefined {
|
||||
const cwd = input.cwd.trim();
|
||||
if (!input.provider || !cwd) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: input.provider,
|
||||
cwd,
|
||||
...(input.modeOptions.length > 0 && input.selectedMode !== ""
|
||||
? { modeId: input.selectedMode }
|
||||
: {}),
|
||||
...(input.effectiveModelId ? { model: input.effectiveModelId } : {}),
|
||||
...(input.effectiveThinkingOptionId
|
||||
? { thinkingOptionId: input.effectiveThinkingOptionId }
|
||||
: {}),
|
||||
...(input.featureValues ? { featureValues: input.featureValues } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDraftStatusControls(input: {
|
||||
formState: UseAgentFormStateResult;
|
||||
features?: DraftAgentStatusBarProps["features"];
|
||||
|
||||
@@ -85,16 +85,19 @@ vi.mock("./use-agent-form-state", () => ({
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
rows: [
|
||||
{
|
||||
favoriteKey: "codex:gpt-5.4",
|
||||
provider: "codex",
|
||||
providerLabel: "Codex",
|
||||
modelId: "gpt-5.4",
|
||||
modelLabel: "gpt-5.4",
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
modelSelection: {
|
||||
kind: "models",
|
||||
rows: [
|
||||
{
|
||||
favoriteKey: "codex:gpt-5.4",
|
||||
provider: "codex",
|
||||
providerLabel: "Codex",
|
||||
modelId: "gpt-5.4",
|
||||
modelLabel: "gpt-5.4",
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
isAllModelsLoading: false,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveDraftKey } from "./use-agent-input-draft-core";
|
||||
import {
|
||||
buildDraftComposerCommandConfig,
|
||||
resolveDraftKey,
|
||||
buildDraftCommandConfig,
|
||||
resolveEffectiveComposerModelId,
|
||||
resolveEffectiveComposerThinkingOptionId,
|
||||
} from "./use-agent-input-draft-core";
|
||||
} from "@/provider-selection/provider-selection";
|
||||
|
||||
describe("resolveDraftKey", () => {
|
||||
it("returns a string draft key unchanged", () => {
|
||||
@@ -30,8 +30,12 @@ describe("resolveEffectiveComposerModelId", () => {
|
||||
it("returns the selected model trimmed", () => {
|
||||
expect(
|
||||
resolveEffectiveComposerModelId({
|
||||
selectedModel: " gpt-5.4-mini ",
|
||||
provider: "codex",
|
||||
modelId: " gpt-5.4-mini ",
|
||||
modeId: "",
|
||||
thinkingOptionId: "",
|
||||
availableModels: [],
|
||||
modeOptions: [],
|
||||
}),
|
||||
).toBe("gpt-5.4-mini");
|
||||
});
|
||||
@@ -39,8 +43,12 @@ describe("resolveEffectiveComposerModelId", () => {
|
||||
it("returns empty string when no model selected", () => {
|
||||
expect(
|
||||
resolveEffectiveComposerModelId({
|
||||
selectedModel: "",
|
||||
provider: "codex",
|
||||
modelId: "",
|
||||
modeId: "",
|
||||
thinkingOptionId: "",
|
||||
availableModels: [],
|
||||
modeOptions: [],
|
||||
}),
|
||||
).toBe("");
|
||||
});
|
||||
@@ -63,21 +71,33 @@ describe("resolveEffectiveComposerThinkingOptionId", () => {
|
||||
|
||||
it("prefers the selected thinking option when present", () => {
|
||||
expect(
|
||||
resolveEffectiveComposerThinkingOptionId({
|
||||
selectedThinkingOptionId: "medium",
|
||||
availableModels: models,
|
||||
effectiveModelId: "gpt-5.4",
|
||||
}),
|
||||
resolveEffectiveComposerThinkingOptionId(
|
||||
{
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.4",
|
||||
modeId: "",
|
||||
thinkingOptionId: "medium",
|
||||
availableModels: models,
|
||||
modeOptions: [],
|
||||
},
|
||||
"gpt-5.4",
|
||||
),
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
it("falls back to the model default thinking option", () => {
|
||||
expect(
|
||||
resolveEffectiveComposerThinkingOptionId({
|
||||
selectedThinkingOptionId: "",
|
||||
availableModels: models,
|
||||
effectiveModelId: "gpt-5.4",
|
||||
}),
|
||||
resolveEffectiveComposerThinkingOptionId(
|
||||
{
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.4",
|
||||
modeId: "",
|
||||
thinkingOptionId: "",
|
||||
availableModels: models,
|
||||
modeOptions: [],
|
||||
},
|
||||
"gpt-5.4",
|
||||
),
|
||||
).toBe("high");
|
||||
});
|
||||
});
|
||||
@@ -85,11 +105,16 @@ describe("resolveEffectiveComposerThinkingOptionId", () => {
|
||||
describe("buildDraftComposerCommandConfig", () => {
|
||||
it("returns undefined when cwd is empty", () => {
|
||||
expect(
|
||||
buildDraftComposerCommandConfig({
|
||||
provider: "codex",
|
||||
buildDraftCommandConfig({
|
||||
selection: {
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.4",
|
||||
modeId: "",
|
||||
thinkingOptionId: "",
|
||||
availableModels: [],
|
||||
modeOptions: [],
|
||||
},
|
||||
cwd: " ",
|
||||
modeOptions: [],
|
||||
selectedMode: "",
|
||||
effectiveModelId: "gpt-5.4",
|
||||
effectiveThinkingOptionId: "high",
|
||||
}),
|
||||
@@ -98,11 +123,16 @@ describe("buildDraftComposerCommandConfig", () => {
|
||||
|
||||
it("builds the draft command config from derived composer state", () => {
|
||||
expect(
|
||||
buildDraftComposerCommandConfig({
|
||||
provider: "codex",
|
||||
buildDraftCommandConfig({
|
||||
selection: {
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.4",
|
||||
modeId: "auto",
|
||||
thinkingOptionId: "high",
|
||||
availableModels: [],
|
||||
modeOptions: [{ id: "auto", label: "Auto" }],
|
||||
},
|
||||
cwd: "/repo",
|
||||
modeOptions: [{ id: "auto", label: "Auto" }],
|
||||
selectedMode: "auto",
|
||||
effectiveModelId: "gpt-5.4",
|
||||
effectiveThinkingOptionId: "high",
|
||||
}),
|
||||
|
||||
@@ -10,14 +10,17 @@ import {
|
||||
import { useDraftAgentFeatures } from "@/hooks/use-draft-agent-features";
|
||||
import {
|
||||
areAttachmentsEqual,
|
||||
buildDraftComposerCommandConfig,
|
||||
buildDraftStatusControls,
|
||||
hasDraftContent,
|
||||
resolveDraftKey,
|
||||
resolveEffectiveComposerModelId,
|
||||
resolveEffectiveComposerThinkingOptionId,
|
||||
type DraftKeyInput,
|
||||
} from "@/hooks/use-agent-input-draft-core";
|
||||
import {
|
||||
buildDraftCommandConfig,
|
||||
resolveEffectiveComposerModelId,
|
||||
resolveEffectiveComposerThinkingOptionId,
|
||||
type ProviderSelectionState,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
import { useDraftStore } from "@/stores/draft-store";
|
||||
|
||||
type AttachmentUpdater =
|
||||
@@ -199,23 +202,33 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
|
||||
formState.setWorkingDir(lockedWorkingDir);
|
||||
}, [composerOptions, formState, lockedWorkingDir]);
|
||||
|
||||
const providerSelection = useMemo<ProviderSelectionState>(
|
||||
() => ({
|
||||
provider: formState.selectedProvider,
|
||||
modelId: formState.selectedModel,
|
||||
modeId: formState.selectedMode,
|
||||
thinkingOptionId: formState.selectedThinkingOptionId,
|
||||
availableModels: formState.availableModels,
|
||||
modeOptions: formState.modeOptions,
|
||||
}),
|
||||
[
|
||||
formState.availableModels,
|
||||
formState.modeOptions,
|
||||
formState.selectedMode,
|
||||
formState.selectedModel,
|
||||
formState.selectedProvider,
|
||||
formState.selectedThinkingOptionId,
|
||||
],
|
||||
);
|
||||
|
||||
const effectiveModelId = useMemo(
|
||||
() =>
|
||||
resolveEffectiveComposerModelId({
|
||||
selectedModel: formState.selectedModel,
|
||||
availableModels: formState.availableModels,
|
||||
}),
|
||||
[formState.availableModels, formState.selectedModel],
|
||||
() => resolveEffectiveComposerModelId(providerSelection),
|
||||
[providerSelection],
|
||||
);
|
||||
|
||||
const effectiveThinkingOptionId = useMemo(
|
||||
() =>
|
||||
resolveEffectiveComposerThinkingOptionId({
|
||||
selectedThinkingOptionId: formState.selectedThinkingOptionId,
|
||||
availableModels: formState.availableModels,
|
||||
effectiveModelId,
|
||||
}),
|
||||
[effectiveModelId, formState.availableModels, formState.selectedThinkingOptionId],
|
||||
() => resolveEffectiveComposerThinkingOptionId(providerSelection, effectiveModelId),
|
||||
[effectiveModelId, providerSelection],
|
||||
);
|
||||
|
||||
const workingDir = lockedWorkingDir || formState.workingDir;
|
||||
@@ -236,11 +249,9 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
|
||||
const commandDraftConfig = useMemo(
|
||||
() =>
|
||||
composerOptions
|
||||
? buildDraftComposerCommandConfig({
|
||||
provider: formState.selectedProvider,
|
||||
? buildDraftCommandConfig({
|
||||
selection: providerSelection,
|
||||
cwd: workingDir,
|
||||
modeOptions: formState.modeOptions,
|
||||
selectedMode: formState.selectedMode,
|
||||
effectiveModelId,
|
||||
effectiveThinkingOptionId,
|
||||
featureValues: draftFeatureValues,
|
||||
@@ -251,10 +262,8 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
|
||||
effectiveModelId,
|
||||
effectiveThinkingOptionId,
|
||||
draftFeatureValues,
|
||||
providerSelection,
|
||||
workingDir,
|
||||
formState.modeOptions,
|
||||
formState.selectedMode,
|
||||
formState.selectedProvider,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -303,6 +312,7 @@ export const __private__ = {
|
||||
resolveDraftKey,
|
||||
resolveEffectiveComposerModelId,
|
||||
resolveEffectiveComposerThinkingOptionId,
|
||||
buildDraftComposerCommandConfig,
|
||||
buildDraftCommandConfig,
|
||||
buildDraftComposerCommandConfig: buildDraftCommandConfig,
|
||||
buildDraftStatusControls,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user