feat(app): travelling harmonic dot + bottom-up card entry animation - #266
Merged
Conversation
The harmonic dot now tracks the LAST prose-fragment card within a running text-part row rather than staying pinned to the first text line. As new paragraph cards settle during streaming, the dot slides smoothly down the rail (CSS transition, 150ms ease-out), keeping the loading indicator visible near the newest content. Changes: - measureDotCentre targets last [data-prose-fragment] instead of first text node when fragments are present (works for both running and done states) - ThoughtRail accepts a settled prop; dot gets .thought-rail-dot--settled class after first measurement to gate the transition (prevents slide on initial mount) - Rail line gets matching height transition so spine extends in sync - Prose-fragment cards get a dedicated entry animation: 150ms ease-out, translateY(10px) + opacity fade, no blur (snappier than the general timeline-enter) - All transitions/animations honour prefers-reduced-motion: reduce - Sanity ceiling (80px) removed for fragment-targeting measurements since the dot can travel far down a long response Closes #265
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
When a text part is followed by another part (e.g. a tool call), the text
content is finalized — the model has moved on. Previously, the streaming
signal was keyed only on message.time.completed (message-level), so the
text tail stayed withheld until the entire turn finished. This caused a
visual ordering bug: the tool row ('Shell [command]') rendered before the
preceding text was visible.
Now the streaming signal also checks whether subsequent parts exist in the
same message. If the text part is not the last part, it's marked as done
immediately, flushing the withheld tail so text renders before its
following tool row.
Part of #265
The done-dot (filled circle) should mark the row's origin — the first text line — while only the running harmonic dot tracks the last prose-fragment card. Previously measureDotCentre always targeted the last fragment regardless of state, which left the done-dot at the bottom of long responses. Now the measurement is conditional: fragments are targeted only when the rail is running (r.last && r.running). When the turn completes and the ResizeObserver re-measures, it falls back to the original first- text-node behavior and the done-dot snaps to the top.
Tighten the gap between streaming output and the composer to create an iMessage-like feel where content and input share the same surface. Spacing hierarchy: - Within a turn (pt-3): 12px - Text-part margin-top: 12px - Between turns (TurnGap): 24px - Bottom to composer (paddingEnd): 24px Changes: - Hide ThinkingMeta during streaming (historical record only) - Add hover tooltip on running harmonic dot (elapsed + tokens) - Anchor timer to userMessage.time.created (survives session switch) - Thread turnStartedAt through Thinking/AssistantPart row data - Update stop button tooltip to 'Interrupt (Esc)' - Collapse text-part-copy-wrapper to position:absolute (no layout impact) - Remove pt-3 from ThinkingMeta row - Simplify TimelineThinkingMetaRow (remove dead turnRunning branch) - Fix shouldRenderRail test (Thinking row always provides first node) - Update e2e smoke test for new spacer height
Replace the per-text-part hover copy overlay (position: absolute, translateY, z-index conflicts) with a single persistent copy-trace button on the completed-turn footer row. The button copies the full assistant trace — all text parts plus tool command/output, skipping exploration noise (read/glob/grep/list). Changes: - Remove text-part-copy-wrapper from TextPartDisplay and its CSS - Extract buildTrace() into @opencode-ai/session-ui/build-trace (tested) - Add TurnFooter component to AssistantParts (session-ui path) - Add copy button to TimelineThinkingMetaRow (app timeline path) - Add placement prop to MessageActionButton (tooltip below) - Add turn-footer and session-turn-thinking-meta flex layout styles - Remove text-part margin-top: 12px (parent gap handles spacing) - Add i18n key ui.message.copyTrace
jeonghun-jj-lee
marked this pull request as ready for review
August 28, 2026 16:55
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.
Summary
The harmonic dot now tracks the last prose-fragment card within a running text-part row rather than staying pinned to the first text line. As new paragraph cards settle during streaming, the dot slides smoothly down the rail, keeping the loading indicator visible near the newest content.
Changes
measureDotCentretargets last[data-prose-fragment]instead of first text node when fragments are present (both running and done states)ThoughtRailaccepts asettledprop; dot gets.thought-rail-dot--settledclass after first measurement to gate the transition (prevents slide on initial mount)translateY(10px)+ opacity fade, no blur (snappier than the general timeline-enter)prefers-reduced-motion: reduceTest coverage
CSS regression test (
travelling-dot.test.ts) guards:Closes #265