feat(ui): animate session switching — pill reveal + bubble pop - #192
Draft
itsdestin wants to merge 1 commit into
Draft
feat(ui): animate session switching — pill reveal + bubble pop#192itsdestin wants to merge 1 commit into
itsdestin wants to merge 1 commit into
Conversation
Switching conversations was two hard cuts: the active pill's name snapped open, and the transcript was replaced instantly. Session pill (SessionStrip.tsx) — the label had TWO independent reasons it could not animate. maxWidth was `undefined` when active, leaving no numeric pair to interpolate; and the transition was hard-disabled for every pack-expanded pill, which packSessions guarantees the active pill always is. The pill you just clicked was exactly the one with animation switched off. Fixed by animating grid-template-columns 0fr->1fr (interpolates to intrinsic width, so the active pill keeps its uncapped shrink-and-ellipsis) and by arming a short window on active-id change so the existing 'none' still suppresses repack churn everywhere else. Chat bubbles (ChatView.tsx, globals.css) — bubbles pop away on exit and pop back in on enter, on the same overshoot curve the pills already use. Only entries carrying .in-view animate, reusing the class the bubble IntersectionObserver already maintains, so off-screen bubbles cost nothing and the work stays flat on a long transcript. Panes are mounted and stacked, so the outgoing one is held for 120ms to finish its exit while the incoming bubbles wait out an equal animation-delay — sequential, not a crossfade. Gated on both prefers-reduced-motion and the app's reducedEffects setting. Desktop-only by nature: Android's forceSingle remounts its single pill, so there is no before-state to transition from and no expansion to show. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Switching conversations was two hard cuts: the active pill's name snapped open,
and the transcript was replaced instantly. This adds motion to both.
The pill had two independent reasons it couldn't animate
Worth recording, because "just add a transition" was the wrong diagnosis — one
already existed and was being suppressed.
maxWidthwasundefinedwhen the pill was active (SessionStrip.tsx:785-787),leaving no numeric pair for the browser to interpolate, so width snapped to
intrinsic.
transitionwas hard-set to'none'for every pack-expanded pill(
:789) — andpackSessionsputs the active pill inexpandedunconditionally (
pack-sessions.ts:53). The pill you just clicked wasprecisely the one with animation switched off.
Fixed by animating
grid-template-columns: 0fr → 1fr, which interpolates to thelabel's intrinsic width —
max-widthcan't do that without imposing a hard cap,and the active pill is meant to be uncapped so it flex-shrinks and ellipsizes only
when the strip itself is narrow. Non-active hover reveals keep their 120px cap.
The
'none'is preserved for repack churn and overridden only inside a shortwindow armed by an active-id change.
Bubbles
bubble-switch-out/bubble-switch-inkeyframes on the same overshoot curve thepills already use (
cubic-bezier(0.34, 1.56, 0.64, 1)), 30ms stagger capped at 8.Applied only to
.timeline-entry.in-view— reusing the class the bubbleIntersectionObserver already maintains for the glass optimization
(
ChatView.tsx:273-291). Off-screen bubbles are never animated, so cost stays flatregardless of transcript length. This was a hard requirement from the design review.
Panes stay mounted and stacked, so the outgoing one is held for 120ms to finish its
exit while the incoming bubbles wait out an equal
animation-delaywithbothfill — sequential, not a crossfade.
Both keyframes end on
transform: nonerather thanscale(1), matching therow-fade-inconvention (globals.css:1168-1171) that avoids leaving a persistentcompositing layer under
fill-mode: both.Scope
prefers-reduced-motionand the app'sreducedEffectssetting.The session strip had no reduced-motion gate at all before this.
forceSinglefilters to one pill keyed bysession id, so it remounts on every switch — no before-state to transition from,
and no expansion to show, since its single pill is always named. Not faked.
SessionPillis a plain dropdown with always-visible labels; thepill fix does not apply there and it is untouched.
Outstanding before merge
"disappear" instead of a cut, and it delays the incoming conversation by 120ms.
Eyeballed in a dev instance and it reads fine, but it hasn't been lived with.
Cutting the exit half is a one-line change if it grates.
** (chat↔terminal). A ChatView'ssessionIdnever changes, sovisible` is the only edge available and the view toggle isindistinguishable from a session switch at this layer. Suppressing it needs a
different signal threaded down from App.
SessionStriporChatView(only
packSessions' 9 pure cases are pinned), so nothing here is guardedagainst regression. Worth deciding whether the
.in-view-only rule — the loadbearing perf property — deserves a pinning test.
own
transitionandsuppressClick; theactiveSwapwindow has not beentested mid-drag.
packs differently.
Verification
tsc --noEmitclean; full suite green (2784 passed, 1 skipped file). Eyeballed in adev instance on
run-dev.shacross themes. Interactive verification was handed toDestin per the workspace rule rather than scripted.
🤖 Generated with Claude Code