Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6719336
feat(messages): reveal linked message metadata inline
tellaho Aug 17, 2026
2da0cb4
feat(messages): distinguish deleted message links
tellaho Aug 17, 2026
53631f9
fix(desktop): soften tooltip and disabled chip colors
tellaho Aug 18, 2026
277fea5
fix(desktop): keep tooltip metadata on one line
tellaho Aug 18, 2026
2985d29
test(desktop): prove DM tooltip metadata stays inline
tellaho Aug 18, 2026
6157244
fix(desktop-messages): fade deleted message chips
tellaho Aug 18, 2026
0ff61b2
fix(desktop-messages): show project context in entity tooltips
tellaho Aug 18, 2026
632b1bd
fix(desktop-messages): preserve unresolved message navigation
tellaho Aug 18, 2026
bffcba3
fix(desktop-tooltips): restore shared primary surface
tellaho Aug 18, 2026
eaa0260
fix(desktop-messages): mute unavailable link chips
tellaho Aug 18, 2026
de57b65
fix(desktop-messages): explain unavailable message links
tellaho Aug 19, 2026
80fd6e2
fix(desktop): dismiss tooltips when pointer leaves trigger
tellaho Aug 19, 2026
b4eef23
fix(desktop): wrap metadata chips inline
tellaho Aug 19, 2026
54ffd6c
fix(desktop): position wrapped chip tooltips
tellaho Aug 19, 2026
57fe759
fix(desktop): cap metadata chip widths
tellaho Aug 19, 2026
0329a5c
fix(desktop): refine metadata chip truncation
tellaho Aug 19, 2026
e4cd714
fix(desktop): wrap capped metadata chip labels
tellaho Aug 19, 2026
92a9cdf
fix(desktop-tooltips): use semantic secondary surfaces
tellaho Aug 18, 2026
fda6688
fix(desktop-tooltips): align remaining semantic tokens
tellaho Aug 19, 2026
7306b11
test(desktop): align wrapped chip markup assertions
tellaho Aug 19, 2026
57c3ee1
fix(desktop-messages): show fallback entity bylines only
tellaho Aug 19, 2026
d06cec7
test(desktop-messages): align delayed entity project context
tellaho Aug 19, 2026
893dcc3
fix(desktop-messages): keep message chips width stable
tellaho Aug 19, 2026
34fdceb
fix(desktop-issues): keep issue chips width stable
tellaho Aug 19, 2026
fd3a839
fix(desktop-tooltips): restore entity footer contrast
tellaho Aug 19, 2026
82ad04e
fix(desktop-tooltips): mute supporting metadata
tellaho Aug 19, 2026
f87b6ba
fix(desktop-chips): render bare project links
tellaho Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions desktop/src/features/agents/ui/RestartDiffBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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 (
Expand Down Expand Up @@ -180,7 +180,7 @@ export function RestartDiffBadge({
<TooltipContent className="max-w-72 text-xs" side="bottom">
<p className="mb-1.5 font-semibold">Config changed since last start:</p>
<DiffList cap={TOOLTIP_CAP} entries={restartDiff} />
<p className="mt-1.5 text-primary-foreground/70">
<p className="mt-1.5 text-secondary-foreground/70">
{autoRestartEnabled ? AUTO_RESTART_ON_BLURB : AUTO_RESTART_OFF_BLURB}
</p>
</TooltipContent>
Expand Down
10 changes: 6 additions & 4 deletions desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function AddChannelBotTeamsSection({
<div className="space-y-1.5">
<p className="font-medium">{team.name}</p>
{team.description ? (
<p className="text-2xs text-primary-foreground/80">
<p className="text-2xs text-secondary-foreground/80">
{team.description}
</p>
) : null}
Expand All @@ -153,15 +153,17 @@ export function AddChannelBotTeamsSection({
inChannelPersonaIds?.has(persona.id) ?? false;
return (
<div
className="flex items-center gap-1 rounded-full bg-primary-foreground/10 px-1.5 py-0.5"
className="flex items-center gap-1 rounded-full bg-secondary-foreground/10 px-1.5 py-0.5"
data-testid="team-tooltip-persona-chip"
key={persona.id}
>
<ProfileAvatar
avatarUrl={persona.avatarUrl}
className="h-4 w-4 text-3xs bg-primary-foreground/20 text-primary-foreground"
className="h-4 w-4 text-3xs bg-secondary-foreground/20 text-secondary-foreground"
label={persona.displayName}
testId="team-tooltip-persona-avatar"
/>
<span className="text-2xs text-primary-foreground">
<span className="text-2xs text-secondary-foreground">
{persona.displayName}
</span>
{personaInChannel ? (
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/features/communities/useCommunityInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -77,6 +78,7 @@ async function resetCommunityState({
resetLinkPreviewPreparations();
clearSearchHitEventCache();
clearMarkdownNodeCache();
resetMessageLinkMetadataCache();
}

type CommunityInitResult =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
12 changes: 8 additions & 4 deletions desktop/src/features/messages/lib/composerMessageLinkNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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,
};
}

Expand Down
24 changes: 24 additions & 0 deletions desktop/src/features/messages/lib/messageLinkMetadata.test.mjs
Original file line number Diff line number Diff line change
@@ -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![secret](https://example.com/a.png) ||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("🦄"));
});
29 changes: 29 additions & 0 deletions desktop/src/features/messages/lib/messageLinkMetadata.ts
Original file line number Diff line number Diff line change
@@ -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()}…`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
];
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/messages/lib/remarkEntityLinks.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/messages/ui/SentFromThreadLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function ProjectAuthorIdentity({
/>
<span className="min-w-0">
<span className="block truncate font-medium">{label}</span>
<span className="block text-primary-foreground/70">
<span className="block text-secondary-foreground/70">
{roleLabel}
</span>
</span>
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/projects/ui/ProjectCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ function RepositoryUnavailableIndicator({
</TooltipTrigger>
<TooltipContent className="max-w-64">
<p className="font-medium">{label}</p>
<p className="text-muted-foreground">{description}</p>
<p className="text-secondary-foreground">{description}</p>
</TooltipContent>
</Tooltip>
);
Expand Down
7 changes: 7 additions & 0 deletions desktop/src/shared/lib/useResolvedLinkPreviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LinkPreviewMetadata | null> {
return (await entityMetadataLoader.load(href)).metadata;
}

/** Clear ephemeral metadata when the active relay/community changes. */
export function resetLinkPreviewMetadataCache(): void {
metadataLoader.reset();
Expand Down
44 changes: 44 additions & 0 deletions desktop/src/shared/styles/globals/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions desktop/src/shared/styles/globals/tooltipSemantics.test.mjs
Original file line number Diff line number Diff line change
@@ -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)?/,
);
});
18 changes: 8 additions & 10 deletions desktop/src/shared/styles/globals/utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down
Loading
Loading