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: 6 additions & 0 deletions docs/plugin-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ 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.

The type's functions of config alone (`defaults`, `importShared`, `adopt`,
`carry`, `describeMigration`, `migrationWarning` and `summary`) go to both
calls. The service needs them to create and retype agents, and screens call
them while rendering. `retypePreview` computes a type change's preview from
them, so a screen can show it where the service does not run.

A plugin, bundled or installed, can add a tool to every agent session in the
app with `ctx.agents.addTool({ name, description, inputSchema, call })`. `call`
gets the agent's Buzz client and where it is working, so the tool acts as the
Expand Down
62 changes: 35 additions & 27 deletions src/bundled/claude-code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { repositoriesContext } from "../../features/communities/repositories";
import { isTauri } from "@tauri-apps/api/core";
import { pubkeyOf } from "../../features/agents/membership";
import { ownerReads } from "../../features/agents/reads";
import type { AgentTypeConfig } from "../../features/agents/service";
import type { PluginModule } from "../../plugins/api";
import { ClaudeLogoIcon } from "../../shared/design-system/icons";
import { claudeRun, TYPE_ID } from "./run";
Expand All @@ -25,6 +26,38 @@ const supported = () => isTauri() && /Mac|Linux/i.test(navigator.platform);
/** What the type's UI reaches of its runtime. */
export type ClaudeExposed = Parameters<typeof createTabs>[0];

/** The type's pure config functions: the service needs them to create and
* retype agents, and screens to show them. */
const typeConfig = {
defaults: () => ({ config: DEFAULT_CONFIG }),
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
workspace: carry.workspace ?? saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== DEFAULT_CONFIG.workspace && { workspace }),
};
},
describeMigration: (saved) => {
const { model, effort, scope } = config(saved);
return [
{ id: "model", label: "Model", isOverride: Boolean(model.trim()) },
{ id: "thinking", label: "Thinking", isOverride: Boolean(effort) },
{
id: "sessions",
label: "Sessions",
isOverride: scope !== DEFAULT_CONFIG.scope,
unchangedKey: scope,
},
];
},
summary: (agent) => config(agent.config).model || "Claude",
} satisfies AgentTypeConfig<Config>;

export const inject = ["agents"];
export const apply: PluginModule["apply"] = (ctx) => {
if (!supported()) return;
Expand All @@ -38,6 +71,7 @@ export const apply: PluginModule["apply"] = (ctx) => {
ActivityTab,
} = createTabs(exposed);
return {
...typeConfig,
icon: ClaudeLogoIcon,
Create,
CreateModel,
Expand Down Expand Up @@ -122,38 +156,12 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
title: "Claude",
description:
"Claude on this computer. Each thread it is mentioned in is one continuing session.",
defaults: () => ({ config: DEFAULT_CONFIG }),
...typeConfig,
expose: {
setup,
defaults: providerDefaults,
runtime,
} satisfies ClaudeExposed,
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
workspace: carry.workspace ?? saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== DEFAULT_CONFIG.workspace && { workspace }),
};
},
describeMigration: (saved) => {
const { model, effort, scope } = config(saved);
return [
{ id: "model", label: "Model", isOverride: Boolean(model.trim()) },
{ id: "thinking", label: "Thinking", isOverride: Boolean(effort) },
{
id: "sessions",
label: "Sessions",
isOverride: scope !== DEFAULT_CONFIG.scope,
unchangedKey: scope,
},
];
},
summary: (agent) => config(agent.config).model || "Claude",
...(spawn &&
readFile && {
usage: (agent, { since, signal }) =>
Expand Down
90 changes: 49 additions & 41 deletions src/bundled/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { repositoriesContext } from "../../features/communities/repositories";
import { isTauri } from "@tauri-apps/api/core";
import { pubkeyOf } from "../../features/agents/membership";
import { ownerReads } from "../../features/agents/reads";
import type { AgentTypeConfig } from "../../features/agents/service";
import type { PluginModule } from "../../plugins/api";
import { CodexLogoIcon } from "../../shared/design-system/icons";
import { type Config, config, defaults, validWorkspace } from "./config";
Expand All @@ -24,6 +25,52 @@ type CodexExposed = Readonly<{
setup: Parameters<typeof createTabs>[3];
}>;

/** The type's pure config functions: the service needs them to create and
* retype agents, and screens to show them. */
const typeConfig = {
defaults: () => ({ config: { ...defaults } }),
importShared: (source) =>
source.runtime === "codex" &&
(!source.provider || source.provider === "openai")
? { ...defaults, model: source.model }
: undefined,
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
// Codex runs only in an absolute or home workspace.
workspace:
carry.workspace && validWorkspace(carry.workspace)
? carry.workspace
: saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== defaults.workspace && { workspace }),
};
},
describeMigration: (saved) => {
const { model, effort, scope } = config(saved);
return [
{ id: "model", label: "Model", isOverride: Boolean(model.trim()) },
{
id: "thinking",
label: "Thinking",
isOverride: Boolean(effort),
},
{
id: "sessions",
label: "Sessions",
isOverride: scope !== defaults.scope,
unchangedKey: scope,
},
];
},
migrationWarning: "Any commands Codex left running will stop.",
summary: (agent) => config(agent.config).model || "Codex",
} satisfies AgentTypeConfig<Config>;

