diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index 7d06c4da91b..fb2d2b85a00 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -21,6 +21,7 @@ export default defineConfig({
testMatch: [
"**/smoke.spec.ts",
"**/sidebar-offcanvas-rail.spec.ts",
+ "**/tooltip-semantics.spec.ts",
"**/search-scope-screenshots.spec.ts",
"**/onboarding-docked-cta-screenshots.spec.ts",
"**/identity-key-help.spec.ts",
diff --git a/desktop/src/features/agents/ui/RestartDiffBadge.tsx b/desktop/src/features/agents/ui/RestartDiffBadge.tsx
index 1bdb781226f..e15a57fde49 100644
--- a/desktop/src/features/agents/ui/RestartDiffBadge.tsx
+++ b/desktop/src/features/agents/ui/RestartDiffBadge.tsx
@@ -88,8 +88,8 @@ function ChangeDescription({ change }: { change: RestartChange }) {
const TOOLTIP_CAP = 6;
/**
- * `tooltip` — renders inside the dark `bg-primary` tooltip; uses
- * `text-primary-foreground` variants for contrast there.
+ * `tooltip` — renders inside the semantic secondary tooltip surface; uses
+ * `text-secondary-foreground` variants for contrast there.
* `inline` — renders inside the amber Runtime banner or other light
* surfaces; inherits foreground from the container instead.
*/
@@ -107,10 +107,10 @@ function DiffList({
cap !== undefined && entries.length > cap ? entries.length - cap : 0;
const valueClass =
- variant === "tooltip" ? "text-primary-foreground/80" : "text-foreground";
+ variant === "tooltip" ? "text-secondary-foreground/80" : "text-foreground";
const overflowClass =
variant === "tooltip"
- ? "text-primary-foreground/60"
+ ? "text-secondary-foreground/60"
: "text-muted-foreground";
return (
@@ -180,7 +180,7 @@ export function RestartDiffBadge({
Config changed since last start:
-
+
{autoRestartEnabled ? AUTO_RESTART_ON_BLURB : AUTO_RESTART_OFF_BLURB}
diff --git a/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx b/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
index 512ee6899fb..326866cf63e 100644
--- a/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
@@ -143,7 +143,7 @@ export function AddChannelBotTeamsSection({
{team.name}
{team.description ? (
-
+
{team.description}
) : null}
@@ -153,15 +153,17 @@ export function AddChannelBotTeamsSection({
inChannelPersonaIds?.has(persona.id) ?? false;
return (
-
+
{persona.displayName}
{personaInChannel ? (
diff --git a/desktop/src/features/communities/useCommunityInit.ts b/desktop/src/features/communities/useCommunityInit.ts
index e1cdee41a76..e792358713f 100644
--- a/desktop/src/features/communities/useCommunityInit.ts
+++ b/desktop/src/features/communities/useCommunityInit.ts
@@ -34,6 +34,7 @@ import { resetAvatarPresentations } from "@/features/profile/avatarPresentationS
import { resetAvatarProfileSync } from "@/features/profile/avatarProfileSync";
import { resetSidebarRelayConnectionCardState } from "@/features/sidebar/ui/useSidebarRelayConnectionCard";
import { clearMarkdownNodeCache } from "@/shared/ui/markdown/nodeCache";
+import { resetMessageLinkMetadataCache } from "@/shared/ui/markdown/useMessageLinkMetadata";
import { resetVideoPlayerState } from "@/shared/ui/videoPlayerState";
import {
@@ -77,6 +78,7 @@ async function resetCommunityState({
resetLinkPreviewPreparations();
clearSearchHitEventCache();
clearMarkdownNodeCache();
+ resetMessageLinkMetadataCache();
}
type CommunityInitResult =
diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs
index 5aee675592e..c248b9b88f1 100644
--- a/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs
+++ b/desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs
@@ -21,6 +21,8 @@ const OWNER = "a".repeat(64);
const REPO_HREF = `buzz://repo?owner=${OWNER}&d=buzz-world`;
const ISSUE_ID = "b".repeat(64);
const ISSUE_HREF = `buzz://issue?id=${ISSUE_ID}&owner=${OWNER}&d=buzz-world`;
+const PR_ID = "c".repeat(64);
+const PR_HREF = `buzz://pr?id=${PR_ID}&owner=${OWNER}&d=buzz-world`;
test("resolves a composer preview and canonicalizes the underlying href", () => {
assert.deepEqual(
@@ -205,7 +207,9 @@ test("composer node uses the sent-message chip presentation", () => {
assert.match(rendered[1].class, /inline-chip-with-icon/);
assert.match(rendered[1].class, /inline-chip-icon-message/);
assert.equal(rendered[1]["data-buzz-link"], "");
- assert.equal(rendered[2], "general · root-eve");
+ // Channel label only — no event hash, so the chip does not change width when
+ // the draft is sent and the rendered chip resolves its metadata.
+ assert.equal(rendered[2], "general");
});
test("composer node renders channel and entity chip presentations", () => {
@@ -233,7 +237,14 @@ test("composer node renders channel and entity chip presentations", () => {
const issue = render(ISSUE_HREF);
assert.equal(issue[1]["data-buzz-link-kind"], "issue");
assert.match(issue[1].class, /inline-chip-icon-issue/);
- assert.equal(issue[2], "buzz-world · bbbbbbbb");
+ // Repository name only — the rendered chip never widens into the issue
+ // title, so the composer must not widen into the event hash either.
+ assert.equal(issue[2], "buzz-world");
+
+ const pullRequest = render(PR_HREF);
+ assert.equal(pullRequest[1]["data-buzz-link-kind"], "pr");
+ assert.match(pullRequest[1].class, /inline-chip-icon-pr/);
+ assert.equal(pullRequest[2], "buzz-world · cccccccc");
});
test("markdown rendering stores identity in attributes, not visible id text", () => {
diff --git a/desktop/src/features/messages/lib/composerMessageLinkNode.ts b/desktop/src/features/messages/lib/composerMessageLinkNode.ts
index a01109e010d..c3182a3038f 100644
--- a/desktop/src/features/messages/lib/composerMessageLinkNode.ts
+++ b/desktop/src/features/messages/lib/composerMessageLinkNode.ts
@@ -232,7 +232,9 @@ function composerLinkPresentation(
"data-message-link": "",
},
icon: "message",
- label: `${resolvedChannelName} · ${message.value.messageId.slice(0, 8)}`,
+ // Matches the rendered inline message chip, which never shows the event
+ // hash — the label must not change when the draft is sent.
+ label: resolvedChannelName,
};
}
@@ -276,10 +278,12 @@ function composerLinkPresentation(
channelName: "",
dataAttributes: { "data-buzz-link-kind": entity.value.type },
icon: entity.value.type,
+ // Only pull requests carry their short id inline; issue chips match the
+ // rendered message chip, which shows the repository name alone.
label:
- entity.value.type === "repo" || entity.value.type === "project"
- ? entity.value.dtag
- : `${entity.value.dtag} · ${shortId}`,
+ entity.value.type === "pr"
+ ? `${entity.value.dtag} · ${shortId}`
+ : entity.value.dtag,
};
}
diff --git a/desktop/src/features/messages/lib/messageLinkMetadata.test.mjs b/desktop/src/features/messages/lib/messageLinkMetadata.test.mjs
new file mode 100644
index 00000000000..5b44c799c8f
--- /dev/null
+++ b/desktop/src/features/messages/lib/messageLinkMetadata.test.mjs
@@ -0,0 +1,24 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { summarizeMessageLinkContent } from "./messageLinkMetadata.ts";
+
+test("summarizeMessageLinkContent projects markdown to bounded plain text", () => {
+ assert.equal(
+ summarizeMessageLinkContent(
+ "**Hello** [team](https://example.com)\n\n ||hidden||",
+ ),
+ "Hello team",
+ );
+ assert.equal(
+ summarizeMessageLinkContent("https://example.com"),
+ "No message text",
+ );
+});
+
+test("summarizeMessageLinkContent truncates on grapheme-safe character boundaries", () => {
+ const result = summarizeMessageLinkContent(`Lead ${"🦄".repeat(200)}`);
+ assert.ok(Array.from(result).length <= 160);
+ assert.ok(result.endsWith("…"));
+ assert.ok(!result.includes("\ud83e") || result.includes("🦄"));
+});
diff --git a/desktop/src/features/messages/lib/messageLinkMetadata.ts b/desktop/src/features/messages/lib/messageLinkMetadata.ts
new file mode 100644
index 00000000000..848b207d693
--- /dev/null
+++ b/desktop/src/features/messages/lib/messageLinkMetadata.ts
@@ -0,0 +1,29 @@
+const MESSAGE_LINK_SNIPPET_MAX_LENGTH = 160;
+
+/** Build a compact, non-recursive plain-text preview for a linked message. */
+export function summarizeMessageLinkContent(content: string): string {
+ const normalized = Array.from(content, (character) => {
+ const codePoint = character.codePointAt(0) ?? 0;
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)
+ ? " "
+ : character;
+ })
+ .join("")
+ .replace(/\|\|[^|]*(?:\|(?!\|)[^|]*)*\|\|/g, " ")
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
+ .replace(/(?:https?|buzz):\/\/\S+>?/g, " ")
+ .replace(/[`*_~>#|]/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+ if (!normalized) return "No message text";
+
+ const characters = Array.from(normalized);
+ if (characters.length <= MESSAGE_LINK_SNIPPET_MAX_LENGTH) return normalized;
+ const clipped = characters
+ .slice(0, MESSAGE_LINK_SNIPPET_MAX_LENGTH - 1)
+ .join("");
+ const lastSpace = clipped.lastIndexOf(" ");
+ const snippet = lastSpace > 96 ? clipped.slice(0, lastSpace) : clipped;
+ return `${snippet.trimEnd()}…`;
+}
diff --git a/desktop/src/features/messages/lib/remarkEntityLinks.test.mjs b/desktop/src/features/messages/lib/remarkEntityLinks.test.mjs
index 1d7fdeacb3d..99fa0ed510a 100644
--- a/desktop/src/features/messages/lib/remarkEntityLinks.test.mjs
+++ b/desktop/src/features/messages/lib/remarkEntityLinks.test.mjs
@@ -17,6 +17,7 @@ test("turns every bare Buzz entity permalink family into a chip node", () => {
const id = "cd".repeat(32);
const links = [
`buzz://repo?owner=${owner}&d=buzz`,
+ `buzz://project?owner=${owner}&d=onboarding`,
`buzz://pr?id=${id}&owner=${owner}&d=buzz`,
`buzz://issue?id=${id}&owner=${owner}&d=buzz`,
];
diff --git a/desktop/src/features/messages/lib/remarkEntityLinks.ts b/desktop/src/features/messages/lib/remarkEntityLinks.ts
index 41cf4af20b5..85ba43f7744 100644
--- a/desktop/src/features/messages/lib/remarkEntityLinks.ts
+++ b/desktop/src/features/messages/lib/remarkEntityLinks.ts
@@ -1,7 +1,7 @@
-/** Detect bare `buzz://pr|issue|repo?…` URLs in markdown text nodes. */
+/** Detect bare `buzz://pr|issue|repo|project?…` URLs in markdown text nodes. */
import { createRemarkPrefixPlugin } from "../../../shared/lib/createRemarkPrefixPlugin.ts";
-const ENTITY_URL_PATTERN = /buzz:\/\/(?:pr|issue|repo)\?[^\s<>"')\]]+/g;
+const ENTITY_URL_PATTERN = /buzz:\/\/(?:pr|issue|repo|project)\?[^\s<>"')\]]+/g;
const TRAILING_PUNCTUATION_PATTERN = /[.,;:!?]+$/;
export default function remarkEntityLinks() {
diff --git a/desktop/src/features/messages/ui/SentFromThreadLine.tsx b/desktop/src/features/messages/ui/SentFromThreadLine.tsx
index 75e8d1ae1f1..a78d7df2cb4 100644
--- a/desktop/src/features/messages/ui/SentFromThreadLine.tsx
+++ b/desktop/src/features/messages/ui/SentFromThreadLine.tsx
@@ -44,6 +44,9 @@ export function SentFromThreadLine({
channels={channels}
interactive
link={link}
+ onOpenChannel={(targetChannelId) => {
+ void goChannel(targetChannelId);
+ }}
onOpenMessageLink={onOpenMessageLink}
threadExcerpt={reference.rootExcerpt}
variant="sent-from-thread"
diff --git a/desktop/src/features/projects/ui/ProjectAuthorIdentity.tsx b/desktop/src/features/projects/ui/ProjectAuthorIdentity.tsx
index 81262928947..21a70d6dcb0 100644
--- a/desktop/src/features/projects/ui/ProjectAuthorIdentity.tsx
+++ b/desktop/src/features/projects/ui/ProjectAuthorIdentity.tsx
@@ -60,7 +60,7 @@ export function ProjectAuthorIdentity({
/>
{label}
-
+
{roleLabel}
diff --git a/desktop/src/features/projects/ui/ProjectCards.tsx b/desktop/src/features/projects/ui/ProjectCards.tsx
index 8eb4853376b..067e67dd390 100644
--- a/desktop/src/features/projects/ui/ProjectCards.tsx
+++ b/desktop/src/features/projects/ui/ProjectCards.tsx
@@ -330,7 +330,7 @@ function RepositoryUnavailableIndicator({
{label}
- {description}
+ {description}
);
diff --git a/desktop/src/shared/lib/useResolvedLinkPreviews.ts b/desktop/src/shared/lib/useResolvedLinkPreviews.ts
index 8b210f6ab35..6e71de2181a 100644
--- a/desktop/src/shared/lib/useResolvedLinkPreviews.ts
+++ b/desktop/src/shared/lib/useResolvedLinkPreviews.ts
@@ -381,6 +381,13 @@ const entityMetadataLoader = createMetadataLoader({
fetcher: fetchBuzzEntityMetadata,
});
+/** Share deduplicated relay-native entity metadata across cards and inline tooltips. */
+export async function loadBuzzEntityMetadata(
+ href: string,
+): Promise {
+ return (await entityMetadataLoader.load(href)).metadata;
+}
+
/** Clear ephemeral metadata when the active relay/community changes. */
export function resetLinkPreviewMetadataCache(): void {
metadataLoader.reset();
diff --git a/desktop/src/shared/styles/globals/markdown.css b/desktop/src/shared/styles/globals/markdown.css
index 7d0a238c179..f39de74c522 100644
--- a/desktop/src/shared/styles/globals/markdown.css
+++ b/desktop/src/shared/styles/globals/markdown.css
@@ -87,6 +87,25 @@
color: hsl(var(--primary));
}
+.message-markdown .mention-chip.wrapping-inline-chip {
+ display: inline;
+ line-height: calc(
+ 1em +
+ var(--inline-chip-padding-block-start) +
+ var(--inline-chip-padding-block-end) +
+ 0.25rem
+ );
+ overflow-wrap: anywhere;
+ text-align: left;
+ white-space: normal;
+}
+
+.message-markdown .mention-chip.buzz-link-unavailable,
+.message-markdown .mention-chip.buzz-link-unavailable:hover {
+ background: hsl(var(--secondary));
+ color: hsl(var(--secondary-foreground) / 0.7);
+}
+
.message-markdown .mention-chip.inbox-channel-chip {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground) / 0.82);
@@ -137,6 +156,7 @@
.message-markdown.inbox-preview-markdown .inline-code-chip,
.message-markdown.inbox-preview-markdown :not(pre) > code {
display: inline;
+ line-height: 1;
overflow: visible;
overflow-wrap: normal;
text-overflow: clip;
@@ -166,6 +186,30 @@
transform: translateY(-50%);
}
+.message-markdown .wrapping-inline-chip.inline-chip-with-icon {
+ padding-left: var(--inline-chip-padding-inline);
+}
+
+.message-markdown .wrapping-inline-chip.inline-chip-with-icon::before {
+ display: none;
+}
+
+.message-markdown .inline-chip-leading-fragment.inline-chip-with-icon {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ padding-left: calc(
+ var(--inline-chip-icon-size) +
+ var(--inline-chip-icon-gap)
+ );
+ white-space: nowrap;
+}
+
+.message-markdown .inline-chip-leading-fragment.inline-chip-with-icon::before {
+ display: block;
+ left: 0;
+}
+
.message-markdown .inline-chip-icon-message::before {
mask:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'/%3E%3C/svg%3E")
diff --git a/desktop/src/shared/styles/globals/tooltipSemantics.test.mjs b/desktop/src/shared/styles/globals/tooltipSemantics.test.mjs
new file mode 100644
index 00000000000..7fdc1d6097e
--- /dev/null
+++ b/desktop/src/shared/styles/globals/tooltipSemantics.test.mjs
@@ -0,0 +1,27 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+const utilitiesCss = readFileSync(
+ new URL("./utilities.css", import.meta.url),
+ "utf8",
+);
+const huddleTooltipRule = utilitiesCss.match(
+ /\.buzz-huddle-tooltip\s*\{([\s\S]*?)\n\s*\}/,
+)?.[1];
+
+test("huddle tooltips consume their dedicated semantic tokens", () => {
+ assert.ok(huddleTooltipRule, "missing .buzz-huddle-tooltip rule");
+ assert.match(
+ huddleTooltipRule,
+ /background:\s*hsl\(\s*var\(--huddle-tooltip-surface,/,
+ );
+ assert.match(
+ huddleTooltipRule,
+ /color:\s*hsl\(\s*var\(\s*--huddle-tooltip-foreground,/,
+ );
+ assert.doesNotMatch(
+ huddleTooltipRule,
+ /--(?:primary|secondary)(?:-foreground)?/,
+ );
+});
diff --git a/desktop/src/shared/styles/globals/utilities.css b/desktop/src/shared/styles/globals/utilities.css
index a9da737bcef..0d5e0f453f4 100644
--- a/desktop/src/shared/styles/globals/utilities.css
+++ b/desktop/src/shared/styles/globals/utilities.css
@@ -38,16 +38,9 @@
}
.buzz-huddle-tooltip {
- --primary: var(
- --huddle-tooltip-surface,
- var(--huddle-control-surface, 0 0% 20%)
- );
- --primary-foreground: var(
- --huddle-tooltip-foreground,
- var(--huddle-control-foreground, 0 0% 98%)
+ background: hsl(
+ var(--huddle-tooltip-surface, var(--huddle-control-surface, 0 0% 20%))
);
-
- background: hsl(var(--primary));
border: 1px solid
hsl(
var(
@@ -57,7 +50,12 @@
0.72
);
box-shadow: 0 10px 22px rgb(0 0 0 / 35%);
- color: hsl(var(--primary-foreground));
+ color: hsl(
+ var(
+ --huddle-tooltip-foreground,
+ var(--huddle-control-foreground, 0 0% 98%)
+ )
+ );
}
.buzz-huddle-drawer .buzz-huddle-control-button,
diff --git a/desktop/src/shared/ui/markdown.test.mjs b/desktop/src/shared/ui/markdown.test.mjs
index 5e829f62434..d35cf790634 100644
--- a/desktop/src/shared/ui/markdown.test.mjs
+++ b/desktop/src/shared/ui/markdown.test.mjs
@@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// These are copied here to avoid importing from .ts files that depend on
// React (which isn't resolvable outside the bundler). Same pattern as
@@ -656,6 +657,13 @@ test("buzzDeepLinkUrlTransform: preserves buzz://repo entity link href", () => {
assert.doesNotMatch(html, /href=""/);
});
+test("buzzDeepLinkUrlTransform: preserves buzz://project autolink href", () => {
+ const projectLink = `buzz://project?owner=${OWNER_HEX}&d=onboarding`;
+ const html = renderMarkdown(`<${projectLink}>`);
+ assert.match(html, /href="buzz:\/\/project\?/);
+ assert.doesNotMatch(html, /href=""/);
+});
+
test("buzzDeepLinkUrlTransform: strips malformed buzz://pr (unknown param)", () => {
// Strict parser rejects unknown params — transform falls back to default sanitizer.
const html = renderMarkdown(
@@ -1080,32 +1088,123 @@ test("bare Buzz permalinks render cohesive icon-prefixed chips", () => {
});
const html = renderToStaticMarkup(
React.createElement(
- MarkdownRuntimeContext.Provider,
- {
- value: {
- channels: [{ id: channelId, name: "engineering" }],
- onOpenChannel: () => {},
- onOpenEntityLink: () => {},
- onOpenMessageLink: () => {},
- relayOrigin: null,
+ QueryClientProvider,
+ { client: new QueryClient() },
+ React.createElement(
+ MarkdownRuntimeContext.Provider,
+ {
+ value: {
+ channels: [{ id: channelId, name: "engineering" }],
+ onOpenChannel: () => {},
+ onOpenEntityLink: () => {},
+ onOpenMessageLink: () => {},
+ relayOrigin: null,
+ },
},
- },
- markdown,
+ markdown,
+ ),
),
);
+ const visibleText = html.replace(/<[^>]+>/g, "");
assert.equal((html.match(/data-buzz-link=""/g) ?? []).length, 6);
- assert.equal((html.match(/inline-chip-icon-message/g) ?? []).length, 2);
- assert.equal((html.match(/>engineering · c3b589faengineering);
+ assert.match(html, /wrapping-inline-chip/);
+ assert.match(html, /inline-chip-leading-fragment[^>]*>e);
assert.match(html, /inline-chip-icon-pr/);
assert.match(html, /inline-chip-icon-issue/);
assert.match(html, /inline-chip-icon-repo/);
- assert.equal((html.match(/>buzz-world · c3b589fabuzz-world);
+ // Only the pull-request chip carries a short id; the issue chip is the bare
+ // repository name and the repo chip has never had one.
+ assert.equal((visibleText.match(/buzz-world · c3b589fa/g) ?? []).length, 1);
+ assert.match(visibleText, /buzz-world/);
+});
+
+test("inline issue chips show the repository name without the event hash", () => {
+ const renderEntityChip = (href) =>
+ renderToStaticMarkup(
+ renderEntityLinkAnchor({
+ children: null,
+ href,
+ onOpenEntityLink: () => {},
+ relayOrigin: null,
+ }),
+ );
+
+ const issueHtml = renderEntityChip(
+ `buzz://issue?id=${EVENT_HEX}&owner=${OWNER_HEX}&d=buzz-world`,
+ );
+ const issueText = issueHtml.replace(/<[^>]+>/g, "");
+ assert.equal(issueText, "buzz-world");
+ assert.doesNotMatch(issueText, /c3b589fa/);
+ assert.doesNotMatch(issueText, /·/);
+ // Identity, icon, and navigation affordances survive the shorter label.
+ assert.match(issueHtml, /data-buzz-link-kind="issue"/);
+ assert.match(issueHtml, /inline-chip-icon-issue/);
+ assert.match(
+ issueHtml,
+ /aria-label="Open issue c3b589fa in repository buzz-world"/,
+ );
+
+ // Pull-request chips are untouched by the issue-only policy.
+ const pullRequestText = renderEntityChip(
+ `buzz://pr?id=${EVENT_HEX}&owner=${OWNER_HEX}&d=buzz-world`,
+ ).replace(/<[^>]+>/g, "");
+ assert.equal(pullRequestText, "buzz-world · c3b589fa");
+});
+
+test("inline message chips omit fetched metadata and the event hash", () => {
+ const channelId = "580ca78b-9dae-46f3-8854-bd671853ba32";
+ const markdown = renderCachedMarkdown({
+ components: createMarkdownComponents(true, false),
+ content: `buzz://message?channel=${channelId}&id=${EVENT_HEX}`,
+ variant: "inline-message-chip-metadata-test",
+ });
+ // A readable channel is the case that used to swap the chip label from the
+ // truncated event hash to the fetched snippet once metadata resolved.
+ const html = renderToStaticMarkup(
+ React.createElement(
+ QueryClientProvider,
+ { client: new QueryClient() },
+ React.createElement(
+ MarkdownRuntimeContext.Provider,
+ {
+ value: {
+ channels: [
+ {
+ id: channelId,
+ isMember: true,
+ name: "engineering",
+ visibility: "open",
+ },
+ ],
+ onOpenChannel: () => {},
+ onOpenEntityLink: () => {},
+ onOpenMessageLink: () => {},
+ relayOrigin: null,
+ },
+ },
+ markdown,
+ ),
+ ),
+ );
+
+ const visibleText = html.replace(/<[^>]+>/g, "");
+ assert.equal((html.match(/data-message-link=""/g) ?? []).length, 1);
+ assert.equal(visibleText.trim(), "engineering");
+ assert.doesNotMatch(visibleText, /c3b589fa/);
+ assert.doesNotMatch(visibleText, /·/);
});
test("authored Buzz permalink labels remain ordinary links", () => {
@@ -1174,8 +1273,11 @@ test("bare Buzz permalinks shorten unavailable channel identifiers", () => {
),
);
- assert.match(html, />580ca78b · c3b589fa);
- assert.match(html, />580ca78b);
+ assert.equal(
+ (html.match(/inline-chip-leading-fragment[^>]*>5<\/span>80ca78b/g) ?? [])
+ .length,
+ 2,
+ );
assert.doesNotMatch(html, /#channel/);
});
@@ -1204,7 +1306,9 @@ test("channel references replace the authored hash with the channel icon", () =>
);
assert.match(html, /inline-chip-icon-channel/);
- assert.match(html, />engineering);
+ assert.match(html, /wrapping-inline-chip/);
+ assert.match(html, /inline-chip-leading-fragment[^>]*>e);
+ assert.match(html.replace(/<[^>]+>/g, ""), /engineering/);
assert.doesNotMatch(html, />#engineering);
});
@@ -1281,8 +1385,15 @@ test("renderEntityLinkAnchor renders Buzz entity links as chips", () => {
});
const html = renderToStaticMarkup(el);
assert.match(html, /data-buzz-link=""/);
- assert.match(html, /