From d04d1c2382606b7ecc018be93468cacdcf88340d Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Tue, 27 Oct 2026 10:43:07 -0400 Subject: [PATCH] refactor(goose): split the goose harness into runtime and UI The goose agent entry registers the runtime half and exposes setup, session and history handles. The UI entry presents the icon, create form, provider dialog and tabs from those handles only. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- src/bundled/goose/index.test.ts | 16 +++---- src/bundled/goose/index.ts | 80 +++++++++++++++++++-------------- src/bundled/goose/tabs.tsx | 22 +++++++-- 3 files changed, 70 insertions(+), 48 deletions(-) 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);