Skip to content

fix(channels): render the channel details description as linked Markdown with a label-only Edit target - #2506

Merged
matt2e merged 3 commits into
mainfrom
broken-11
Nov 5, 2026
Merged

matt2e merged 3 commits into
mainfrom
broken-11

Conversation

@matt2e

@matt2e matt2e commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

What changed

In Messages → channel → ⋯ Channel actions → View channel details, the Description row now renders through MessageMarkdown, the same renderer messages use, instead of plain text.

  • URLs, Buzz links and #channel references are real links for every viewer: owners, admins and plain members. #name only links when it exactly matches a channel name, including case.
  • Bold, italics and other Markdown render, instead of showing raw ** / _ markers.
  • Only the Description label and its pencil open Edit channel details. Clicking the description text no longer opens the editor. The pencil still reveals on row hover, the label is a 24px target, and the row has no hover fill. "No description" and "Not available" stay plain text. Members and Channel ID keep their whole-row actions.
  • The panel's Escape handler now has ThreadPanel's guard: it skips events that are already handled or that come from a portalled layer. A description link's context menu or preview card can't close the panel.

Link safety is the same as in messages: raw HTML is skipped, only Buzz, profile and http(s) URLs pass safeLinkUrl, images render as alt text, and external links get target=_blank rel="noopener noreferrer".

Why

Before this change, the description was plain text, so its URLs and #channel references couldn't be followed. For owners and admins it was worse: the Edit button covered the whole row with an ::after overlay, so clicking a URL in the description opened Edit channel details. docs/channels.md documented that whole-row behaviour on purpose, and this PR updates it. Lifting the links above the overlay was rejected, because DESIGN.md says to "keep an independent row action outside the row's primary click target". The label-only SettingsDetail variant follows that rule.

Scope

This PR covers channel details only. ⌘K and the join notice are left alone on purpose. Unjoined channels still show only "Public channel · not joined" in ⌘K, and the join notice still says only "Join channel to send messages". Showing the description there edits the same lines as #2333 (member counts in channel results) and #2301 (joining read-only spaces), so it will be a separate follow-up PR stacked on those two. BuzzLinkPreview is also out of scope (see Open points).

Before / after

