diff --git a/src/bundled/goose/index.test.ts b/src/bundled/goose/index.test.ts index 7fcb83b261..213827209f 100644 --- a/src/bundled/goose/index.test.ts +++ b/src/bundled/goose/index.test.ts @@ -1,8 +1,8 @@ // @vitest-environment jsdom import { afterEach, expect, it, vi } from "vitest"; -import type { AgentType } from "../../features/agents/service"; import { GooseRuntime } from "./runtime"; -import { apply } from "./index"; +import * as goose from "./index"; +import { appliedType } from "../../features/agents/plugin-testing"; vi.mock("@tauri-apps/api/core", () => ({ isTauri: () => true })); afterEach(() => vi.restoreAllMocks()); @@ -22,18 +22,12 @@ it("observes saved conversations with the status of open sessions", () => { // A one-off turn joins no conversation. { key: "timer/x #1", busy: true }, ]); - let type: AgentType | undefined; - apply({ + const type = appliedType(goose, { pluginOwner: { id: "buzz.goose", revision: "r1" }, host: { request: vi.fn() }, - agents: { - register: (registered: AgentType) => { - type = registered; - }, - relay: () => undefined, - }, + agents: { relay: () => undefined }, effect: vi.fn(), - } as never); + }); // A reload stops its turns, so the owner is asked to resume them. expect(type?.resumes).toBe(true); expect( diff --git a/src/bundled/goose/index.ts b/src/bundled/goose/index.ts index 25053b0bbb..55f7008cb0 100644 --- a/src/bundled/goose/index.ts +++ b/src/bundled/goose/index.ts @@ -15,9 +15,52 @@ import { readUsage } from "./usage"; export const TYPE_ID = "goose"; -export const inject = ["agents", "host"]; +/** What the type's UI reaches of its runtime. */ +type GooseExposed = Parameters[0]; + +export const inject = ["agents"]; export const apply: PluginModule["apply"] = (ctx) => { // Only the desktop app can start `goose-acp`; no unusable type elsewhere. + if (!isTauri()) return; + ctx.agents.present(TYPE_ID, (exposed) => { + const { + Create, + CreateModel, + SettingsTab, + ActivityTab, + DefaultControl, + ProviderDialogBody, + } = createTabs(exposed); + return { + icon: GooseLogoIcon, + Create, + CreateModel, + provider: { DialogBody: ProviderDialogBody, DefaultControl }, + tabs: [ + { + id: "settings", + title: "Settings", + role: "settings", + splitSettings: true, + component: SettingsTab, + }, + { + id: "activity", + title: "Activity", + role: "activity", + component: ActivityTab, + }, + ], + }; + }); +}; + +// The runtime, which runs where the agents do. +export const agent: NonNullable = { + inject: ["agents", "host"], + apply: (ctx) => applyAgent(ctx), +}; +function applyAgent(ctx: Parameters[0]) { if (!isTauri()) return; const { agents, host } = ctx; const typeKey = `${ctx.pluginOwner?.id ?? "buzz.goose"}/${TYPE_ID}`; @@ -38,18 +81,6 @@ export const apply: PluginModule["apply"] = (ctx) => { : undefined; const setup = new GooseSetup(desktop, () => runtime.defaultsChanged()); const history = new GooseHistory(desktop); - const { - Create, - CreateModel, - SettingsTab, - ActivityTab, - DefaultControl, - ProviderDialogBody, - } = createTabs({ - setup, - runtime, - history, - }); const sync = () => { const snapshot = agents.snapshot(); if (snapshot.status === "loading") return; @@ -67,7 +98,6 @@ export const apply: PluginModule["apply"] = (ctx) => { agents.register({ id: TYPE_ID, title: "Goose", - icon: GooseLogoIcon, description: "goose on this computer, with any provider goose supports. Each thread it is mentioned in is one continuing session.", defaults: () => ({ config: DEFAULT_CONFIG }), @@ -79,14 +109,11 @@ export const apply: PluginModule["apply"] = (ctx) => { provider: source.provider ?? "", } : undefined, - Create, - CreateModel, + expose: { setup, runtime, history } satisfies GooseExposed, provider: { snapshot: setup.snapshot, subscribe: setup.subscribe, check: setup.check, - DialogBody: ProviderDialogBody, - DefaultControl, }, adopt: (carry, saved) => ({ ...saved, @@ -141,22 +168,7 @@ export const apply: PluginModule["apply"] = (ctx) => { signal, ), }), - tabs: [ - { - id: "settings", - title: "Settings", - role: "settings", - splitSettings: true, - component: SettingsTab, - }, - { - id: "activity", - title: "Activity", - role: "activity", - component: ActivityTab, - }, - ], models: (agent) => setup.picks(config(agent.config)), ...gooseRun(runtime), }); -}; +} diff --git a/src/bundled/goose/tabs.tsx b/src/bundled/goose/tabs.tsx index e5440198df..51c3a29a60 100644 --- a/src/bundled/goose/tabs.tsx +++ b/src/bundled/goose/tabs.tsx @@ -132,14 +132,30 @@ const placeOf = (key: string): Place => { const titled = (name: string, title: string) => title.startsWith(`${name} ยท `) ? title.slice(name.length + 3) : title; +// What the tabs reach of the runtime: stores and calls returning promises, +// as an agent type's `expose` allows. +type SetupHandle = Pick< + GooseSetup, + | "snapshot" + | "subscribe" + | "check" + | "providers" + | "models" + | "efforts" + | "saved" + | "save" + | "saveDefaults" + | "test" +>; + export function createTabs({ setup, runtime, history, }: Readonly<{ - setup: GooseSetup; - runtime: GooseRuntime; - history: GooseHistory; + setup: SetupHandle; + runtime: Pick; + history: Pick; }>) { function DefaultControl({ onConfigure }: { onConfigure(): void }) { const state = useSyncExternalStore(setup.subscribe, setup.snapshot);