Merge remote-tracking branch 'origin/main' into duncan/agent-usage-archive

* origin/main: (22 commits)
  feat(catalog): resolve publisher display name in catalog detail pane (#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
  Refine agent sharing dialog (#3699)
  desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
  fix: align responsive agent views (#3688)
  Add macOS agent menu-bar menu (#3565)
  Fix pending message feedback (#3543)
  fix(desktop): remove remaining Projects panel fills (#3742)
  feat(mobile): desktop-parity emoji and thread experience (#3485)
  desktop: restore direct community member adds (#3634)
  fix(desktop): explain open agent access (#2561)
  fix(cli): resolve agents from owner records (#3178)
  fix(desktop): remove Projects overview card fills (#3416)
  feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268)
  fix(git): channel binding tooling + author remediation for unbound repos (#3626)
  feat: configure S3 URL addressing style (#3400)
  feat: add first-class OpenRouter provider support (#1975)
  feat(agent,acp): wire provider total_tokens through NIP-AM publish chain (#3593)
  chore(release): release Buzz Desktop version 0.5.2 (#3624)
  ...

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Will Pfleger
2026-07-30 11:47:48 -04:00
213 changed files with 21545 additions and 2545 deletions

View File

@@ -0,0 +1,6 @@
import { invokeTauri } from "@/shared/api/tauri";
/** Clears community-scoped agent activity from the native tray menu. */
export function clearTrayAgentActivity(): Promise<void> {
return invokeTauri("clear_tray_agent_activity");
}

View File

@@ -27,6 +27,42 @@ test("mediaProxyUrl: uses the IPv4 loopback literal for the localhost proxy", ()
);
});
test("media-proxy port store: resolved port publishes and reset notifies subscribers", async () => {
const previousWindow = globalThis.window;
let notifications = 0;
globalThis.window = {
__TAURI_INTERNALS__: {
invoke(command) {
if (command === "get_media_proxy_port") return Promise.resolve(54321);
if (command === "get_relay_http_url") {
return Promise.resolve("https://relay.example");
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
},
},
};
try {
const mediaUrl = await import(`./mediaUrl.ts?portStore=${Date.now()}`);
const unsubscribe = mediaUrl.subscribeMediaProxyPort(() => notifications++);
mediaUrl.rewriteRelayUrl(`https://relay.example/media/${HASH}.png`);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(mediaUrl.getCachedMediaProxyPort(), 54321);
assert.equal(notifications, 1);
mediaUrl.resetMediaCaches();
assert.equal(mediaUrl.getCachedMediaProxyPort(), null);
assert.equal(notifications, 2);
unsubscribe();
} finally {
globalThis.window = previousWindow;
}
});
test("relay-origin store: publishes are canonicalized at the store boundary", () => {
// The store must hold the invariant that `cachedRelayOrigin` is always a
// canonical URL origin — consumers compare `new URL(src).origin` against it

View File

@@ -63,10 +63,17 @@ let cacheGeneration = 0;
/** `useSyncExternalStore` listeners for relay-origin changes. */
const relayOriginListeners = new Set<() => void>();
/** `useSyncExternalStore` listeners for media-proxy port changes. */
const mediaProxyPortListeners = new Set<() => void>();
function notifyRelayOriginListeners(): void {
for (const listener of relayOriginListeners) listener();
}
function notifyMediaProxyPortListeners(): void {
for (const listener of mediaProxyPortListeners) listener();
}
/**
* Publish a resolved relay origin, but only if `generation` is still current
* (the fetch wasn't superseded by a workspace switch). The stored snapshot is
@@ -108,6 +115,18 @@ export function subscribeRelayOrigin(listener: () => void): () => void {
};
}
/**
* Subscribe to media-proxy port changes. Returns a stable unsubscribe function
* for `useSyncExternalStore` consumers that need to re-run `rewriteRelayUrl`
* once the async Tauri proxy lookup has resolved.
*/
export function subscribeMediaProxyPort(listener: () => void): () => void {
mediaProxyPortListeners.add(listener);
return () => {
mediaProxyPortListeners.delete(listener);
};
}
const POLL_INTERVAL_MS = 100;
const POLL_TIMEOUT_MS = 5000;
@@ -188,7 +207,10 @@ async function fetchProxyPort(): Promise<number | null> {
deadline,
);
if (port !== null && port > 0 && generation === cacheGeneration) {
cachedPort = port;
if (cachedPort !== port) {
cachedPort = port;
notifyMediaProxyPortListeners();
}
}
} catch {
// invoke failed (e.g. Tauri IPC not ready yet) — keep retrying
@@ -227,8 +249,12 @@ if (typeof window !== "undefined") {
*/
export function resetMediaCaches(): void {
cacheGeneration += 1;
const hadCachedPort = cachedPort !== null;
cachedPort = null;
portPromise = null;
if (hadCachedPort) {
notifyMediaProxyPortListeners();
}
if (cachedRelayOrigin !== null) {
cachedRelayOrigin = null;
notifyRelayOriginListeners();
@@ -246,6 +272,14 @@ export function getCachedRelayOrigin(): string | null {
return cachedRelayOrigin;
}
/**
* The localhost proxy port if it has been resolved, else `null`. Synchronous
* best-effort read of the same cache `rewriteRelayUrl` uses.
*/
export function getCachedMediaProxyPort(): number | null {
return cachedPort;
}
/**
* Build the local proxy URL with an IPv4 literal. The Rust proxy binds
* `127.0.0.1:0`, not `::1`, and some WebViews resolve `localhost` to IPv6

View File

@@ -23,6 +23,34 @@ export function safeNpub(pubkey: string): string | null {
}
}
const HEX_PUBKEY_REGEX = /^[0-9a-f]{64}$/;
/**
* Parse user-entered public key input — either a 64-character hex pubkey or
* a bech32 `npub1…` string — into a lowercase hex pubkey. Returns null for
* anything else (does NOT throw — intended for live form validation).
*
* The input is trimmed first; surrounding whitespace from copy-paste is
* tolerated.
*/
export function parsePubkeyInput(input: string): string | null {
const trimmed = input.trim().toLowerCase();
if (HEX_PUBKEY_REGEX.test(trimmed)) {
return trimmed;
}
if (trimmed.startsWith("npub1")) {
try {
const decoded = decode(trimmed);
if (decoded.type === "npub") {
return decoded.data;
}
} catch {
return null;
}
}
return null;
}
/**
* Decode a bech32 nsec string and derive the matching npub. Returns null if
* the input is not a syntactically valid `nsec1…` (does NOT throw — this is

View File

@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parsePubkeyInput } from "./nostrUtils.ts";
const HEX = "ea9b4d7a7a78a3e3729e5568b14d764d4962be0e1f20f749bcf8d9dbbf9a9328";
const NPUB = "npub1a2d567n60z37xu57245tzntkf4yk90swrus0wjdulrvah0u6jv5qusyp60";
describe("parsePubkeyInput", () => {
it("accepts a lowercase 64-char hex pubkey", () => {
assert.equal(parsePubkeyInput(HEX), HEX);
});
it("lowercases an uppercase hex pubkey", () => {
assert.equal(parsePubkeyInput(HEX.toUpperCase()), HEX);
});
it("decodes an npub to its hex pubkey", () => {
assert.equal(parsePubkeyInput(NPUB), HEX);
});
it("tolerates surrounding whitespace from copy-paste", () => {
assert.equal(parsePubkeyInput(` ${NPUB}\n`), HEX);
assert.equal(parsePubkeyInput(` ${HEX} `), HEX);
});
it("rejects an npub with a corrupted checksum", () => {
assert.equal(parsePubkeyInput(`${NPUB.slice(0, -1)}q`), null);
});
it("rejects other bech32 entities such as nsec", () => {
assert.equal(
parsePubkeyInput(
"nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5",
),
null,
);
});
it("rejects hex of the wrong length", () => {
assert.equal(parsePubkeyInput(HEX.slice(0, 63)), null);
assert.equal(parsePubkeyInput(`${HEX}0`), null);
});
it("rejects non-hex non-npub input", () => {
assert.equal(parsePubkeyInput(""), null);
assert.equal(parsePubkeyInput("alice"), null);
assert.equal(parsePubkeyInput(`z${HEX.slice(1)}`), null);
});
});

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { getCachedMediaProxyPort, subscribeMediaProxyPort } from "./mediaUrl";
/**
* The resolved localhost media-proxy port, re-rendering when it resolves or
* changes. Components that call `rewriteRelayUrl` during render can subscribe
* to this so an initial `buzz-media://` fallback is replaced by the loopback
* proxy URL as soon as the Tauri backend reports the port.
*/
export function useMediaProxyPort(): number | null {
return React.useSyncExternalStore(
subscribeMediaProxyPort,
getCachedMediaProxyPort,
() => null,
);
}

View File

@@ -481,6 +481,13 @@
color: var(--buzz-muted-foreground);
}
/* Match the hover treatment used by inactive channel rows. */
:root[data-buzz-sidebar]
[data-testid="app-sidebar"]
[data-testid="sidebar-profile-card"]:hover {
background-color: var(--buzz-hover-surface);
}
/*
* App and settings sidebar section headings plus secondary footer text use the
* same tint. Navigation rows keep their normal foreground in both sidebars.