Both sides ran the same Playwright script against the same fixture, viewport (1280×800), light theme and timeline. Before is 0b2dfbf988 (this branch's base). After is 8e1ffa8dcc; the last commit only changes tests. The fixture description is **R2 ops** on-call. Runbook: https://example.test/r2-runbook · escalate in #Beta · _quiet hours after 22:00 UTC_.

description-before-after.mp4

Each side gets the same clicks at the same moment. Before: clicking the URL (11.4s) and #Beta (15.9s) both open "Edit channel details". After: the URL opens a new window, #Beta goes to the Beta channel, and only the Description label (21.7s) opens the editor. The pointer, red click dot and action captions come from Playwright's showActions and are not app UI.

Side-by-side at t=9.7s with the pointer on "on-call": before shows raw ** and _ markers, a plain-text URL and the whole row greyed as one button; after shows bold, underlined URL and #Beta links, italics, and only the pencil next to "Description"

Pointer resting on "on-call". Before: raw ** and _ markers, the URL as plain text wrapping at its hyphen, and the whole row greyed because the row is one button. After: bold text, underlined URL and #Beta links, italics, and only the pencil next to "Description", with no row fill.

Before (0b2dfbf988) After (8e1ffa8dcc)
Before: Description row shows raw Markdown markers and a plain-text URL, and the whole row is greyed on hover After: Description row shows rendered Markdown with URL and #Beta links, and only the pencil appears on hover

Full-window stills behind the side-by-side image, captured losslessly with page.screenshot at the same moment (no pointer overlay).

Commits

  1. fix(channels): link the description in channel details (43db4ea): ChannelSettingsPanel renders a non-empty description with MessageMarkdown, using a synthetic row, the session's reference directory and the page's openLink / canOpenLink. ChannelsPage passes the session and link handlers. A new label-only SettingsDetail variant drops the whole-row overlay for Description. CSS and docs/channels.md are updated, and Vitest and browser cases are added.
  2. fix(channels): tighten the linked description row in channel details (8e1ffa8): follow-ups from the first AI review (below).
  3. test(channels): prove the description row neither grows nor highlights (fce07e4): follow-ups from the second AI review (below). Test-only.

AI review follow-ups

First review of 43db4ea found no blockers and five polish suggestions. All five, plus the Escape guard, landed in 8e1ffa8:

  • Moved useReferenceDirectory and the synthetic row into a small ChannelDescription component. It mounts only for a non-empty description with a session and link handler. Profile and channel-list snapshots no longer re-render the whole panel and its editor surface for DM, solo or empty-description channels.
  • Dropped aria-description from the label-only Edit button. It made screen readers read the raw Markdown source right before the rendered value.
  • Gave the label a 24px minimum target, as DESIGN.md asks for inline targets, with a negative block margin so the row doesn't move. The pencil reveals on row hover again, including under reduced motion, but only the label takes the click.
  • Set line-height: inherit on the description cell, so it lines up with Members and Channel ID instead of using message line height.
  • Restored "no animation for reduced motion" in the Channel ID docs, and described the row-hover reveal and the label target.
  • Guarded the panel's Escape handler like ThreadPanel's.

Second review of 8e1ffa8 found no merge blockers and confirmed all six follow-ups. It raised two test-strength suggestions, both applied in fce07e4:

  • The 24px label-target check would still have passed if the negative margin-block stopped applying, because the row would just grow. It now checks that the Description value sits at the same offset inside its row as Members' value (40px inside the top border).
  • The "no row highlight" check hovered the padding and read the background straight away. It now hovers the value text, waits for the row-hover pencil reveal as proof the row is hovered, then checks the background. It also hovers the label, which catches a highlight rule that loses its label-only exclusion.

Tests

  • Vitest (ChannelDetailsEditor.test.tsx): an editor and a member each get link roles, onOpenLink targets, an editor that stays closed on link clicks, and a label that still opens it. ChannelSettingsPanel.test.tsx covers the Escape guard and the missing accessible description.
  • Browser case added: "description links" → "an owner's description links open their targets, not the editor" in tests/browser/channel-settings.spec.mjs. It's a browser case because it checks layout: which element gets a click at a given point, the 24px label target, row offsets and hover paint. jsdom can't model any of that.
  • Browser case updated: tests/browser/channel-header-menu.spec.mjs now expects the Description row to be label-only (pencil revealed, no row fill). Members still expects a whole-row action. No browser cases were removed.
  • Fail-then-pass: the commit 1 Vitest and browser cases fail on 0b2dfbf988. The commit 2 Escape and accessible-description assertions fail on 43db4ea. Each commit 3 assertion fails under a deliberate CSS break in Chromium and WebKit; for example, removing the margin gives 44 vs 39.98.

Checks run

On fce07e4 (the PR head), locally on a Blox workstation:

  • typecheck, lint, design checks and Biome: pass
  • Vitest on src/bundled/channels: pass (1,000 tests across 60 files)
  • tests/browser/channel-settings.spec.mjs and tests/browser/channel-header-menu.spec.mjs: pass in Chromium and WebKit (18 runs)

Not run:

  • The full browser suite.
  • Any checks against current main. This branch is based on 0b2dfbf988. git merge-tree shows a clean merge with main, but main has since changed docs/channels.md, Channels.module.css, ChannelsPage.tsx and channel-header-menu.spec.mjs.

Hosted CI is the first run on a merged tree.

Open points

  1. The #channel preview card can cover the Description label. Hovering #Beta in the description shows MessageLink's existing channel preview card. The card can hide the "Description" label and pencil, which is now the only way to start editing. In the recording the card had dismissed before the pointer reached the label, so the click worked, but this interaction is new to the panel.
  2. The panel now parses descriptions as Markdown, but BuzzLinkPreview still shows them raw. The same channel can look different in the two places; for example, # Ops becomes a heading in the panel. This trade-off was accepted for this PR, and BuzzLinkPreview was out of scope.
  3. Not yet tried in the native app. Only Vitest, the browser specs and headless Chromium recordings have exercised this change. The human test step below is still pending.

How to test

  1. Open a channel you own. Choose ⋯ Channel actions → Edit channel details and set the description to **Ops** runbook: https://example.com · see #<another channel's exact name> · _quiet hours_.
  2. Choose ⋯ Channel actions → View channel details. Expected: bold and italics render, and the URL and #channel are underlined links. Hovering the row shows the pencil next to "Description" but doesn't fill the row.
  3. Click the URL. Expected: it opens in the browser, and the editor doesn't open.
  4. Click the #channel link. Expected: it goes to that channel.
  5. Go back, reopen details, and click the "Description" label. Expected: Edit channel details opens.
  6. Repeat steps 2–4 as a plain member. Expected: the same links, and no pencil.

🤖 Generated with Claude Code

@matt2e
matt2e marked this pull request as ready for review November 4, 2026 22:13
@matt2e
matt2e requested a review from a team as a code owner November 4, 2026 22:13
@matt2e
matt2e force-pushed the broken-11 branch 2 times, most recently from 25ec2bf to bf3e4fe Compare November 5, 2026 00:45
matt2e and others added 3 commits November 5, 2026 01:24
Channel details showed the description as plain text, so its URLs and
#channel references could not be followed. For owners and admins, the
Description row's Edit button covered the whole row with an ::after
overlay, so clicking anywhere in the text opened Edit channel details.

The description now renders through MessageMarkdown, with the session's
reference directory and the page's openLink and canOpenLink handlers.
URLs, Buzz links and #channel references open their targets for every
viewer. The Description row uses a new label-only SettingsDetail
variant: the "Description" label and pencil open the editor, and the
value no longer does. This follows DESIGN.md's rule that an
independent action stays outside a row's primary click target. "No
description" and "Not available" stay plain text. Members and Channel
ID keep their whole-row actions.

docs/channels.md describes the new contract. A Vitest case covers an
editor and a member: link roles, onOpenLink targets, the editor staying
closed, and the label still opening it. A browser case covers layout
hit-testing, which jsdom can't model. Both fail before the fix. The
header-menu journey now asserts the label-only Description row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Follow-ups from review of the linked channel description:

- Move the reference directory and synthetic row into a small
  ChannelDescription component. It mounts only for a non-empty
  description with a session and link handler, so profile and channel
  list snapshots no longer re-render the whole panel and its editor
  surface for DM, solo or empty-description channels.
- Drop aria-description from the label-only Edit button. It read the
  raw Markdown source right before the rendered value.
- Give the label a 24px minimum target, as DESIGN.md asks for inline
  targets, with a negative block margin so the row doesn't move. The
  pencil reveals on row hover again, including under reduced motion,
  but only the label takes the click.
- Inherit the values' line height in the description cell, so it lines
  up with Members and Channel ID.
- Restore "no animation for reduced motion" in the Channel ID docs, and
  describe the row-hover reveal and the label target.
- Guard the panel's Escape handler like ThreadPanel's: skip it when the
  event is already handled or comes from a portalled layer, so a
  description link's menu or preview card can't close the panel.

New tests cover the Escape guard, the missing accessible description,
the 24px target and the row-hover reveal. The Escape and description
assertions fail against the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Two browser assertions from review were weaker than they looked:

- The 24px label target check passed even if the negative margin-block
  stopped applying, because the row would just grow. The description
  case now checks that the Description value sits at the same offset
  as Members' (40px inside each row's top border). It doesn't measure
  the dt, which is 24px because the button's margins collapse through
  it. Removing the margin fails with 44 vs 39.98.
