Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions src/bundled/codex/index.test.ts
Original file line number Diff line number Diff line change
@@ -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 }));
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
99 changes: 65 additions & 34 deletions src/bundled/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createTabs>[0];
runtime: Parameters<typeof createTabs>[1];
transcript: Parameters<typeof createTabs>[2];
setup: Parameters<typeof createTabs>[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<CodexExposed, Config>(
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<PluginModule["agent"]> = {
inject: ["agents", "host"],
apply: (ctx) => applyAgent(ctx),
};
function applyAgent(ctx: Parameters<PluginModule["apply"]>[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));
Expand Down Expand Up @@ -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<Config>({
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")
Expand Down Expand Up @@ -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) => {
Expand All @@ -177,26 +223,11 @@ 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);
if (providerController.snapshot().readiness === "checking")
await providerController.check();
}),
});
};
}
6 changes: 3 additions & 3 deletions src/bundled/codex/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 }),
Expand All @@ -454,7 +454,7 @@ function fixture(
effect: (effect: () => () => void) => {
pluginDispose = effect();
},
} as unknown as Parameters<typeof apply>[0]);
} as unknown as Parameters<typeof codexAgent.apply>[0]);
}
const starts = () => sent.filter((wire) => wire.method === "turn/start");
const interrupt = () => {
Expand Down
11 changes: 8 additions & 3 deletions src/bundled/codex/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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(<ProviderDialogBody onClose={vi.fn()} />);
Expand Down
52 changes: 42 additions & 10 deletions src/bundled/codex/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"}`;
Expand Down Expand Up @@ -310,7 +315,7 @@ function CodexSignIn({
check,
disabled,
}: {
login: CodexSetup;
login: SetupHandle;
step: "install" | "login";
checking: boolean;
check(): void;
Expand Down Expand Up @@ -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<CodexTranscript | undefined>;
/** Transcripts read with `spawn`, named by the runtime. */
export const transcripts =
(spawn: Spawn, runtime: Pick<CodexRuntime, "name">): 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;

Expand Down Expand Up @@ -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 =>
Expand Down
Loading