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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export default defineConfig({
"**/global-agent-config-screenshots.spec.ts",
"**/doctor-states.spec.ts",
"**/agent-lifecycle-feedback.spec.ts",
"**/agent-provider-dropdowns.spec.ts",
],
use: {
...devices["Desktop Chrome"],
Expand Down
9 changes: 8 additions & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,14 @@ const overrides = new Map([
// +23 rebase onto #1667: behavioral quad fields (respond_to/parallelism/toolsets)
// plumbed through AgentInstanceEditDialog from PersonaAdvancedFields.
// +2 provider-aware effort: model/provider props threaded to BuzzAgentModelTuningFields.
["src/features/agents/ui/AgentInstanceEditDialog.tsx", 1165],
// +15 provider/model dropdown fixes: useBakedBuildEnvKeysQuery + hideProviderIds
// for Databricks v1 gate; prospectiveRuntimeId default fallback for builtins.
["src/features/agents/ui/AgentInstanceEditDialog.tsx", 1180],
// AgentDefinitionDialog grew past 1000 with the following load-bearing fixes:
// isRuntimeAutoSeededRef tracking for edit-mode seeding (Fizz shows models);
// runtimeSupportsLlmProviderSelection guard on discovery provider (codex fix);
// hideProviderIds computation for Databricks v1 gate. Queued to split.
["src/features/agents/ui/AgentDefinitionDialog.tsx", 1035],
]);

await runFileSizeCheck({
Expand Down
38 changes: 0 additions & 38 deletions desktop/src/features/agents/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import type {
} from "@/shared/api/types";
import type {
AttachManagedAgentToChannelInput,
AttachManagedAgentToChannelResult,
CreateChannelManagedAgentInput,
CreateChannelManagedAgentsResult,
CreateChannelManagedAgentResult,
Expand All @@ -84,10 +83,6 @@ export const acpRuntimesQueryKey = ["acp-runtimes"] as const;
export const managedAgentPrereqsQueryKey = ["managed-agent-prereqs"] as const;
export const backendProvidersQueryKey = ["backend-providers"] as const;

export type EnsureGooseInChannelResult = AttachManagedAgentToChannelResult & {
created: boolean;
};

async function invalidateAgentQueries(
queryClient: ReturnType<typeof useQueryClient>,
channelId: string | null,
Expand Down Expand Up @@ -590,39 +585,6 @@ export function useCreateChannelManagedAgentsMutation(
});
}

export function useEnsureGooseInChannelMutation(channelId: string | null) {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (): Promise<EnsureGooseInChannelResult> => {
if (!channelId) {
throw new Error("No channel selected.");
}

const attached = await ensureChannelAgentPresetInChannel(channelId, {
runtime: {
id: "goose",
label: "Goose",
command: "goose",
defaultArgs: ["acp"],
mcpCommand: null,
},
role: "bot",
});

return {
agent: attached.agent,
membershipAdded: attached.membershipAdded,
started: attached.started,
created: attached.created,
};
},
onSettled: () => {
invalidateAgentQueriesInBackground(queryClient, channelId);
},
});
}

export function useExportPersonaJsonMutation() {
return useMutation({
mutationFn: (id: string) => exportPersonaToJson(id),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ const claudeRuntime = {
mcpCommand: null,
};

const buzzAgentRuntime = {
...gooseRuntime,
id: "buzz-agent",
label: "Buzz Agent",
command: "buzz-agent-cmd",
mcpCommand: null,
};

function persona(overrides = {}) {
return {
id: "p-1",
Expand Down Expand Up @@ -346,3 +354,40 @@ test("row 6: unfetched query refetches instead of resolving empty", async () =>
"an unfetched query must fetch, not spuriously report no runtimes",
);
});

// ── item-13 regression: buzz-agent-first default runtime ─────────────────────
//
// Before this fix, resolveStartRuntimeForDefinition used runtimes[0] (catalog
// order: goose, claude, codex, buzz-agent), so an installed goose would beat
// the bundled buzz-agent sidecar as the default for runtime-less personas.
// The fix applies the preference order: buzz-agent → goose → first available.

test("item-13: goose+buzz-agent both available — persona with no runtime resolves buzz-agent", () => {
const { runtime, warnings } = resolveStartRuntimeForDefinition(
persona({ runtime: undefined }),
[gooseRuntime, claudeRuntime, buzzAgentRuntime],
);
assert.equal(
runtime.id,
"buzz-agent",
"buzz-agent must win over catalog-first goose for runtime-less personas",
);
assert.deepEqual(warnings, []);
});

test("item-13: goose-only available — persona with no runtime resolves goose", () => {
const { runtime, warnings } = resolveStartRuntimeForDefinition(
persona({ runtime: undefined }),
[gooseRuntime, claudeRuntime],
);
assert.equal(runtime.id, "goose");
assert.deepEqual(warnings, []);
});

test("item-13: no runtimes available — refuses with actionable error", () => {
assert.throws(
() => resolveStartRuntimeForDefinition(persona({ runtime: undefined }), []),
/No available runtime/,
"empty runtime list must throw, not silently return null",
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import type { MeshAgentPresetPatch } from "@/features/mesh-compute/applyMeshAgentPreset";
import type { MeshServeTarget } from "@/shared/api/tauriMesh";
import {
getDefaultPersonaRuntime,
resolvePersonaRuntime,
type ResolvePersonaRuntimeResult,
} from "./resolvePersonaRuntime";
Expand Down Expand Up @@ -47,7 +48,10 @@ export function resolveStartRuntimeForDefinition(
persona: AgentPersona,
runtimes: readonly AcpRuntime[],
): { runtime: AcpRuntime; warnings: string[] } {
const defaultRuntime = runtimes[0] ?? null;
// Use the buzz-agent-first preference (buzz-agent → goose → first available)
// so a freshly installed goose never beats the bundled buzz-agent sidecar
// for runtime-less personas (item 13 regression guard).
const defaultRuntime = getDefaultPersonaRuntime(runtimes);
const { runtime, warnings, isOverridden }: ResolvePersonaRuntimeResult =
resolvePersonaRuntime(persona.runtime, runtimes, defaultRuntime);

Expand Down
26 changes: 25 additions & 1 deletion desktop/src/features/agents/lib/resolvePersonaRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
import type { AcpRuntime } from "@/shared/api/types";
import type { AcpRuntime, AcpRuntimeCatalogEntry } from "@/shared/api/types";

/**
* Select the best default runtime from a catalog, using the same preference
* order as the UI picker: buzz-agent first (bundled sidecar), then goose,
* then the first available entry, then null when nothing is available.
*
* Generic so that passing AcpRuntime[] (the already-filtered start-path
* list) returns AcpRuntime | null while passing AcpRuntimeCatalogEntry[]
* (the full catalog) returns AcpRuntimeCatalogEntry | null. Both call sites
* share one preference-order implementation.
*/
export function getDefaultPersonaRuntime<T extends AcpRuntimeCatalogEntry>(
runtimes: readonly T[],
): T | null {
const available = runtimes.filter(
(runtime) => runtime.availability === "available",
);
return (
available.find((runtime) => runtime.id === "buzz-agent") ??
available.find((runtime) => runtime.id === "goose") ??
available[0] ??
null
);
}

/**
* Result of resolving a persona's preferred runtime against the set of
Expand Down
13 changes: 8 additions & 5 deletions desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/features/agents/lib/teamPersonas";
import {
collectRuntimeWarnings,
getDefaultPersonaRuntime,
resolvePersonaRuntime,
} from "@/features/agents/lib/resolvePersonaRuntime";
import { useChannelsQuery } from "@/features/channels/hooks";
Expand Down Expand Up @@ -65,8 +66,10 @@ export function AddTeamToChannelDialog({
[channelsQuery.data],
);

const providers = providersQuery.data ?? [];
const defaultProvider = providers[0] ?? null;
const runtimes = providersQuery.data ?? [];
// Use the buzz-agent-first preference so the team-deploy fallback mirrors the
// single-agent start path (buzz-agent → goose → first available).
const defaultProvider = getDefaultPersonaRuntime(runtimes);

const teamPersonaResolution = React.useMemo(
() =>
Expand All @@ -80,8 +83,8 @@ export function AddTeamToChannelDialog({
// This dialog has no runtime selector, so the fallback is always
// `defaultProvider` (the first available runtime).
const runtimeWarnings = React.useMemo(
() => collectRuntimeWarnings(resolved, providers, defaultProvider),
[resolved, providers, defaultProvider],
() => collectRuntimeWarnings(resolved, runtimes, defaultProvider),
[resolved, runtimes, defaultProvider],
);

function reset() {
Expand Down Expand Up @@ -122,7 +125,7 @@ export function AddTeamToChannelDialog({
const inputs = resolved.map((persona) => {
const { runtime: personaRuntime } = resolvePersonaRuntime(
persona.runtime,
providers,
runtimes,
defaultProvider,
);
const runtimeToUse = personaRuntime ?? defaultProvider;
Expand Down
Loading
Loading