Skip to content

feat(search): show channel type icon in search result subtitles - #2521

Merged
matt2e merged 9 commits into
mainfrom
broken-2
Nov 5, 2026
Merged

matt2e merged 9 commits into
mainfrom
broken-2

Conversation

@matt2e

@matt2e matt2e commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Message search results now start their subtitle with the conversation's type icon: # for public channels and forums, a lock for private channels, and a chat bubble for DMs, group DMs and Solo chats. The icon is styled like an inline channel mention: text-sized, with the hash flush against the name and the lock or bubble a --space-half gap away. Screen readers hear the type ("Public channel", "Private channel", "Direct message", "Chat") as sr-only text before the name.

Before / after

search-subtitle-before-after.mp4

Final frame: the same "offsite" search on the base (left) and this branch (right), covering a public channel, a private channel, a Solo chat, a DM and a group DM.

Before and after: the same five message hits, with a hash, lock or chat bubble now leading each subtitle

Channel mention in a message vs. search subtitle at 14px text: the hash sits flush, the lock and bubble sit 2px from the name.

A channel mention and a search subtitle side by side for the hash, lock and chat bubble, with box and ink gaps labelled

Zoomed to 8px per CSS px: each icon's box, where the name starts, and the baseline.

Zoomed icon and first letter for the hash, lock and chat bubble, in a message mention and a search subtitle

What changed

  • SearchChoices / SearchResults: SearchDestination gains optional detailIcon and detailIconLabel. Message hits set both from session.channels.get, and the palette renders the icon and an sr-only label before the detail text. Grid rows add the label to their aria-label. The label stays out of detail, so ranking is unchanged, and a hit with no known channel keeps the text-only subtitle.
  • channelIconLabel sits next to channelIcon in features/channels/channel-icon.ts, so the icon and its words stay in step.
  • InlineReference.module.css gains two modifiers. .flush (no margin) replaces the [data-link-kind="channel"] .icon selector, and .snug sets margin-right: var(--space-half). channelIconClass(Icon) in src/shared/inline-reference.ts returns .flush for a hash and .snug for any other icon, and both the search subtitle and LinkLabel use it. As a result, a private channel's lock in a message mention now also sits a small gap from the name.
  • Tests: the SearchResults subtitle test covers a public channel, a private forum (lock) and a group DM (bubble, participant names), plus the option and grid-row accessible names. It also checks that rows without an icon get none. InlineLink adds a test that a public channel's hash is flush. The InlineLink and MessageMarkdown lock expectations now use .snug. A channel-icon test covers channelIconLabel.
  • docs/buzz-help.md: one sentence on the icon and what screen readers announce.

Notes for reviewers

  • DM and Solo hits show a chat bubble that repeats the row's leading bubble. This is on purpose, so the subtitle matches what channelIcon shows elsewhere (channel header, tabs).
  • With the smallest spacing token, the lock looks about 1.6px looser than the hash; the bubble matches. design:check doesn't allow a literal em gap here.
  • Not yet checked with VoiceOver/WebKit or against a live relay. Accessible names were checked in jsdom and headless Chromium.
  • The demo media used an uncommitted fixture patch to seed a private channel and DM/Solo messages. The images are labelled 7cdba49f29, a pre-rebase commit; this head renders the same, since the only later commit moves the class choice into a helper without changing the markup.

🤖 Generated with Claude Code

@matt2e
matt2e requested a review from a team as a code owner November 4, 2026 23:09
matt2e and others added 9 commits November 5, 2026 12:10
Message search results named their conversation in the subtitle without
any type cue, so a private channel looked the same as a public one.

The subtitle now starts with the icon the sidebar shows for that
conversation, chosen by the shared `channelIcon` helper: a hash for
public channels and forums, a lock for private channels, and a chat
bubble for DMs, group DMs and Solo chats. The channel comes from
`session.channels.get`, which `useSearchMessages` already requires for
every listed hit (readable, not a Huddle, metadata settled), so the
search model needs no new field. `SearchDestination` gains an optional
`detailIcon` that the palette renders inline before the detail text;
other rows are unchanged, and a hit with no known channel keeps the
text-only subtitle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
The type icon leading a message result's conversation name is
decorative, so screen readers heard "plans" whether the channel was
public or private.

`SearchDestination` gains an optional `detailIconLabel`, which the
palette renders as sr-only text read in place of the icon: "Public
channel", "Private channel", "Direct message" (1:1 and group) or
"Chat" (Solo), using the wording ChannelCompletion, BuzzLinkPreview
and the palette's own conversation rows already use. The words come
from `channelIconLabel`, next to `channelIcon` so the two stay in step.
They stay out of `detail`, which feeds ranking, but grid rows (shown
beside inline settings) add them to their own `aria-label`.

