Skip to content

feat(activity): open mention threads in a side panel with scroll-to-message, pulse & slide#3561

Open
adamleithp wants to merge 7 commits into
mainfrom
activity-thread-mention-scroll
Open

feat(activity): open mention threads in a side panel with scroll-to-message, pulse & slide#3561
adamleithp wants to merge 7 commits into
mainfrom
activity-thread-mention-scroll

Conversation

@adamleithp

@adamleithp adamleithp commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Reworks the Activity page (and aligns the channel feed) so opening a mention keeps you in place and takes you straight to the message.

2026-07-18 07 22 41 2026-07-18 07 24 17
  • Open in a side panel, no navigation — clicking an Activity notification opens the task thread in the right-hand ThreadSidebar instead of routing to the task. The list stays put.
  • Scroll to the mentioned message — the thread scrolls to and centers the mention (via data-thread-message-id) instead of jumping to the bottom, and re-centers while late-loading agent turns stream in so it can't drift off-screen. Re-clicking the same settled message is a no-op.
  • Pulse highlight — the mentioned row flashes once (thread-mention-highlight, reduced-motion aware).
  • Jump-to-latest pill — a floating scroll-to-bottom button in ThreadPanel, matching the experimental ChatThread scroller.
  • Slide in/out — the thread dock animates its width (auto ↔ 0) so the list/feed reflows in lockstep, at the docked sidebar's exact 200ms / cubic-bezier(0,0,0.2,1) contract. Extracted to a shared AnimatedThreadDock; overflow-hidden is applied only while animating so the resize handle isn't clipped once open. Reduced-motion → opacity-only.
  • Esc closes the thread from anywhere in Activity or the channel (not just when focused inside the panel).

Testing

  • Activity: click a mention deep in a long, active thread → panel slides in, centers + pulses the message, holds position while content loads.
  • Click a different mention while open → inner thread swaps, no reopen flash.
  • Scroll up → jump-to-latest pill fades in; click → smooth scroll to newest.
  • Esc (focus anywhere) → thread slides out.
  • Channel feed: open/close a task thread → same slide + Esc behavior.
  • prefers-reduced-motion on → appears/disappears instantly, no pulse.

🤖 Generated with Claude Code

adamleithp and others added 7 commits July 18, 2026 05:47
…the message

Clicking an Activity notification now opens the task thread in a right-hand
ThreadSidebar instead of navigating away. The thread scrolls to the mentioned
message (centered) and pulses it once, rather than jumping to the bottom.

- ThreadPanel/ThreadSidebar take a focusMessageId; rows tag their ThreadItem
  with data-thread-message-id for targeting.
- Bottom auto-scroll is suppressed while deep-linked; the mention is re-centered
  on timeline changes during the pulse so late-loading agent turns can't drift
  it off-screen.
- Re-clicking the same settled message is a no-op (focusedRef guard).
- Adds a thread-mention-highlight keyframe for the pulse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
Floating scroll-to-bottom button matching the experimental ChatThread scroller:
appears when the viewport is scrolled up off the bottom (gap below the fold >
100px), fades/scales in, and smooth-scrolls to the newest message on click.
Especially useful for the deep-linked mention view, which lands mid-thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
A window keydown listener (active only while a thread is open) clears the
selection on Esc, so focus needn't be inside the panel. Skips when the event
was already handled (e.g. an open dropdown consuming Esc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
Apply the pulse to a square (rounded-none) ThreadItem so the flash covers the
whole row rather than an inset rounded box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
Wrap the Activity thread mount in AnimatePresence and animate the wrapper width
(auto <-> 0) plus opacity, so the mention list reflows in lockstep with the
panel. Matches the docked sidebar's slide contract exactly: 200ms /
cubic-bezier(0,0,0.2,1). width:auto once open keeps ThreadSidebar's resizable
width intact; the wrapper is keyed stable so switching mentions swaps the inner
panel without a close/reopen. Honors prefers-reduced-motion (opacity-only,
instant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
Apply the Activity thread's slide to the channel view's ThreadSidebar: wrap the
mount in AnimatePresence, animate wrapper width (auto <-> 0) + opacity at
200ms / cubic-bezier(0,0,0.2,1) so the feed reflows in lockstep. Also add a
window Esc listener (active while a thread is open) that closes it from anywhere
in the channel, matching Activity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
…+ a11y

Address review findings on the thread slide-in:
- Extract the duplicated AnimatePresence/width-slide wrapper (Activity + channel)
  into a shared AnimatedThreadDock.
- Apply overflow-hidden only while animating, so the settled panel no longer
  clips the ResizableSidebar resize handle (it overhangs the inner edge).
- Guard the mention-pulse and jump-pill transition with prefers-reduced-motion.
- Give all highlighted thread rows rounded-none for a consistent full-bleed pulse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K18PGBxVQy1fnC9ADbst3y
@adamleithp
adamleithp requested a review from k11kirky July 18, 2026 06:13
@trunk-io

trunk-io Bot commented Jul 18, 2026

Copy link
Copy Markdown

✨ Submitted to Merge by @adamleithp. It will be added to the merge queue once all branch protection rules pass, there are no merge conflicts with the target branch, and impacted targets for the current PR head commit have been uploaded. See more details here.

@adamleithp adamleithp added the Stamphog This will request an autostamp by stamphog on small changes label Jul 18, 2026
@github-actions

Copy link
Copy Markdown

React Doctor found 4 issues in 2 files · 1 error & 3 warnings.

Errors

3 warnings

src/features/canvas/components/AnimatedThreadDock.tsx

src/features/canvas/components/ThreadPanel.tsx

Reviewed by React Doctor for commit 790c5af.

@adamleithp adamleithp added the trunk-merge-queue-submit Adding this label to a pull request enqueues it, and removing this label dequeues it label Jul 18, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Solid UI-only implementation with no correctness or security issues found, but a T1d-complex behavioral change (activity mention click now opens an in-place side panel instead of navigating) with zero reviews warrants a human sign-off before auto-approval.

@stamphog stamphog Bot removed the Stamphog This will request an autostamp by stamphog on small changes label Jul 18, 2026
@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "refactor(canvas): extract AnimatedThread..." | Re-trigger Greptile

Comment on lines +188 to +190
const onKeyDown = (event: KeyboardEvent) => {
// Let an open menu/popover consume Esc first (it preventDefaults).
if (event.key === "Escape" && !event.defaultPrevented) setSelected(null);

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.

P2 Escape Can Close Two Layers

When a focused menu or popover handles Escape without calling preventDefault() on the native event, this window listener also clears the selected thread. One key press then closes both the overlay and the underlying thread instead of only the topmost layer.

Comment on lines +133 to +136
const onKeyDown = (event: KeyboardEvent) => {
// Let an open menu/popover consume Esc first (it preventDefaults).
if (event.key === "Escape" && !event.defaultPrevented)
closeThread(channelId);

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.

P2 Escape Can Close Two Layers

When a focused menu or popover handles Escape without calling preventDefault() on the native event, this window listener also closes the channel thread. One key press then dismisses both UI layers rather than only the active overlay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trunk-merge-queue-submit Adding this label to a pull request enqueues it, and removing this label dequeues it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant