Skip to content
Open
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
18 changes: 14 additions & 4 deletions src/app/entity-navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ 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.
// Bestie adds its own tabs beside it.
const providers = () =>
current.agents.attentionProviders().map(({ provider }) => provider);
const tabs = () =>
Expand All @@ -692,14 +692,24 @@ it("lists active primary pages in their sidebar and header placements", async ()
.map((tab) => tab.key)
.sort();
await waitFor(() =>
expect(tabs()).toEqual(["buzz.bestie/bestie", "buzz.watching/watching"]),
expect(tabs()).toEqual([
"buzz.bestie/bestie",
"buzz.bestie/former-bestie",
"buzz.watching/watching",
]),
);
await act(() => current.plugins.change("disable", "buzz.watching"));
await waitFor(() => expect(tabs()).toEqual(["buzz.bestie/bestie"]));
await waitFor(() =>
expect(tabs()).toEqual(["buzz.bestie/bestie", "buzz.bestie/former-bestie"]),
);
expect(providers()).toContain("attention/command");
await act(() => current.plugins.change("enable", "buzz.watching"));
await waitFor(() =>
expect(tabs()).toEqual(["buzz.bestie/bestie", "buzz.watching/watching"]),
expect(tabs()).toEqual([
"buzz.bestie/bestie",
"buzz.bestie/former-bestie",
"buzz.watching/watching",
]),
);

await act(() => current.plugins.change("disable", "buzz.agents"));
Expand Down
9 changes: 9 additions & 0 deletions src/bundled/agents/BestieChoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { Button } from "../../shared/design-system/ui/Button";

type BestieAgent = AgentChoicesSnapshot["selectable"][number];

const made = new Set<(pubkey: string) => void>();
/** Hears each agent the viewer makes Bestie on this device, once it is
* saved. A choice synced from elsewhere isn't heard. */
export function onBestieMade(listener: (pubkey: string) => void) {
made.add(listener);
return () => void made.delete(listener);
}

/** Making an agent Bestie, wherever it is offered: `make` asks before
* replacing an explicitly saved Bestie, in the dialog `view` renders. */
export function useBestieChoice(session: RelaySession, scope: string) {
Expand All @@ -37,6 +45,7 @@ export function useBestieChoice(session: RelaySession, scope: string) {
setSavingBestie(true);
chooseDefaultAgent(session, scope, pubkey)
.then(() => {
for (const listener of made) listener(pubkey);
if (choice === latest.current) setReplacement(undefined);
})
.catch((failure: Error) => {
Expand Down
83 changes: 82 additions & 1 deletion src/bundled/bestie/BestieTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { afterEach, expect, it, vi } from "vitest";
import type { Agent, AgentChange, Agents } from "../../features/agents/service";
import type { RelayData } from "../../features/relay/service";
import { writeView } from "../../shared/view-state";
import { bestieTab } from "./BestieTab";
import { onBestieMade } from "../agents/BestieChoice";
import { bestieTab, formerBestieTab } from "./BestieTab";
import { validateObject } from "../../features/agents/attention";
import { checkinHourly, memory, withFeature } from "./features";

Expand Down Expand Up @@ -246,6 +247,11 @@ it("keeps memory on through the owner's edits, and offers Restore for edited mar
rerender(<Tab agent={shown(on.replace(close, ""))} />);
expect(screen.queryByRole("button", { name: "Restore" })).toBeNull();
expect(screen.getByText(/Fix its start and end lines there/)).toBeVisible();
// Check-ins wait on memory.
expect(screen.getByRole("switch", { name: "Every hour" })).toHaveAttribute(
"aria-disabled",
"true",
);
});

it("says why a save failed and lets the switch be tried again", async () => {
Expand Down Expand Up @@ -395,3 +401,78 @@ it("checks in every hour only while memory is on, with a timer in the owner's DM
expect(hourly()).toBeChecked();
expect(hourly()).not.toHaveAttribute("aria-disabled", "true");
});

it("shows a Former Bestie tab while a former Bestie has a setting on, with a way back", async () => {
const user = userEvent.setup();
const { relay, mainAgent } = community(bea.pubkey);
const tab = formerBestieTab(relay, host().agents);
const remembering = {
...ada,
config: { instructions: withFeature("Be brief.", memory, true) },
} as Agent;
const stop = tab.subscribe?.(() => {});
// Nobody is former before the account's choice is read.
expect(tab.when?.(remembering)).toBe(false);
await vi.waitFor(() => expect(tab.when?.(remembering)).toBe(true));
// Not on the Bestie, nor on an agent with nothing on.
expect(tab.when?.(bea)).toBe(false);
expect(tab.when?.(ada)).toBe(false);

const made = vi.fn();
const stopMade = onBestieMade(made);
const Tab = tab.component;
render(<Tab agent={remembering} />);
expect(
screen.getByText("Ada is no longer your Bestie. These are still on."),
).toBeVisible();
expect(screen.getByRole("switch", { name: "Remembers you" })).toBeChecked();
const hourly = screen.getByRole("switch", { name: "Every hour" });
expect(hourly).toHaveAttribute("aria-disabled", "true");
expect(screen.getByText("Only your Bestie checks in.")).toBeVisible();
expect(screen.getByText("Private channels Ada can read")).toBeVisible();

await user.click(
screen.getByRole("button", { name: "Make my Bestie again" }),
);
const confirmation = await screen.findByRole("alertdialog", {
name: "Make Ada your bestie?",
});
await user.click(
within(confirmation).getByRole("button", { name: "Switch to Ada" }),
);
expect(mainAgent.write).toHaveBeenCalledWith(ada.pubkey);
// This device made it Bestie, so the plugin hears of it.
await vi.waitFor(() => expect(made).toHaveBeenCalledWith(ada.pubkey));
await vi.waitFor(() => expect(tab.when?.(remembering)).toBe(false));
stopMade();
stop?.();
});

it("lets a former Bestie's leftover check-in be turned off", async () => {
const user = userEvent.setup();
const { relay } = community(bea.pubkey);
const { agents, save } = host();
const Tab = formerBestieTab(relay, agents).component;
const checking = {
...ada,
config: {
instructions: withFeature(
withFeature("Be brief.", memory, true),
checkinHourly,
true,
),
},
attention: {
"interest/bestie-hourly-check-in": {},
"watch/bestie-hourly-check-in": {},
},
} as unknown as Agent;
render(<Tab agent={checking} />);
const hourly = screen.getByRole("switch", { name: "Every hour" });
expect(hourly).toBeChecked();
expect(hourly).not.toHaveAttribute("aria-disabled", "true");
await user.click(hourly);
expect(save).toHaveBeenLastCalledWith(ada.key, {
config: { instructions: withFeature("Be brief.", memory, true) },
});
});
Loading
Loading