From 5179b87aef3d629199f9d63ce73a7acc5618fe9f Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:56:10 -0500 Subject: [PATCH] fix(app): agent normalization (#19169) --- .../app/src/context/global-sync/bootstrap.ts | 4 +-- .../app/src/context/global-sync/utils.test.ts | 35 +++++++++++++++++++ packages/app/src/context/global-sync/utils.ts | 16 ++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 packages/app/src/context/global-sync/utils.test.ts diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 9158fb46e7..4790011a53 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -15,7 +15,7 @@ import { retry } from "@opencode-ai/util/retry" import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" -import { cmp, normalizeProviderList } from "./utils" +import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" type GlobalStore = { @@ -174,7 +174,7 @@ export async function bootstrapDirectory(input: { seededProject ? Promise.resolve() : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), - () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))), + () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))), () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), () => retry(() => diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts new file mode 100644 index 0000000000..6d44ac9a89 --- /dev/null +++ b/packages/app/src/context/global-sync/utils.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "bun:test" +import type { Agent } from "@opencode-ai/sdk/v2/client" +import { normalizeAgentList } from "./utils" + +const agent = (name = "build") => + ({ + name, + mode: "primary", + permission: {}, + options: {}, + }) as Agent + +describe("normalizeAgentList", () => { + test("keeps array payloads", () => { + expect(normalizeAgentList([agent("build"), agent("docs")])).toEqual([agent("build"), agent("docs")]) + }) + + test("wraps a single agent payload", () => { + expect(normalizeAgentList(agent("docs"))).toEqual([agent("docs")]) + }) + + test("extracts agents from keyed objects", () => { + expect( + normalizeAgentList({ + build: agent("build"), + docs: agent("docs"), + }), + ).toEqual([agent("build"), agent("docs")]) + }) + + test("drops invalid payloads", () => { + expect(normalizeAgentList({ name: "AbortError" })).toEqual([]) + expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")]) + }) +}) diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index 6b78134a61..cac58f3174 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -1,7 +1,21 @@ -import type { Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" +import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) +function isAgent(input: unknown): input is Agent { + if (!input || typeof input !== "object") return false + const item = input as { name?: unknown; mode?: unknown } + if (typeof item.name !== "string") return false + return item.mode === "subagent" || item.mode === "primary" || item.mode === "all" +} + +export function normalizeAgentList(input: unknown): Agent[] { + if (Array.isArray(input)) return input.filter(isAgent) + if (isAgent(input)) return [input] + if (!input || typeof input !== "object") return [] + return Object.values(input).filter(isAgent) +} + export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { return { ...input,