Keep hidden harness defaults in sync

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-07-27 13:25:07 +01:00
parent f07dfcfd91
commit 4b7a817eb5
8 changed files with 135 additions and 13 deletions
+3 -1
View File
@@ -91,7 +91,9 @@ with a TypeScript lookup table or an id comparison in a component.
from new choices while the raw Rust catalog remains authoritative for
installation, capabilities, and existing agent configuration. Turning a
runtime off must not uninstall it, stop it, or invalidate an existing
agent that already uses it.
agent that already uses it. If the disabled runtime was the saved global
default, consumers immediately ignore that preference and the defaults
editor persists its visible fallback on the next save.
10. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
@@ -4,6 +4,7 @@ import test from "node:test";
import {
ACP_RUNTIME_VISIBILITY_STORAGE_KEY,
filterEnabledAcpRuntimes,
maskDisabledAcpRuntimePreference,
nextDisabledAcpRuntimeIds,
parseDisabledAcpRuntimeIds,
readDisabledAcpRuntimeIds,
@@ -51,3 +52,18 @@ test("stored runtime visibility is read from the versioned device key", () => {
assert.deepEqual(readDisabledAcpRuntimeIds(storage), ["claude"]);
});
test("a disabled saved runtime is removed from the effective preference", () => {
const config = {
env_vars: {},
provider: null,
model: null,
preferred_runtime: "Goose",
};
assert.deepEqual(maskDisabledAcpRuntimePreference(config, ["goose"]), {
...config,
preferred_runtime: null,
});
assert.equal(maskDisabledAcpRuntimePreference(config, ["claude"]), config);
});
@@ -95,6 +95,28 @@ export function filterEnabledAcpRuntimes<T extends { id: string }>(
);
}
/**
* Prevent a disabled runtime from remaining the effective global preference.
*
* The persisted config is left untouched until the user next saves defaults;
* consumers immediately fall back through the normal runtime selection path.
*/
export function maskDisabledAcpRuntimePreference<
T extends { preferred_runtime: string | null },
>(config: T, disabledRuntimeIds: readonly string[]): T {
const preferredRuntime = config.preferred_runtime;
if (
!preferredRuntime ||
!disabledRuntimeIds
.map(normalizeRuntimeId)
.includes(normalizeRuntimeId(preferredRuntime))
) {
return config;
}
return { ...config, preferred_runtime: null };
}
function getDisabledRuntimeIdsSnapshot(): readonly string[] {
const storage = getLocalStorage();
if (!storage) return EMPTY_DISABLED_RUNTIME_IDS;
@@ -27,6 +27,7 @@ import {
import {
formatRuntimeOptionLabel,
getDefaultPersonaRuntime,
reconcilePreferredRuntimeFallback,
PERSONA_FIELD_CONTROL_CLASS,
PERSONA_FIELD_SHELL_CLASS,
resetConfigForHarnessChange,
@@ -154,16 +155,14 @@ export function AgentDefaultsEditor({
sortedRuntimes[0]
);
}, [config.preferred_runtime, sortedRuntimes]);
const renderedConfig = React.useMemo(
() =>
config.preferred_runtime || !selectedRuntime
? config
: { ...config, preferred_runtime: selectedRuntime.id },
[config, selectedRuntime],
);
const { data: runtimeFileConfig } = useRuntimeFileConfigQuery(
selectedRuntime?.id ?? "",
);
const effectiveConfig = React.useMemo(
() =>
reconcilePreferredRuntimeFallback(config, selectedRuntime?.id ?? null),
[config, selectedRuntime],
);
const harnessOptions = React.useMemo(
() =>
sortedRuntimes.map((runtime) => ({
@@ -187,7 +186,7 @@ export function AgentDefaultsEditor({
}
function handleHarnessChange(runtimeId: string) {
handleConfigChange(resetConfigForHarnessChange(config, runtimeId));
handleConfigChange(resetConfigForHarnessChange(effectiveConfig, runtimeId));
setConfigIsValid(false);
setIsCustomModelEditing(false);
setIsCustomProvider(false);
@@ -196,7 +195,11 @@ export function AgentDefaultsEditor({
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.
const submittedConfig = config;
const draftAtSubmit = configRef.current;
const submittedConfig = reconcilePreferredRuntimeFallback(
draftAtSubmit,
selectedRuntime?.id ?? null,
);
onSavingChange?.(true);
setSaveState("saving");
setSaveError(null);
@@ -206,7 +209,7 @@ export function AgentDefaultsEditor({
// IPC window. If the user edited, keep their newer value and leave dirty=true
// so they can save again. setDirty(false) runs inside the updater so both
// state updates batch into the same render (React 18 automatic batching).
const savedCurrentDraft = configRef.current === submittedConfig;
const savedCurrentDraft = configRef.current === draftAtSubmit;
setConfig((current) => {
if (!savedCurrentDraft) {
// Mid-flight edit detected — do not overwrite newer user input.
@@ -242,7 +245,7 @@ export function AgentDefaultsEditor({
<AgentConfigFields
bakedEnv={bakedEnv}
selectedRuntime={selectedRuntime}
config={renderedConfig}
config={effectiveConfig}
disclosure={flatLayout ? "progressive-defaults" : "full"}
isCustomModelEditing={isCustomModelEditing}
isCustomProvider={isCustomProvider}
@@ -5,6 +5,7 @@ import {
getDefaultPersonaRuntime,
getPersonaModelOptions,
getPersonaProviderOptions,
reconcilePreferredRuntimeFallback,
resetConfigForHarnessChange,
runtimeSupportsLlmProviderSelection,
} from "./agentConfigOptions.tsx";
@@ -189,6 +190,23 @@ test("resetConfigForHarnessChange does not carry relay mesh to Goose", () => {
assert.equal(resetConfigForHarnessChange(config, "goose").provider, null);
});
test("reconcilePreferredRuntimeFallback updates a hidden saved default", () => {
const config = {
env_vars: { BUZZ_AGENT_THINKING_EFFORT: "high", SHARED: "kept" },
provider: "anthropic",
model: "claude-opus",
preferred_runtime: "goose",
};
assert.deepEqual(reconcilePreferredRuntimeFallback(config, "buzz-agent"), {
env_vars: { SHARED: "kept" },
provider: "anthropic",
model: null,
preferred_runtime: "buzz-agent",
});
assert.equal(reconcilePreferredRuntimeFallback(config, "goose"), config);
});
// ── getPersonaModelOptions — codex/claude do not use global provider ──────────
//
// The discovery call in AgentDefinitionDialog passes
@@ -198,6 +198,25 @@ export function resetConfigForHarnessChange(
};
}
/**
* Align a stale or hidden saved preference with the fallback shown by a
* runtime selector, clearing values that are not portable across harnesses.
*/
export function reconcilePreferredRuntimeFallback(
config: GlobalAgentConfig,
fallbackRuntimeId: string | null,
): GlobalAgentConfig {
if (
!config.preferred_runtime ||
!fallbackRuntimeId ||
config.preferred_runtime === fallbackRuntimeId
) {
return config;
}
return resetConfigForHarnessChange(config, fallbackRuntimeId);
}
function effectiveModelProviderForOptions(
runtimeId: string,
providerId: string | null | undefined,
@@ -9,8 +9,14 @@
* On fetch error the query falls back to EMPTY_CONFIG (safe — the absence of
* a global config is never an error state for callers).
*/
import * as React from "react";
import { useQuery } from "@tanstack/react-query";
import {
maskDisabledAcpRuntimePreference,
useDisabledAcpRuntimeIds,
} from "@/features/agents/lib/runtimeVisibilityPreference";
import { getGlobalAgentConfig } from "@/shared/api/tauriGlobalAgentConfig";
import type { GlobalAgentConfig } from "@/shared/api/types";
@@ -27,6 +33,7 @@ export function useGlobalAgentConfig(): {
globalConfig: GlobalAgentConfig;
isLoading: boolean;
} {
const disabledRuntimeIds = useDisabledAcpRuntimeIds();
const { data, isPending } = useQuery({
queryKey: globalAgentConfigQueryKey,
queryFn: getGlobalAgentConfig,
@@ -36,9 +43,17 @@ export function useGlobalAgentConfig(): {
// Never show a stale empty flash while a background refetch runs.
placeholderData: EMPTY_CONFIG,
});
const globalConfig = React.useMemo(
() =>
maskDisabledAcpRuntimePreference(
data ?? EMPTY_CONFIG,
disabledRuntimeIds,
),
[data, disabledRuntimeIds],
);
return {
globalConfig: data ?? EMPTY_CONFIG,
globalConfig,
isLoading: isPending,
};
}
+27
View File
@@ -240,6 +240,12 @@ test.describe("Doctor panel state screenshots", () => {
CLAUDE_AVAILABLE_LOGGED_IN,
BUZZ_AGENT_AVAILABLE,
],
globalAgentConfig: {
env_vars: {},
provider: "anthropic",
model: "claude-opus",
preferred_runtime: "goose",
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -251,6 +257,27 @@ test.describe("Doctor panel state screenshots", () => {
await gooseToggle.click();
await expect(gooseToggle).not.toBeChecked();
const defaultHarness = page.getByTestId("global-agent-default-harness");
await expect(defaultHarness).toHaveText("Buzz Agent");
const provider = page.getByTestId("global-agent-provider");
await provider.click();
await page.getByTestId("global-agent-provider-option-openai").click();
await page.getByRole("button", { name: "Save defaults" }).click();
const savedConfig = await page.evaluate(async () =>
(
window as typeof window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload: unknown,
) => Promise<unknown>;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null),
);
expect(savedConfig).toMatchObject({
provider: "openai",
preferred_runtime: "buzz-agent",
});
await page.getByRole("button", { name: "Back to app" }).click();
await page.getByTestId("open-agents-view").click();
await page.getByTestId("new-agent-card").click();