diff --git a/desktop/src/features/agents/ui/AgentConfigFields.tsx b/desktop/src/features/agents/ui/AgentConfigFields.tsx
index 61382ea4cf..e9c795a339 100644
--- a/desktop/src/features/agents/ui/AgentConfigFields.tsx
+++ b/desktop/src/features/agents/ui/AgentConfigFields.tsx
@@ -200,8 +200,16 @@ export function AgentConfigFields({
() => getGlobalModelFallback(bakedEnv, effectiveProvider, config.env_vars),
[bakedEnv, config.env_vars, effectiveProvider],
);
+ const modelField = fieldModel.fields.find(
+ (field) => field.kind === "model" && field.render === "control",
+ );
+ // CLI-login harnesses apply this setting through ACP rather than an env var
+ // and provide their own default when no model override is persisted.
+ const modelIsOptional = modelField?.targetApplication.kind === "acpNative";
const modelIsValid =
- (config.model?.trim().length ?? 0) > 0 || fallbackModel !== null;
+ modelIsOptional ||
+ (config.model?.trim().length ?? 0) > 0 ||
+ fallbackModel !== null;
React.useEffect(() => {
onValidityChange?.(modelIsValid);
}, [modelIsValid, onValidityChange]);
@@ -653,6 +661,7 @@ export function AgentConfigFields({
isCustomModelEditing={isCustomModelEditing}
isRequired={
showRequiredIndicators &&
+ !modelIsOptional &&
fallbackModel === null &&
!dependentFieldsDisabled
}
diff --git a/desktop/src/features/agents/ui/AgentDefaultsEditor.tsx b/desktop/src/features/agents/ui/AgentDefaultsEditor.tsx
index f1056931bc..52cd01dd3a 100644
--- a/desktop/src/features/agents/ui/AgentDefaultsEditor.tsx
+++ b/desktop/src/features/agents/ui/AgentDefaultsEditor.tsx
@@ -20,6 +20,13 @@ import type { GlobalAgentConfig } from "@/shared/api/types";
import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri";
import { globalAgentConfigQueryKey } from "@/features/agents/useGlobalAgentConfig";
import { useAcpRuntimesQuery } from "@/features/agents/hooks";
+import {
+ formatRuntimeOptionLabel,
+ getDefaultPersonaRuntime,
+ resetConfigForHarnessChange,
+ sortPersonaRuntimes,
+} from "@/features/agents/ui/agentConfigOptions";
+import { AgentDropdownSelect } from "@/features/agents/ui/agentConfigControls";
import {
AgentConfigFields,
EMPTY_GLOBAL_CONFIG,
@@ -105,17 +112,36 @@ export function AgentDefaultsEditor({
});
}, []);
- // Resolve the buzz-agent runtime catalog entry for model discovery.
const runtimesQuery = useAcpRuntimesQuery();
- const buzzAgentRuntime = React.useMemo(
- () => (runtimesQuery.data ?? []).find((r) => r.id === "buzz-agent"),
+ const sortedRuntimes = React.useMemo(
+ () => sortPersonaRuntimes(runtimesQuery.data ?? []),
[runtimesQuery.data],
);
+ // A missing/stale preference displays the same effective fallback the backend
+ // would use; it is persisted only after the user edits and saves this form.
+ // Keep persona ordering here so this shared editor matches agent dialogs.
+ const selectedRuntime = React.useMemo(
+ () =>
+ sortedRuntimes.find(
+ (runtime) => runtime.id === config.preferred_runtime,
+ ) ??
+ getDefaultPersonaRuntime(sortedRuntimes) ??
+ sortedRuntimes[0],
+ [config.preferred_runtime, sortedRuntimes],
+ );
+ const harnessOptions = React.useMemo(
+ () =>
+ sortedRuntimes.map((runtime) => ({
+ label: formatRuntimeOptionLabel(runtime),
+ value: runtime.id,
+ })),
+ [sortedRuntimes],
+ );
const configSurfaceLoading = isLoading || runtimesQuery.isLoading;
const configSurfaceError =
loadError ||
runtimesQuery.isError ||
- (!configSurfaceLoading && buzzAgentRuntime === undefined);
+ (!configSurfaceLoading && selectedRuntime === undefined);
function handleConfigChange(next: GlobalAgentConfig) {
configRef.current = next;
@@ -125,6 +151,12 @@ export function AgentDefaultsEditor({
setSaveError(null);
}
+ function handleHarnessChange(runtimeId: string) {
+ handleConfigChange(resetConfigForHarnessChange(config, runtimeId));
+ setIsCustomModelEditing(false);
+ setIsCustomProvider(false);
+ }
+
async function handleSave() {
// Snapshot the config being submitted so we can detect edits that arrive
// during the IPC round-trip and avoid clobbering the user's newer input.
@@ -183,18 +215,36 @@ export function AgentDefaultsEditor({
Couldn't load agent defaults. Restart the app to try again.
) : (
-
+ <>
+
+
+
+
+
+ >
)}
{/* Save bar */}
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
index f1c05908e3..d0690cefc3 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
+++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs
@@ -5,6 +5,7 @@ import {
getDefaultPersonaRuntime,
getPersonaModelOptions,
getPersonaProviderOptions,
+ resetConfigForHarnessChange,
runtimeSupportsLlmProviderSelection,
} from "./agentConfigOptions.tsx";
import { formatModelDiscoveryErrorStatus } from "./personaModelDiscoveryStatus.ts";
@@ -145,6 +146,49 @@ test("runtimeSupportsLlmProviderSelection is false for codex and claude", () =>
assert.equal(runtimeSupportsLlmProviderSelection("claude"), false);
});
+test("resetConfigForHarnessChange clears harness-specific values", () => {
+ const config = {
+ env_vars: { BUZZ_AGENT_THINKING_EFFORT: "high", KEEP_ME: "yes" },
+ model: "claude-opus",
+ preferred_runtime: "buzz-agent",
+ provider: "anthropic",
+ };
+
+ assert.deepEqual(resetConfigForHarnessChange(config, "claude"), {
+ env_vars: { KEEP_ME: "yes" },
+ model: null,
+ preferred_runtime: "claude",
+ provider: null,
+ });
+});
+
+test("resetConfigForHarnessChange preserves compatible provider selection", () => {
+ const config = {
+ env_vars: { KEEP_ME: "yes" },
+ model: "old-model",
+ preferred_runtime: "claude",
+ provider: "anthropic",
+ };
+
+ assert.deepEqual(resetConfigForHarnessChange(config, "goose"), {
+ env_vars: { KEEP_ME: "yes" },
+ model: null,
+ preferred_runtime: "goose",
+ provider: "anthropic",
+ });
+});
+
+test("resetConfigForHarnessChange does not carry relay mesh to Goose", () => {
+ const config = {
+ env_vars: {},
+ model: "auto",
+ preferred_runtime: "buzz-agent",
+ provider: "relay-mesh",
+ };
+
+ assert.equal(resetConfigForHarnessChange(config, "goose").provider, null);
+});
+
// ── getPersonaModelOptions — codex/claude do not use global provider ──────────
//
// The discovery call in AgentDefinitionDialog passes
diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx
index 5272f559bc..6ae81ff6cb 100644
--- a/desktop/src/features/agents/ui/agentConfigOptions.tsx
+++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx
@@ -1,4 +1,8 @@
-import type { AcpRuntimeCatalogEntry } from "@/shared/api/types";
+import type {
+ AcpRuntimeCatalogEntry,
+ GlobalAgentConfig,
+} from "@/shared/api/types";
+import { BUZZ_AGENT_THINKING_EFFORT } from "./buzzAgentConfig";
import type { RuntimeFileConfigSubset } from "@/shared/api/tauri";
// Dialogs import getDefaultPersonaRuntime via this re-export; lib code imports
// directly from lib/resolvePersonaRuntime.
@@ -173,6 +177,27 @@ export function runtimeSupportsLlmProviderSelection(runtimeId: string) {
return runtimeId === "buzz-agent" || runtimeId === "goose";
}
+/** Clears values whose meaning or support changes with the selected harness. */
+export function resetConfigForHarnessChange(
+ config: GlobalAgentConfig,
+ runtimeId: string,
+): GlobalAgentConfig {
+ const nextEnvVars = { ...config.env_vars };
+ delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
+
+ return {
+ ...config,
+ env_vars: nextEnvVars,
+ model: null,
+ preferred_runtime: runtimeId || null,
+ provider:
+ runtimeSupportsLlmProviderSelection(runtimeId) &&
+ config.provider !== "relay-mesh"
+ ? config.provider
+ : null,
+ };
+}
+
function effectiveModelProviderForOptions(
runtimeId: string,
providerId: string | null | undefined,
diff --git a/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx b/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx
index 23455f47c9..68a01f053d 100644
--- a/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx
+++ b/desktop/src/features/onboarding/ui/DefaultConfigStep.tsx
@@ -5,8 +5,7 @@ import {
AgentConfigFields,
EMPTY_GLOBAL_CONFIG,
} from "@/features/agents/ui/AgentConfigFields";
-import { BUZZ_AGENT_THINKING_EFFORT } from "@/features/agents/ui/buzzAgentConfig";
-import { runtimeSupportsLlmProviderSelection } from "@/features/agents/ui/agentConfigOptions";
+import { resetConfigForHarnessChange } from "@/features/agents/ui/agentConfigOptions";
import { AgentDropdownSelect } from "@/features/agents/ui/agentConfigControls";
import { createSaveCoalescer } from "./saveCoalescer";
import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri";
@@ -26,6 +25,7 @@ import {
type OnboardingTransitionDirection,
OnboardingSlideTransition,
} from "./OnboardingSlideTransition";
+import { ONBOARDING_RUNTIME_ORDER } from "./onboardingRuntimeSelection";
import type { DefaultConfigStepActions } from "./types";
type DefaultConfigStepProps = {
@@ -34,8 +34,6 @@ type DefaultConfigStepProps = {
selectedRuntimeIds: readonly string[];
};
-const RUNTIME_ORDER = ["claude", "codex", "goose", "buzz-agent"];
-
function formatHarnessLabel(runtime: AcpRuntimeCatalogEntry | undefined) {
if (!runtime) return "Select a harness";
return runtime.id === "buzz-agent" ? "Buzz" : runtime.label;
@@ -49,11 +47,11 @@ function sortSelectedRuntimes(
return runtimes
.filter((runtime) => selectedRuntimeIdSet.has(runtime.id))
.sort((left, right) => {
- const leftIndex = RUNTIME_ORDER.indexOf(left.id);
- const rightIndex = RUNTIME_ORDER.indexOf(right.id);
+ const leftIndex = ONBOARDING_RUNTIME_ORDER.indexOf(left.id);
+ const rightIndex = ONBOARDING_RUNTIME_ORDER.indexOf(right.id);
return (
- (leftIndex === -1 ? RUNTIME_ORDER.length : leftIndex) -
- (rightIndex === -1 ? RUNTIME_ORDER.length : rightIndex)
+ (leftIndex === -1 ? ONBOARDING_RUNTIME_ORDER.length : leftIndex) -
+ (rightIndex === -1 ? ONBOARDING_RUNTIME_ORDER.length : rightIndex)
);
});
}
@@ -146,20 +144,7 @@ function AgentDefaultsSection({
const handleHarnessChange = React.useCallback(
(runtimeId: string) => {
- const nextEnvVars = { ...config.env_vars };
- delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
- const nextProvider =
- runtimeSupportsLlmProviderSelection(runtimeId) &&
- config.provider !== "relay-mesh"
- ? config.provider
- : null;
- const next = {
- ...config,
- env_vars: nextEnvVars,
- model: null,
- preferred_runtime: runtimeId || null,
- provider: nextProvider,
- };
+ const next = resetConfigForHarnessChange(config, runtimeId);
setIsCustomModelEditing(false);
setIsCustomProvider(false);
setConfig(next);
diff --git a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx
index 2d040ff348..dfd4e03045 100644
--- a/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx
+++ b/desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx
@@ -10,8 +10,7 @@ import {
importIdentity,
persistCurrentIdentity,
} from "@/shared/api/tauriIdentity";
-import { runtimeSupportsLlmProviderSelection } from "@/features/agents/ui/agentConfigOptions";
-import { BUZZ_AGENT_THINKING_EFFORT } from "@/features/agents/ui/buzzAgentConfig";
+import { resetConfigForHarnessChange } from "@/features/agents/ui/agentConfigOptions";
import { Button } from "@/shared/ui/button";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { BackupStep } from "./BackupStep";
@@ -94,20 +93,9 @@ export function MachineOnboardingFlow({
return;
}
- const nextEnvVars = { ...current.env_vars };
- delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT];
- await setGlobalAgentConfig({
- ...current,
- env_vars: nextEnvVars,
- model: null,
- preferred_runtime: preferredRuntimeId,
- provider:
- preferredRuntimeId &&
- runtimeSupportsLlmProviderSelection(preferredRuntimeId) &&
- current.provider !== "relay-mesh"
- ? current.provider
- : null,
- });
+ await setGlobalAgentConfig(
+ resetConfigForHarnessChange(current, preferredRuntimeId ?? ""),
+ );
});
runtimeSaveChain.current = save.then(
() => undefined,
diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs
index 2910d64905..046cca93a0 100644
--- a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs
+++ b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs
@@ -101,14 +101,29 @@ test("provider-backed selections drive the default model config step", () => {
getDefaultModelConfigRuntimeId(["claude", "codex", "goose", "buzz-agent"]),
"buzz-agent",
);
+});
+
+test("onboarding display order drives the preferred runtime", () => {
assert.equal(
- getPreferredRuntimeIdForSelection(["claude", "codex", "goose"]),
- "goose",
+ getPreferredRuntimeIdForSelection([
+ "buzz-agent",
+ "goose",
+ "codex",
+ "claude",
+ ]),
+ "claude",
);
assert.equal(
- getPreferredRuntimeIdForSelection(["claude", "codex"]),
- "claude",
+ getPreferredRuntimeIdForSelection(["buzz-agent", "goose", "codex"]),
+ "codex",
+ );
+ assert.equal(
+ getPreferredRuntimeIdForSelection(["buzz-agent", "goose"]),
+ "goose",
);
+ assert.equal(getPreferredRuntimeIdForSelection(["buzz-agent"]), "buzz-agent");
+ assert.equal(getPreferredRuntimeIdForSelection(["custom"]), "custom");
+ assert.equal(getPreferredRuntimeIdForSelection([]), null);
});
test("any harness selection drives the defaults step", () => {
diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts
index 2b77e9a51b..3ff78e9b5f 100644
--- a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts
+++ b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts
@@ -1,11 +1,13 @@
import type { AcpRuntimeCatalogEntry } from "@/shared/api/types";
-const KNOWN_ONBOARDING_RUNTIME_IDS = new Set([
- "buzz-agent",
+export const ONBOARDING_RUNTIME_ORDER = [
"claude",
"codex",
"goose",
-]);
+ "buzz-agent",
+];
+
+const KNOWN_ONBOARDING_RUNTIME_IDS = new Set(ONBOARDING_RUNTIME_ORDER);
export function runtimeUsesDefaultModelConfig(runtimeId: string) {
return runtimeId === "buzz-agent" || runtimeId === "goose";
@@ -22,7 +24,14 @@ export function getDefaultModelConfigRuntimeId(runtimeIds: readonly string[]) {
export function getPreferredRuntimeIdForSelection(
runtimeIds: readonly string[],
) {
- return getDefaultModelConfigRuntimeId(runtimeIds) ?? runtimeIds[0] ?? null;
+ const selectedRuntimeIds = new Set(runtimeIds);
+ return (
+ ONBOARDING_RUNTIME_ORDER.find((runtimeId) =>
+ selectedRuntimeIds.has(runtimeId),
+ ) ??
+ runtimeIds[0] ??
+ null
+ );
}
export function runtimeSelectionNeedsDefaultModelConfig(
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 50985cc6c1..7282b74002 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -6696,6 +6696,8 @@ function withMockRuntimeConfigMetadata(
): RawAcpRuntimeCatalogEntry {
return {
...runtime,
+ node_required: runtime.node_required ?? false,
+ auth_status: runtime.auth_status ?? { status: "unknown" },
model_env_var:
"model_env_var" in runtime
? runtime.model_env_var
diff --git a/desktop/tests/e2e/global-agent-config-screenshots.spec.ts b/desktop/tests/e2e/global-agent-config-screenshots.spec.ts
index da4214058e..59d6759e3d 100644
--- a/desktop/tests/e2e/global-agent-config-screenshots.spec.ts
+++ b/desktop/tests/e2e/global-agent-config-screenshots.spec.ts
@@ -198,6 +198,61 @@ test.describe("global agent config screenshots", () => {
});
});
+ test("settings renders and saves the persisted preferred harness", async ({
+ page,
+ }) => {
+ await installMockBridge(page, {
+ globalAgentConfig: {
+ preferred_runtime: "claude",
+ provider: null,
+ model: null,
+ env_vars: {},
+ },
+ acpRuntimesCatalog: [...CATALOG_WITH_CLAUDE, CATALOG_WITH_CODEX[1]],
+ });
+
+ await openAiDefaultsSettings(page);
+
+ const harness = page.getByTestId("global-agent-default-harness");
+ await expect(harness).toHaveText("Claude Code");
+ await expect(page.getByText("Provider", { exact: true })).toHaveCount(0);
+ await expect(page.locator("#global-agent-model")).toBeVisible();
+
+ // Make the form dirty, then return to Claude with no model override. The
+ // harness-native default keeps the now-actionable Save button enabled.
+ await harness.press("Enter");
+ await page.getByTestId("global-agent-default-harness-option-codex").click();
+ await harness.press("Enter");
+ await page
+ .getByTestId("global-agent-default-harness-option-claude")
+ .click();
+ await expect(page.getByTestId("global-agent-model")).toHaveText(
+ /Default model/,
+ );
+ await expect(
+ page.getByRole("button", { name: "Save defaults" }),
+ ).toBeEnabled();
+
+ await harness.press("Enter");
+ await page.getByTestId("global-agent-default-harness-option-codex").click();
+ const model = page.getByTestId("global-agent-model");
+ await model.click();
+ await page.getByTestId("global-agent-model-option-gpt-5.5[high]").click();
+ await page.getByRole("button", { name: "Save defaults" }).click();
+
+ const saved = await page.evaluate(async () =>
+ (
+ window as typeof window & {
+ __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
+ command: string,
+ payload: unknown,
+ ) => Promise;
+ }
+ ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null),
+ );
+ expect(saved).toMatchObject({ preferred_runtime: "codex" });
+ });
+
test("02-create-global-provider-shows-top-level-api-key", async ({
page,
}) => {
diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
index d56d8f83a7..4d1a293ecf 100644
--- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
+++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
@@ -119,7 +119,7 @@ test("requires a runtime selection and routes Buzz Agent to config", async ({
expect(await readSavedRuntime(page)).toBe("buzz-agent");
});
-test("rapid harness toggles serialize the persisted preferred runtime", async ({
+test("rapid harness toggles serialize the prioritized preferred runtime", async ({
page,
}) => {
await installMockBridge(
@@ -143,7 +143,7 @@ test("rapid harness toggles serialize the persisted preferred runtime", async ({
await page.waitForTimeout(300);
await expect(next).toBeDisabled();
await expect(next).toBeEnabled({ timeout: 700 });
- expect(await readSavedRuntime(page)).toBe("buzz-agent");
+ expect(await readSavedRuntime(page)).toBe("goose");
});
test("authenticated Claude saves the selected runtime and routes to defaults", async ({
@@ -557,7 +557,7 @@ test("changing setup-page harness clears an incompatible saved model", async ({
await expect.poll(() => readSavedRuntime(page)).toBe("claude");
});
-test("selecting all harnesses routes defaults through Buzz Agent", async ({
+test("selecting all harnesses prioritizes Claude for defaults", async ({
page,
}) => {
await installMockBridge(
@@ -592,10 +592,10 @@ test("selecting all harnesses routes defaults through Buzz Agent", async ({
await expect(page.getByTestId("onboarding-setup-next")).toBeEnabled();
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
- expect(await readSavedRuntime(page)).toBe("buzz-agent");
+ expect(await readSavedRuntime(page)).toBe("claude");
const harnessSelect = page.getByTestId("global-agent-default-harness");
- await expect(harnessSelect).toHaveText("Buzz");
+ await expect(harnessSelect).toHaveText("Claude");
await harnessSelect.click();
await expect(
page.getByTestId("global-agent-default-harness-option-claude"),
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index 0fd8338ce0..25a93820f8 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -369,6 +369,7 @@ type MockBridgeOptions = {
env_vars: Record;
provider: string | null;
model: string | null;
+ preferred_runtime?: string | null;
};
bakedBuildEnv?: Array<{
key: string;