- The "no row highlight" check hovered the padding and snapshotted the
  background at once. It now hovers the value text, waits for the
  row-hover pencil reveal as proof the row is hovered, then checks the
  background. It also hovers the label after a :hover barrier. The
  label hover catches a highlight rule that loses its label-only
  exclusion, which hovering the value can't, because the label-only
  button doesn't cover the value. The value hover catches a plain
  row-level :hover background.

Each assertion fails under its deliberate CSS break in Chromium and
WebKit and passes on the current CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>

@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 and approved with no comments.

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

@matt2e
matt2e merged commit a8f49f5 into main Nov 5, 2026
42 checks passed
@matt2e
matt2e deleted the broken-11 branch November 5, 2026 05:52
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
wesbillman pushed a commit that referenced this pull request Nov 5, 2026
…2402)

Scope: let ⇧Esc reach Mark all as read while focus is in the Channel
settings panel. Non-goals: other Escape handlers and any change to the
shortcut itself. Reuses the guard from the sibling panels. One file, 2
production lines.

## What was wrong

With focus inside a channel's settings panel, ⇧Esc (Mark all as read,
from #2170) closed the panel and marked nothing read.

## Why

On `main`, the panel's `onKeyDown` in
`src/bundled/channels/ChannelSettingsPanel.tsx:101` closes on any
`Escape` from inside the panel and calls `event.stopPropagation()`.
Since #2506 it skips events that are already handled or come from a
portalled layer, but it still has no `!event.shiftKey` check. The
sibling panels got that check in #2170
(`src/features/panels/PanelCard.tsx:32`,
`src/bundled/channels/useChannelPanels.tsx:152`). Because the panel
stops the event, the window shortcut dispatcher
(`src/features/shortcuts/service.ts`,
`src/app/shell/MarkAllReadShortcut.tsx`) never sees ⇧Esc.

The fix adds the same `!event.shiftKey` guard and comment the siblings
use, next to the #2506 checks. Plain Esc still closes the panel.

## How verified

- Extended the existing Escape test in
`src/bundled/channels/ChannelSettingsPanel.test.tsx`, "closes with
Escape or its close button without swallowing other keys": ⇧Esc does not
call `close` and reaches a `window` keydown listener; plain Esc still
closes and does not reach it. It fails without the fix and passes with
it.
- `bin/pnpm vitest run
src/bundled/channels/ChannelSettingsPanel.test.tsx`: 28 passed after
merging `main`, including #2506's handled and portalled Escape test.
- `bin/just iterate`: passed.

## In-app check

Build: this PR at `4b286248b`, with #2403, #2404 and #2405 at their
heads then, merged locally onto `main` at `cb8c971e9`. Run as the macOS
desktop app with `BUZZ_DEV_VIEWER= just desktop`. The branch was later
merged with `main` twice to resolve conflicts with #1624 (test file
only) and #2506 (the same Escape handler). The production behavior of
this fix did not change: ⇧Esc still passes through and plain Esc from a
focused control in the panel still closes it. #2506 only adds its own
skips for handled and portalled events.

With the mouse alone, focus never lands inside the panel in the WebKit
app, so these steps need macOS Keyboard navigation (System Settings >
Keyboard) or Tab focus.

1. Open Channel actions > View channel details. Press Tab until a
control in the panel shows a focus ring. Esc closes the panel.
2. Reopen the panel and Tab in again. ⇧Esc keeps the panel open and
shows "Mark everything as read?".

Where focus goes after opening the panel from the menu is a separate
issue.

buzz-review-completed

---------

Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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