Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5c5f7b5
fix(agents): export pack avatars in .agent.png snapshots
tulsi-builder Oct 28, 2026
53b5cc0
feat(agents): rework the Share window into Marketplace and Export rows
tulsi-builder Oct 28, 2026
67d358a
fix(agents): confirm a Marketplace update by its content
tulsi-builder Oct 28, 2026
82d95aa
feat(agents): send an agent to people from the Share window
tulsi-builder Oct 28, 2026
1832ce7
feat(agents): share an agent to a channel, laid out like Share access
tulsi-builder Oct 28, 2026
2dbf04a
feat(agents): show shared agents as cards in chat; publish in one step
tulsi-builder Oct 28, 2026
a9423ca
fix(agents): show sent agent cards in chat; publish on Done; align rows
tulsi-builder Oct 28, 2026
7c02cfd
feat(agents): import like Identity; Published label with a View action
tulsi-builder Oct 28, 2026
b7bf256
feat(agents): explain import edits; open an agent you already have
tulsi-builder Oct 28, 2026
4c16618
fix(agents): tighten the import dialog's header and footer gaps
tulsi-builder Oct 28, 2026
cd6e8f2
fix(agents): read cards in packaged builds; send edited agents anew
tulsi-builder Oct 28, 2026
5bdada0
fix(agents): scroll to a published agent opened from View
tulsi-builder Oct 28, 2026
085a830
fix(agents): bound card reads; keep sends and failed reads recoverable
tulsi-builder Oct 28, 2026
a8169b9
fix(agents): recover unconfirmed Share sends from the durable outbox
tulsi-builder Oct 28, 2026
b1811f5
fix(agents): let Outbox remove delivered Share receipts
tulsi-builder Oct 28, 2026
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
4 changes: 3 additions & 1 deletion docs/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@ the user must inspect the conversation before explicitly sending a new message.