Accessible names trim each element's edges, so the separating space
sits outside the sr-only span, before the icon. On screen that space
falls at the start of the line and collapses. Headless Chromium names
the option "match in plans Private channel, plans · …" with the icon
still flush left.

The icon's gap is now `--space-1`, matching the inline agent icon by
a message author. The help guide and `detailIcon` comment no longer say
the icon matches the sidebar, which shows avatars and participant
counts for DMs. It matches the channel header and page tabs.

The subtitle test now covers a private forum (lock) and a group DM
(bubble, participant names), and asserts the option and grid-row
accessible names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
…span

The comment by a message hit's sr-only channel-type label said accessible
names trim the span's edges. That trimming comes from jsdom's
dom-accessibility-api, which trims each element's text; Chromium's
accessibility tree gives the same name with the space inside, outside or
missing. The comment now gives that reason, so nobody moves the space in
the repo's other sr-only spans or assumes browsers need it outside. Shown,
the space still collapses at the line's start. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
A message hit's channel-type icon sat 4px from the conversation name at
1em (14px), nudged down -0.125em inline. Those values came from the agent
icon beside a message author's name, which isn't a channel icon next to
a channel name, so the subtitle looked unlike everywhere else.

The sidebar row, the channel header's tab and PanelHeaderLabel (drafts
header) all show a 16px icon, 8px before the name, centred with flex
align-items. The subtitle now does the same. It is a flex row with
`--space-row-gap`, the gap the header tab and PanelHeaderLabel use. Its
icon is 16px and doesn't shrink, and the detail text truncates in its
own span. Rows without an icon look the same, and the sr-only type label
is unchanged. Between flex items the space after it isn't shown.

In headless Chromium, for Alpha at 16px root text the sidebar, header
and subtitle each measure 16x16 with an 8px gap, icon centre 0.5px
below the text's (subtitle 0.48px). Before, the subtitle was 14x14 with
a 4px gap. At 24px root text all three measure 24x24 with a 12px gap,
where the subtitle was 21x21 with 6px. The option's name is still
"… Public channel, Alpha · …", and at 360px wide the text still ends in
an ellipsis.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
The rule for a message hit's channel-type icon had a comment saying
1rem, but the rule set no size. The size came from `size={16}` on the
icon in SearchChoices.tsx, which `defineIcon` turns into 1rem.
PanelHeaderLabel and `.buzz-tabs-icon`, the two that comment cites, set
their 1rem in CSS.

`.search-palette-detail-icon` now sets `width: 1rem; height: 1rem`, and
the icon no longer passes `size`. Size, gap and alignment now all sit
in SearchChoices.css under the comment that describes them. Without the
prop the SVG's width and height attributes are 1em, but the CSS
overrides them.

Rendering is unchanged. In headless Chromium the real SearchChoices
with a public and a private channel hit measures 16x16 with an 8px gap
at 16px root text, 24x24 with a 12px gap at 24px, the same as before.
Long details still end in an ellipsis at 360px, the option's name is
unchanged, and before and after screenshots are byte-identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
A message hit's channel-type icon was a 16px flex item 8px before the
conversation name, spaced like the sidebar and channel header. It now
looks like a channel mention in a message body, where `#general` reads
as one inline unit: the icon at text size, flush against the name.

Mentions get that look from LinkLabel (bundled/links/InlineLink.tsx).
It wraps a channel reference's label in `data-link-kind="channel"` and
gives the icon InlineReference.module.css's `.icon`: inline-block,
1em square, `vertical-align: -0.125em`. Its 0.375rem gap is reset to 0
under a channel label. The subtitle now reuses both: the same `.icon`
class, and the same attribute on the subtitle when it has an icon. It
drops the local flex rules. The link chrome stays out, because the
anchor and `.link` own the link colour, underline, hover padding and
click handling. So the pair keeps the subtitle's subtle colour and is
not a link. The subtitle is a single truncating line again, and the
sr-only type label's space collapses at the line's start.

In headless Chromium, the real app showed "Offsite agenda: see #general
and #leadership" in #general and search hits for the same channels.
Message text and subtitle are both 14px. For the mention and the
subtitle alike, hash and lock measure 14x14 (1em) with a 0px gap, the
icon's bottom 1.75px (0.125em) below the baseline and its centre 0.25px
below the first letter's. At 24px root text both measure 21x21, 0px
gap and 2.63px below the baseline. Chat bubbles for Solo and DM hits
match. Dark mode uses the subtle role (icon and text alike). At 360px
wide, long subtitles still end in an ellipsis with the icon first. The
option's name is unchanged ("… Public channel, general · …").

