From 8d8c03829bc9d62a4c73f23ff4ed2b473daf5adb Mon Sep 17 00:00:00 2001
From: Riley Crane
Date: Thu, 29 Oct 2026 17:30:12 -0700
Subject: [PATCH 1/4] feat(bestie): show a Bestie tab on your Bestie, with
Change
Plugin agent tabs take an optional `when(agent)`, and a `subscribe` that
tells the host when it may answer differently. Watching sets neither and
is unchanged. The Bestie plugin adds a Bestie tab to the agent saved as
your Bestie: "This is your Bestie", Change (which picks another agent and
opens the existing replace dialog) and read-only settings rows.
The replace dialog and its save logic move from AgentsPage into
BestieChoice so the Agents page and the tab share one path.
Co-authored-by: Opus 5.5 Max
Co-Authored-By: Claude Opus 5.5
Signed-off-by: Riley Crane
---
docs/plugin-architecture.md | 6 +-
src/bundled/agents/AgentsPage.tsx | 125 ++----------------------
src/bundled/agents/BestieChoice.tsx | 127 ++++++++++++++++++++++++
src/bundled/bestie/BestieTab.test.tsx | 115 ++++++++++++++++++++++
src/bundled/bestie/BestieTab.tsx | 135 ++++++++++++++++++++++++++
src/bundled/bestie/README.md | 7 ++
src/bundled/bestie/index.tsx | 2 +
src/features/agents/service.test.ts | 38 ++++++++
src/features/agents/service.ts | 24 ++++-
src/features/agents/tabs.test.tsx | 41 +++++++-
src/features/agents/tabs.tsx | 23 +++--
src/features/solo/default-agent.ts | 18 ++++
12 files changed, 531 insertions(+), 130 deletions(-)
create mode 100644 src/bundled/agents/BestieChoice.tsx
create mode 100644 src/bundled/bestie/BestieTab.test.tsx
create mode 100644 src/bundled/bestie/BestieTab.tsx
diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md
index 6a38ead880..b181c3f5d1 100644
--- a/docs/plugin-architecture.md
+++ b/docs/plugin-architecture.md
@@ -294,7 +294,11 @@ agent-record migrations; blank per-agent values inherit them.
A plugin, bundled or installed, can add a tab to the settings of every agent
running on this device, whatever its type, with
`ctx.agents.registerTab({ id, title, component })`. The component receives
-`{ agent }`; it reads and saves through the plugin's own `ctx.agents`. The tab
+`{ agent }`; it reads and saves through the plugin's own `ctx.agents`. An
+optional `when(agent)` limits the tab to the agents it returns true for, and an
+optional `subscribe(listener)` tells the host when `when` may answer
+differently, so the tab appears or goes without a reload. A `when` that throws
+hides only its own tab. The tab
follows Activity and precedes History and Settings, in the agent's Build view and
its profile panel, and is not offered for hosted or elsewhere agents. It is
removed when the plugin is turned off or its scope ends. The bundled Agent
diff --git a/src/bundled/agents/AgentsPage.tsx b/src/bundled/agents/AgentsPage.tsx
index 89ba2a3819..3ca0db630a 100644
--- a/src/bundled/agents/AgentsPage.tsx
+++ b/src/bundled/agents/AgentsPage.tsx
@@ -55,19 +55,13 @@ import type {
TeamPort,
} from "../../features/agents/service";
import { useRelayConnection } from "../../features/relay/react";
-import { useAgentChoices } from "../../features/agents/use-choices";
-import type { AgentChoicesSnapshot } from "../../features/agents/choices";
import type { RelaySession } from "../../features/relay/session";
import {
readSnapshot,
type SharedDefinition,
} from "../../features/agents/agent-share";
-import {
- chooseDefaultAgent,
- restoreDefaultAgent,
- useDefaultAgent,
- useSavedAgent,
-} from "../../features/solo/default-agent";
+import { useDefaultAgent } from "../../features/solo/default-agent";
+import { useBestieChoice } from "./BestieChoice";
import type { RelayData } from "../../features/relay/service";
import type { Navigation } from "../../features/navigation/controller";
import { targetKey } from "../../features/navigation/targets";
@@ -84,7 +78,6 @@ import {
XIcon,
} from "../../shared/design-system/icons";
import { Button } from "../../shared/design-system/ui/Button";
-import { AlertDialog } from "../../shared/design-system/ui/AlertDialog";
import { Avatar } from "../../shared/design-system/ui/Avatar";
import { Checkbox } from "../../shared/design-system/ui/Checkbox";
import { ChoiceRow } from "../../shared/design-system/ui/ChoiceRow";
@@ -738,8 +731,6 @@ type InventoryProps = {
onViewListing?: ((agent: SharedAgent) => void) | undefined;
};
-type BestieAgent = AgentChoicesSnapshot["selectable"][number];
-
/** Agents keeps Morgan's account-synced selection owner; this surface only
* names an explicitly saved selection Bestie and asks before replacing it. */
function DefaultAgentInventory({
@@ -750,125 +741,23 @@ function DefaultAgentInventory({
// Keep Morgan's automatic fallback/adoption behavior active even though the
// Bestie pill names only the explicit account-saved choice.
useDefaultAgent(session, scope);
- const saved = useSavedAgent(session, scope);
- const choices = useAgentChoices(session, true, true);
- const [error, setError] = useState("");
- const [savingBestie, setSavingBestie] = useState(false);
- const [replacement, setReplacement] = useState<{
- current: BestieAgent;
- replacing: BestieAgent;
- }>();
- // Only the latest choice reports its failure.
- const latest = useRef(0);
- const eligible = (pubkey: string) =>
- session.agentChoices
- .snapshot()
- .selectable.find((agent) => agent.pubkey === pubkey);
- const save = (pubkey: string) => {
- if (!eligible(pubkey)) return;
- const choice = ++latest.current;
- setError("");
- setSavingBestie(true);
- chooseDefaultAgent(session, scope, pubkey)
- .then(() => {
- if (choice === latest.current) setReplacement(undefined);
- })
- .catch((failure: Error) => {
- if (choice === latest.current) setError(failure.message);
- })
- .finally(() => {
- if (choice === latest.current) setSavingBestie(false);
- });
- };
+ const bestie = useBestieChoice(session, scope);
return (
<>
agent.pubkey))
+ new Set(bestie.choices.selectable.map((agent) => agent.pubkey))
}
- onMakeBestie={(pubkey) => {
- const replacing = eligible(pubkey);
- if (!replacing || pubkey === saved) return;
- const previous = choices.identities.find(
- (agent) => agent.pubkey === saved,
- );
- if (previous) setReplacement({ current: previous, replacing });
- else save(pubkey);
- }}
+ onMakeBestie={bestie.make}
/>
- {replacement && (
- {
- if (error) {
- restoreDefaultAgent(
- scope,
- replacement.replacing.pubkey,
- replacement.current.pubkey,
- );
- setError("");
- }
- setReplacement(undefined);
- }}
- onChoose={() => {
- // A choice changed elsewhere while this dialog was open wins.
- if (saved === replacement.current.pubkey)
- save(replacement.replacing.pubkey);
- else setReplacement(undefined);
- }}
- />
- )}
- {error && !replacement &&
);
}
From 89d58c10f7a8a1cdd57db00dba29badbc0422860 Mon Sep 17 00:00:00 2001
From: Riley Crane
Date: Fri, 30 Oct 2026 14:33:29 -0700
Subject: [PATCH 4/4] fix(bestie): keep the Bestie tab working without a window
The Bestie tab follows the saved agent as soon as it is registered. Outside
a browser, such as in the app runtime tests, following the device's choice
reached for window and stopped the Bestie plugin from starting. The README's
Bestie tab section is now part of Getting started, and the navigation test
counts the Bestie tab beside the Attention tab.
Co-authored-by: eggboy <3986e5374b8c11a274f4a53fa9a68e1e18b53e712f8aa5218add2485bf5a2b09@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5
Signed-off-by: Riley Crane
---
src/app/entity-navigation.test.tsx | 16 +++++++++-------
src/bundled/bestie/README.md | 2 +-
src/features/solo/default-agent.ts | 7 ++++++-
3 files changed, 16 insertions(+), 9 deletions(-)
diff --git a/src/app/entity-navigation.test.tsx b/src/app/entity-navigation.test.tsx
index 409493dcaa..beb99fce86 100644
--- a/src/app/entity-navigation.test.tsx
+++ b/src/app/entity-navigation.test.tsx
@@ -683,21 +683,23 @@ it("lists active primary pages in their sidebar and header placements", async ()
);
// The Attention tab is on by default, and attention keeps running without it.
+ // Bestie adds its own tab beside it.
const providers = () =>
current.agents.attentionProviders().map(({ provider }) => provider);
+ const tabs = () =>
+ current.agents
+ .addedTabs()
+ .map((tab) => tab.key)
+ .sort();
await waitFor(() =>
- expect(current.agents.addedTabs().map((tab) => tab.key)).toEqual([
- "buzz.watching/watching",
- ]),
+ expect(tabs()).toEqual(["buzz.bestie/bestie", "buzz.watching/watching"]),
);
await act(() => current.plugins.change("disable", "buzz.watching"));
- await waitFor(() => expect(current.agents.addedTabs()).toEqual([]));
+ await waitFor(() => expect(tabs()).toEqual(["buzz.bestie/bestie"]));
expect(providers()).toContain("attention/command");
await act(() => current.plugins.change("enable", "buzz.watching"));
await waitFor(() =>
- expect(current.agents.addedTabs().map((tab) => tab.key)).toEqual([
- "buzz.watching/watching",
- ]),
+ expect(tabs()).toEqual(["buzz.bestie/bestie", "buzz.watching/watching"]),
);
await act(() => current.plugins.change("disable", "buzz.agents"));
diff --git a/src/bundled/bestie/README.md b/src/bundled/bestie/README.md
index f2e988eb0c..835ad71a30 100644
--- a/src/bundled/bestie/README.md
+++ b/src/bundled/bestie/README.md
@@ -10,7 +10,7 @@ Open **Bestie** and choose **Set up Bestie**. Setup signs you in to Builderlab,
creates your hosted Bestie, gives it its instructions, creates your private
channel and sends a first message on your behalf so Bestie introduces itself.
-## The Bestie tab
+### The Bestie tab
The agent you made your Bestie (with **Make my bestie** in Agents) gets a
**Bestie** tab on its agent page while it runs on this device. **Change** picks
diff --git a/src/features/solo/default-agent.ts b/src/features/solo/default-agent.ts
index 9150b5585c..1b5d350a46 100644
--- a/src/features/solo/default-agent.ts
+++ b/src/features/solo/default-agent.ts
@@ -141,7 +141,12 @@ export function savedAgentStore(session: RelaySession, scope: string) {
snapshot: () => (account ? saved.get(account) : chosenAgent(scope)),
subscribe(listener: () => void) {
if (account) void adopt(account, scope);
- const view = subscribeView(scope, listener);
+ // The device's choice is browser storage; without a window there is none
+ // to follow.
+ const view =
+ typeof window === "undefined"
+ ? () => {}
+ : subscribeView(scope, listener);
const choices = subscribeSaved(listener);
return () => {
view();