mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
Pinky, an AI agent, is updating this PR on Wes's behalf. ## Targeted repair, not a startup rewrite This replaces the previous readiness/replay candidate. Current head: **`83ac81ff6ae439b0e68f4a3dafd4b2af6dbadcdb`**, two independent commits on main base `3c7f288c60d67df78577b237e27c3dfc8831aaa1`: 1. **`0b6681de2` — keep quota backoff on its transport.** HTTP 429s retain native HTTP backoff without arming the renderer WebSocket gate. WebSocket quota/concurrency CLOSED retains its retry deadline without damping HTTP's separate quota. The explicit `rate-limited: shared admission unavailable` signal still damps HTTP too. Reuse existing all-channel live streams for Home mention invalidation instead of a second per-channel mention subscription; retain kind, recipient, self, membership, replay-deduplication, notification and unread boundaries. 2. **`83ac81ff6` — reuse complete membership rosters within each channel fetch.** Discovery already returns full paginated rosters. Retain those results and request only uncovered directory/pending-owner channel IDs. Skip empty fallback filters and preserve covered counts/pubkeys if that tolerant fallback fails. The next fetch reads current membership again; there is no persistent cache or new invalidation state. No send-route, AUTH-readiness, replay-ordering, scheduler, pacing, relay policy, dependency, or authorization changes. Refresh triggers and activity batching stay unchanged. Independent tracing did not establish a safe-to-delete duplicate normal-startup refresh; recovery invalidations at different replay stages are not interchangeable. **Size:** 18 files, +1,066/-350 overall; nine production paths +76/-205 (**129 fewer production lines**). The roster follow-up itself touches only `fetch.rs` (+31/-14) and its new 378-line regression file. Test bulk is not new startup machinery. ## Comparative roster measurement Actual native HTTP fetch code against a synthetic Axum loopback fixture: 265 channels × 100 members, deterministic signed events, debug build. Seven iterations per condition; medians below exclude the first warm-up. Baseline is `0b6681de2` production fetch, candidate is `83ac81ff6` production fetch, with the same temporary benchmark harness. | Measure | Baseline | Candidate | | --- | ---: | ---: | | HTTP reads per fetch | 7 | 6 | | JSON response-body bytes per fetch | 4,803,136 | 2,453,380 | | Median, no artificial request delay | 262.9 ms | 149.3 ms | | Median, 25 ms artificial delay per request | 376.7 ms | 282.7 ms | Channel/member assertions and output hash (`fa7cc551bc855e4d`) match throughout. This removes **one batched roster request**, not 265 requests. The measured ~43%/~25% reductions describe this large-roster synthetic fixture, **not production startup latency**. Activity requests run concurrently with roster enrichment, so real savings vary. Counts now use the discovery snapshot rather than a later reread within that same non-atomic fetch. ## Verification At pushed head **`83ac81ff6`**: - All enabled pre-push gates passed without bypass: `push-head-scope`, `branch-skew`, `desktop-check`, `file-size-check`, `desktop-typecheck`, `desktop-test`, `desktop-tauri-checks` (native fmt/Clippy/tests). - Full Desktop frontend suite: **6,457 passed**, zero failures; separate tooling suite: **10 passed**. - Full native Desktop workspace: **3,276 passed**, zero failures, 20 ignored, across unit/integration/doc bins. The main native lib accounts for 3,175 passed / 19 ignored; this is not 3,276 lib tests. - `pnpm build` passed the protected-feature artifact matrix; `pnpm build:e2e` also passed. Existing chunk-size/dynamic-import warnings remain. No full repository `just ci` run locally. - **130 selected headless mock-bridge smoke cases passed across two runs**: messaging, reconnect, reconnect affordances, inbox live updates, sidebar snapshots, send-channel binding, boot splash. The first invocation hit the shell's 300-second limit after 117 passes and no failures; all remaining 13 passed in a separate invocation. This is not a claim the interrupted invocation completed. - Six new tests invoke production `fetch_channels` through the real native HTTP client and cover 265/501-channel pagination (including same-timestamp continuation), exact requests, complete/deduplicated rosters, directory/pending fallback, hidden DMs, repeated membership/identity/relay changes, empty lists and errors. - Falsification in an isolated source copy: restoring the redundant baseline fetch fails four roster regressions; removing missing-roster fallback fails two. Restored candidate passes all six. Earlier transport mutations fail HTTP→WS coupling, WS→HTTP coupling, omitted shared-outage damping and duplicate-mention regressions. No mutation was applied to the committed worktree. - Brain's independent static reviews found no blockers in the transport repair or immutable roster patch. Source tracing confirms relay discovery serializes complete rosters, not projected matching p-tags. This was source review, not independent test execution or approval. ### Hosted CI [CI run 34185670504](https://github.com/block/buzz/actions/runs/34185670504) **completed successfully** for this head. GitHub checked out merge ref `25bcf09627e129887481d4441a1af3c6cb0af1ff`; its Git tree is identical to `83ac81ff6` (`49a01f3c2ebf4e547bbe3cd31406f984ac6cefe8`), verified through the GitHub Git API. The final PR rollup has 33 successful checks and 28 skipped, with none failing or pending. - Desktop Core passed frontend/native suites and compiled-feature verification. Windows Rust tests, Windows Desktop build, macOS `.app`/`.dmg` build, all four smoke shards and both relay-backed E2E job shards passed, as did Rust lint, DCO, Semgrep and zizmor. - Browser job totals: **1,620 passed, 13 flaky cases passing automatic retry #1, five skipped, zero terminal failures**. Smoke accounts for 1,389 passed / 12 flaky / three skipped; integration jobs report 231 passed / one flaky / two skipped. No manual CI reruns or source changes to obtain green. - The retries cover channel-browser, empty-edit, huddle menu, runtime-install, overscroll, onboarding avatar, video attachment and workflow UI assertions. They are disclosed, not collectively declared unrelated. The video case reaches the changed real TS all-channel hook despite mocking native I/O: independent tracing found no regression mechanism or blocker, but the missing second-video player remains **unexplained**, and an indirect timing effect is not excluded. The self-authored, unmentioned event cannot use either the new mention branch or the removed old mention handler as a delivery path. A passing retry does not diagnose its cause. - The separate “Desktop Release Candidate” check skipped its validation step; it is not additional release validation. Codex security review was skipped, not passed. ## Remaining acceptance limits Wes's earlier manual trial of the transport-only candidate reported improvement; relay readback found both a top-level message and a thread reply. That was not a fingerprinted or timed trial of the new roster commit. The remaining few-second top-level send wait is **unattributed**, and this PR does not claim to resolve every startup delay. No visible app launch, relaunch, laptop input automation, or production test messages during this follow-up. Local native tests use fixtures; browser suites do not run the Tauri backend. CI building an app does not validate bundled macOS alerts or inactive-channel notification delivery. Those bundled real-workflow checks remain outstanding. No visual UI changes or screenshots; no approval or merge performed. Originating conversation: buzz://message?channel=5b130804-d759-40ad-a564-d64cc907fa8e&id=cb432417e290e06a1842639cfe0268f9d43d186defbcb59260930d129a26af83 --------- Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
1071 lines
30 KiB
TypeScript
1071 lines
30 KiB
TypeScript
import { invoke as tauriInvoke } from "@tauri-apps/api/core";
|
|
import {
|
|
fromRawInstallRuntimeResult,
|
|
type RawInstallRuntimeResult,
|
|
} from "@/shared/api/installTypes";
|
|
import type {
|
|
AddChannelMembersInput,
|
|
AddChannelMembersResult,
|
|
BackendProviderCandidate,
|
|
BackendProviderProbeResult,
|
|
CanvasResponse,
|
|
GetHomeFeedInput,
|
|
HomeFeedResponse,
|
|
ManagedAgent,
|
|
ManagedAgentBackend,
|
|
RelayAgent,
|
|
RelayMember,
|
|
RelayMemberRole,
|
|
PresenceLookup,
|
|
PresenceStatus,
|
|
RelayEvent,
|
|
SearchMessagesInput,
|
|
SearchMessagesResponse,
|
|
SetCanvasInput,
|
|
SetCanvasResult,
|
|
ThreadCursor,
|
|
ThreadRepliesResponse,
|
|
CreateManagedAgentInput,
|
|
AgentModelsResponse,
|
|
UpdateManagedAgentInput,
|
|
AcpAvailabilityStatus,
|
|
AcpRuntimeCatalogEntry,
|
|
AuthStatus,
|
|
CommandAvailability,
|
|
InstallRuntimeResult,
|
|
GitBashPrerequisite,
|
|
RuntimeConfigSurface,
|
|
} from "@/shared/api/types";
|
|
|
|
export * from "@/shared/api/tauriChannels";
|
|
export { sendChannelMessage } from "@/shared/api/tauriMessages";
|
|
export { getEventById, getEventsByIds } from "@/shared/api/tauriEvents";
|
|
|
|
type RawPresenceLookup = Record<string, PresenceStatus>;
|
|
|
|
type RawAddChannelMembersResult = {
|
|
added: string[];
|
|
errors: Array<{
|
|
pubkey: string;
|
|
error: string;
|
|
}>;
|
|
};
|
|
|
|
type RawFeedItem = {
|
|
id: string;
|
|
kind: number;
|
|
pubkey: string;
|
|
content: string;
|
|
created_at: number;
|
|
channel_id: string | null;
|
|
channel_name: string;
|
|
channel_type: string | null;
|
|
tags: string[][];
|
|
category: HomeFeedResponse["feed"]["mentions"][number]["category"];
|
|
};
|
|
|
|
type RawHomeFeedResponse = {
|
|
feed: {
|
|
mentions: RawFeedItem[];
|
|
needs_action: RawFeedItem[];
|
|
activity: RawFeedItem[];
|
|
agent_activity: RawFeedItem[];
|
|
};
|
|
meta: {
|
|
since: number;
|
|
total: number;
|
|
generated_at: number;
|
|
};
|
|
};
|
|
|
|
type RawSearchHit = {
|
|
event_id: string;
|
|
content: string;
|
|
kind: number;
|
|
pubkey: string;
|
|
channel_id: string | null;
|
|
channel_name: string | null;
|
|
created_at: number;
|
|
score: number;
|
|
};
|
|
|
|
type RawSearchResponse = {
|
|
hits: RawSearchHit[];
|
|
found: number;
|
|
};
|
|
|
|
type RawRelayAgent = {
|
|
pubkey: string;
|
|
owner_pubkey?: string | null;
|
|
name: string;
|
|
agent_type: string;
|
|
channels: string[];
|
|
channel_ids: string[];
|
|
capabilities: string[];
|
|
status: RelayAgent["status"];
|
|
respond_to?: RelayAgent["respondTo"];
|
|
respond_to_allowlist?: string[];
|
|
};
|
|
import type { RestartDiffEntry as RawRestartDiffEntry } from "./restartDiff";
|
|
export type RawManagedAgent = {
|
|
pubkey: string;
|
|
name: string;
|
|
persona_id: string | null;
|
|
// Optional: pre-feature fixtures may omit it. The record's harness/runtime id.
|
|
runtime?: string | null;
|
|
team_id?: string | null;
|
|
relay_url: string;
|
|
acp_command: string;
|
|
agent_command: string;
|
|
agent_command_override?: string | null;
|
|
agent_args: string[];
|
|
mcp_command: string;
|
|
turn_timeout_seconds: number;
|
|
idle_timeout_seconds: number | null;
|
|
max_turn_duration_seconds: number | null;
|
|
parallelism: number;
|
|
system_prompt: string | null;
|
|
avatar_url?: string | null;
|
|
model: string | null;
|
|
model_source?: ManagedAgent["modelSource"];
|
|
provider: string | null;
|
|
persona_out_of_date: boolean;
|
|
persona_orphaned: boolean;
|
|
needs_restart: boolean;
|
|
restart_diff?: RawRestartDiffEntry[];
|
|
env_vars?: Record<string, string>;
|
|
status: ManagedAgent["status"];
|
|
pid: number | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
last_started_at: string | null;
|
|
last_stopped_at: string | null;
|
|
last_exit_code: number | null;
|
|
last_error: string | null;
|
|
last_error_code: number | null;
|
|
log_path: string;
|
|
start_on_app_launch: boolean;
|
|
auto_restart_on_config_change?: boolean;
|
|
backend: ManagedAgentBackend;
|
|
backend_agent_id: string | null;
|
|
// Pre-feature fixtures may omit these; mapped to "owner-only"/[] in fromRawManagedAgent.
|
|
respond_to?: ManagedAgent["respondTo"];
|
|
respond_to_allowlist?: string[];
|
|
};
|
|
|
|
type RawCreateManagedAgentResponse = {
|
|
agent: RawManagedAgent;
|
|
private_key_nsec: string;
|
|
profile_sync_error: string | null;
|
|
spawn_error: string | null;
|
|
};
|
|
|
|
type RawManagedAgentLog = {
|
|
content: string;
|
|
log_path: string;
|
|
};
|
|
|
|
export type RawAcpRuntimeCatalogEntry = {
|
|
id: string;
|
|
label: string;
|
|
avatar_url: string;
|
|
availability: AcpAvailabilityStatus;
|
|
command: string | null;
|
|
binary_path: string | null;
|
|
default_args: string[];
|
|
mcp_command: string | null;
|
|
model_env_var?: string | null;
|
|
provider_env_var?: string | null;
|
|
thinking_env_var?: string | null;
|
|
max_tokens_env_var?: string | null;
|
|
context_limit_env_var?: string | null;
|
|
max_rounds_env_var?: string | null;
|
|
install_hint: string;
|
|
install_instructions_url: string;
|
|
can_auto_install: boolean;
|
|
requires_external_cli?: boolean;
|
|
underlying_cli_path: string | null;
|
|
node_required: boolean;
|
|
auth_status: AuthStatus;
|
|
login_hint?: string;
|
|
source: "builtin" | "preset" | "custom";
|
|
/** Definition-level env vars for `source: custom` entries; absent for builtin/preset. */
|
|
definition_env?: Record<string, string>;
|
|
max_parallelism?: number;
|
|
effort_canonical_values?: string[] | null;
|
|
};
|
|
|
|
export type {
|
|
RawInstallRuntimeResult,
|
|
RawInstallStepResult,
|
|
} from "./installTypes";
|
|
|
|
type RawGitBashPrerequisite = {
|
|
available: boolean;
|
|
path: string | null;
|
|
install_instructions_url: string;
|
|
install_hint: string;
|
|
};
|
|
|
|
type RawCommandAvailability = {
|
|
command: string;
|
|
resolved_path: string | null;
|
|
available: boolean;
|
|
};
|
|
|
|
type RawManagedAgentPrereqs = {
|
|
acp: RawCommandAvailability;
|
|
mcp: RawCommandAvailability;
|
|
};
|
|
|
|
type RawRelayMember = {
|
|
pubkey: string;
|
|
role: string;
|
|
added_by: string | null;
|
|
created_at: string;
|
|
};
|
|
|
|
type RawListRelayMembersResponse = {
|
|
members: RawRelayMember[];
|
|
};
|
|
|
|
type RawCanvasResponse = {
|
|
content: string | null;
|
|
updated_at: number | null;
|
|
author: string | null;
|
|
};
|
|
|
|
type RawSetCanvasResult = {
|
|
ok: boolean;
|
|
event_id: string;
|
|
};
|
|
|
|
/** Error normalized from a rejected Tauri invocation with its wire payload. */
|
|
export class TauriInvokeError extends Error {
|
|
readonly payload: unknown;
|
|
|
|
constructor(message: string, payload: unknown) {
|
|
super(message);
|
|
this.name = "TauriInvokeError";
|
|
this.payload = payload;
|
|
}
|
|
}
|
|
|
|
function toTauriError(error: unknown): Error {
|
|
if (error instanceof Error) {
|
|
return error;
|
|
}
|
|
|
|
if (typeof error === "string") {
|
|
return new TauriInvokeError(error, error);
|
|
}
|
|
|
|
if (
|
|
typeof error === "object" &&
|
|
error !== null &&
|
|
"message" in error &&
|
|
typeof error.message === "string"
|
|
) {
|
|
return new TauriInvokeError(error.message, error);
|
|
}
|
|
|
|
try {
|
|
return new TauriInvokeError(JSON.stringify(error), error);
|
|
} catch {
|
|
return new TauriInvokeError("Unknown Tauri error", error);
|
|
}
|
|
}
|
|
|
|
export async function invokeTauri<T>(
|
|
command: string,
|
|
args?: Record<string, unknown>,
|
|
): Promise<T> {
|
|
try {
|
|
return await tauriInvoke<T>(command, args);
|
|
} catch (error) {
|
|
// HTTP backoff lives in Rust. Do not apply its separate ApiCalls quota
|
|
// to the WebSocket gate, but preserve the failure for the caller.
|
|
throw toTauriError(error);
|
|
}
|
|
}
|
|
|
|
export function fromRawFeedItem(item: RawFeedItem) {
|
|
return {
|
|
id: item.id,
|
|
kind: item.kind,
|
|
pubkey: item.pubkey,
|
|
content: item.content,
|
|
createdAt: item.created_at,
|
|
channelId: item.channel_id,
|
|
channelName: item.channel_name,
|
|
// Canonicalize the wire `null` to undefined so FeedItem's optional
|
|
// channelType contract holds at runtime (enrichment and the DM
|
|
// notification filter both key off `=== undefined`).
|
|
channelType: item.channel_type ?? undefined,
|
|
tags: item.tags,
|
|
category: item.category,
|
|
};
|
|
}
|
|
|
|
function fromRawSearchHit(hit: RawSearchHit) {
|
|
return {
|
|
eventId: hit.event_id,
|
|
content: hit.content,
|
|
kind: hit.kind,
|
|
pubkey: hit.pubkey,
|
|
channelId: hit.channel_id,
|
|
channelName: hit.channel_name,
|
|
createdAt: hit.created_at,
|
|
score: hit.score,
|
|
};
|
|
}
|
|
|
|
export async function getPresence(pubkeys: string[]): Promise<PresenceLookup> {
|
|
const response = await invokeTauri<RawPresenceLookup>("get_presence", {
|
|
pubkeys,
|
|
});
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(response).map(([pubkey, status]) => [
|
|
pubkey.toLowerCase(),
|
|
status,
|
|
]),
|
|
);
|
|
}
|
|
|
|
export function getDefaultRelayUrl(): Promise<string> {
|
|
return invokeTauri<string>("get_default_relay_url");
|
|
}
|
|
|
|
export function autoConnectDefaultRelayEnabled(): Promise<boolean> {
|
|
return invokeTauri<boolean>("auto_connect_default_relay_enabled");
|
|
}
|
|
|
|
export function isSharedIdentity(): Promise<boolean> {
|
|
return invokeTauri<boolean>("is_shared_identity");
|
|
}
|
|
|
|
export function getRelayWsUrl(): Promise<string> {
|
|
return invokeTauri<string>("get_relay_ws_url");
|
|
}
|
|
|
|
export function getRelayHttpUrl(): Promise<string> {
|
|
return invokeTauri<string>("get_relay_http_url");
|
|
}
|
|
|
|
export async function addChannelMembers(
|
|
input: AddChannelMembersInput,
|
|
): Promise<AddChannelMembersResult> {
|
|
return invokeTauri<RawAddChannelMembersResult>("add_channel_members", input);
|
|
}
|
|
|
|
export async function removeChannelMember(
|
|
channelId: string,
|
|
pubkey: string,
|
|
): Promise<void> {
|
|
await invokeTauri("remove_channel_member", { channelId, pubkey });
|
|
}
|
|
|
|
export async function changeChannelMemberRole(
|
|
channelId: string,
|
|
pubkey: string,
|
|
role: string,
|
|
): Promise<void> {
|
|
await invokeTauri("change_channel_member_role", { channelId, pubkey, role });
|
|
}
|
|
|
|
export async function joinChannel(channelId: string): Promise<void> {
|
|
await invokeTauri("join_channel", { channelId });
|
|
}
|
|
|
|
export async function leaveChannel(channelId: string): Promise<void> {
|
|
await invokeTauri("leave_channel", { channelId });
|
|
}
|
|
|
|
export async function getCanvas(channelId: string): Promise<CanvasResponse> {
|
|
const response = await invokeTauri<RawCanvasResponse>("get_canvas", {
|
|
channelId,
|
|
});
|
|
return {
|
|
content: response.content,
|
|
// Normalize absent keys to null: ensureWelcomeCanvas treats null as
|
|
// "no canvas yet", and `undefined !== null` would make every fresh
|
|
// channel look already-seeded.
|
|
updatedAt: response.updated_at ?? null,
|
|
author: response.author ?? null,
|
|
};
|
|
}
|
|
|
|
export async function setCanvas(
|
|
input: SetCanvasInput,
|
|
): Promise<SetCanvasResult> {
|
|
const response = await invokeTauri<RawSetCanvasResult>("set_canvas", {
|
|
channelId: input.channelId,
|
|
content: input.content,
|
|
});
|
|
return {
|
|
ok: response.ok,
|
|
eventId: response.event_id,
|
|
};
|
|
}
|
|
|
|
export async function getHomeFeed(
|
|
input: GetHomeFeedInput = {},
|
|
): Promise<HomeFeedResponse> {
|
|
const response = await invokeTauri<RawHomeFeedResponse>("get_feed", input);
|
|
|
|
return {
|
|
feed: {
|
|
mentions: response.feed.mentions.map(fromRawFeedItem),
|
|
needsAction: response.feed.needs_action.map(fromRawFeedItem),
|
|
activity: response.feed.activity.map(fromRawFeedItem),
|
|
agentActivity: response.feed.agent_activity.map(fromRawFeedItem),
|
|
},
|
|
meta: {
|
|
since: response.meta.since,
|
|
total: response.meta.total,
|
|
generatedAt: response.meta.generated_at,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function searchMessages(
|
|
input: SearchMessagesInput,
|
|
): Promise<SearchMessagesResponse> {
|
|
const response = await invokeTauri<RawSearchResponse>("search_messages", {
|
|
q: input.q,
|
|
limit: input.limit,
|
|
channelId: input.channelId,
|
|
authors: input.authors,
|
|
since: input.since,
|
|
until: input.until,
|
|
});
|
|
|
|
return {
|
|
hits: response.hits.map(fromRawSearchHit),
|
|
found: response.found,
|
|
};
|
|
}
|
|
|
|
type RawThreadCursor = {
|
|
created_at: number;
|
|
event_id: string;
|
|
};
|
|
|
|
type RawThreadRepliesResponse = {
|
|
events: RelayEvent[];
|
|
next_cursor: RawThreadCursor | null;
|
|
};
|
|
|
|
/**
|
|
* Fetch the full reply subtree under a thread root, server-side.
|
|
*
|
|
* Unlike the channel timeline (which the desktop assembles from its local
|
|
* cache), this walks `thread_metadata` on the relay, so a thread renders
|
|
* complete even when its replies fell outside the channel cold-load window —
|
|
* the descendant gap that made deep/old threads silently incomplete.
|
|
*
|
|
* Paging is forward keyset on `(createdAt, eventId)`: pass the returned
|
|
* `nextCursor` back as `cursor` for the next page. `nextCursor` is non-null only
|
|
* when a full page was returned. The returned `events` are raw nostr events
|
|
* (`RelayEvent`), chronological (oldest first). These are the *replies* under
|
|
* the root (depth >= 1); the root event itself is NOT returned (the relay query
|
|
* keys on `root_event_id`, which a root row lacks). The caller already holds
|
|
* the root — it is the open thread head.
|
|
*/
|
|
export async function getThreadReplies(
|
|
rootEventId: string,
|
|
channelId?: string | null,
|
|
options?: {
|
|
limit?: number;
|
|
depthLimit?: number;
|
|
cursor?: ThreadCursor | null;
|
|
},
|
|
): Promise<ThreadRepliesResponse> {
|
|
const response = await invokeTauri<RawThreadRepliesResponse>(
|
|
"get_thread_replies",
|
|
{
|
|
rootEventId,
|
|
channelId: channelId ?? null,
|
|
limit: options?.limit ?? null,
|
|
depthLimit: options?.depthLimit ?? null,
|
|
cursor: options?.cursor
|
|
? {
|
|
created_at: options.cursor.createdAt,
|
|
event_id: options.cursor.eventId,
|
|
}
|
|
: null,
|
|
},
|
|
);
|
|
|
|
return {
|
|
events: response.events,
|
|
nextCursor: response.next_cursor
|
|
? {
|
|
createdAt: response.next_cursor.created_at,
|
|
eventId: response.next_cursor.event_id,
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
export type BlobDescriptor = {
|
|
url: string;
|
|
sha256: string;
|
|
size: number;
|
|
type: string;
|
|
uploaded: number;
|
|
dim?: string;
|
|
blurhash?: string;
|
|
thumb?: string;
|
|
duration?: number;
|
|
image?: string;
|
|
/** Original filename captured client-side. */
|
|
filename?: string;
|
|
};
|
|
|
|
export async function uploadMedia(
|
|
filePath: string,
|
|
isTemp: boolean,
|
|
): Promise<BlobDescriptor> {
|
|
return invokeTauri<BlobDescriptor>("upload_media", {
|
|
filePath,
|
|
isTemp,
|
|
});
|
|
}
|
|
export async function pickAndUploadMedia(
|
|
progressId?: string,
|
|
): Promise<BlobDescriptor[]> {
|
|
return invokeTauri<BlobDescriptor[]>("pick_and_upload_media", { progressId });
|
|
}
|
|
export async function uploadMediaBytes(
|
|
data: number[],
|
|
filename?: string,
|
|
/** Correlation id for `media-upload-progress` events from the Rust side. */
|
|
progressId?: string,
|
|
): Promise<BlobDescriptor> {
|
|
return invokeTauri<BlobDescriptor>("upload_media_bytes", {
|
|
data,
|
|
filename,
|
|
progressId,
|
|
});
|
|
}
|
|
|
|
export { editMessage } from "@/shared/api/editMessage";
|
|
|
|
export async function deleteMessage(
|
|
channelId: string,
|
|
eventId: string,
|
|
): Promise<void> {
|
|
await invokeTauri("delete_message", { channelId, eventId });
|
|
}
|
|
|
|
export async function addReaction(
|
|
eventId: string,
|
|
emoji: string,
|
|
emojiUrl?: string,
|
|
): Promise<void> {
|
|
await invokeTauri("add_reaction", { eventId, emoji, emojiUrl });
|
|
}
|
|
|
|
export async function removeReaction(
|
|
eventId: string,
|
|
emoji: string,
|
|
): Promise<void> {
|
|
await invokeTauri("remove_reaction", { eventId, emoji });
|
|
}
|
|
|
|
export async function signRelayEvent(input: {
|
|
kind: number;
|
|
content: string;
|
|
createdAt?: number;
|
|
tags: string[][];
|
|
}): Promise<RelayEvent> {
|
|
const eventJson = await invokeTauri<string>("sign_event", input);
|
|
return JSON.parse(eventJson) as RelayEvent;
|
|
}
|
|
|
|
export async function createAuthEvent(input: {
|
|
challenge: string;
|
|
relayUrl: string;
|
|
}): Promise<RelayEvent> {
|
|
const eventJson = await invokeTauri<string>("create_auth_event", input);
|
|
return JSON.parse(eventJson) as RelayEvent;
|
|
}
|
|
function fromRawRelayAgent(agent: RawRelayAgent): RelayAgent {
|
|
return {
|
|
pubkey: agent.pubkey,
|
|
ownerPubkey: agent.owner_pubkey ?? null,
|
|
name: agent.name,
|
|
agentType: agent.agent_type,
|
|
channels: agent.channels,
|
|
channelIds: agent.channel_ids ?? [],
|
|
capabilities: agent.capabilities,
|
|
status: agent.status,
|
|
respondTo: agent.respond_to ?? null,
|
|
respondToAllowlist: agent.respond_to_allowlist ?? [],
|
|
};
|
|
}
|
|
|
|
export function fromRawManagedAgent(agent: RawManagedAgent): ManagedAgent {
|
|
return {
|
|
pubkey: agent.pubkey,
|
|
name: agent.name,
|
|
personaId: agent.persona_id,
|
|
runtime: agent.runtime ?? null,
|
|
teamId: agent.team_id ?? null,
|
|
relayUrl: agent.relay_url,
|
|
acpCommand: agent.acp_command,
|
|
agentCommand: agent.agent_command,
|
|
agentCommandOverride: agent.agent_command_override ?? null,
|
|
agentArgs: agent.agent_args,
|
|
mcpCommand: agent.mcp_command,
|
|
turnTimeoutSeconds: agent.turn_timeout_seconds,
|
|
idleTimeoutSeconds: agent.idle_timeout_seconds,
|
|
maxTurnDurationSeconds: agent.max_turn_duration_seconds,
|
|
parallelism: agent.parallelism,
|
|
systemPrompt: agent.system_prompt,
|
|
avatarUrl: agent.avatar_url ?? null,
|
|
model: agent.model,
|
|
modelSource: agent.model_source ?? null,
|
|
provider: agent.provider ?? null,
|
|
personaOutOfDate: agent.persona_out_of_date ?? false,
|
|
personaOrphaned: agent.persona_orphaned ?? false,
|
|
needsRestart: agent.needs_restart ?? false,
|
|
restartDiff: agent.restart_diff ?? [],
|
|
envVars: agent.env_vars ?? {},
|
|
status: agent.status,
|
|
pid: agent.pid,
|
|
createdAt: agent.created_at,
|
|
updatedAt: agent.updated_at,
|
|
lastStartedAt: agent.last_started_at,
|
|
lastStoppedAt: agent.last_stopped_at,
|
|
lastExitCode: agent.last_exit_code,
|
|
lastError: agent.last_error,
|
|
lastErrorCode: agent.last_error_code ?? null,
|
|
logPath: agent.log_path,
|
|
startOnAppLaunch: agent.start_on_app_launch,
|
|
autoRestartOnConfigChange: agent.auto_restart_on_config_change ?? true,
|
|
backend: agent.backend,
|
|
backendAgentId: agent.backend_agent_id,
|
|
respondTo: agent.respond_to ?? "owner-only",
|
|
respondToAllowlist: agent.respond_to_allowlist ?? [],
|
|
};
|
|
}
|
|
|
|
export function fromRawAcpRuntimeCatalogEntry(
|
|
entry: RawAcpRuntimeCatalogEntry,
|
|
): AcpRuntimeCatalogEntry {
|
|
return {
|
|
id: entry.id,
|
|
label: entry.label,
|
|
avatarUrl: entry.avatar_url,
|
|
availability: entry.availability,
|
|
command: entry.command,
|
|
binaryPath: entry.binary_path,
|
|
defaultArgs: entry.default_args,
|
|
mcpCommand: entry.mcp_command,
|
|
modelEnvVar: entry.model_env_var ?? null,
|
|
providerEnvVar: entry.provider_env_var ?? null,
|
|
thinkingEnvVar: entry.thinking_env_var ?? null,
|
|
maxTokensEnvVar: entry.max_tokens_env_var ?? null,
|
|
contextLimitEnvVar: entry.context_limit_env_var ?? null,
|
|
maxRoundsEnvVar: entry.max_rounds_env_var ?? null,
|
|
installHint: entry.install_hint,
|
|
installInstructionsUrl: entry.install_instructions_url,
|
|
canAutoInstall: entry.can_auto_install,
|
|
requiresExternalCli: entry.requires_external_cli ?? false,
|
|
underlyingCliPath: entry.underlying_cli_path,
|
|
nodeRequired: entry.node_required,
|
|
authStatus: entry.auth_status,
|
|
loginHint: entry.login_hint ?? null,
|
|
source: entry.source,
|
|
definitionEnv: entry.definition_env ?? {},
|
|
effortCanonicalValues: entry.effort_canonical_values ?? null,
|
|
...(entry.max_parallelism !== undefined && {
|
|
maxParallelism: entry.max_parallelism,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function fromRawCommandAvailability(
|
|
command: RawCommandAvailability,
|
|
): CommandAvailability {
|
|
return {
|
|
command: command.command,
|
|
resolvedPath: command.resolved_path,
|
|
available: command.available,
|
|
};
|
|
}
|
|
|
|
// ── Relay Members ────────────────────────────────────────────────────────────
|
|
|
|
function fromRawRelayMember(raw: RawRelayMember): RelayMember {
|
|
return {
|
|
pubkey: raw.pubkey,
|
|
role: raw.role as RelayMemberRole,
|
|
addedBy: raw.added_by,
|
|
createdAt: raw.created_at,
|
|
};
|
|
}
|
|
|
|
export async function listRelayMembers(): Promise<RelayMember[]> {
|
|
const response =
|
|
await invokeTauri<RawListRelayMembersResponse>("list_relay_members");
|
|
return response.members.map(fromRawRelayMember);
|
|
}
|
|
|
|
export async function getMyRelayMembership(): Promise<RelayMember | null> {
|
|
try {
|
|
const raw = await invokeTauri<RawRelayMember>("get_my_relay_membership");
|
|
return fromRawRelayMember(raw);
|
|
} catch (error) {
|
|
// "relay returned 404 Not Found" = not a relay member — return null so
|
|
// the UI hides the Members tab. Re-throw real errors (network, auth, 500)
|
|
// so React Query surfaces them.
|
|
if (
|
|
error instanceof Error &&
|
|
error.message.startsWith("relay returned 404")
|
|
) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function addRelayMember(
|
|
targetPubkey: string,
|
|
role: string,
|
|
): Promise<void> {
|
|
await invokeTauri("add_relay_member", { targetPubkey, role });
|
|
}
|
|
|
|
export async function removeRelayMember(targetPubkey: string): Promise<void> {
|
|
await invokeTauri("remove_relay_member", { targetPubkey });
|
|
}
|
|
|
|
export async function changeRelayMemberRole(
|
|
targetPubkey: string,
|
|
newRole: string,
|
|
): Promise<void> {
|
|
await invokeTauri("change_relay_member_role", { targetPubkey, newRole });
|
|
}
|
|
|
|
export async function listRelayAgents(): Promise<RelayAgent[]> {
|
|
return (await invokeTauri<RawRelayAgent[]>("list_relay_agents")).map(
|
|
fromRawRelayAgent,
|
|
);
|
|
}
|
|
|
|
export async function listManagedAgents(): Promise<ManagedAgent[]> {
|
|
return (await invokeTauri<RawManagedAgent[]>("list_managed_agents")).map(
|
|
fromRawManagedAgent,
|
|
);
|
|
}
|
|
export async function createManagedAgent(input: CreateManagedAgentInput) {
|
|
const response = await invokeTauri<RawCreateManagedAgentResponse>(
|
|
"create_managed_agent",
|
|
{
|
|
input: {
|
|
name: input.name,
|
|
personaId: input.personaId,
|
|
teamId: input.teamId,
|
|
relayUrl: input.relayUrl,
|
|
acpCommand: input.acpCommand,
|
|
agentCommand: input.agentCommand,
|
|
harnessOverride: input.harnessOverride ?? false,
|
|
agentArgs: input.agentArgs,
|
|
mcpCommand: input.mcpCommand,
|
|
turnTimeoutSeconds: input.turnTimeoutSeconds,
|
|
idleTimeoutSeconds: input.idleTimeoutSeconds,
|
|
maxTurnDurationSeconds: input.maxTurnDurationSeconds,
|
|
parallelism: input.parallelism,
|
|
systemPrompt: input.systemPrompt,
|
|
avatarUrl: input.avatarUrl,
|
|
model: input.model,
|
|
provider: input.provider,
|
|
envVars: input.envVars ?? {},
|
|
spawnAfterCreate: input.spawnAfterCreate,
|
|
startOnAppLaunch: input.startOnAppLaunch,
|
|
backend: input.backend,
|
|
respondTo: input.respondTo,
|
|
respondToAllowlist: input.respondToAllowlist,
|
|
relayMesh: input.relayMesh,
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
agent: fromRawManagedAgent(response.agent),
|
|
privateKeyNsec: response.private_key_nsec,
|
|
profileSyncError: response.profile_sync_error,
|
|
spawnError: response.spawn_error,
|
|
};
|
|
}
|
|
|
|
export async function deleteManagedAgent(
|
|
pubkey: string,
|
|
forceRemoteDelete?: boolean,
|
|
): Promise<void> {
|
|
await invokeTauri("delete_managed_agent", {
|
|
pubkey,
|
|
forceRemoteDelete: forceRemoteDelete ?? null,
|
|
});
|
|
}
|
|
|
|
export async function getManagedAgentLog(pubkey: string, lineCount?: number) {
|
|
const response = await invokeTauri<RawManagedAgentLog>(
|
|
"get_managed_agent_log",
|
|
{
|
|
pubkey,
|
|
lineCount,
|
|
},
|
|
);
|
|
|
|
return {
|
|
content: response.content,
|
|
logPath: response.log_path,
|
|
};
|
|
}
|
|
|
|
export async function discoverGitBashPrerequisite(): Promise<GitBashPrerequisite | null> {
|
|
const prerequisite = await invokeTauri<RawGitBashPrerequisite | null>(
|
|
"discover_git_bash_prerequisite",
|
|
);
|
|
return (
|
|
prerequisite && {
|
|
available: prerequisite.available,
|
|
path: prerequisite.path,
|
|
installInstructionsUrl: prerequisite.install_instructions_url,
|
|
installHint: prerequisite.install_hint,
|
|
}
|
|
);
|
|
}
|
|
|
|
/** Input shape for creating or updating a custom harness. */
|
|
export type HarnessDefinitionInput = {
|
|
id: string;
|
|
label: string;
|
|
command: string;
|
|
args?: string[];
|
|
env?: Record<string, string>;
|
|
installInstructionsUrl?: string;
|
|
installHint?: string;
|
|
};
|
|
|
|
/** Save (create or overwrite) a custom harness definition. Returns the catalog entry. */
|
|
export async function saveCustomHarness(
|
|
definition: HarnessDefinitionInput,
|
|
originalId?: string,
|
|
): Promise<AcpRuntimeCatalogEntry> {
|
|
const raw = await invokeTauri<RawAcpRuntimeCatalogEntry>(
|
|
"save_custom_harness",
|
|
{
|
|
definition: {
|
|
id: definition.id,
|
|
label: definition.label,
|
|
command: definition.command,
|
|
args: definition.args ?? [],
|
|
env: definition.env ?? {},
|
|
installInstructionsUrl: definition.installInstructionsUrl ?? "",
|
|
installHint: definition.installHint ?? "",
|
|
},
|
|
originalId: originalId ?? null,
|
|
},
|
|
);
|
|
return fromRawAcpRuntimeCatalogEntry(raw);
|
|
}
|
|
|
|
/** Delete a custom harness definition by id. No-op if already gone. */
|
|
export async function deleteCustomHarness(id: string): Promise<void> {
|
|
await invokeTauri<void>("delete_custom_harness", { id });
|
|
}
|
|
|
|
export async function installAcpRuntime(
|
|
runtimeId: string,
|
|
): Promise<InstallRuntimeResult> {
|
|
const raw = await invokeTauri<RawInstallRuntimeResult>(
|
|
"install_acp_runtime",
|
|
{ runtimeId },
|
|
);
|
|
return fromRawInstallRuntimeResult(raw);
|
|
}
|
|
|
|
export async function discoverManagedAgentPrereqs(input: {
|
|
acpCommand?: string;
|
|
mcpCommand?: string;
|
|
}) {
|
|
const response = await invokeTauri<RawManagedAgentPrereqs>(
|
|
"discover_managed_agent_prereqs",
|
|
{
|
|
input: {
|
|
acpCommand: input.acpCommand,
|
|
mcpCommand: input.mcpCommand,
|
|
},
|
|
},
|
|
);
|
|
|
|
return {
|
|
acp: fromRawCommandAvailability(response.acp),
|
|
mcp: fromRawCommandAvailability(response.mcp),
|
|
};
|
|
}
|
|
|
|
// ── Model discovery ───────────────────────────────────────────────────────────
|
|
|
|
export async function getAgentModels(pubkey: string) {
|
|
return invokeTauri<AgentModelsResponse>("get_agent_models", { pubkey });
|
|
}
|
|
|
|
export async function getAgentConfigSurface(
|
|
pubkey: string,
|
|
): Promise<RuntimeConfigSurface> {
|
|
return invokeTauri<RuntimeConfigSurface>("get_agent_config_surface", {
|
|
pubkey,
|
|
});
|
|
}
|
|
|
|
export async function putAgentSessionConfig(
|
|
pubkey: string,
|
|
payload: unknown,
|
|
): Promise<void> {
|
|
return invokeTauri<void>("put_agent_session_config", { pubkey, payload });
|
|
}
|
|
|
|
/** File-layer config for a runtime (e.g. `~/.config/goose/config.yaml`). */
|
|
export type RuntimeFileConfigSubset = {
|
|
/** Provider set in the harness config file. */
|
|
provider: string | null;
|
|
/** Model set in the harness config file. */
|
|
model: string | null;
|
|
/** Credential env key names whose values are present in the file config. */
|
|
satisfiedEnvKeys: string[];
|
|
};
|
|
|
|
/**
|
|
* Get the file-layer config for a runtime so dialogs can show "Set in goose config" instead of
|
|
* surfacing a false required-field marker. Returns `null` when unavailable or unparseable.
|
|
*/
|
|
export async function getRuntimeFileConfig(
|
|
runtimeId: string,
|
|
): Promise<RuntimeFileConfigSubset | null> {
|
|
return invokeTauri<RuntimeFileConfigSubset | null>(
|
|
"get_runtime_file_config",
|
|
{
|
|
runtimeId,
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return the key names of all non-empty baked build env vars. Internal (Block) builds bake
|
|
* provider credentials into the binary at compile time; this returns *key names only* (never
|
|
* values) so dialogs treat them as satisfied without exposing secrets. OSS builds return [].
|
|
*/
|
|
export async function getBakedBuildEnvKeys(): Promise<string[]> {
|
|
return invokeTauri<string[]>("get_baked_build_env_keys");
|
|
}
|
|
|
|
/**
|
|
* A single baked build env entry.
|
|
*
|
|
* The value is already masked in Rust for secret keys (keys not in the
|
|
* explicit safe-to-reveal allowlist: `BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL`,
|
|
* `DATABRICKS_HOST`, `DATABRICKS_MODEL`, `DATABRICKS_MODEL_FILTER`). Non-allowlisted keys have their
|
|
* values replaced with `••••••`. Non-secret values are shown as-is.
|
|
* Empty-value keys are filtered out.
|
|
*/
|
|
export type BakedEnvEntry = {
|
|
key: string;
|
|
/** Display value — real value or `••••••` for masked keys. */
|
|
value: string;
|
|
/** `true` when the value was replaced by the mask placeholder in Rust. */
|
|
masked: boolean;
|
|
};
|
|
|
|
/**
|
|
* Return the baked build env entries with values shown (masked where
|
|
* appropriate) for display in the Agent defaults card.
|
|
*
|
|
* Provider and model arrive as `BUZZ_AGENT_PROVIDER` / `BUZZ_AGENT_MODEL`
|
|
* keys and are included in the list alongside other baked vars.
|
|
*
|
|
* OSS builds return an empty array — the baked-env section is hidden.
|
|
*/
|
|
export async function getBakedBuildEnv(): Promise<BakedEnvEntry[]> {
|
|
return invokeTauri<BakedEnvEntry[]>("get_baked_build_env");
|
|
}
|
|
|
|
type RawUpdateManagedAgentResponse = {
|
|
agent: RawManagedAgent;
|
|
profile_sync_error: string | null;
|
|
};
|
|
|
|
export async function updateManagedAgent(
|
|
input: UpdateManagedAgentInput,
|
|
): Promise<{ agent: ManagedAgent; profileSyncError: string | null }> {
|
|
const response = await invokeTauri<RawUpdateManagedAgentResponse>(
|
|
"update_managed_agent",
|
|
{ input },
|
|
);
|
|
return {
|
|
agent: fromRawManagedAgent(response.agent),
|
|
profileSyncError: response.profile_sync_error,
|
|
};
|
|
}
|
|
|
|
// ── Backend provider discovery ────────────────────────────────────────────────
|
|
|
|
export async function discoverBackendProviders(): Promise<
|
|
BackendProviderCandidate[]
|
|
> {
|
|
return invokeTauri<BackendProviderCandidate[]>("discover_backend_providers");
|
|
}
|
|
|
|
export async function probeBackendProvider(
|
|
binaryPath: string,
|
|
): Promise<BackendProviderProbeResult> {
|
|
return invokeTauri<BackendProviderProbeResult>("probe_backend_provider", {
|
|
binaryPath,
|
|
});
|
|
}
|
|
|
|
// ── NIP-44 encrypt-to-self ───────────────────────────────────────────────────
|
|
|
|
export async function nip44EncryptToSelf(plaintext: string): Promise<string> {
|
|
return invokeTauri<string>("nip44_encrypt_to_self", { plaintext });
|
|
}
|
|
|
|
export async function nip44DecryptFromSelf(
|
|
ciphertext: string,
|
|
): Promise<string> {
|
|
return invokeTauri<string>("nip44_decrypt_from_self", { ciphertext });
|
|
}
|
|
|
|
export async function startPairing(): Promise<string> {
|
|
return invokeTauri<string>("start_pairing");
|
|
}
|
|
|
|
export async function confirmPairingSas(): Promise<void> {
|
|
await invokeTauri("confirm_pairing_sas");
|
|
}
|
|
|
|
export async function cancelPairing(): Promise<void> {
|
|
await invokeTauri("cancel_pairing");
|
|
}
|
|
|
|
// Validate a candidate repos dir without mutating the filesystem. Rejects
|
|
// with a human-readable reason; resolves for a valid or empty path.
|
|
export async function validateReposDir(dir: string): Promise<void> {
|
|
await invokeTauri("validate_repos_dir", { dir });
|
|
}
|
|
|
|
export const setPreventSleepActive = (active: boolean) =>
|
|
invokeTauri("set_prevent_sleep_active", { active });
|
|
|
|
/** Returns true on macOS, Windows, and Linux AppImage installs.
|
|
* Returns false on Linux non-AppImage packages (e.g. .deb) where
|
|
* Tauri's updater cannot swap the binary. */
|
|
export function isAutoUpdateSupported(): Promise<boolean> {
|
|
return invokeTauri<boolean>("is_auto_update_supported");
|
|
}
|