The subtitle test now finds the icon as `.icon` inside the channel
label, and fails if the label's attribute is dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
A message hit's subtitle borrowed `data-link-kind="channel"` so that its
channel-type icon would drop the reference icon's gap. That attribute
names a link's target, and LinkLabel sets it from `linkKind(href)`. The
subtitle isn't a link, the attribute covered the whole line (" · Alice,
Bob" included), and DM and Solo hits had a chat bubble tagged "channel",
where LinkLabel gives a bubble kind "message". Any later rule, copy
serializer or unscoped e2e locator for channel links would have matched
search rows too.

InlineReference.module.css now has a `.flush` modifier: `.icon.flush`
replaces `[data-link-kind="channel"] .icon` in the zero-margin rule. It
has to live in that unlayered module, because SearchChoices.css sits in
`@layer components` and would lose to `.icon` there. The subtitle icon
takes `.icon` and `.flush` and no data attribute. LinkLabel gives its
default channel icon the same pair, so one class owns the flush look.
LinkLabel keeps its `data-link-kind` span. Only PullLink passes a custom
icon, and only for GitHub links, so every icon the old selector matched
now has the class.

The SearchResults subtitle test now finds the icon as `.icon.flush` and
checks that a page row with a plain detail has no reference icon. Both
checks fail if the class is dropped or an icon is added to icon-less
rows. The InlineLink and MessageMarkdown lock-icon expectations include
`.flush`.

Rendering is unchanged. In headless Chromium at 2x, the earlier
mention-vs-subtitle spec (#general and #leadership mentioned in
a message, then searched) gave byte-identical message and search
screenshots for 5178bd89b3 and this tree. Measurements matched apart
from class names: each hash and lock is 14x14 with a 0px gap, 1.75px
below the baseline, with a computed margin-right of 0. The subtitle span
no longer has `data-link-kind`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
A lock or chat bubble looked tighter against a conversation name than
a hash did, in message search subtitles and in channel mentions in
messages. Since the previous commit all three sat flush.

The tabler hash reaches the right of its box only at its two bars'
ends; between them its diagonal sits about 0.15em further in. A lock's
body and a chat bubble's curve fill that edge. InlineReference.module.css
now has a `.snug` modifier, `margin-right: var(--space-half)`, beside
`.flush`. The subtitle gives a hash icon `.flush` and any other icon
`.snug`. LinkLabel does the same for channel links: a hash stays flush,
a lock is snug. Message, thread and service link icons keep the
reference icon's default gap.

The request was for an em gap, 0.125em or 0.1875em. design:check
(check-app-foundations) rejects any literal px, rem or em in margins
outside the design system, so the gap uses the smallest spacing token.
Both hosts set this text at 0.875rem, so --space-half is 0.143em there,
between the two candidates.

Measured in headless Chromium at 4x, with every icon followed by "l"
(lounge, leadership, lists). "Mean" is the white between icon ink and
letter ink averaged over the letter's rows. "Edge" is closest ink to
closest ink. Search subtitle, 14px text, light and dark alike, before
then after:

  hash    mean 5.11, edge 3.45 (unchanged)
  lock    mean 4.26 -> 6.01 at 0.125em, 7.01 at 0.1875em, 6.26 token
  bubble  mean 3.41 -> 5.16 at 0.125em, 6.16 at 0.1875em, 5.41 token

0.125em beat 0.1875em for both icons, at 12.25px, 14px and 21px text
alike. The token is 0.25px wider than 0.125em at 14px text. The bubble
now matches the hash (+0.30px). The lock goes from 0.85px tighter to
1.15px looser by the mean. By the edge measure the hash and lock were
already equal (both 2.33px of box white), so the lock alone would match
best at about half the gap. Message mentions agree within about 0.5px.
Chromium snaps an inline SVG to whole CSS pixels, so a mention's icon
shifts with where it falls in the line.

A run of the unmodified HEAD reproduced the "before" numbers exactly.
Accessible names are unchanged ("… Private channel, leadership · …").
At 360px wide, subtitles that truncated still end in an ellipsis with
the icon first, and the others still fit.

The SearchResults subtitle test expects `.flush` on hash icons and
`.snug` on the rest. The InlineLink test now also checks that a public
channel's hash is flush, and the InlineLink and MessageMarkdown lock
expectations use `.snug`. All three fail against HEAD's components.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>
The message search subtitle and LinkLabel each decided for themselves
that a hash icon takes `.flush` and any other channel icon `.snug`, so
the rule had two owners.

