Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion desktop/src/features/agents/ui/AgentConfigFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -653,6 +661,7 @@ export function AgentConfigFields({
isCustomModelEditing={isCustomModelEditing}
isRequired={
showRequiredIndicators &&
!modelIsOptional &&
fallbackModel === null &&
!dependentFieldsDisabled
}
Expand Down
82 changes: 66 additions & 16 deletions desktop/src/features/agents/ui/AgentDefaultsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Comment thread
wesbillman marked this conversation as resolved.
() =>
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;
Expand All @@ -125,6 +151,12 @@ export function AgentDefaultsEditor({
setSaveError(null);
}

function handleHarnessChange(runtimeId: string) {
Comment thread
wesbillman marked this conversation as resolved.
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.
Expand Down Expand Up @@ -183,18 +215,36 @@ export function AgentDefaultsEditor({
Couldn't load agent defaults. Restart the app to try again.
</div>
) : (
<AgentConfigFields
bakedEnv={bakedEnv}
selectedRuntime={buzzAgentRuntime}
config={config}
isCustomModelEditing={isCustomModelEditing}
isCustomProvider={isCustomProvider}
onConfigChange={handleConfigChange}
onCustomModelEditingChange={setIsCustomModelEditing}
onIsCustomProviderChange={setIsCustomProvider}
onValidityChange={setConfigIsValid}
useCustomSelect
/>
<>
<div className="space-y-1.5">
<label
className="text-sm font-medium text-foreground"
htmlFor="global-agent-default-harness"
>
Default harness
</label>
<AgentDropdownSelect
id="global-agent-default-harness"
onValueChange={handleHarnessChange}
options={harnessOptions}
placeholder="Select a harness"
testId="global-agent-default-harness"
value={selectedRuntime?.id ?? ""}
/>
</div>
<AgentConfigFields
bakedEnv={bakedEnv}
selectedRuntime={selectedRuntime}
config={config}
isCustomModelEditing={isCustomModelEditing}
isCustomProvider={isCustomProvider}
onConfigChange={handleConfigChange}
onCustomModelEditingChange={setIsCustomModelEditing}
onIsCustomProviderChange={setIsCustomProvider}
onValidityChange={setConfigIsValid}
useCustomSelect
/>
</>
)}

{/* Save bar */}
Expand Down
44 changes: 44 additions & 0 deletions desktop/src/features/agents/ui/agentConfigOptions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getDefaultPersonaRuntime,
getPersonaModelOptions,
getPersonaProviderOptions,
resetConfigForHarnessChange,
runtimeSupportsLlmProviderSelection,
} from "./agentConfigOptions.tsx";
import { formatModelDiscoveryErrorStatus } from "./personaModelDiscoveryStatus.ts";
Expand Down Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion desktop/src/features/agents/ui/agentConfigOptions.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 7 additions & 22 deletions desktop/src/features/onboarding/ui/DefaultConfigStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,6 +25,7 @@ import {
type OnboardingTransitionDirection,
OnboardingSlideTransition,
} from "./OnboardingSlideTransition";
import { ONBOARDING_RUNTIME_ORDER } from "./onboardingRuntimeSelection";
import type { DefaultConfigStepActions } from "./types";

type DefaultConfigStepProps = {
Expand All @@ -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;
Expand All @@ -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)
);
});
}
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 4 additions & 16 deletions desktop/src/features/onboarding/ui/MachineOnboardingFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading
Loading