From d8171a399c4587a9e1e618a5b7552daf9f9e9018 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Tue, 27 Oct 2026 10:44:31 -0400 Subject: [PATCH] refactor(pi): split the Pi harness into runtime and UI The Pi agent entry registers the runtime half and exposes the setup and runtime handles. The UI entry presents the icon, create form 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/pi/index.test.ts | 23 +++++++++++++ src/bundled/pi/index.ts | 63 ++++++++++++++++++++++-------------- src/bundled/pi/tabs.tsx | 2 +- 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 src/bundled/pi/index.test.ts diff --git a/src/bundled/pi/index.test.ts b/src/bundled/pi/index.test.ts new file mode 100644 index 0000000000..f3eeff49c7 --- /dev/null +++ b/src/bundled/pi/index.test.ts @@ -0,0 +1,23 @@ +// @vitest-environment jsdom +import { expect, it, vi } from "vitest"; +import { appliedType } from "../../features/agents/plugin-testing"; +import * as pi from "./index"; + +vi.mock("@tauri-apps/api/core", () => ({ isTauri: () => true })); + +it("shows the Pi type's screens from the runtime its agent entry registers", () => { + // The UI needs no host; only the runtime starts Pi. + expect(pi.inject).toEqual(["agents"]); + expect(pi.agent.inject).toEqual(["agents", "host"]); + const type = appliedType(pi, { + pluginOwner: { id: "buzz.pi", revision: "r1" }, + host: { request: vi.fn() }, + agents: { relay: () => undefined }, + effect: vi.fn(), + }); + expect(type).toMatchObject({ id: "pi", title: "Pi", timeoutMs: 120_000 }); + expect(type?.Create).toBeTypeOf("function"); + expect(type?.CreateModel).toBeTypeOf("function"); + expect(type?.icon).toBeDefined(); + expect(type?.tabs?.map((tab) => tab.id)).toEqual(["settings", "activity"]); +}); diff --git a/src/bundled/pi/index.ts b/src/bundled/pi/index.ts index 6449c7e243..b816450be9 100644 --- a/src/bundled/pi/index.ts +++ b/src/bundled/pi/index.ts @@ -16,9 +16,45 @@ export const TYPE_ID = "pi"; * which includes the first install of Pi's packages. */ const HANDOVER_MS = 2 * 60_000; -export const inject = ["agents", "host"]; +/** What the type's UI reaches of its runtime. */ +type PiExposed = Parameters[0]; + +export const inject = ["agents"]; export const apply: PluginModule["apply"] = (ctx) => { // Only the desktop app can start the Pi host; no unusable type elsewhere. + if (!isTauri()) return; + ctx.agents.present(TYPE_ID, (exposed) => { + const { ActivityTab, Create, CreateModel, SettingsTab } = + createTabs(exposed); + return { + icon: PiLogoIcon, + Create, + CreateModel, + 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.pi"}/${TYPE_ID}`; @@ -30,10 +66,6 @@ export const apply: PluginModule["apply"] = (ctx) => { repositoriesContext, (key) => agents.handle(key), ); - const { ActivityTab, Create, CreateModel, SettingsTab } = createTabs({ - setup: runtime.setup, - runtime, - }); const sync = () => { const snapshot = agents.snapshot(); if (snapshot.status === "loading") return; @@ -51,12 +83,10 @@ export const apply: PluginModule["apply"] = (ctx) => { agents.register({ id: TYPE_ID, title: "Pi", - icon: PiLogoIcon, description: "Pi on this computer, with any provider Pi supports. Each thread it is mentioned in is one continuing conversation.", defaults: () => ({ config: DEFAULT_CONFIG }), - Create, - CreateModel, + expose: { setup: runtime.setup, runtime } satisfies PiExposed, adopt: (carry, saved) => ({ ...saved, instructions: carry.instructions ?? saved.instructions, @@ -81,21 +111,6 @@ export const apply: PluginModule["apply"] = (ctx) => { list: (key) => runtime.sessions(key), subscribe: (listener) => runtime.subscribe(listener), }, - tabs: [ - { - id: "settings", - title: "Settings", - role: "settings", - splitSettings: true, - component: SettingsTab, - }, - { - id: "activity", - title: "Activity", - role: "activity", - component: ActivityTab, - }, - ], sessionControls: { scope: (agent) => config(agent.config).scope, state: (agent, place) => runtime.sessionState(agent, place), @@ -107,4 +122,4 @@ export const apply: PluginModule["apply"] = (ctx) => { runOnce: (turn) => runtime.runOnce(turn), timeoutMs: HANDOVER_MS, }); -}; +} diff --git a/src/bundled/pi/tabs.tsx b/src/bundled/pi/tabs.tsx index aabc413304..cc4c24075b 100644 --- a/src/bundled/pi/tabs.tsx +++ b/src/bundled/pi/tabs.tsx @@ -182,7 +182,7 @@ export function createTabs({ setup, runtime, }: Readonly<{ - setup: PiSetup; + setup: Pick; runtime: Pick; }>) { /** The provider, its key and its model, and a test that they answer. As