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
6 changes: 4 additions & 2 deletions docs/plugin-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,10 @@ from `apply`: `ctx.agents.present(typeId, (exposed) => ({ icon, Peek, Create,
CreateModel, tabs, provider: { DialogBody, DefaultControl } }))`. The type's
readers see one type with both halves. `exposed` is the type's `expose`: the
handles its UI reaches of the runtime. Each is JSON, a store (`snapshot` and
`subscribe`) or a call returning a promise, so it can later cross to another
page. Claude is the first harness split this way.
`subscribe`), a call returning a promise, or a keyed read (`keyedRead` in
`features/agents/handles.ts`: `read(key)` and `subscribe`, such as one agent's
sessions), so it can later cross to another page. Claude is the first harness
split this way.

The type's functions of config alone (`defaults`, `importShared`, `adopt`,
`carry`, `describeMigration`, `migrationWarning` and `summary`) go to both
Expand Down
10 changes: 8 additions & 2 deletions src/bundled/claude-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import type { PluginModule } from "../../plugins/api";
import { ClaudeLogoIcon } from "../../shared/design-system/icons";
import { claudeRun, TYPE_ID } from "./run";
import { ClaudeDefaults } from "./defaults";
import { ClaudeRuntime, type Config, config, DEFAULT_CONFIG } from "./runtime";
import {
activityHandle,
ClaudeRuntime,
type Config,
config,
DEFAULT_CONFIG,
} from "./runtime";
import { ClaudeSetup, withScript } from "./setup";
import { createTabs } from "./tabs";
import { readUsage } from "./usage";
Expand Down Expand Up @@ -160,7 +166,7 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
expose: {
setup,
defaults: providerDefaults,
runtime,
runtime: activityHandle(runtime),
} satisfies ClaudeExposed,
...(spawn &&
readFile && {
Expand Down
19 changes: 19 additions & 0 deletions src/bundled/claude-code/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// so one agent works in several conversations at once. Claude acts in Buzz with
// the in-process Buzz tools, signed by the agent's native-held key; the runtime
// only reports a turn that fails.
import { keyedRead } from "../../features/agents/handles";
import type { Context } from "../../buzz-mcp/client";
import { RESUMED_PROMPT } from "../../buzz-mcp/prompt";
import { type Listing, respond } from "../../buzz-mcp/rpc";
Expand Down Expand Up @@ -941,3 +942,21 @@ export function modelName(model: string) {
const name = family[0]?.toUpperCase() + family.slice(1);
return id ? `${name} ${id[2]}${id[3] ? `.${id[3]}` : ""}` : name;
}

/** What the Activity tab reaches of `runtime`. */
export const activityHandle = (runtime: ClaudeRuntime) =>
Object.freeze({
find: (...args: Parameters<ClaudeRuntime["find"]>) => runtime.find(...args),
transcript: (...args: Parameters<ClaudeRuntime["transcript"]>) =>
runtime.transcript(...args),
sessions: keyedRead(
(key) => runtime.sessions(key),
(listener) => runtime.subscribe(listener),
[],
),
conversations: keyedRead(
(key) => runtime.conversations(key),
(listener) => runtime.subscribe(listener),
[],
),
});
17 changes: 8 additions & 9 deletions src/bundled/claude-code/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { Field } from "../../shared/design-system/ui/Field";
import { Input } from "../../shared/design-system/ui/Input";
import { Select } from "../../shared/design-system/ui/Select";
import {
type ClaudeRuntime,
type activityHandle,
type Config,
config as readConfig,
effortChoices,
Expand All @@ -64,10 +64,7 @@ type SetupHandle = Pick<
| "cancel"
>;
type DefaultsHandle = Pick<ClaudeDefaults, "snapshot" | "subscribe" | "save">;
type ActivityHandle = Pick<
ClaudeRuntime,
"subscribe" | "sessions" | "conversations" | "find" | "transcript"
>;
type ActivityHandle = ReturnType<typeof activityHandle>;
import { TURN_LIMIT } from "./transcript";

const message = (error: unknown) =>
Expand Down Expand Up @@ -716,11 +713,13 @@ export function createTabs({
agent: AgentViewProps<Config>["agent"];
conversation?: AgentViewProps<Config>["conversation"];
}) {
const sessions = useSyncExternalStore(
(listener) => runtime.subscribe(listener),
() => runtime.sessions(agent.key),
const sessions = useSyncExternalStore(runtime.sessions.subscribe, () =>
runtime.sessions.read(agent.key),
);
const conversations = useSyncExternalStore(
runtime.conversations.subscribe,
() => runtime.conversations.read(agent.key),
);
const conversations = runtime.conversations(agent.key);
const busy = new Map(
sessions.map((session) => [session.key, session.busy]),
);
Expand Down
4 changes: 2 additions & 2 deletions src/bundled/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { PluginModule } from "../../plugins/api";
import { CodexLogoIcon } from "../../shared/design-system/icons";
import { type Config, config, defaults, validWorkspace } from "./config";
import { codexRun, TYPE_ID } from "./run";
import { CodexRuntime } from "./runtime";
import { activityHandle, CodexRuntime } from "./runtime";
import { CodexSetup, withScript } from "./setup";
import { CodexProvider, createTabs, transcripts } from "./tabs";
import { readUsage } from "./usage";
Expand Down Expand Up @@ -198,7 +198,7 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
...typeConfig,
expose: {
provider: providerController,
runtime,
runtime: activityHandle(runtime),
transcript: transcripts(start, runtime),
setup,
} satisfies CodexExposed,
Expand Down
17 changes: 17 additions & 0 deletions src/bundled/codex/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { keyedRead } from "../../features/agents/handles";
import type { WorkOutcome } from "../../features/agents/local-activity";
import { fromOwner, workflowOf } from "../../features/agents/owner-attestation";
import {
Expand Down Expand Up @@ -1857,3 +1858,19 @@ export function programs(command: string | undefined): string | undefined {
? `${list.slice(0, 3).join(", ")} and ${list.length - 3} more`
: list.join(", ");
}

/** What the Activity tab reaches of `runtime`. */
export const activityHandle = (runtime: CodexRuntime) =>
Object.freeze({
find: (...args: Parameters<CodexRuntime["find"]>) => runtime.find(...args),
sessions: keyedRead(
(key) => runtime.sessions(key),
(listener) => runtime.subscribe(listener),
[],
),
conversations: keyedRead(
(key) => runtime.conversations(key),
(listener) => runtime.subscribe(listener),
[],
),
});
6 changes: 3 additions & 3 deletions src/bundled/codex/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { PanelWorkspace } from "../../features/panels/PanelWorkspace";
import { defaults, type Config } from "./config";
import { sessionTag } from "./prompt";
import { CodexProvider, createTabs, transcripts } from "./tabs";
import { CodexRuntime } from "./runtime";
import { activityHandle, CodexRuntime } from "./runtime";
import type { Wire } from "./rpc";
import { CodexSetup } from "./setup";
function deferred<T>() {
Expand Down Expand Up @@ -202,7 +202,7 @@ function fixture({
);
const tabs = createTabs(
provider,
runtime,
activityHandle(runtime),
transcripts(spawn, runtime),
setup,
);
Expand Down Expand Up @@ -1295,7 +1295,7 @@ it("installs and signs in Buzz's copy of Codex from its provider settings", asyn
const provider = new CodexProvider(f.spawn, localStore(), f.setup);
const { ProviderDialogBody } = createTabs(
provider,
f.runtime,
activityHandle(f.runtime),
transcripts(f.spawn, f.runtime),
f.setup,
);
Expand Down
74 changes: 43 additions & 31 deletions src/bundled/codex/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ import {
type Model,
} from "./config";
import { AppServer, listModels, needsSignIn, type Spawn } from "./rpc";
import type { CodexRuntime, SavedConversation } from "./runtime";
import type {
activityHandle,
CodexRuntime,
SavedConversation,
} from "./runtime";
import type { CodexSetup } from "./setup";
import {
readTranscript,
Expand Down Expand Up @@ -214,26 +218,7 @@ export class CodexProvider {
this.set({ defaults });
}
resolve(raw: unknown): Config {
const explicit = readConfig(raw);
const inheritedModel = this.state.defaults.model;
const unavailable = Boolean(
inheritedModel && this.state.data && !this.model(inheritedModel),
);
const model = explicit.model || (unavailable ? "" : inheritedModel);
if (explicit.effort) return { ...explicit, model };
const selected = this.model(model);
const preferred =
unavailable && !explicit.model ? "" : this.state.defaults.effort;
const effort = !preferred
? ""
: !this.state.data
? preferred
: selected?.supportedReasoningEfforts.some(
(choice) => choice.reasoningEffort === preferred,
)
? preferred
: (selected?.defaultReasoningEffort ?? "");
return { ...explicit, model, effort };
return resolveConfig(raw, this.state);
}
dispose() {
this.offSetup?.();
Expand Down Expand Up @@ -291,6 +276,32 @@ const catalogModel = (model: string, data?: Catalog) => {
const resolved = resolveModel(model, data.models);
return data.models.find((choice) => choice.model === resolved);
};
/** `raw` with the provider's defaults filled in, as the catalog in `state`
* allows. */
function resolveConfig(
raw: unknown,
{ defaults, data }: Pick<ProviderState, "defaults" | "data">,
): Config {
const explicit = readConfig(raw);
const inheritedModel = defaults.model;
const unavailable = Boolean(
inheritedModel && data && !catalogModel(inheritedModel, data),
);
const model = explicit.model || (unavailable ? "" : inheritedModel);
if (explicit.effort) return { ...explicit, model };
const selected = catalogModel(model, data);
const preferred = unavailable && !explicit.model ? "" : defaults.effort;
const effort = !preferred
? ""
: !data
? preferred
: selected?.supportedReasoningEfforts.some(
(choice) => choice.reasoningEffort === preferred,
)
? preferred
: (selected?.defaultReasoningEffort ?? "");
return { ...explicit, model, effort };
}
const providerDefaultLabel = (model: string, data?: Catalog) => {
const selected = catalogModel(model, data);
const name = selected?.displayName || selected?.model;
Expand Down Expand Up @@ -421,12 +432,9 @@ function CodexSignIn({
// as an agent type's `expose` allows.
type ProviderHandle = Pick<
CodexProvider,
"snapshot" | "subscribe" | "check" | "resolve" | "saveDefaults"
>;
type ActivityHandle = Pick<
CodexRuntime,
"subscribe" | "sessions" | "conversations" | "find"
"snapshot" | "subscribe" | "check" | "saveDefaults"
>;
type ActivityHandle = ReturnType<typeof activityHandle>;
type SetupHandle = Pick<
CodexSetup,
"snapshot" | "subscribe" | "run" | "cancel"
Expand Down Expand Up @@ -850,10 +858,13 @@ export function createTabs(
agent: AgentViewProps<Config>["agent"];
conversation?: AgentViewProps<Config>["conversation"];
}) {
const sessions = useSyncExternalStore(runtime.subscribe, () =>
runtime.sessions(agent.key),
const sessions = useSyncExternalStore(runtime.sessions.subscribe, () =>
runtime.sessions.read(agent.key),
);
const conversations = useSyncExternalStore(
runtime.conversations.subscribe,
() => runtime.conversations.read(agent.key),
);
const conversations = runtime.conversations(agent.key);
const {
session: here,
searching,
Expand Down Expand Up @@ -991,16 +1002,17 @@ export function createTabs(
useEffect(() => {
if (error) saveErrorRef.current?.focus();
}, [error]);
const state = useProvider();
const {
data,
error: catalogError,
checking,
defaults: providerDefaults,
} = useProvider();
} = state;
useEffect(() => {
setDraft(readConfig(agent.config));
}, [agent.config]);
const effective = provider.resolve({ ...draft, effort: "" });
const effective = resolveConfig({ ...draft, effort: "" }, state);
const selected = catalogModel(effective.model, data);
const supported = selected?.supportedReasoningEfforts ?? [];
const efforts = [
Expand Down
14 changes: 12 additions & 2 deletions src/bundled/goose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { isTauri } from "@tauri-apps/api/core";
import type { AgentTypeConfig } from "../../features/agents/service";
import type { PluginModule } from "../../plugins/api";
import { GooseLogoIcon } from "../../shared/design-system/icons";
import { type Config, config, DEFAULT_CONFIG, GooseRuntime } from "./runtime";
import {
activityHandle,
type Config,
config,
DEFAULT_CONFIG,
GooseRuntime,
} from "./runtime";
import { gooseRun } from "./run";
import { GooseSetup } from "./setup";
import { createTabs } from "./tabs";
Expand Down Expand Up @@ -133,7 +139,11 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
description:
"goose on this computer, with any provider goose supports. Each thread it is mentioned in is one continuing session.",
...typeConfig,
expose: { setup, runtime, history } satisfies GooseExposed,
expose: {
setup,
runtime: activityHandle(runtime),
history,
} satisfies GooseExposed,
provider: {
snapshot: setup.snapshot,
subscribe: setup.subscribe,
Expand Down
11 changes: 11 additions & 0 deletions src/bundled/goose/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// goose acts in Buzz with the in-process Buzz tools over loopback HTTP MCP,
// signed by the agent's native-held key; the runtime only reports a turn that
// fails.
import { keyedRead } from "../../features/agents/handles";
import type { Context } from "../../buzz-mcp/client";
import {
MemoryReads,
Expand Down Expand Up @@ -829,3 +830,13 @@ export class GooseRuntime {
if (reads) this.memoryReads.forget(reads.connection, pubkey);
}
}

/** What the Activity tab reaches of `runtime`. */
export const activityHandle = (runtime: GooseRuntime) =>
Object.freeze({
sessions: keyedRead(
(key) => runtime.sessions(key),
(listener) => runtime.subscribe(listener),
[],
),
});
13 changes: 9 additions & 4 deletions src/bundled/goose/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ import type {
} from "../../features/agents/service";
import type { Agent } from "../../features/agents/service";
import { fakeSpawn } from "./goose-testing";
import { type Config, DEFAULT_CONFIG, type GooseRuntime } from "./runtime";
import {
activityHandle,
type Config,
DEFAULT_CONFIG,
type GooseRuntime,
} from "./runtime";
import { GooseSetup } from "./setup";
import { createTabs } from "./tabs";
import { GooseHistory } from "./transcript";

/** New agent's fields read no sessions. */
const unused = {
runtime: {} as GooseRuntime,
runtime: activityHandle({} as GooseRuntime),
history: new GooseHistory(undefined),
};

Expand Down Expand Up @@ -727,10 +732,10 @@ it("lists the agent's goose sessions and shows one's transcript", async () => {
});
// The runtime keeps one snapshot until its sessions change.
const none: never[] = [];
const runtime = {
const runtime = activityHandle({
subscribe: () => () => {},
sessions: () => none,
} as unknown as GooseRuntime;
} as unknown as GooseRuntime);
const { ActivityTab } = createTabs({
setup: new GooseSetup(fake),
runtime,
Expand Down
Loading
Loading