Rename web app to Buzz (#959)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-10 14:07:02 -07:00
committed by GitHub
parent 510009c11d
commit c5a54dcc39
25 changed files with 146 additions and 95 deletions

View File

@@ -176,7 +176,7 @@ or invoke with the full path.
### Deep Links
`sprout://message?channel=<uuid>&id=<hex>` links reference a specific message
`buzz://message?channel=<uuid>&id=<hex>` links reference a specific message
thread. To read the linked thread:
```bash

View File

@@ -11,7 +11,7 @@ FROM --platform=linux/amd64 node:24-bookworm-slim AS web-builder
WORKDIR /build
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY web/ web/
RUN corepack enable && pnpm install --frozen-lockfile --filter sprout-web
RUN corepack enable && pnpm install --frozen-lockfile --filter buzz-web
RUN pnpm -C web build
# ── Runtime stage ───────────────────────────────────────────

View File

@@ -246,7 +246,7 @@ fn shutdown_managed_agents(app: &tauri::AppHandle) -> Result<(), String> {
Ok(())
}
/// Parse the query string of a `sprout://message?…` URL into the JSON
/// Parse the query string of a `buzz://message?…` URL into the JSON
/// payload emitted on `deep-link-message`. Returns `None` when a required
/// param (`channel`, `id`) is missing or empty — mirroring the validation
/// policy of the `connect` arm so the frontend never sees a half-formed
@@ -278,10 +278,10 @@ fn parse_message_deep_link(url: &Url) -> Option<serde_json::Value> {
}))
}
/// Handle an incoming `sprout://` deep link URL.
/// Handle an incoming `buzz://` deep link URL, with `sprout://` accepted as a legacy alias.
///
/// Currently supports:
/// - `sprout://connect?relay=<ws(s)://...>` — emits `deep-link-connect` to the frontend
/// - `buzz://connect?relay=<ws(s)://...>` — emits `deep-link-connect` to the frontend
fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
let url = match Url::parse(url_str) {
Ok(u) => u,
@@ -291,8 +291,8 @@ fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
}
};
if url.scheme() != "sprout" {
eprintln!("sprout-desktop: ignoring non-sprout deep link: {url_str}");
if url.scheme() != "sprout" && url.scheme() != "buzz" {
eprintln!("sprout-desktop: ignoring unsupported deep link scheme: {url_str}");
return;
}
@@ -324,7 +324,7 @@ fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
let _ = app.emit("deep-link-connect", relay_url);
}
Some("message") => {
// `sprout://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]`
// `buzz://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]`
//
// Validation policy mirrors the `connect` arm: parse what we
// need, refuse to emit anything if a required param is missing
@@ -373,7 +373,7 @@ pub fn run() {
}
// Forward any deep link URLs from the duplicate launch.
for arg in &argv {
if arg.starts_with("sprout://") {
if arg.starts_with("sprout://") || arg.starts_with("buzz://") {
handle_deep_link_url(app, arg);
}
}
@@ -869,6 +869,14 @@ mod tests {
assert!(payload["threadRootId"].is_null());
}
#[test]
fn parse_message_deep_link_accepts_buzz_scheme() {
let url = Url::parse("buzz://message?channel=abc&id=xyz").unwrap();
let payload = parse_message_deep_link(&url).expect("required params present");
assert_eq!(payload["channelId"], "abc");
assert_eq!(payload["messageId"], "xyz");
}
#[test]
fn parse_message_deep_link_includes_thread_root() {
let url = Url::parse("sprout://message?channel=abc&id=xyz&thread=root1").unwrap();

View File

@@ -43,7 +43,7 @@
},
"deep-link": {
"desktop": {
"schemes": ["sprout"]
"schemes": ["sprout", "buzz"]
}
}
},

View File

@@ -524,7 +524,7 @@ export function AppShell() {
unreadChannelIds.size,
]);
// Dispatch `sprout://message` deep links into the router.
// Dispatch `buzz://message` deep links into the router.
useMessageDeepLinks();
React.useEffect(() => {

View File

@@ -15,7 +15,7 @@ const THREAD =
test("buildMessageLink → parseMessageLink round-trips without thread", () => {
const url = buildMessageLink({ channelId: CHANNEL, messageId: MESSAGE });
assert.equal(url, `sprout://message?channel=${CHANNEL}&id=${MESSAGE}`);
assert.equal(url, `buzz://message?channel=${CHANNEL}&id=${MESSAGE}`);
const parsed = parseMessageLink(url);
assert.equal(parsed.ok, true);
@@ -52,8 +52,8 @@ test("buildMessageLink treats null/empty thread as absent", () => {
messageId: MESSAGE,
threadRootId: "",
});
assert.equal(a, `sprout://message?channel=${CHANNEL}&id=${MESSAGE}`);
assert.equal(b, `sprout://message?channel=${CHANNEL}&id=${MESSAGE}`);
assert.equal(a, `buzz://message?channel=${CHANNEL}&id=${MESSAGE}`);
assert.equal(b, `buzz://message?channel=${CHANNEL}&id=${MESSAGE}`);
});
test("buildMessageLink rejects missing required params", () => {
@@ -61,7 +61,7 @@ test("buildMessageLink rejects missing required params", () => {
assert.throws(() => buildMessageLink({ channelId: CHANNEL, messageId: "" }));
});
test("parseMessageLink rejects non-sprout schemes", () => {
test("parseMessageLink rejects unsupported schemes", () => {
const r = parseMessageLink(
`https://example.com/?channel=${CHANNEL}&id=${MESSAGE}`,
);
@@ -69,20 +69,20 @@ test("parseMessageLink rejects non-sprout schemes", () => {
assert.equal(r.ok === false && r.reason, "wrong-scheme");
});
test("parseMessageLink rejects sprout:// with wrong host", () => {
const r = parseMessageLink(`sprout://connect?relay=wss://example.com`);
test("parseMessageLink rejects buzz:// with wrong host", () => {
const r = parseMessageLink(`buzz://connect?relay=wss://example.com`);
assert.equal(r.ok, false);
assert.equal(r.ok === false && r.reason, "wrong-host");
});
test("parseMessageLink rejects missing channel", () => {
const r = parseMessageLink(`sprout://message?id=${MESSAGE}`);
const r = parseMessageLink(`buzz://message?id=${MESSAGE}`);
assert.equal(r.ok, false);
assert.equal(r.ok === false && r.reason, "missing-channel");
});
test("parseMessageLink rejects missing id", () => {
const r = parseMessageLink(`sprout://message?channel=${CHANNEL}`);
const r = parseMessageLink(`buzz://message?channel=${CHANNEL}`);
assert.equal(r.ok, false);
assert.equal(r.ok === false && r.reason, "missing-id");
});
@@ -93,11 +93,28 @@ test("parseMessageLink rejects malformed URL strings", () => {
assert.equal(r.ok === false && r.reason, "invalid-url");
});
test("isMessageLink matches only sprout://message", () => {
test("parseMessageLink accepts legacy sprout://message links", () => {
const r = parseMessageLink(
`sprout://message?channel=${CHANNEL}&id=${MESSAGE}`,
);
assert.equal(r.ok, true);
assert.deepEqual(r.ok && r.value, {
channelId: CHANNEL,
messageId: MESSAGE,
threadRootId: null,
});
});
test("isMessageLink matches buzz://message and legacy sprout://message", () => {
assert.equal(
isMessageLink(`buzz://message?channel=${CHANNEL}&id=${MESSAGE}`),
true,
);
assert.equal(
isMessageLink(`sprout://message?channel=${CHANNEL}&id=${MESSAGE}`),
true,
);
assert.equal(isMessageLink("buzz://connect?relay=wss://x"), false);
assert.equal(isMessageLink("sprout://connect?relay=wss://x"), false);
assert.equal(isMessageLink("https://example.com"), false);
assert.equal(isMessageLink(undefined), false);

View File

@@ -1,12 +1,11 @@
/**
* `sprout://message` link encoding for "Copy link" / deep-link-to-message.
* `buzz://message` link encoding for "Copy link" / deep-link-to-message.
*
* Format: `sprout://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]`
*
* Mirrors the existing `sprout://connect?relay=…` scheme already registered
* in `tauri.conf.json` and handled in `desktop/src-tauri/src/lib.rs`.
* Format: `buzz://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]`
*/
const MESSAGE_LINK_SCHEME = "buzz:";
const LEGACY_MESSAGE_LINK_SCHEME = "sprout:";
const MESSAGE_LINK_HOST = "message";
export type MessageLinkInput = {
@@ -35,7 +34,7 @@ export type MessageLinkParseResult =
| { ok: false; reason: string };
/**
* Build a `sprout://message` URL for a given channel + message.
* Build a `buzz://message` URL for a given channel + message.
*
* Empty `threadRootId` is treated as "no thread" so callers can pass through
* the result of `getThreadReference(tags).rootId` without extra null checks.
@@ -54,12 +53,14 @@ export function buildMessageLink(input: MessageLinkInput): string {
if (input.threadRootId) {
params.set("thread", input.threadRootId);
}
return `sprout://${MESSAGE_LINK_HOST}?${params.toString()}`;
return `${MESSAGE_LINK_SCHEME}//${MESSAGE_LINK_HOST}?${params.toString()}`;
}
/**
* Parse a `sprout://message?…` URL. Returns a discriminated result so
* callers can render a fallback (e.g. a plain link) without throwing.
* Parse a `buzz://message?…` URL. Legacy `sprout://message?…` links are
* accepted so already-copied message links keep working during the rename.
* Returns a discriminated result so callers can render a fallback (e.g. a
* plain link) without throwing.
*/
export function parseMessageLink(url: string): MessageLinkParseResult {
let parsed: URL;
@@ -69,10 +70,13 @@ export function parseMessageLink(url: string): MessageLinkParseResult {
return { ok: false, reason: "invalid-url" };
}
if (parsed.protocol !== "sprout:") {
if (
parsed.protocol !== MESSAGE_LINK_SCHEME &&
parsed.protocol !== LEGACY_MESSAGE_LINK_SCHEME
) {
return { ok: false, reason: "wrong-scheme" };
}
// `new URL("sprout://message?…")` puts "message" in `hostname`.
// `new URL("buzz://message?…")` puts "message" in `hostname`.
if (parsed.hostname !== MESSAGE_LINK_HOST) {
return { ok: false, reason: "wrong-host" };
}
@@ -97,10 +101,15 @@ export function parseMessageLink(url: string): MessageLinkParseResult {
}
/**
* Convenience: returns true if the given href is a `sprout://message` link.
* Convenience: returns true if the given href is a supported message link.
* Cheap pre-check used by the markdown renderer before parsing.
*/
export function isMessageLink(href: string | undefined | null): boolean {
if (!href) return false;
return href.startsWith("sprout://message?") || href === "sprout://message";
return (
href.startsWith("buzz://message?") ||
href === "buzz://message" ||
href.startsWith("sprout://message?") ||
href === "sprout://message"
);
}

View File

@@ -1,12 +1,13 @@
/**
* Remark plugin that detects bare `sprout://message?…` URLs in text nodes and
* replaces each with a custom `message-link` HAST element. The `markdown.tsx`
* Remark plugin that detects bare `buzz://message?…` URLs in text nodes and
* replaces each with a custom `message-link` HAST element. Legacy
* `sprout://message?…` URLs are accepted during the rename. The `markdown.tsx`
* components map renders that as an inline pill (channel name + click-to-open)
* instead of the raw 100-char URL.
*
* Why this plugin exists: `remark-gfm`'s autolinker only covers `http(s)://`
* and `www.`. Custom schemes like `sprout://` only reach the `<a>` component
* override when the user wrote an explicit `[label](sprout://…)` link.
* and `www.`. Custom schemes like `buzz://` only reach the `<a>` component
* override when the user wrote an explicit `[label](buzz://…)` link.
*
* Mirrors `remarkChannelLinks` / `remarkMentions` — same factory, same HAST
* shape — so the rendering layer treats all three uniformly. Trailing
@@ -19,7 +20,7 @@
// --experimental-strip-types`. `tsconfig.json` enables `allowImportingTsExtensions`.
import { createRemarkPrefixPlugin } from "../../../shared/lib/createRemarkPrefixPlugin.ts";
const MESSAGE_URL_PATTERN = /sprout:\/\/message\?[^\s<>"')\]]+/g;
const MESSAGE_URL_PATTERN = /(?:buzz|sprout):\/\/message\?[^\s<>"')\]]+/g;
const TRAILING_PUNCTUATION_PATTERN = /[.,;:!?]+$/;
function trimMessageLinkMatch(matchText: string) {

View File

@@ -304,10 +304,11 @@ export function useRichTextEditor({
openOnClick: false,
autolink: true,
linkOnPaste: true,
// Allow `sprout://` (used by Copy-link-to-message + sprout://connect)
// through TipTap's URL sanitiser. http(s) and mailto are accepted by
// default; non-listed protocols are stripped on paste/typed input.
protocols: ["sprout"],
// Allow Buzz message links through TipTap's URL sanitiser. Keep the
// legacy Sprout protocol so already-copied links survive paste.
// http(s) and mailto are accepted by default; non-listed protocols are
// stripped on paste/typed input.
protocols: ["buzz", "sprout"],
HTMLAttributes: {
class: "text-primary underline underline-offset-4 cursor-pointer",
},

View File

@@ -14,7 +14,7 @@ export interface DeepLinkDeps {
}
/**
* Payload emitted by the Rust deep-link handler for `sprout://message?…`.
* Payload emitted by the Rust deep-link handler for `buzz://message?…`.
* Field names match the JSON shape produced in `desktop/src-tauri/src/lib.rs`.
*/
export type MessageDeepLinkPayload = {
@@ -26,11 +26,11 @@ export type MessageDeepLinkPayload = {
/**
* Register listeners for deep-link events emitted by the Rust backend.
*
* When a `sprout://connect?relay=<url>` link is opened, the handler
* When a `buzz://connect?relay=<url>` link is opened, the handler
* adds a workspace for the relay (deduplicating by URL) and switches
* to it. Returns an unlisten function to tear down all listeners.
*
* `sprout://message?…` is handled separately by `listenForMessageDeepLinks`,
* `buzz://message?…` is handled separately by `listenForMessageDeepLinks`,
* because it needs to dispatch into the router which only exists below the
* `RouterProvider` in the component tree.
*/

View File

@@ -428,12 +428,12 @@ test("rehypeImageGallery: mixed content paragraph is not image-only", () => {
assert.equal(tree.children.length, 3);
});
// ── messageLinkUrlTransform: sprout:// link preservation ──────────────
// ── messageLinkUrlTransform: buzz:// link preservation ──────────────
// Regression test: react-markdown's `defaultUrlTransform` strips unknown
// schemes (returns `""`) before our `a` component override can see them,
// which would break copy → paste → click for `sprout://message?…` links
// which would break copy → paste → click for `buzz://message?…` links
// end-to-end. We pass a custom `urlTransform` that delegates to the
// default for everything except `sprout://message` hrefs.
// default for `buzz://message` and legacy `sprout://message` hrefs.
//
// This test renders real `<ReactMarkdown>` with the production transform
// and asserts the link href survives to the rendered DOM. Mirrors the
@@ -462,19 +462,19 @@ function renderMarkdown(content) {
);
}
test("messageLinkUrlTransform: preserves sprout://message href", () => {
test("messageLinkUrlTransform: preserves buzz://message href", () => {
const html = renderMarkdown(
"Click [here](sprout://message?channel=abc&id=xyz)",
"Click [here](buzz://message?channel=abc&id=xyz)",
);
// HTML-encoded `&` in attributes is fine — the browser decodes back to `&`.
assert.match(html, /href="sprout:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
assert.match(html, /href="buzz:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
});
test("messageLinkUrlTransform: preserves sprout://message href with thread", () => {
test("messageLinkUrlTransform: preserves buzz://message href with thread", () => {
const html = renderMarkdown(
"[link](sprout://message?channel=c1&id=m1&thread=t1)",
"[link](buzz://message?channel=c1&id=m1&thread=t1)",
);
assert.match(html, /href="sprout:\/\/message\?[^"]*thread=t1"/);
assert.match(html, /href="buzz:\/\/message\?[^"]*thread=t1"/);
});
test("messageLinkUrlTransform: still strips javascript: scheme", () => {
@@ -489,18 +489,25 @@ test("messageLinkUrlTransform: passes http(s) through unchanged", () => {
assert.match(html, /href="https:\/\/example\.com\/path"/);
});
test("messageLinkUrlTransform: leaves non-message sprout:// schemes to default", () => {
// `sprout://connect?relay=…` is handled by a different code path (Tauri
test("messageLinkUrlTransform: preserves legacy sprout://message href", () => {
const html = renderMarkdown(
"Click [here](sprout://message?channel=abc&id=xyz)",
);
assert.match(html, /href="sprout:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
});
test("messageLinkUrlTransform: leaves non-message buzz:// schemes to default", () => {
// `buzz://connect?relay=…` is handled by a different code path (Tauri
// single-instance). The markdown renderer should let it pass through
// defaultUrlTransform (which strips it) since it's not clickable in-app.
const html = renderMarkdown(
"[connect](sprout://connect?relay=wss://relay.example)",
"[connect](buzz://connect?relay=wss://relay.example)",
);
assert.match(html, /href=""/);
});
// ── remarkMessageLinks: bare-URL → message-link node ──────────────────
// `remark-gfm`'s autolinker only covers http(s)://, so bare `sprout://message`
// `remark-gfm`'s autolinker only covers http(s)://, so bare `buzz://message`
// URLs in plain text never reach any rendering path without this plugin.
// The plugin emits a custom `message-link` HAST element which markdown.tsx
// renders as an inline pill. Tests operate on the mdast tree directly —
@@ -521,18 +528,26 @@ function text(value) {
return { type: "text", value };
}
test("remarkMessageLinks: bare sprout://message URL is replaced", () => {
test("remarkMessageLinks: bare buzz://message URL is replaced", () => {
const tree = runPlugin(paragraph(text("buzz://message?channel=c&id=m")));
const para = tree.children[0];
assert.equal(para.children.length, 1);
assert.equal(para.children[0].type, "message-link");
assert.equal(para.children[0].value, "buzz://message?channel=c&id=m");
assert.equal(para.children[0].data.hName, "message-link");
});
test("remarkMessageLinks: legacy bare sprout://message URL is replaced", () => {
const tree = runPlugin(paragraph(text("sprout://message?channel=c&id=m")));
const para = tree.children[0];
assert.equal(para.children.length, 1);
assert.equal(para.children[0].type, "message-link");
assert.equal(para.children[0].value, "sprout://message?channel=c&id=m");
assert.equal(para.children[0].data.hName, "message-link");
});
test("remarkMessageLinks: mid-sentence URL splits surrounding text", () => {
const tree = runPlugin(
paragraph(text("see sprout://message?channel=c&id=m here")),
paragraph(text("see buzz://message?channel=c&id=m here")),
);
const kids = tree.children[0].children;
assert.equal(kids.length, 3);
@@ -547,28 +562,28 @@ test("remarkMessageLinks: two URLs in one text node both replaced", () => {
const tree = runPlugin(
paragraph(
text(
"first sprout://message?channel=a&id=1 then sprout://message?channel=b&id=2 done",
"first buzz://message?channel=a&id=1 then buzz://message?channel=b&id=2 done",
),
),
);
const kids = tree.children[0].children;
const links = kids.filter((c) => c.type === "message-link");
assert.equal(links.length, 2);
assert.equal(links[0].value, "sprout://message?channel=a&id=1");
assert.equal(links[1].value, "sprout://message?channel=b&id=2");
assert.equal(links[0].value, "buzz://message?channel=a&id=1");
assert.equal(links[1].value, "buzz://message?channel=b&id=2");
});
test("remarkMessageLinks: trailing sentence punctuation stays outside URL", () => {
for (const punctuation of [".", ",", ";", ":", "!", "?"]) {
const tree = runPlugin(
paragraph(text(`see sprout://message?channel=c&id=m${punctuation}`)),
paragraph(text(`see buzz://message?channel=c&id=m${punctuation}`)),
);
const kids = tree.children[0].children;
assert.equal(kids.length, 3, punctuation);
assert.equal(kids[0].value, "see ", punctuation);
assert.equal(kids[1].type, "message-link", punctuation);
assert.equal(kids[1].value, "sprout://message?channel=c&id=m", punctuation);
assert.equal(kids[1].value, "buzz://message?channel=c&id=m", punctuation);
assert.equal(kids[2].type, "text", punctuation);
assert.equal(kids[2].value, punctuation, punctuation);
}
@@ -576,20 +591,20 @@ test("remarkMessageLinks: trailing sentence punctuation stays outside URL", () =
test("remarkMessageLinks: URL inside parens keeps closing paren outside", () => {
const tree = runPlugin(
paragraph(text("see (sprout://message?channel=c&id=m) for details")),
paragraph(text("see (buzz://message?channel=c&id=m) for details")),
);
const kids = tree.children[0].children;
assert.equal(kids.length, 3);
assert.equal(kids[0].value, "see (");
assert.equal(kids[1].type, "message-link");
assert.equal(kids[1].value, "sprout://message?channel=c&id=m");
assert.equal(kids[1].value, "buzz://message?channel=c&id=m");
assert.equal(kids[2].type, "text");
assert.equal(kids[2].value, ") for details");
});
test("remarkMessageLinks: URL without trailing punctuation matches end-to-end", () => {
const value = "sprout://message?channel=c&id=m";
const value = "buzz://message?channel=c&id=m";
const tree = runPlugin(paragraph(text(value)));
const kids = tree.children[0].children;
@@ -598,8 +613,8 @@ test("remarkMessageLinks: URL without trailing punctuation matches end-to-end",
assert.equal(kids[0].value, value);
});
test("remarkMessageLinks: non-message sprout:// URLs are not matched", () => {
const original = "sprout://connect?relay=wss://x.example";
test("remarkMessageLinks: non-message buzz:// URLs are not matched", () => {
const original = "buzz://connect?relay=wss://x.example";
const tree = runPlugin(paragraph(text(original)));
const kids = tree.children[0].children;
assert.equal(kids.length, 1);
@@ -618,7 +633,7 @@ test("remarkMessageLinks: text inside inlineCode is left alone", () => {
{
type: "paragraph",
children: [
{ type: "inlineCode", value: "sprout://message?channel=c&id=m" },
{ type: "inlineCode", value: "buzz://message?channel=c&id=m" },
],
},
],
@@ -627,5 +642,5 @@ test("remarkMessageLinks: text inside inlineCode is left alone", () => {
const kids = tree.children[0].children;
assert.equal(kids.length, 1);
assert.equal(kids[0].type, "inlineCode");
assert.equal(kids[0].value, "sprout://message?channel=c&id=m");
assert.equal(kids[0].value, "buzz://message?channel=c&id=m");
});

View File

@@ -120,7 +120,7 @@ function useStableArray<T>(arr: T[]): T[] {
}
/**
* `urlTransform` for `<ReactMarkdown>` that preserves `sprout://message?…`
* `urlTransform` for `<ReactMarkdown>` that preserves `buzz://message?…`
* links. The default transform strips unknown schemes (returns `""`) before
* the `a` component override can see them, which would break copy → paste →
* click end-to-end. Everything else delegates to `defaultUrlTransform`.
@@ -709,7 +709,7 @@ function createMarkdownComponents(
);
}
// Intercept `sprout://message?channel=…&id=…` links so a click navigates
// Intercept `buzz://message?channel=…&id=…` links so a click navigates
// in-app instead of opening the URL in the OS browser. http(s) links
// continue to use the existing target="_blank" behavior.
if (isMessageLink(href)) {
@@ -730,7 +730,7 @@ function createMarkdownComponents(
</a>
);
}
// Malformed sprout://message link — fall through to the default
// Malformed message deep link — fall through to the default
// anchor (renders as a normal external link).
}
return (
@@ -991,7 +991,7 @@ function createMarkdownComponents(
const href = String(children ?? "");
const parsed = parseMessageLink(href);
if (!parsed.ok) {
// Malformed `sprout://message?…` — render the raw URL as plain text
// Malformed `buzz://message?…` — render the raw URL as plain text
// rather than a misleading clickable pill.
return <span data-message-link="">{href}</span>;
}

View File

@@ -4,7 +4,7 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { listenForMessageDeepLinks } from "@/shared/deep-link";
/**
* Subscribe to `sprout://message` deep links emitted by the Tauri backend
* Subscribe to `buzz://message` deep links emitted by the Tauri backend
* and route them through the app's navigation helpers.
*
* Lives in a hook (not inline in `AppShell`) so it can be unit-tested
@@ -12,7 +12,7 @@ import { listenForMessageDeepLinks } from "@/shared/deep-link";
*
* Mirrors the cold-start race handling of the `connect` listener in
* `App.tsx`: late-arriving payloads from a fresh launch are picked up the
* first time the listener mounts. Routing matches the in-app sprout://
* first time the listener mounts. Routing matches the in-app buzz://
* handler in `markdown.tsx`: always `goChannel` with `messageId` and let
* the channel route's existing scroll-into-view + getEventById backfill
* resolve the target (works for both stream replies and forum threads).

View File

@@ -1,5 +1,5 @@
{
"name": "sprout-workspace",
"name": "buzz-workspace",
"private": true,
"packageManager": "pnpm@11.4.0",
"scripts": {

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sprout</title>
<title>Buzz</title>
</head>
<body>

View File

@@ -1,5 +1,5 @@
{
"name": "sprout-web",
"name": "buzz-web",
"private": true,
"version": "0.1.0",
"type": "module",

View File

@@ -13,7 +13,7 @@ function RootLayout() {
to="/"
className="text-sm font-semibold tracking-tight text-foreground"
>
Sprout
Buzz
</Link>
<ThemeToggle />
</header>

View File

@@ -28,7 +28,7 @@ import { relayHttpBaseUrl } from "@/shared/lib/relay-url";
/** Get a repo-specific LightningFS instance backed by IndexedDB. */
export function getFs(owner: string, repoName: string): LightningFS {
return new LightningFS(`sprout-git-${owner}-${repoName}`);
return new LightningFS(`buzz-git-${owner}-${repoName}`);
}
/** Working directory inside the virtual FS. */

View File

@@ -4,13 +4,13 @@ import { relayWsUrl } from "@/shared/lib/relay-url";
import { Button } from "@/shared/ui/button";
export function ConnectButton({ className }: { className?: string }) {
const deepLink = `sprout://connect?relay=${encodeURIComponent(relayWsUrl())}`;
const deepLink = `buzz://connect?relay=${encodeURIComponent(relayWsUrl())}`;
return (
<Button asChild className={className}>
<a href={deepLink}>
<ExternalLink className="h-4 w-4" />
Open in Sprout
Open in Buzz
</a>
</Button>
);

View File

@@ -24,7 +24,7 @@ export function OrgSidebar({ repos }: { repos: Repo[] }) {
return (
<div className="space-y-6">
{/* Open in Sprout */}
{/* Open in Buzz */}
<ConnectButton className="w-full" />
{/* People section */}

View File

@@ -323,7 +323,7 @@ export function RepoDetailPage() {
{/* Sidebar */}
<aside className="hidden w-72 shrink-0 border-l border-border pl-8 lg:block">
<div className="space-y-6">
{/* Open in Sprout */}
{/* Open in Buzz */}
<ConnectButton className="w-full" />
{/* People */}

View File

@@ -1,4 +1,4 @@
import { BookMarked, GitBranch, Sprout } from "lucide-react";
import { BookMarked, GitBranch, Hexagon } from "lucide-react";
import { toast } from "sonner";
import { useEffect, useMemo, useState } from "react";
@@ -45,14 +45,14 @@ function RelayEmptyState() {
return (
<div className="flex flex-1 flex-col items-center justify-center px-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
<Sprout className="h-8 w-8 text-primary" />
<Hexagon className="h-8 w-8 text-primary" />
</div>
<h1 className="mt-6 text-2xl font-semibold tracking-tight">
This relay is empty
</h1>
<p className="mt-2 max-w-md text-sm leading-relaxed text-muted-foreground">
Repositories pushed to this relay will show up here. Open this relay in
the Sprout desktop app to start pushing code.
the Buzz desktop app to start pushing code.
</p>
<ConnectButton className="mt-6" />
</div>

View File

@@ -82,7 +82,7 @@ export function queryEvents(
ws.addEventListener("open", () => {
// Wait briefly for an AUTH challenge before sending REQ.
// Sprout relays always send AUTH, but other relays may not.
// Buzz relays always send AUTH, but other relays may not.
setTimeout(() => sendReq(), 100);
});

View File

@@ -15,7 +15,7 @@ type ThemeContextValue = {
setTheme: (theme: Theme) => void;
};
const STORAGE_KEY = "sprout-web-theme";
const STORAGE_KEY = "buzz-web-theme";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

View File

@@ -1,8 +1,8 @@
import { expect, test } from "@playwright/test";
test("home page loads with Sprout heading", async ({ page }) => {
test("home page loads with Buzz heading", async ({ page }) => {
await page.goto("/");
await expect(page.locator("header")).toContainText("Sprout");
await expect(page.locator("header")).toContainText("Buzz");
});
test("home page shows repositories section", async ({ page }) => {