Files
buzz/desktop-new/src/shared/runtime/client.ts

646 lines
18 KiB
TypeScript

import { Channel, invoke, isTauri } from "@tauri-apps/api/core";
import type { ChannelWindowRequest, RawChannelEvent } from "./channelWindow";
import type {
Channel as BuzzChannel,
Identity,
Message,
Participant,
} from "@/features/sessions/types";
import type { ObserverEvent } from "./observerEvent";
type RawIdentity = { pubkey: string; display_name: string };
type RawChannel = {
id: string;
name: string;
channel_type: string;
visibility: string;
description: string;
member_count: number;
last_message_at?: string | null;
};
type RawChannels = { hash: string; channels: RawChannel[] | null };
type RawMember = {
pubkey: string;
role: string;
is_agent: boolean;
display_name?: string | null;
};
type RawEvent = RawChannelEvent;
const LIVE_MESSAGE_KINDS = [9, 40002, 40008];
type MockState = {
identity: Identity;
channels: BuzzChannel[];
messages: Record<string, Message[]>;
participants: Record<string, Participant[]>;
sessionLinks: Record<string, string[]>;
};
const mockState: MockState = {
identity: { pubkey: "morgan-pubkey", displayName: "Morgan" },
channels: [
{
id: "design",
name: "design",
channelType: "stream",
visibility: "private",
description: "Interface direction and product design",
memberCount: 4,
lastMessageAt: new Date().toISOString(),
},
{
id: "buzz-interface",
name: "buzz-interface",
channelType: "stream",
visibility: "private",
description: "The team building Buzz together",
memberCount: 7,
lastMessageAt: new Date(Date.now() - 86_400_000).toISOString(),
},
{
id: "launch-planning",
name: "launch-planning",
channelType: "stream",
visibility: "private",
description: "Planning the next Buzz release",
memberCount: 5,
lastMessageAt: new Date(Date.now() - 172_800_000).toISOString(),
},
{
id: "cynthia",
name: "Cynthia Chen",
channelType: "dm",
visibility: "private",
description: "Direct message",
memberCount: 2,
lastMessageAt: new Date(Date.now() - 900_000).toISOString(),
},
{
id: "design-sync",
name: "Cynthia Chen, Alex Kim",
channelType: "dm",
visibility: "private",
description: "Direct message",
memberCount: 3,
lastMessageAt: new Date(Date.now() - 3_600_000).toISOString(),
},
{
id: "session-navigation",
name: "Refine the conversation hierarchy",
channelType: "stream",
visibility: "private",
description: "Focused work",
memberCount: 2,
lastMessageAt: new Date(Date.now() - 1_800_000).toISOString(),
},
{
id: "session-agent-setup",
name: "Review agent setup",
channelType: "stream",
visibility: "private",
description: "Focused work",
memberCount: 1,
lastMessageAt: new Date(Date.now() - 10_800_000).toISOString(),
},
],
messages: {
design: [
{
id: "message-1",
pubkey: "cynthia-pubkey",
content: "The session should begin as simply as a thought: just type.",
createdAt: Math.floor(Date.now() / 1000) - 360,
kind: 9,
tags: [["h", "design"]],
},
],
},
sessionLinks: {
design: ["session-navigation"],
},
participants: {
design: [
{
pubkey: "morgan-pubkey",
role: "owner",
isAgent: false,
displayName: "Morgan",
},
{
pubkey: "cynthia-pubkey",
role: "member",
isAgent: false,
displayName: "Cynthia",
},
{
pubkey: "vogue-agent",
role: "bot",
isAgent: true,
displayName: "Vogue",
},
],
},
};
const useMock = !isTauri() || new URLSearchParams(location.search).has("mock");
/** Browser fixtures use the same runtime adapter as the native client. */
export const isMockRuntime = useMock;
function relayFrames(delivery: unknown): unknown[] {
const envelopes = Array.isArray(delivery) ? delivery : [delivery];
return envelopes.flatMap((envelope) => {
if (
typeof envelope === "object" &&
envelope !== null &&
"type" in envelope &&
envelope.type === "Text" &&
"data" in envelope &&
typeof envelope.data === "string"
) {
try {
return [JSON.parse(envelope.data)];
} catch {
return [];
}
}
return [];
});
}
async function subscribeRelay(
filter: Record<string, unknown>,
onEvent: (event: RawEvent) => void,
): Promise<() => void> {
const relayUrl = await invoke<string>("get_relay_ws_url");
const subscriptionId = `desktop-next-${crypto.randomUUID()}`;
let connectionId: number | null = null;
let subscribed = false;
const send = async (frame: unknown[]) => {
if (connectionId === null) return;
await invoke("plugin:websocket|send", {
id: connectionId,
message: { type: "Text", data: JSON.stringify(frame) },
});
};
const inbound = new Channel<unknown>((delivery) => {
for (const frame of relayFrames(delivery)) {
if (!Array.isArray(frame)) continue;
if (frame[0] === "AUTH" && typeof frame[1] === "string") {
void invoke<string>("create_auth_event", {
challenge: frame[1],
relayUrl,
}).then((eventJson) => send(["AUTH", JSON.parse(eventJson)]));
continue;
}
if (frame[0] === "OK" && frame[2] === true && !subscribed) {
subscribed = true;
void send(["REQ", subscriptionId, filter]);
continue;
}
if (frame[0] === "EVENT" && frame[1] === subscriptionId && frame[2]) {
onEvent(frame[2] as RawEvent);
}
}
});
connectionId = await invoke<number>("plugin:websocket|connect", {
url: relayUrl,
onMessage: inbound,
config: {},
});
return () => {
if (connectionId === null) return;
void send(["CLOSE", subscriptionId]).finally(() =>
invoke("plugin:websocket|disconnect", { id: connectionId }),
);
};
}
function channel(raw: RawChannel): BuzzChannel {
return {
id: raw.id,
name: raw.name,
channelType: raw.channel_type,
visibility: raw.visibility,
description: raw.description,
memberCount: raw.member_count,
lastMessageAt: raw.last_message_at,
};
}
function message(raw: RawEvent): Message {
return {
id: raw.id,
pubkey: raw.pubkey,
content: raw.content,
createdAt: raw.created_at,
kind: raw.kind,
tags: raw.tags,
};
}
export const runtime = {
async identity(): Promise<Identity> {
if (useMock) return mockState.identity;
const raw = await invoke<RawIdentity>("get_identity");
return { pubkey: raw.pubkey, displayName: raw.display_name };
},
async relayUrl(): Promise<string> {
if (useMock) return "mock://buzz-community";
return invoke<string>("get_relay_ws_url");
},
async pendingSessionBootstraps() {
if (useMock) return [];
return invoke<
{
operation_id: string;
relay_url: string;
signer_pubkey: string;
channel_id: string;
parent_channel_id: string;
content: string;
}[]
>("list_pending_session_bootstraps");
},
async channelSessions(parentChannelId: string) {
if (useMock) {
return (mockState.sessionLinks[parentChannelId] ?? []).map(
(sessionChannelId) => ({
parent_channel_id: parentChannelId,
session_channel_id: sessionChannelId,
creator_pubkey: mockState.identity.pubkey,
created_at: Math.floor(Date.now() / 1000) - 3_600,
channel: (() => {
const channel = mockState.channels.find(
(candidate) => candidate.id === sessionChannelId,
);
return channel
? {
id: channel.id,
name: channel.name,
channel_type: channel.channelType,
visibility: channel.visibility,
description: channel.description,
member_count: channel.memberCount,
}
: null;
})(),
}),
);
}
return invoke<
{
parent_channel_id: string;
session_channel_id: string;
creator_pubkey: string;
created_at: number;
channel: {
id: string;
name: string;
channel_type: string;
visibility: string;
description: string;
member_count: number;
} | null;
}[]
>("list_channel_sessions", { parentChannelId });
},
async resumeSessionBootstrap(operation: {
operation_id: string;
content: string;
parent_channel_id: string;
}): Promise<void> {
if (useMock) return;
const [relay, identity] = await Promise.all([
this.relayUrl(),
this.identity(),
]);
await invoke("bootstrap_session", {
operationId: operation.operation_id,
content: operation.content,
parentChannelId: operation.parent_channel_id,
expectedRelayUrl: relay,
expectedSignerPubkey: identity.pubkey,
});
},
async channels(): Promise<BuzzChannel[]> {
if (useMock) return [...mockState.channels];
const raw = await invoke<RawChannels>("get_channels", { knownHash: null });
return (raw.channels ?? []).map(channel);
},
async channelWindow({
channelId,
cursor = null,
}: ChannelWindowRequest): Promise<RawEvent[]> {
if (useMock) {
return [
...(mockState.messages[channelId] ?? []).map((message) => ({
id: message.id,
pubkey: message.pubkey,
content: message.content,
created_at: message.createdAt,
kind: message.kind,
tags: message.tags,
})),
{
id: `bounds-${channelId}`,
pubkey: "relay",
content: '{"has_more":false,"next_cursor":null}',
created_at: 0,
kind: 39006,
tags: [
["h", channelId],
["d", `${channelId}:head`],
],
},
];
}
const events = await invoke<RawEvent[]>("get_channel_window", {
channelId,
limitRows: 80,
cursor: cursor
? { created_at: cursor.createdAt, event_id: cursor.id }
: null,
});
return events;
},
async participants(channelId: string): Promise<Participant[]> {
if (useMock) return [...(mockState.participants[channelId] ?? [])];
const response = await invoke<{ members: RawMember[] }>(
"get_channel_members",
{
channelId,
},
);
return response.members.map((member) => ({
pubkey: member.pubkey,
role: member.role,
isAgent: member.is_agent,
displayName: member.display_name,
}));
},
async createSession(
firstMessage: string,
parentChannelId: string,
operationId: string = crypto.randomUUID(),
): Promise<BuzzChannel> {
if (useMock) {
const id = crypto.randomUUID();
const created: BuzzChannel = {
id,
name: firstMessage.trim().slice(0, 56) || "New session",
channelType: "stream",
visibility: "private",
description: "",
memberCount: 1,
lastMessageAt: new Date().toISOString(),
};
mockState.channels.unshift(created);
mockState.messages[id] = [];
mockState.participants[id] = [
{ ...mockState.identity, role: "owner", isAgent: false },
];
await this.sendMessage(id, firstMessage);
return created;
}
const identity = await this.identity();
const relay = await invoke<string>("get_relay_http_url");
const created = await invoke<{
channel_id: string;
created_at: number;
}>("bootstrap_session", {
operationId,
content: firstMessage,
parentChannelId,
expectedRelayUrl: relay,
expectedSignerPubkey: identity.pubkey,
});
return {
id: created.channel_id,
name: firstMessage.trim().slice(0, 56) || "New session",
channelType: "stream",
visibility: "private",
description: "",
memberCount: 1,
lastMessageAt: new Date(created.created_at * 1000).toISOString(),
};
},
async sendMessage(channelId: string, content: string): Promise<Message> {
if (useMock) {
const created: Message = {
id: crypto.randomUUID(),
pubkey: mockState.identity.pubkey,
content,
createdAt: Math.floor(Date.now() / 1000),
kind: 9,
tags: [["h", channelId]],
};
mockState.messages[channelId] = [
...(mockState.messages[channelId] ?? []),
created,
];
return created;
}
const identity = await this.identity();
const relay = await invoke<string>("get_relay_http_url");
const response = await invoke<{ event_id: string; created_at: number }>(
"send_channel_message",
{
channelId,
content,
parentEventId: null,
rootEventId: null,
mediaTags: null,
emojiTags: null,
mentionTags: null,
linkPreviewTags: null,
sentFromThreadTag: null,
mentionPubkeys: null,
kind: 9,
expectedRelayUrl: relay,
expectedSignerPubkey: identity.pubkey,
},
);
return {
id: response.event_id,
pubkey: identity.pubkey,
content,
createdAt: response.created_at,
kind: 9,
tags: [["h", channelId]],
};
},
async subscribeMessages(
channelId: string,
onMessage: (message: Message) => void,
) {
if (useMock) return () => undefined;
return subscribeRelay(
{
kinds: LIVE_MESSAGE_KINDS,
"#h": [channelId],
since: Math.floor(Date.now() / 1000),
},
(event) => onMessage(message(event)),
);
},
async searchPeople(query: string) {
if (useMock) {
return [
{ pubkey: "vogue-agent", display_name: "Vogue", is_agent: true },
{ pubkey: "cynthia-pubkey", display_name: "Cynthia", is_agent: false },
].filter((person) =>
person.display_name.toLowerCase().includes(query.toLowerCase()),
);
}
const [result, managedAgents] = await Promise.all([
invoke<{ users: Record<string, unknown>[] }>("search_users", {
query,
limit: 8,
cursor: null,
}),
invoke<{ pubkey: string; name: string }[]>("list_managed_agents"),
]);
const normalized = query.trim().toLowerCase();
const agents = managedAgents
.filter((agent) => agent.name.toLowerCase().includes(normalized))
.map((agent) => ({
pubkey: agent.pubkey,
display_name: agent.name,
is_agent: true,
}));
const people = result.users as {
pubkey: string;
display_name?: string;
is_agent?: boolean;
}[];
return [
...new Map(
[...agents, ...people].map((person) => [person.pubkey, person]),
).values(),
];
},
async addParticipant(channelId: string, pubkey: string, isAgent: boolean) {
if (useMock) return { added: [pubkey], errors: [] };
const identity = await this.identity();
const managedAgents = isAgent
? await invoke<{ pubkey: string; status: string }[]>(
"list_managed_agents",
)
: [];
const managedAgent = managedAgents.find(
(candidate) => candidate.pubkey === pubkey,
);
const replayFloorUnix = Math.floor(Date.now() / 1000);
const relay = await invoke<string>("get_relay_http_url");
const outcome = await invoke<{ added: string[]; errors: unknown[] }>(
"add_channel_members",
{
channelId,
pubkeys: [pubkey],
role: isAgent ? "bot" : "member",
expectedRelayUrl: relay,
expectedSignerPubkey: identity.pubkey,
},
);
if (
managedAgent &&
managedAgent.status !== "running" &&
managedAgent.status !== "deployed" &&
outcome.errors.length === 0
) {
await invoke("start_managed_agent", {
pubkey,
expectedRelayUrl: await invoke<string>("get_relay_ws_url"),
expectedSignerPubkey: identity.pubkey,
replayFloorUnix,
});
}
return outcome;
},
async observe(onEvent: (event: ObserverEvent) => void) {
if (useMock) {
const events: ObserverEvent[] = [
{
seq: 1,
timestamp: new Date().toISOString(),
kind: "turn_started",
channelId: "design",
sessionId: "vogue-session",
turnId: "turn-1",
agentPubkey: "vogue-agent",
payload: {
agentName: "Vogue",
title: "Reviewing the Session flow",
status: "running",
},
},
...[
"Read the interaction plan",
"Checked participant states",
"Refined the conversation hierarchy",
"Validated narrow layout",
].map((title, index) => ({
seq: index + 2,
timestamp: new Date(Date.now() + index).toISOString(),
kind: "tool_call_completed",
channelId: "design",
sessionId: "vogue-session",
turnId: "turn-1",
agentPubkey: "vogue-agent",
payload: {
agentName: "Vogue",
title,
toolCallId: `tool-${index}`,
status: "completed",
},
})),
];
const timer = window.setTimeout(() => events.forEach(onEvent), 120);
return () => window.clearTimeout(timer);
}
const identity = await this.identity();
return subscribeRelay(
{
kinds: [24200],
"#p": [identity.pubkey],
since: Math.floor(Date.now() / 1000),
},
async (signedEvent) => {
try {
const decrypted = await invoke<ObserverEvent>(
"decrypt_observer_event",
{
eventJson: JSON.stringify(signedEvent),
},
);
const nested =
decrypted.kind === "batch" &&
Array.isArray(decrypted.payload.events)
? (decrypted.payload.events as ObserverEvent[])
: [decrypted];
for (const event of nested) {
onEvent({ ...event, agentPubkey: signedEvent.pubkey });
}
} catch {
// Invalid, forged, or non-owner observer events are ignored.
}
},
);
},
};