export const inject = ["agents"];
export const apply: PluginModule["apply"] = (ctx) => {
if (!supported()) return;
Expand All @@ -39,6 +86,7 @@ export const apply: PluginModule["apply"] = (ctx) => {
ActivityTab,
} = createTabs(provider, runtime, transcript, setup);
return {
...typeConfig,
icon: CodexLogoIcon,
Create,
CreateModel,
Expand Down Expand Up @@ -147,53 +195,13 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
title: "Codex",
description:
"Codex on this computer, using its native coding tools and your account.",
defaults: () => ({ config: { ...defaults } }),
...typeConfig,
expose: {
provider: providerController,
runtime,
transcript: transcripts(start, runtime),
setup,
} satisfies CodexExposed,
importShared: (source) =>
source.runtime === "codex" &&
(!source.provider || source.provider === "openai")
? { ...defaults, model: source.model }
: undefined,
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
// Codex runs only in an absolute or home workspace.
workspace:
carry.workspace && validWorkspace(carry.workspace)
? carry.workspace
: saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== defaults.workspace && { workspace }),
};
},
describeMigration: (saved) => {
const { model, effort, scope } = config(saved);
return [
{ id: "model", label: "Model", isOverride: Boolean(model.trim()) },
{
id: "thinking",
label: "Thinking",
isOverride: Boolean(effort),
},
{
id: "sessions",
label: "Sessions",
isOverride: scope !== defaults.scope,
unchangedKey: scope,
},
];
},
migrationWarning: "Any commands Codex left running will stop.",
summary: (agent) => config(agent.config).model || "Codex",
...(spawn &&
readFile && {
usage: (agent, { since, signal }) =>
Expand Down
58 changes: 33 additions & 25 deletions src/bundled/goose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { repositoriesContext } from "../../features/communities/repositories";
// in it; goose answers with the in-process Buzz tools, served to it over
// loopback HTTP MCP.
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";
Expand All @@ -18,6 +19,36 @@ export const TYPE_ID = "goose";
/** What the type's UI reaches of its runtime. */
type GooseExposed = Parameters<typeof createTabs>[0];

/** The type's pure config functions: the service needs them to create and
* retype agents, and screens to show them. */
const typeConfig = {
defaults: () => ({ config: DEFAULT_CONFIG }),
importShared: (source) =>
source.runtime === "goose" || source.runtime === "goose-ai"
? {
...DEFAULT_CONFIG,
model: source.model,
provider: source.provider ?? "",
}
: undefined,
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
workspace: carry.workspace ?? saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== DEFAULT_CONFIG.workspace && { workspace }),
};
},
summary: (agent) => {
const { model, provider } = config(agent.config);
return [provider, model].filter(Boolean).join(" · ") || "Goose";
},
} satisfies AgentTypeConfig<Config>;

