diff --git a/packages/app/src/context/local-agent.test.ts b/packages/app/src/context/local-agent.test.ts new file mode 100644 index 0000000000..c7a95f9a5b --- /dev/null +++ b/packages/app/src/context/local-agent.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import { hasCustomAgent, resolveAgent } from "./local-agent" + +describe("hasCustomAgent", () => { + test("detects explicitly custom agents", () => { + expect(hasCustomAgent([{ native: true }, { native: false }])).toBe(true) + }) + + test("ignores built-in and unclassified agents", () => { + expect(hasCustomAgent([{ native: true }, {}])).toBe(false) + }) +}) + +describe("resolveAgent", () => { + const agents = [{ name: "plan" }, { name: "build" }, { name: "custom" }] + + test("uses the requested available agent", () => { + expect(resolveAgent(agents, "custom")?.name).toBe("custom") + }) + + test("defaults to build", () => { + expect(resolveAgent(agents)?.name).toBe("build") + expect(resolveAgent(agents, "missing")?.name).toBe("build") + }) + + test("uses the first agent when build is unavailable", () => { + expect(resolveAgent([{ name: "custom" }], "missing")?.name).toBe("custom") + }) +}) diff --git a/packages/app/src/context/local-agent.ts b/packages/app/src/context/local-agent.ts new file mode 100644 index 0000000000..f5c76d1469 --- /dev/null +++ b/packages/app/src/context/local-agent.ts @@ -0,0 +1,7 @@ +export function hasCustomAgent(items: Array<{ native?: boolean }>) { + return items.some((item) => item.native === false) +} + +export function resolveAgent(items: T[], name?: string) { + return items.find((item) => item.name === name) ?? items.find((item) => item.name === "build") ?? items[0] +} diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index a12fd832fd..83db571385 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -4,8 +4,10 @@ import { useParams } from "@solidjs/router" import { batch, createEffect, createMemo, startTransition } from "solid-js" import { createStore } from "solid-js/store" import { useModels } from "@/context/models" +import { useSettings } from "@/context/settings" import { useProviders } from "@/hooks/use-providers" import { Persist, persisted } from "@/utils/persist" +import { hasCustomAgent, resolveAgent } from "./local-agent" import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant" import { useSDK } from "./sdk" import { useSync } from "./sync" @@ -62,9 +64,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const serverSDK = useServerSDK() const providers = useProviders(() => sdk().directory) const models = useModels() + const settings = useSettings() const id = createMemo(() => params.id || undefined) const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden)) + const agentsVisible = createMemo(() => settings.visibility.customAgents() || hasCustomAgent(list())) const connected = createMemo(() => new Set(providers.connected().map((item) => item.id))) const [saved, setSaved, , savedReady] = persisted( @@ -107,9 +111,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } const pickAgent = (name: string | undefined) => { - const items = list() - if (items.length === 0) return - return items.find((item) => item.name === name) ?? items[0] + return resolveAgent(list(), name) } createEffect(() => { @@ -180,8 +182,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const agent = { list, + visible: agentsVisible, current() { - return pickAgent(scope()?.agent ?? store.current) + return pickAgent(agentsVisible() ? (scope()?.agent ?? store.current) : "build") }, set(name: string | undefined) { const item = pickAgent(name) diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index 8d5d96719c..3ead9599bb 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -41,7 +41,7 @@ export function createPromptInputController(input: { options: local.agent.list().map((agent) => agent.name), current: local.agent.current()?.name ?? "", loading: agentsQuery.isLoading, - visible: settings.visibility.customAgents(), + visible: local.agent.visible(), select: local.agent.set, }, model: { diff --git a/packages/app/src/pages/session/use-composer-commands.tsx b/packages/app/src/pages/session/use-composer-commands.tsx index 9b75c7bf4c..e7e51489ee 100644 --- a/packages/app/src/pages/session/use-composer-commands.tsx +++ b/packages/app/src/pages/session/use-composer-commands.tsx @@ -1,7 +1,6 @@ import { useCommand, type CommandOption } from "@/context/command" import { useLanguage } from "@/context/language" import { useLocal, type ModelSelection } from "@/context/local" -import { useSettings } from "@/context/settings" import { useDialog } from "@opencode-ai/ui/context/dialog" import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom" import { useSessionLayout } from "./session-layout" @@ -19,7 +18,6 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => { const dialog = useDialog() const language = useLanguage() const local = useLocal() - const settings = useSettings() const { sessionKey } = useSessionLayout() const sessionOwnership = createSessionOwnership(sessionKey) const model = input.model ?? local.model @@ -70,7 +68,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => { description: language.t("command.agent.cycle.description"), keybind: "mod+.", slash: "agent", - disabled: !settings.visibility.customAgents(), + disabled: !local.agent.visible(), onSelect: () => local.agent.move(1), }), agentCommand({ @@ -78,7 +76,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => { title: language.t("command.agent.cycle.reverse"), description: language.t("command.agent.cycle.reverse.description"), keybind: "shift+mod+.", - disabled: !settings.visibility.customAgents(), + disabled: !local.agent.visible(), onSelect: () => local.agent.move(-1), }), ])