From 8f2f30c88ff7b500e6fc430ad3aa7e607f70e150 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Tue, 27 Oct 2026 10:39:10 -0400 Subject: [PATCH] refactor(codex): split the Codex harness into runtime and UI Codex's setup, provider, runtime, process sync and update timer move into its agent entry, which registers the type and exposes handles for its UI. The UI entry presents the icon, Create, tabs and provider dialogs. The Activity tab no longer starts Codex itself to read a transcript: the runtime exposes a transcript read, so the UI holds no process capability. No behavior changes. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- src/bundled/codex/index.test.ts | 15 ++--- src/bundled/codex/index.ts | 99 ++++++++++++++++++++----------- src/bundled/codex/runtime.test.ts | 6 +- src/bundled/codex/tabs.test.tsx | 11 +++- src/bundled/codex/tabs.tsx | 52 ++++++++++++---- 5 files changed, 123 insertions(+), 60 deletions(-) diff --git a/src/bundled/codex/index.test.ts b/src/bundled/codex/index.test.ts index 518f375e86..bad5863eda 100644 --- a/src/bundled/codex/index.test.ts +++ b/src/bundled/codex/index.test.ts @@ -1,11 +1,11 @@ // @vitest-environment jsdom import { afterEach, beforeEach, expect, it, vi } from "vitest"; -import type { AgentType } from "../../features/agents/service"; import { CodexLogoIcon } from "../../shared/design-system/icons"; import { CodexRuntime } from "./runtime"; import { CodexSetup } from "./setup"; import { CodexProvider } from "./tabs"; -import { apply } from "./index"; +import * as codex from "./index"; +import { appliedType } from "../../features/agents/plugin-testing"; const desktop = vi.hoisted(() => ({ tauri: true })); vi.mock("@tauri-apps/api/core", () => ({ isTauri: () => desktop.tauri })); @@ -21,23 +21,18 @@ afterEach(() => { /** Applies the plugin to a stand-in context and returns the registered type. */ function applied() { - let type: AgentType | undefined; const subscribe = () => () => undefined; const ctx = { pluginOwner: { id: "buzz.codex", revision: "r1" }, host: { spawn: vi.fn(), request: vi.fn() }, agents: { - register: (registered: AgentType) => { - type = registered; - }, snapshot: () => ({ status: "ready", agents: [], running: [] }), relay: () => undefined, subscribe, }, effect: vi.fn(), }; - apply(ctx as never); - return type; + return appliedType(codex, ctx); } it("registers only where the desktop app can run codex and base64", () => { @@ -123,7 +118,7 @@ it("stops agents that stop running even when the agent list could not be read", const agent = { pubkey: "agent", type: "buzz.codex/codex" }; let state = { status: "ready", agents: [agent], running: [agent] }; let notify = () => undefined; - apply({ + codex.agent.apply({ pluginOwner: { id: "buzz.codex" }, host: { spawn: vi.fn(), request: vi.fn() }, agents: { @@ -187,7 +182,7 @@ it("starts the server once Codex is updated and checked, only while a Codex agen notify(); }; let cleanup: () => void = () => undefined; - apply({ + codex.agent.apply({ pluginOwner: { id: "buzz.codex" }, host: { spawn: vi.fn(), request: vi.fn() }, agents: { diff --git a/src/bundled/codex/index.ts b/src/bundled/codex/index.ts index 39206660f5..4db1e8585f 100644 --- a/src/bundled/codex/index.ts +++ b/src/bundled/codex/index.ts @@ -8,15 +8,68 @@ import { type Config, config, defaults, validWorkspace } from "./config"; import { codexRun, TYPE_ID } from "./run"; import { CodexRuntime } from "./runtime"; import { CodexSetup, withScript } from "./setup"; -import { CodexProvider, createTabs } from "./tabs"; +import { CodexProvider, createTabs, transcripts } from "./tabs"; import { readUsage } from "./usage"; /** How often Buzz's copy of Codex looks for a new release. */ const UPDATE_MS = 6 * 60 * 60_000; -export const inject = ["agents", "host"]; +// Only the desktop app can start `codex`, and setup and file uploads run +// `bash` and `base64`, which stock Windows lacks; no unusable type elsewhere. +const supported = () => isTauri() && /Mac|Linux/i.test(navigator.platform); + +/** What the type's UI reaches of its runtime. */ +type CodexExposed = Readonly<{ + provider: Parameters[0]; + runtime: Parameters[1]; + transcript: Parameters[2]; + setup: Parameters[3]; +}>; + +export const inject = ["agents"]; export const apply: PluginModule["apply"] = (ctx) => { - // Only the desktop app can start `codex`, and setup and file uploads run - // `bash` and `base64`, which stock Windows lacks; no unusable type elsewhere. - if (!isTauri() || !/Mac|Linux/i.test(navigator.platform)) return; + if (!supported()) return; + ctx.agents.present( + TYPE_ID, + ({ provider, runtime, transcript, setup }) => { + const { + Create, + CreateModel, + DefaultControl, + ProviderDialogBody, + SettingsTab, + ActivityTab, + } = createTabs(provider, runtime, transcript, setup); + return { + icon: CodexLogoIcon, + 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 (!supported()) return; const spawn = ctx.host.spawn && withScript(ctx.host.spawn.bind(ctx.host)); const readFile = ctx.host.readFile?.bind(ctx.host); const setup = new CodexSetup(spawn, (input) => ctx.host.request(input)); @@ -89,23 +142,18 @@ export const apply: PluginModule["apply"] = (ctx) => { runtime.dispose(); setup.dispose(); }); - const { - Create, - CreateModel, - DefaultControl, - ProviderDialogBody, - SettingsTab, - ActivityTab, - } = createTabs(providerController, runtime, start, setup); ctx.agents.register({ id: TYPE_ID, title: "Codex", - icon: CodexLogoIcon, description: "Codex on this computer, using its native coding tools and your account.", defaults: () => ({ config: { ...defaults } }), - Create, - CreateModel, + expose: { + provider: providerController, + runtime, + transcript: transcripts(start, runtime), + setup, + } satisfies CodexExposed, importShared: (source) => source.runtime === "codex" && (!source.provider || source.provider === "openai") @@ -155,8 +203,6 @@ export const apply: PluginModule["apply"] = (ctx) => { snapshot: providerController.snapshot, subscribe: providerController.subscribe, check: () => providerController.check(), - DialogBody: ProviderDialogBody, - DefaultControl, }, sessions: { list: (key) => { @@ -177,21 +223,6 @@ export const apply: PluginModule["apply"] = (ctx) => { }, subscribe: runtime.subscribe, }, - tabs: [ - { - id: "settings", - title: "Settings", - role: "settings", - splitSettings: true, - component: SettingsTab, - }, - { - id: "activity", - title: "Activity", - role: "activity", - component: ActivityTab, - }, - ], // A mention during the first download waits for it, within its deadline. ...codexRun(runtime, async (signal) => { await setup.installed(signal); @@ -199,4 +230,4 @@ export const apply: PluginModule["apply"] = (ctx) => { await providerController.check(); }), }); -}; +} diff --git a/src/bundled/codex/runtime.test.ts b/src/bundled/codex/runtime.test.ts index 78452aab95..9aef47f211 100644 --- a/src/bundled/codex/runtime.test.ts +++ b/src/bundled/codex/runtime.test.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { schnorr } from "@noble/curves/secp256k1.js"; import { bytesToHex } from "nostr-tools/utils"; -import { apply } from "./index"; +import { agent as codexAgent } from "./index"; import { describe, afterEach, expect, it, vi } from "vitest"; import { pubkeyOf } from "../../features/agents/membership"; import { ownerReads } from "../../features/agents/reads"; @@ -436,7 +436,7 @@ function fixture( if (plugin) { vi.stubGlobal("localStorage", local); vi.stubGlobal("navigator", { platform: "MacIntel" }); - apply({ + codexAgent.apply({ host: { spawn }, agents: { snapshot: () => ({ status: "ready", agents: [], running }), @@ -454,7 +454,7 @@ function fixture( effect: (effect: () => () => void) => { pluginDispose = effect(); }, - } as unknown as Parameters[0]); + } as unknown as Parameters[0]); } const starts = () => sent.filter((wire) => wire.method === "turn/start"); const interrupt = () => { diff --git a/src/bundled/codex/tabs.test.tsx b/src/bundled/codex/tabs.test.tsx index d6b0ae6aeb..a1fe89c0e7 100644 --- a/src/bundled/codex/tabs.test.tsx +++ b/src/bundled/codex/tabs.test.tsx @@ -30,7 +30,7 @@ import type { RelayData } from "../../features/relay/service"; import { PanelWorkspace } from "../../features/panels/PanelWorkspace"; import { defaults, type Config } from "./config"; import { sessionTag } from "./prompt"; -import { CodexProvider, createTabs } from "./tabs"; +import { CodexProvider, createTabs, transcripts } from "./tabs"; import { CodexRuntime } from "./runtime"; import type { Wire } from "./rpc"; import { CodexSetup } from "./setup"; @@ -200,7 +200,12 @@ function fixture({ sessions, (raw) => provider.resolve(raw), ); - const tabs = createTabs(provider, runtime, spawn, setup); + const tabs = createTabs( + provider, + runtime, + transcripts(spawn, runtime), + setup, + ); const props = (settings = defaults, save = vi.fn(async () => {})) => ({ agent: { @@ -1291,7 +1296,7 @@ it("installs and signs in Buzz's copy of Codex from its provider settings", asyn const { ProviderDialogBody } = createTabs( provider, f.runtime, - f.spawn, + transcripts(f.spawn, f.runtime), f.setup, ); render(); diff --git a/src/bundled/codex/tabs.tsx b/src/bundled/codex/tabs.tsx index 685b7532b2..82d8164a96 100644 --- a/src/bundled/codex/tabs.tsx +++ b/src/bundled/codex/tabs.tsx @@ -52,7 +52,12 @@ import { import { AppServer, listModels, needsSignIn, type Spawn } from "./rpc"; import type { CodexRuntime, SavedConversation } from "./runtime"; import type { CodexSetup } from "./setup"; -import { readTranscript, TURN_LIMIT, type CodexTranscript } from "./transcript"; +import { + readTranscript, + TURN_LIMIT, + type CodexTranscript, + type Lookup, +} from "./transcript"; const label = ({ channelId, name, root }: SavedConversation) => `${name ? `#${name}` : channelId.slice(0, 8)} ยท ${root ? `thread ${root.slice(0, 8)}` : "channel"}`; @@ -310,7 +315,7 @@ function CodexSignIn({ check, disabled, }: { - login: CodexSetup; + login: SetupHandle; step: "install" | "login"; checking: boolean; check(): void; @@ -412,11 +417,39 @@ function CodexSignIn({ ); } +// What the tabs reach of the runtime: stores and calls returning promises, +// as an agent type's `expose` allows. +type ProviderHandle = Pick< + CodexProvider, + "snapshot" | "subscribe" | "check" | "resolve" | "saveDefaults" +>; +type ActivityHandle = Pick< + CodexRuntime, + "subscribe" | "sessions" | "conversations" | "find" +>; +type SetupHandle = Pick< + CodexSetup, + "snapshot" | "subscribe" | "run" | "cancel" +>; +/** Reads one conversation's Codex thread. */ +export type TranscriptRead = ( + agent: Lookup["agent"], + conversation: SavedConversation, + signal: AbortSignal, +) => Promise; +/** Transcripts read with `spawn`, named by the runtime. */ +export const transcripts = + (spawn: Spawn, runtime: Pick): TranscriptRead => + (agent, conversation, signal) => + readTranscript(spawn, { agent, ...conversation }, signal, (pubkey) => + runtime.name(agent.key, pubkey), + ); + export function createTabs( - provider: CodexProvider, - runtime: CodexRuntime, - spawn: Spawn, - setup: CodexSetup, + provider: ProviderHandle, + runtime: ActivityHandle, + transcript: TranscriptRead, + setup: SetupHandle, ) { const login = setup; @@ -738,11 +771,10 @@ export function createTabs( useEffect(() => { const abort = new AbortController(); setLoading(true); - void readTranscript( - spawn, - { agent: { key: agent.key, name: agent.name }, ...conversation }, + void transcript( + { key: agent.key, name: agent.name }, + conversation, abort.signal, - (pubkey) => runtime.name(agent.key, pubkey), ) .then( (transcript): Read =>