You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem: During long streaming text responses, the harmonic dot anchors to the first line of the row and scrolls out of view as auto-scroll follows new content at the bottom. The loading indicator disappears precisely when the user needs it most.
Approach: Redirect dotCentre measurement to the last[data-prose-fragment] card within a running text-part row. Add CSS transitions (150ms ease-out) to the dot's top and the rail line's height so the dot glides down as new cards settle. Pair with a translateY(10px) + opacity entry animation on new prose-fragment cards, gated by auto-scroll state.
Scope:thought-rail.tsx, message-timeline.tsx (measurement logic), index.css / design-polish.css (keyframes + transitions), message-part.tsx or ChunkedStreamMarkdown (auto-scroll gate for data-part-enter).
Assumptions: Text parts remain a single AssistantPart row; the virtualizer and auto-scroll architecture are unchanged; [data-prose-fragment] card boundaries remain the unit of visual chunking.
Acceptance Criteria
While streaming a text response with 2+ settled paragraph cards, the harmonic dot is visually level with the last card (not the first).
The dot slides smoothly (150ms ease-out) when a new card settles — no teleport.
The rail line extends in sync with the dot (same transition timing).
New prose-fragment cards enter with translateY(10px) + opacity: 0 → 1 over 150ms, synchronized with the dot's slide.
Card animation fires only when auto-scroll is active (user is watching). Cards that settle off-screen have no animation when scrolled into view later.
prefers-reduced-motion: reduce disables all transitions and animations (instant positioning).
The dot's initial mount (turn starts) does NOT trigger the slide transition — the grow animation handles first appearance.
When the turn completes, the done-dot sits at the last card's position (not the first).
No per-card done-dots within a row — single dot, continuous spine.
Key Decisions
Decision
Rationale
Reuse measureDotCentre pipeline
Already ResizeObserver-driven; redirect target from "first text node in row" to "first text node in last [data-prose-fragment]"
Prevents stale animation cascade when user scrolls back and catches up
150ms timing
Matches existing thought-rail-grow timing; fast enough to not lag behind streaming cadence
data-settled gate for initial render
Prevents the dot from sliding from top: 0 to its first measured position on mount
Constraints & Invariants
The rail's geometric contract is preserved: dot sits ON the spine, line ends AT the dot, no dangling stubs.
No new DOM elements — only measurement target and CSS changes.
Performance: no new observers (existing ResizeObserver already fires on content growth).
Virtualization compatibility: the dot is absolutely positioned within the row's relative container, unaffected by the virtualizer's absolute positioning of rows.
Implementation detail
Dot position tracking
Current behavior (message-timeline.tsx:1468-1486): measureDotCentre() uses a TreeWalker to find the first non-empty text node in the turn element and measures its vertical centre.
New behavior: When the row is a running text-part AssistantPart, the measurement targets the last[data-prose-fragment] element's first text line instead of the row's first text line. The ResizeObserver already re-measures on content change, so as new cards settle, dotCentre updates automatically.
measureDotCentre():
if (running && turnEl has [data-prose-fragment] children):
target = last [data-prose-fragment] element
walk target's text nodes -> measure first non-empty one's centre
else:
existing behavior (first text node in turnEl)
Smooth dot transition
Add CSS transition to the dot when settled (not on initial mount):
Dot stays at first (and only) card position. No movement until a second card settles or the turn ends.
Turn completes
Dot fills to done-state. Its last position becomes the final done-dot location — aligned with the last card, not the first.
New AssistantPart row starts (tool call after prose)
Dot moves to the new row per existing architecture (new row becomes lastAssistantPart). Previous row's dot becomes a done-dot at its last tracked position.
User scrolls up mid-stream
Auto-scroll pauses. New cards still settle but don't get data-part-enter. Dot still moves (structural) but off-screen so invisible.
Very fast streaming (multiple chunks settle within 150ms)
CSS transition naturally interrupts and redirects — dot glides to latest position without queueing.
Reduced motion
No card animation, no dot transition. Everything instant.
Travelling harmonic dot + bottom-up card animation
Important
Problem: During long streaming text responses, the harmonic dot anchors to the first line of the row and scrolls out of view as auto-scroll follows new content at the bottom. The loading indicator disappears precisely when the user needs it most.
Approach: Redirect
dotCentremeasurement to the last[data-prose-fragment]card within a running text-part row. Add CSS transitions (150ms ease-out) to the dot'stopand the rail line'sheightso the dot glides down as new cards settle. Pair with atranslateY(10px)+ opacity entry animation on new prose-fragment cards, gated by auto-scroll state.Scope:
thought-rail.tsx,message-timeline.tsx(measurement logic),index.css/design-polish.css(keyframes + transitions),message-part.tsxorChunkedStreamMarkdown(auto-scroll gate fordata-part-enter).Assumptions: Text parts remain a single
AssistantPartrow; the virtualizer and auto-scroll architecture are unchanged;[data-prose-fragment]card boundaries remain the unit of visual chunking.Acceptance Criteria
translateY(10px)+opacity: 0 → 1over 150ms, synchronized with the dot's slide.prefers-reduced-motion: reducedisables all transitions and animations (instant positioning).Key Decisions
measureDotCentrepipeline[data-prose-fragment]"data-part-enteron auto-scroll statethought-rail-growtiming; fast enough to not lag behind streaming cadencedata-settledgate for initial rendertop: 0to its first measured position on mountConstraints & Invariants
ResizeObserveralready fires on content growth).Implementation detail
Dot position tracking
Current behavior (
message-timeline.tsx:1468-1486):measureDotCentre()uses aTreeWalkerto find the first non-empty text node in the turn element and measures its vertical centre.New behavior: When the row is a running text-part
AssistantPart, the measurement targets the last[data-prose-fragment]element's first text line instead of the row's first text line. TheResizeObserveralready re-measures on content change, so as new cards settle,dotCentreupdates automatically.Smooth dot transition
Add CSS transition to the dot when settled (not on initial mount):
Card entry animation
Gate: only set
data-part-enterwhen auto-scroll is active (threadshouldAnchorBottomstate to the text-part renderer).Files touched
packages/app/src/pages/session/timeline/message-timeline.tsxmeasureDotCentre()— target last[data-prose-fragment]when runningpackages/app/src/pages/session/timeline/thought-rail.tsxdata-settledattribute after first measure; no structural changepackages/app/src/index.csstransition: top 150ms ease-outon.thought-rail-dot--harmonic[data-settled]; transition on[data-slot="thought-rail-line"]packages/app/src/design-polish.css@keyframes prose-fragment-enter+ conditionaldata-part-enterpackages/session-ui/src/components/message-part.tsx(orChunkedStreamMarkdown)data-part-enterEdge cases
lastAssistantPart). Previous row's dot becomes a done-dot at its last tracked position.data-part-enter. Dot still moves (structural) but off-screen so invisible.