`channelIconClass(Icon)` in src/shared/inline-reference.ts, next to
InlineReference.module.css, now returns `.icon` plus the right
modifier. SearchChoices uses it for a hit's detail icon, and LinkLabel
for its default channel icon. Message, thread and service icons still
get plain `.icon`, and a supplied icon (PullLink's GitHub icon) still
skips this code.

In InlineLink.test.tsx the check that a public channel's hash is flush
moves out of the private-channel lock test into its own test, so a
failure names the right case.

Markup is unchanged. The exact-markup expectations in SearchResults,
InlineLink and MessageMarkdown pass as they were, and swapping the
helper's two classes fails all four tests that cover them (subtitle,
private lock, public hash, Markdown lock).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: mtoohey <[email protected]>

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kyle reviewed this PR and the Nexus review-tracker notes. Approved.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@matt2e
matt2e merged commit ecc5d4b into main Nov 5, 2026
41 checks passed
@matt2e
matt2e deleted the broken-2 branch November 5, 2026 06:18
ljknight added a commit to ljknight/buzz-app that referenced this pull request Nov 5, 2026
* origin/main:
  Keep /leave commands out of channel history (block#2476)
  fix(agents): ask before switching or closing drops unsaved edits (block#2503)
  fix(agents): keep the Model settings row full width (block#2577)
  feat(github): show a pull request's changes beside its summary (block#2492)
  fix(search): open an existing DM from a ⌘K person row without a relay open; cover hidden-DM previews (block#2519)
  feat(search): show channel type icon in search result subtitles (block#2521)
  test(browser): bind port-0 fixture servers where the OS assigns (block#2564)
  test(browser): fix six journey sync flakes from the 2026-11-04 sweep (block#2566)
  fix(channels): ask before Escape, Close or an outside click drops a template draft (block#2501)
  fix(channels): render the channel details description as linked Markdown with a label-only Edit target (block#2506)
  fix(agents): let Back and Forward shortcuts traverse provider dialogs (block#2509)
  Fix Inbox recovery after a delayed Retry (block#2496)

# Conflicts:
#	docs/buzz-help.md
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…-service

* origin/main: (68 commits)
  fix(messages): keep decoded images across virtualized remounts (#2474)
  Keep /leave commands out of channel history (#2476)
  fix(agents): ask before switching or closing drops unsaved edits (#2503)
  fix(agents): keep the Model settings row full width (#2577)
  feat(github): show a pull request's changes beside its summary (#2492)
  fix(search): open an existing DM from a ⌘K person row without a relay open; cover hidden-DM previews (#2519)
  feat(search): show channel type icon in search result subtitles (#2521)
  test(browser): bind port-0 fixture servers where the OS assigns (#2564)
  test(browser): fix six journey sync flakes from the 2026-11-04 sweep (#2566)
  fix(channels): ask before Escape, Close or an outside click drops a template draft (#2501)
  fix(channels): render the channel details description as linked Markdown with a label-only Edit target (#2506)
  fix(agents): let Back and Forward shortcuts traverse provider dialogs (#2509)
  Fix Inbox recovery after a delayed Retry (#2496)
  feat(composer): optional ⌘/Ctrl+Enter to send (#2331)
  test: use keyboard activation for reply caret setup (#2557)
  test: settle member search before invitation expansion (#2532)
  test(agent-command): isolate the copied executable writer (#2537)
  test(sidebar): wait for the rolled-back row to leave Starred before collapsing Work (#2574)
  test(browser): click timeline rows only after the timeline settles (#2560)
  ci: split Vitest across six shards so 3/4 stops hitting the job cap (#2571)
  ...

# Conflicts:
#	src/app/Settings.tsx
#	src/bundled/goose/sessions.test.ts
agude-sq added a commit to agude-sq/buzz-app that referenced this pull request Nov 5, 2026
* origin/main: (43 commits)
  feat(settings): follow the system clock and let people hide message times (block#2440)
  feat(plugins): repository access rules for Git imports (block#2484)
  feat(desktop): add Settings to the macOS app menu (block#2594)
  Preserve and renew external app approval (block#2464)
  fix(direct-messages): show verified identity in new message recipient search (block#2585)
  Offer plugin updates from the Buzz shell (block#2491)
  fix(messages): keep decoded images across virtualized remounts (block#2474)
  Keep /leave commands out of channel history (block#2476)
  fix(agents): ask before switching or closing drops unsaved edits (block#2503)
  fix(agents): keep the Model settings row full width (block#2577)
  feat(github): show a pull request's changes beside its summary (block#2492)
  fix(search): open an existing DM from a ⌘K person row without a relay open; cover hidden-DM previews (block#2519)
  feat(search): show channel type icon in search result subtitles (block#2521)
  test(browser): bind port-0 fixture servers where the OS assigns (block#2564)
  test(browser): fix six journey sync flakes from the 2026-11-04 sweep (block#2566)
  fix(channels): ask before Escape, Close or an outside click drops a template draft (block#2501)
  fix(channels): render the channel details description as linked Markdown with a label-only Edit target (block#2506)
  fix(agents): let Back and Forward shortcuts traverse provider dialogs (block#2509)
  Fix Inbox recovery after a delayed Retry (block#2496)
  feat(composer): optional ⌘/Ctrl+Enter to send (block#2331)
  ...

# Conflicts:
#	docs/buzz-help.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants