Skip to content

feat(web): polished chat & channels UI (0198) - #174

Merged
crs48 merged 4 commits into
mainfrom
claude/0198-chat-ui
Jun 18, 2026
Merged

feat(web): polished chat & channels UI (0198)#174
crs48 merged 4 commits into
mainfrom
claude/0198-chat-ui

Conversation

@crs48

@crs48 crs48 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Rebuilds the chat & channels presentation layer to the standard set by Slack/Discord/Zulip, on top of the existing schema/service/hooks spine (no schema change). Implements exploration 0198 — the core finding was that the data layer already modeled threads, reactions, attachments, edit/delete, and presence status, and the gap was almost entirely in rendering.

What's included

Message feed

  • Message grouping (consecutive same-author rows collapse to one avatar/name), date separators, and a "New messages" divider at the read watermark.
  • Smart stick-to-bottom (only when already near the bottom) + a jump-to-latest pill with the unread-ahead count, replacing the old always-jam scroll.
  • DIDAvatar avatars + Discord-style presence dots (active/idle/dnd) in rows, the header roster, and DM sidebar rows.
  • role="log"/aria-live, aria-pressed reactions, aria-label emoji, and prefers-reduced-motion guards.

Interactions

  • Hover action toolbar (quick-react, add-reaction, reply, edit-if-own, more) reusing the existing safety menu.
  • Emoji reactions over the existing Reaction schema — per-message pills with counts + who-reacted, optimistic toggle, and a dependency-free emoji picker.
  • Right-hand thread pane driven by inReplyTo; replies stay out of the main feed and the root shows a "N replies · last reply" footer.
  • Inline edit-in-place ( on an empty composer edits your last message); composer emoji-insert button; @/#/[[ pickers preserved.
  • Comfortable/compact density toggle in the header.

Header & sidebar

  • Rebuilt channel header: avatar stack, member count, editable topic, members popover.
  • Rebuilt ChatsPanel: presence dots on DMs, unread emphasis.

Verification

  • New unit tests for groupMessages + reaction grouping (17 tests); all 75 comms tests green; tsc --noEmit clean; eslint clean (0 errors).
  • Browser-verified on desktop (1440px) and the mobile BottomNav shell (375×812, single <main>): grouping, reaction toggle (👍 1, aria-pressed), thread reply + footer, editable topic, density toggle. No console errors.

Deferred (noted in the doc)

Windowed virtualization + load-older history (window capped at 100 today), file-attachment upload, message pinning, Cmd+K quick-switch + message search, optional TipTap composer / motion/react, and the optional Zulip-style topic mode.

Changelog

  • Redesigned the chat & channels UI: grouped messages with avatars, emoji reactions, threads, presence dots, editable channel topics, a comfortable/compact density toggle, and a jump-to-latest control.

🤖 Generated with Claude Code

xNet Test and others added 4 commits June 17, 2026 16:29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hreads (0198)

Phase 1+2 of the chat & channels overhaul. Rendering moves to a reusable set of
comms-UI pieces driven by the existing schema/service/hooks (no schema change):

- Message grouping (consecutive same-author rows collapse), date separators and
  a "New messages" divider, smart stick-to-bottom + jump-to-latest pill, role=log.
- Avatars (DIDAvatar) + Discord-style presence dots (active/idle/dnd) in rows,
  the header roster, and DM sidebar rows.
- Emoji reactions over the existing Reaction schema: per-message pills with
  counts + who-reacted, optimistic toggle, dependency-free emoji picker.
- Hover action toolbar (quick-react, add-reaction, reply, edit, more) reusing the
  existing safety menu; inline edit-in-place; up-arrow on empty composer edits last own.
- Right-hand thread pane driven by inReplyTo; replies kept out of the main feed,
  root shows a thread footer (count + participants + last reply).
- Composer gains an emoji insert button; @ / # / [[ pickers preserved.
- Rebuilt channel header (avatar stack, member count, editable topic, members
  popover) and sidebar (presence dots, unread emphasis).

Deferred (noted in 0198): windowed virtualization + load-older (window capped at
100 today), file-attachment upload, density toggle UI (store landed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 polish: a comfortable/compact density toggle in the channel header
(persisted via the chat-prefs store, shared across open channels), and
Escape-to-close on the thread pane when the reply box is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phases 1+2 and the cheap Phase-3 wins shipped and verified in browser preview;
checklist items updated with what landed vs what's deferred (virtualization,
file attach, pins, Cmd+K/search, Zulip topics).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-174 June 18, 2026 00:09 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

github-actions Bot added a commit that referenced this pull request Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #174.

github-actions Bot added a commit that referenced this pull request Jun 18, 2026
)
}

export function MessageRow({
}

/** Derive per-root thread summaries from the loaded reply messages. */
function buildThreadIndex(replies: ChatRow[]): Map<string, ThreadSummary> {
🛡 {hiddenCount} message(s) hidden by your block/mute list
</li>
)}
{rows.map((row) => {
}
}, [channels])

const leadingFor = (channel: ChannelEntry): ReactNode => {
import { ReactionBar } from './ReactionBar'
import { useMessageReactions } from './useMessageReactions'

function ThreadMessage({
)
}

function MembersButton({
const profiles = useProfiles()
const [creating, setCreating] = useState<'channel' | 'dm' | null>(null)

const presenceByDid = useMemo(() => {
)
}

function MessageMentionChips({ message }: { message: ChatRow }) {
rows={2}
aria-label="Edit message"
onChange={(event) => setValue(event.target.value)}
onKeyDown={(event) => {
rows={2}
placeholder="Reply…"
onChange={(event) => setText(event.target.value)}
onKeyDown={(event) => {
@crs48
crs48 merged commit 6247e34 into main Jun 18, 2026
11 of 13 checks passed
@crs48
crs48 deleted the claude/0198-chat-ui branch June 18, 2026 00:25
github-actions Bot added a commit that referenced this pull request Jun 18, 2026
crs48 added a commit that referenced this pull request Jun 18, 2026
## What

Backfills the changelog so it reflects what's live on `main`. The per-PR
fragment system (exploration 0197) was current through PR #163 (PR #178
backfilled #151/#153/#156/#157/#160/#163), but the recent **0198/0199
batch** and a couple of stragglers shipped without entries. This adds 7
fragments, with prose lifted from each PR's own `## Changelog` block
where present.

| PR | Entry | Tags |
|---|---|---|
| #143 | AI chat that knows your workspace | `ai` |
| #167 | Linear-grade Tasks | `tasks` `app` |
| #169 | Cleaner page editor typography | `editor` |
| #170 | Bring your Slack data into xNet | `plugins` `platform` |
| #171 | Smoother, more consistent motion | `app` |
| #173 | Airtable-grade database grids | `app` |
| #174 | A redesigned chat & channels experience | `app` |

## Deliberately excluded

- **In-editor AI (#154 / #158)** — shipped the transform engine +
diff/approval logic, but both PRs state the `/ai` UI wiring into the
running app is **deferred**, so it isn't user-visible yet. An entry
would overclaim.
- **Slack (#170)** — entry scoped to the migration connector +
compatibility layer (genuinely live); avoids claiming the
webhook/slash-command hub endpoints work, since they're
generic-over-an-injected-sink and not mounted in a running server yet.
- Pure infra/ops/docs PRs (cloud M1/M2, CI, runbooks) — not user-facing.

## Verification

- `validate-changelog.ts` → **26 fragments valid** (19 existing + 7 new)
- `prettier --check` on all new files → clean
- Newest-first ordering confirmed — the 7 entries slot in at the top

## Changelog

<!-- This PR only adds changelog fragments for already-merged work; it
ships no user-facing change of its own. -->

🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48 added a commit that referenced this pull request Jun 18, 2026
…g, param-route guard) (#185)

## Why

Our [Visual UI Capture](scripts/visuals/) system silently misses big UI
changes. The trigger: [PR #174](#174)
rewrote the entire chat layer (2,364 lines under `apps/web/src/comms/`)
and its capture comment read **"No visual differences detected in the
changed UI."** — the *absence* of change reported for one of the largest
UI changes in the project.

Root cause (full write-up in [exploration
0200](docs/explorations/0200_%5Bx%5D_VISUAL_CAPTURE_SILENT_COVERAGE_GAPS.md)):
capture targets come from a hand-curated allowlist of param-free routes
+ co-located stories. Chat matched none — no story, no route glob, and
it only renders at the **parameterized** `/channel/$channelId` route
(needs an id + seed data, so the static-route capturer can't reach it).
`computeCaptureSet` then fell back to capturing `home`, which diffs
clean, so the comment said "no differences." A **coverage gap was made
indistinguishable from a no-op**. The 0191 drift-guard that was meant to
catch this explicitly **skips `$`-routes**, the exact place chat lives.

## What this does

**C — make misses loud (keystone).** When changed UI files map to *no*
story/route/flow, `computeCaptureSet` now records `fallbackUsed` +
`unmappedFiles` (threaded capture-set → capture → diff → comment). The
PR comment renders a `> [!WARNING]` listing the unmapped files instead
of "No visual differences detected." This converts every future silent
miss into an actionable nudge.

**A — `chat` flow.** A new interaction flow seeds a channel, posts
messages, reacts, and opens a thread — recording the redesigned
`/channel/$channelId` surface. Maps `apps/web/src/comms/**`, so a
chat-only PR like #174 now resolves to this flow.

**B — close the drift-guard hole.** `manifest-coverage.test.mjs` now
requires every parameterized route to be flow-covered or listed in
`PARAM_EXEMPT` with a reason (+ a stale-exempt guard). `channel` is
covered by the chat flow; 8 others are exempted as explicit TODOs.

**D — chat primitive stories.** `PresenceDot` + `ReactionBar` stories
give the pipeline a stable, seed-free baseline captured by the
co-location rule. (`MessageRow` is router/hook-coupled — not a stable
isolated story; deferred.)

## Verification

- **Fix proven against the real manifest:** PR #174's 19-file diff now
yields `{ flows: ['chat'], fallbackUsed: false }` — maps to the chat
flow, not the silent home fallback.
- **Gap warning end-to-end:** an unmapped file yields `fallbackUsed:
true` and `comment.mjs` renders the `[!WARNING]` block listing it.
- **Guard bites:** with the chat flow removed, the guard reports
`channel.$channelId` uncovered (would go red).
- `pnpm test:visuals` green (27 tests); `xnet-web` typecheck clean;
eslint + prettier clean; Storybook builds and indexes all 4 new comms
stories.
- The `visual-capture` workflow on **this PR** (it touches
`apps/web/src/comms/**`) will run the new `chat` flow and post the GIF —
the live validation of Option A.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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