export const inject = ["agents"];
export const apply: PluginModule["apply"] = (ctx) => {
// Only the desktop app can start `goose-acp`; no unusable type elsewhere.
Expand All @@ -32,6 +63,7 @@ export const apply: PluginModule["apply"] = (ctx) => {
ProviderDialogBody,
} = createTabs(exposed);
return {
...typeConfig,
icon: GooseLogoIcon,
Create,
CreateModel,
Expand Down Expand Up @@ -100,37 +132,13 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
title: "Goose",
description:
"goose on this computer, with any provider goose supports. Each thread it is mentioned in is one continuing session.",
defaults: () => ({ config: DEFAULT_CONFIG }),
importShared: (source) =>
source.runtime === "goose" || source.runtime === "goose-ai"
? {
...DEFAULT_CONFIG,
model: source.model,
provider: source.provider ?? "",
}
: undefined,
...typeConfig,
expose: { setup, runtime, history } satisfies GooseExposed,
provider: {
snapshot: setup.snapshot,
subscribe: setup.subscribe,
check: setup.check,
},
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
workspace: carry.workspace ?? saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== DEFAULT_CONFIG.workspace && { workspace }),
};
},
summary: (agent) => {
const { model, provider } = config(agent.config);
return [provider, model].filter(Boolean).join(" · ") || "Goose";
},
sessions: {
list: (key) => {
const live = runtime.sessions(key);
Expand Down
42 changes: 25 additions & 17 deletions src/bundled/pi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isTauri } from "@tauri-apps/api/core";
import { pubkeyOf } from "../../features/agents/membership";
import { ownerReads } from "../../features/agents/reads";
import { repositoriesContext } from "../../features/communities/repositories";
import type { AgentTypeConfig } from "../../features/agents/service";
import type { PluginModule } from "../../plugins/api";
import { PiLogoIcon } from "../../shared/design-system/icons";
import { type Config, config, DEFAULT_CONFIG, PiRuntime } from "./runtime";
Expand All @@ -19,6 +20,28 @@ const HANDOVER_MS = 2 * 60_000;
/** What the type's UI reaches of its runtime. */
type PiExposed = Parameters<typeof createTabs>[0];

/** The type's pure config functions: the service needs them to create and
* retype agents, and screens to show them. */
const typeConfig = {
defaults: () => ({ config: DEFAULT_CONFIG }),
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
workspace: carry.workspace ?? saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== DEFAULT_CONFIG.workspace && { workspace }),
};
},
summary: (agent) => {
const { model, provider } = config(agent.config);
return [provider, model].filter(Boolean).join(" · ") || "Pi";
},
} satisfies AgentTypeConfig<Config>;

export const inject = ["agents"];
export const apply: PluginModule["apply"] = (ctx) => {
// Only the desktop app can start the Pi host; no unusable type elsewhere.
Expand All @@ -27,6 +50,7 @@ export const apply: PluginModule["apply"] = (ctx) => {
const { ActivityTab, Create, CreateModel, SettingsTab } =
createTabs(exposed);
return {
...typeConfig,
icon: PiLogoIcon,
Create,
CreateModel,
Expand Down Expand Up @@ -85,24 +109,8 @@ function applyAgent(ctx: Parameters<PluginModule["apply"]>[0]) {
title: "Pi",
description:
"Pi on this computer, with any provider Pi supports. Each thread it is mentioned in is one continuing conversation.",
defaults: () => ({ config: DEFAULT_CONFIG }),
...typeConfig,
expose: { setup: runtime.setup, runtime } satisfies PiExposed,
adopt: (carry, saved) => ({
...saved,
instructions: carry.instructions ?? saved.instructions,
workspace: carry.workspace ?? saved.workspace,
}),
carry: (saved) => {
const { instructions, workspace } = config(saved);
return {
...(instructions.trim() && { instructions }),
...(workspace !== DEFAULT_CONFIG.workspace && { workspace }),
};
},
summary: (agent) => {
const { model, provider } = config(agent.config);
return [provider, model].filter(Boolean).join(" · ") || "Pi";
},
...(host.spawn && {
usage: (agent, { since, signal }) =>
runtime.usage(agent.key, since, signal),
Expand Down
Loading
Loading