diff --git a/ui/src/components/AgentConfigForm.render.test.tsx b/ui/src/components/AgentConfigForm.render.test.tsx
index 2c4d24cc6f..1005860c20 100644
--- a/ui/src/components/AgentConfigForm.render.test.tsx
+++ b/ui/src/components/AgentConfigForm.render.test.tsx
@@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Agent, Environment } from "@paperclipai/shared";
import { TooltipProvider } from "@/components/ui/tooltip";
import { AgentConfigForm } from "./AgentConfigForm";
+import { defaultCreateValues } from "./agent-config-defaults";
const mockAgentsApi = vi.hoisted(() => ({
adapterModelProfiles: vi.fn(),
@@ -68,7 +69,9 @@ vi.mock("../adapters", () => ({
adapterType === "hermes_gateway"
?
Hermes Gateway fields
: null,
- buildAdapterConfig: () => ({}),
+ buildAdapterConfig: (values: { model?: string }) => ({
+ model: values.model || undefined,
+ }),
parseStdoutLine: () => [],
}),
}));
@@ -223,6 +226,51 @@ async function renderForm(
return { container, root };
}
+async function renderCreateForm(
+ environments: Environment[],
+ valueOverrides: Partial = {},
+ options: { showAdapterTestEnvironmentButton?: boolean } = {},
+) {
+ mockEnvironmentsApi.list.mockResolvedValue(environments);
+
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ const values = {
+ ...defaultCreateValues,
+ adapterType: "codex_local",
+ ...valueOverrides,
+ };
+ const onChange = vi.fn();
+
+ await act(async () => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+
+ await flushReact();
+ return { container, root, onChange };
+}
+
describe("AgentConfigForm environment selector", () => {
let roots: Root[] = [];
@@ -395,6 +443,87 @@ describe("AgentConfigForm environment selector", () => {
});
});
+ it("tests a Codex agent after clearing the primary model to the adapter default", async () => {
+ const result = await renderForm([
+ makeEnvironment({ id: "local-1", name: "Local", driver: "local" }),
+ ], {
+ adapterConfig: { model: "gpt-5.4" },
+ }, {
+ showAdapterTestEnvironmentButton: true,
+ });
+ roots.push(result.root);
+
+ const modelButton = Array.from(result.container.querySelectorAll("button")).find(
+ (button) => button.textContent?.trim() === "gpt-5.4",
+ );
+ expect(modelButton).toBeTruthy();
+
+ await act(async () => {
+ modelButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ await flushReact();
+
+ const defaultButton = Array.from(document.body.querySelectorAll("button")).find(
+ (button) => button.textContent?.trim() === "Default",
+ );
+ expect(defaultButton).toBeTruthy();
+
+ await act(async () => {
+ defaultButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ await flushReact();
+
+ const testButton = Array.from(result.container.querySelectorAll("button")).find(
+ (button) => button.textContent?.trim() === "Test",
+ );
+ expect(testButton).toBeTruthy();
+
+ await act(async () => {
+ testButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ await flushReact();
+
+ expect(mockAgentsApi.testEnvironment).toHaveBeenCalledTimes(1);
+ expect(mockAgentsApi.testEnvironment.mock.calls[0]?.[2]).toMatchObject({
+ adapterConfig: {},
+ });
+ const adapterConfig = (mockAgentsApi.testEnvironment.mock.calls[0]?.[2] as {
+ adapterConfig: Record;
+ }).adapterConfig;
+ expect(adapterConfig).not.toHaveProperty("model");
+ expect(result.container.textContent).not.toContain("Cannot read properties of undefined");
+ });
+
+ it("omits undefined adapter config entries when testing a create form with the default model", async () => {
+ const result = await renderCreateForm([
+ makeEnvironment({ id: "local-1", name: "Local", driver: "local" }),
+ ], {
+ model: "",
+ }, {
+ showAdapterTestEnvironmentButton: true,
+ });
+ roots.push(result.root);
+
+ const testButton = Array.from(result.container.querySelectorAll("button")).find(
+ (button) => button.textContent?.trim() === "Test",
+ );
+ expect(testButton).toBeTruthy();
+
+ await act(async () => {
+ testButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ await flushReact();
+
+ expect(mockAgentsApi.testEnvironment).toHaveBeenCalledTimes(1);
+ expect(mockAgentsApi.testEnvironment.mock.calls[0]?.[2]).toMatchObject({
+ adapterConfig: {},
+ });
+ const adapterConfig = (mockAgentsApi.testEnvironment.mock.calls[0]?.[2] as {
+ adapterConfig: Record;
+ }).adapterConfig;
+ expect(adapterConfig).not.toHaveProperty("model");
+ });
+
it("flushes pending environment variable edits before testing adapter config", async () => {
const result = await renderForm([
makeEnvironment({ id: "local-1", name: "Local", driver: "local" }),
diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx
index 5b00190ff7..d30af2b4a8 100644
--- a/ui/src/components/AgentConfigForm.tsx
+++ b/ui/src/components/AgentConfigForm.tsx
@@ -55,7 +55,7 @@ import { shouldShowLegacyWorkingDirectoryField } from "../lib/legacy-agent-confi
import { listAdapterOptions, listVisibleAdapterTypes } from "../adapters/metadata";
import { getAdapterDisplay, getAdapterLabel } from "../adapters/adapter-display-registry";
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
-import { buildAgentUpdatePatch, type AgentConfigOverlay } from "../lib/agent-config-patch";
+import { buildAgentUpdatePatch, omitUndefinedEntries, type AgentConfigOverlay } from "../lib/agent-config-patch";
import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities";
import { resolveForcedKubernetesEnvironment } from "../lib/forced-kubernetes-environment";
@@ -195,7 +195,6 @@ function clampDelayMsFromSeconds(value: number) {
return clampInteger(value, 0, MAX_TURN_CONTINUATION_MAX_DELAY_SEC) * 1000;
}
-
/* ---- Form ---- */
export function AgentConfigForm(props: AgentConfigFormProps) {
@@ -538,14 +537,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
if (adapterConfigPatch) {
Object.assign(next, adapterConfigPatch);
}
- return next;
+ return omitUndefinedEntries(next);
}
const base = config as Record;
const next = { ...base, ...overlay.adapterConfig };
if (adapterConfigPatch) {
Object.assign(next, adapterConfigPatch);
}
- return next;
+ return omitUndefinedEntries(next);
}
function buildCheapAdapterConfigForTest(adapterConfigPatch?: Record): Record {
@@ -747,9 +746,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}, [props.onTestFeedbackChange, testActionError, testEnvironment.data, testEnvironment.error]);
// Current model for display
- const currentModelId = isCreate
- ? val!.model
+ const currentModelValue = isCreate
+ ? val!.model ?? ""
: eff("adapterConfig", "model", String(config.model ?? ""));
+ const currentModelId = typeof currentModelValue === "string" ? currentModelValue : "";
async function handleRefreshModels() {
if (!selectedCompanyId) return;
diff --git a/ui/src/lib/agent-config-patch.ts b/ui/src/lib/agent-config-patch.ts
index 9817650738..cc4d4f1e30 100644
--- a/ui/src/lib/agent-config-patch.ts
+++ b/ui/src/lib/agent-config-patch.ts
@@ -19,7 +19,7 @@ export interface AgentConfigOverlay {
modelProfiles?: { cheap?: AgentModelProfileOverlay };
}
-function omitUndefinedEntries(value: Record) {
+export function omitUndefinedEntries(value: Record) {
return Object.fromEntries(
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined),
);