Protected media and uploads are native, because the webview cannot attach Blossom
(kind 24242) authentication itself: `<img>`, `<video>` and `<audio>` send no custom
headers, and the CSP keeps `connect-src` closed to general HTTPS. Relay `/media/`
headers, and the CSP keeps `connect-src` closed to general HTTPS; it admits only
the `buzz-media` scheme, whose bytes the requesting window may already read (such
as an agent card's embedded definition). Relay `/media/`
URLs selected by shared TypeScript render through the `buzz-media` URI scheme;
`<video>` and `<audio>` instead load the same URLs from a native listener on
`127.0.0.1`, since WebKitGTK cannot play a custom scheme. It authorizes each
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
}
],
"security": {
"csp": "default-src 'self'; script-src 'self' blob: 'wasm-unsafe-eval'; connect-src 'self' ipc: http://ipc.localhost https://api.github.com wss:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: buzz-media: http://buzz-media.localhost; media-src 'self' blob: https://dwwgwmfqqjotj.cloudfront.net buzz-media: http://buzz-media.localhost http://127.0.0.1:* https://github.com https://user-images.githubusercontent.com https://private-user-images.githubusercontent.com https://github-production-user-asset-6210df.s3.amazonaws.com https://objects.githubusercontent.com"
"csp": "default-src 'self'; script-src 'self' blob: 'wasm-unsafe-eval'; connect-src 'self' ipc: http://ipc.localhost https://api.github.com wss: buzz-media: http://buzz-media.localhost; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: buzz-media: http://buzz-media.localhost; media-src 'self' blob: https://dwwgwmfqqjotj.cloudfront.net buzz-media: http://buzz-media.localhost http://127.0.0.1:* https://github.com https://user-images.githubusercontent.com https://private-user-images.githubusercontent.com https://github-production-user-asset-6210df.s3.amazonaws.com https://objects.githubusercontent.com"
}
},
"bundle": {
Expand Down
13 changes: 11 additions & 2 deletions src/app/pages.integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,18 @@ test("the app runtime exposes ready bundled pages and removes them on disable",
await vi.waitFor(() =>
assert.equal(services.conversation.tools.snapshot().length, 4),
);
assert.equal(services.conversation.attachments.snapshot().length, 1);
assert.deepEqual(
services.conversation.attachments
.snapshot()
.map((item) => item.pluginId)
.sort(),
["buzz.agents", "buzz.voice-notes"],
);
await services.plugins.change("disable", "buzz.voice-notes");
assert.equal(services.conversation.attachments.snapshot().length, 0);
assert.deepEqual(
services.conversation.attachments.snapshot().map((item) => item.pluginId),
["buzz.agents"],
);
assert.equal(services.conversation.inline.snapshot().length, 1);
await vi.waitFor(() =>
assert.equal(services.conversation.links.snapshot().length, 1),
Expand Down
169 changes: 148 additions & 21 deletions src/bundled/agents/AgentBrowse.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import {
} from "./AgentBrowse";
import { PanelWorkspace } from "../../features/panels/PanelWorkspace";
function AgentBrowse(
props: ReturnType<typeof importing> & { relay: RelayData },
props: ReturnType<typeof importing> & {
relay: RelayData;
onOpen?: (agent: Agent) => void;
},
) {
const [selection, setSelection] = useState<SharedAgentSelection>();
return (
Expand Down Expand Up @@ -68,14 +71,25 @@ const echo = {
importShared: (source: { model: string }) => ({ model: source.model }),
} as RegisteredAgentType;
const notes = { key: "test/notes", title: "Notes" } as RegisteredAgentType;
/** Import's props: one type, and an `agents` whose `port` resolves. */
const importing = (types: readonly RegisteredAgentType[] = [echo]) => ({
agents: {
port: vi.fn(async () => ({ pubkey: "e".repeat(64) }) as Agent),
} as unknown as Agents & { port: ReturnType<typeof vi.fn> },
types,
onImported: vi.fn(),
});
const imported = { pubkey: "e".repeat(64), name: "Shared helper" } as Agent;
/** Import's props: one type, and an `agents` holding `own` whose `port`
* resolves. */
const importing = (
types: readonly RegisteredAgentType[] = [echo],
own: readonly Agent[] = [],
) => {
const snapshot = { agents: own };
return {
agents: {
subscribe: () => () => {},
snapshot: () => snapshot,
types: () => types,
port: vi.fn(async () => ({ agent: imported, notes: [] })),
} as unknown as Agents & { port: ReturnType<typeof vi.fn> },
types,
onImported: vi.fn(),
};
};

const shared: EventData = {
id: "ab".repeat(32),
Expand Down Expand Up @@ -135,6 +149,37 @@ it("browses shared agents and opens read-only details", async () => {
expect(screen.getByText(/^npub…/)).toBeVisible();
});

it("scrolls to an agent selected from elsewhere, not one chosen here", async () => {
const scrollIntoView = vi.fn();
Element.prototype.scrollIntoView = scrollIntoView;
try {
const read = vi.fn().mockResolvedValue([shared]);
const relay = community(read);
const onSelect = vi.fn();
const view = render(<Catalog relay={relay} onSelect={onSelect} />);
const card = await screen.findByRole("button", { name: "Shared helper" });
fireEvent.click(card);
const [{ agent }] = onSelect.mock.calls[0] ?? [];
view.rerender(
<Catalog relay={relay} selectedId={agent.id} onSelect={onSelect} />,
);
expect(card).toHaveAttribute("aria-current", "true");
expect(scrollIntoView).not.toHaveBeenCalled();
// As View on a published agent selects it, before its page has loaded.
view.unmount();
render(<Catalog relay={relay} selectedId={agent.id} onSelect={onSelect} />);
const listed = await screen.findByRole("button", { name: "Shared helper" });
await vi.waitFor(() =>
expect(scrollIntoView).toHaveBeenCalledExactlyOnceWith({
block: "center",
}),
);
expect(scrollIntoView.mock.contexts[0]).toBe(listed);
} finally {
Reflect.deleteProperty(Element.prototype, "scrollIntoView");
}
});

it("asks to connect while no community is ready", () => {
const snapshot = { status: "disconnected", generation: 0 };
render(
Expand Down Expand Up @@ -312,12 +357,20 @@ it("imports a shared agent through the import path, then hands back", async () =
expect(within(dialog).queryByLabelText("Import as")).not.toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: "Import" }));
const confirmation = screen.getByRole("dialog", {
name: "Import Shared helper",
name: "Shared helper",
});
expect(within(confirmation).queryByRole("combobox")).not.toBeInTheDocument();
// Laid out as the Identity tab: the type it runs as is chosen in place.
expect(
within(confirmation).getByRole("combobox", { name: "Harness" }),
).toBeVisible();
expect(
within(confirmation).getByRole("textbox", { name: "Instructions" }),
).toHaveValue("Help with tasks.");
expect(props.agents.port).not.toHaveBeenCalled();
fireEvent.click(within(confirmation).getByRole("button", { name: "Import" }));
await vi.waitFor(() => expect(props.onImported).toHaveBeenCalledOnce());
await vi.waitFor(() =>
expect(props.onImported).toHaveBeenCalledExactlyOnceWith(imported),
);
expect(props.agents.port).toHaveBeenCalledExactlyOnceWith({
source: "shared",
type: "test/echo",
Expand All @@ -328,6 +381,83 @@ it("imports a shared agent through the import path, then hands back", async () =
});
});

it("says how to import, and undoes edits back to what was shared", async () => {
const read = vi.fn().mockResolvedValue([shared]);
render(<AgentBrowse relay={community(read)} {...importing()} />);
fireEvent.click(await screen.findByRole("button", { name: "Shared helper" }));
fireEvent.click(screen.getByRole("button", { name: "Import" }));
const dialog = screen.getByRole("dialog", { name: "Shared helper" });
expect(dialog).toHaveAccessibleDescription(
"Import it as is, or edit anything below first. Changes only affect your copy.",
);
expect(
within(dialog).queryByRole("button", { name: "Undo changes" }),
).not.toBeInTheDocument();
const instructions = within(dialog).getByRole("textbox", {
name: "Instructions",
});
fireEvent.change(instructions, { target: { value: "Help with less." } });
expect(dialog).toHaveAccessibleDescription(
"Edited. Changes only affect your copy.",
);
fireEvent.click(within(dialog).getByRole("button", { name: "Undo changes" }));
expect(instructions).toHaveValue("Help with tasks.");
expect(
within(dialog).getByRole("combobox", { name: "Harness" }),
).toHaveFocus();
expect(
within(dialog).queryByRole("button", { name: "Undo changes" }),
).not.toBeInTheDocument();
});

it("offers to open an agent you already have rather than copy it", async () => {
const read = vi.fn().mockResolvedValue([
{
...shared,
content: JSON.stringify({
display_name: "Shared helper",
model: "test-model",
runtime: "test/echo",
system_prompt: "Help with tasks.",
}),
},
]);
const mine = {
pubkey: "f".repeat(64),
name: "Shared helper",
type: "test/echo",
config: { model: "test-model" },
} as unknown as Agent;
const props = importing(
[
{
...echo,
exportShared: () => ({ model: "test-model", runtime: "test/echo" }),
carry: () => ({ instructions: "Help with tasks." }),
} as unknown as RegisteredAgentType,
],
[mine],
);
const onOpen = vi.fn();
render(<AgentBrowse relay={community(read)} {...props} onOpen={onOpen} />);
fireEvent.click(await screen.findByRole("button", { name: "Shared helper" }));
fireEvent.click(screen.getByRole("button", { name: "Import" }));
const dialog = screen.getByRole("dialog", { name: "Shared helper" });
expect(dialog).toHaveAccessibleDescription("You already have this agent.");
expect(
within(dialog).queryByRole("button", { name: "Cancel" }),
).not.toBeInTheDocument();
fireEvent.click(
within(dialog).getByRole("button", { name: "Open Shared helper" }),
);
expect(onOpen).toHaveBeenCalledExactlyOnceWith(mine);
// A second copy stays a choice.
fireEvent.click(
within(dialog).getByRole("button", { name: "Import a copy" }),
);
await vi.waitFor(() => expect(props.onImported).toHaveBeenCalledOnce());
});

it("refuses a shared agent whose instructions block/buzz would refuse", async () => {
const read = vi.fn().mockResolvedValue([
{
Expand All @@ -347,7 +477,7 @@ it("refuses a shared agent whose instructions block/buzz would refuse", async ()
});
fireEvent.click(within(panel).getByRole("button", { name: "Import" }));
const confirmation = screen.getByRole("dialog", {
name: "Import Shared helper",
name: "Shared helper",
});
expect(within(confirmation).getByRole("alert")).toHaveTextContent(
"Agent instructions contains prohibited invisible or formatting character U+202E",
Expand All @@ -371,14 +501,11 @@ it("imports as the chosen type, and keeps the panel open with the error when it
});
fireEvent.click(within(dialog).getByRole("button", { name: "Import" }));
const confirmation = screen.getByRole("dialog", {
name: "Import Shared helper",
name: "Shared helper",
});
fireEvent.click(
within(confirmation).getByRole("button", { name: "Change settings" }),
);
expect(
within(confirmation).getByRole("combobox", { name: "Import as" }),
).toHaveFocus();
within(confirmation).queryByRole("button", { name: "Change settings" }),
).not.toBeInTheDocument();
fireEvent.click(within(confirmation).getByRole("button", { name: "Import" }));
expect(await within(confirmation).findByRole("alert")).toHaveTextContent(
"Connect to a community first",
Expand All @@ -395,10 +522,10 @@ it("does not silently replace an unavailable shared runtime", async () => {
render(<AgentBrowse relay={community(read)} {...props} />);
fireEvent.click(await screen.findByRole("button", { name: "Shared helper" }));
fireEvent.click(screen.getByRole("button", { name: "Import" }));
const dialog = screen.getByRole("dialog", { name: "Import Shared helper" });
const dialog = screen.getByRole("dialog", { name: "Shared helper" });
expect(within(dialog).getByRole("button", { name: "Import" })).toBeDisabled();
expect(
within(dialog).getByRole("combobox", { name: "Import as" }),
within(dialog).getByRole("combobox", { name: "Harness" }),
).toBeVisible();
expect(props.agents.port).not.toHaveBeenCalled();
});
Expand Down
Loading
Loading