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 — The chat timeline has four UX gaps that break the "seamless messaging" feel: (1) the spherical harmonics dot remounts/jumps on every new step and jitters during rapid streaming instead of staying calm, (2) the rail sits outside the composer's visible left edge, (3) prose messages over-segment into blank/orphaned boxes (labels split from their lists), and (4) the column teleports when new content arrives instead of gliding.
Approach — Hoist the running dot into a sticky-positioned turn-spanning overlay that only repositions on step boundaries; add left inset to align the rail inside the composer; strengthen the chunk-boundary heuristic to keep labels with their content; replace instant scrollToEnd() with a custom 180ms eased scroll.
Scope — Timeline rendering, scroll behaviour, and chunk segmentation. No backend, no protocol, no new dependencies.
Assumptions — Virtualization stays (tanstack/solid-virtual); existing entrance animations (300ms/8px rise/8px blur, 150ms prose-fragment) unchanged; overlay position driven by reactive memo from virtualizer measurements, updating only on step boundaries.
Acceptance Criteria
The HarmonicDot is a single persistent element per turn — never remounts while running
The dot uses CSS sticky positioning within a turn-spanning overlay; stays visible near viewport top during long streaming responses
The dot only repositions on step boundaries (new rows: tool start, tool end, new text part) — NOT per prose-fragment chunk
Dot slide uses a 250ms spring animation (cubic-bezier(0.34, 1.56, 0.64, 1)) with slight overshoot on each step transition
On turn completion, the overlay dot crossfades out (150ms opacity) and the per-row done-dot appears underneath
The rail line for the active turn is continuous (no per-row seams/gaps visible during streaming)
The rail's left edge sits inside the composer's visible left boundary at md+ breakpoints
Done-dots use deterministic offsets by group type (prose=21px, tool group=11px, single tool=16px, thinking=11px) — no TreeWalker
A short label (<40 chars ending with :) followed by any content stays in one card
Consecutive list items / blockquotes never split (existing guard preserved)
No blank/empty bordered cards render in the timeline
New content arriving while anchored to bottom causes a smooth ~180ms ease-out scroll, not a teleport
Smooth scroll cancels immediately on user scroll gesture
Entrance animations unchanged (300ms timeline-enter, 150ms prose-fragment-enter, concurrent with scroll)
prefers-reduced-motion disables smooth scroll, dot spring, and crossfade (instant fallbacks)
Only one overlay dot exists at a time — gated on "is there a running turn"
Key Decisions
Decision
Rationale
Sticky overlay (not per-row dot)
Virtualization makes per-row mounts unstable; overlay avoids remount. Sticky keeps dot visible during long responses without JS position updates.
Track per-row, not per-fragment
Reduces dot movements from ~20-40 per turn to ~5-10. Each move marks a real phase change (tool→prose→tool). Eliminates jitter during rapid streaming.
250ms spring with overshoot for dot slide
Each move is now an infrequent event worth marking. cubic-bezier(0.34, 1.56, 0.64, 1) gives physical character.
Crossfade on turn completion (not morph)
Done-dot renders underneath; fade-out is 150ms, no SMIL choreography needed.
Reactive memo for overlay bounds (not coupled to scroll RAF)
Simpler; only updates on step boundaries; one-frame lag imperceptible at 60fps.
Deterministic dotCentre — hardcoded per group type
Eliminates measurement race; if CSS padding changes, one constant to update (accepted tradeoff).
Custom RAF scroll, 180ms ease-out
Native behavior: 'smooth' is 500ms+ and browser-dependent; custom gives snappy control.
Entrance + scroll run concurrently
Feels natural — column makes room while message arrives.
Left padding on row frame (md:pl-3)
Simplest fix; narrows content from the left only, aligns rail with composer interior.
Label heuristic: <40 chars ending : + any content
Catches orphaned labels (common) without false-positiving on mid-sentence colons (length gate). Doesn't require next line to be a list — also protects label+paragraph.
Constraints & Invariants
The overlay lives inside the scroll container (scrolls with content). A turn-spanning wrapper div covers from the Thinking row's item.start to the last AssistantPart row's item.start + item.size. HarmonicDot inside it is position: sticky; top: 12px.
Overlay top and height only update when new rows appear or rows resize (step boundaries) — never per-fragment.
Done-dots remain per-row (static, no overlay needed).
Smooth scroll must not interfere with shouldAdjustScrollPositionOnItemSizeChange or prepend-anchor logic.
Chunk splitting remains monotonic (once a boundary is emitted during streaming, it never un-emits).
The 40-char label heuristic fires regardless of what follows (list, paragraph, quote) — the length gate prevents false positives on real sentences.
Rail line + dot spring use identical timing (250ms, same bezier) so they move in lockstep.
fix(app): chat UI polish — persistent dot, rail alignment, cohesive segments, smooth scroll
Important
Problem — The chat timeline has four UX gaps that break the "seamless messaging" feel: (1) the spherical harmonics dot remounts/jumps on every new step and jitters during rapid streaming instead of staying calm, (2) the rail sits outside the composer's visible left edge, (3) prose messages over-segment into blank/orphaned boxes (labels split from their lists), and (4) the column teleports when new content arrives instead of gliding.
Approach — Hoist the running dot into a sticky-positioned turn-spanning overlay that only repositions on step boundaries; add left inset to align the rail inside the composer; strengthen the chunk-boundary heuristic to keep labels with their content; replace instant
scrollToEnd()with a custom 180ms eased scroll.Scope — Timeline rendering, scroll behaviour, and chunk segmentation. No backend, no protocol, no new dependencies.
Assumptions — Virtualization stays (tanstack/solid-virtual); existing entrance animations (300ms/8px rise/8px blur, 150ms prose-fragment) unchanged; overlay position driven by reactive memo from virtualizer measurements, updating only on step boundaries.
Acceptance Criteria
stickypositioning within a turn-spanning overlay; stays visible near viewport top during long streaming responsescubic-bezier(0.34, 1.56, 0.64, 1)) with slight overshoot on each step transition:) followed by any content stays in one cardprefers-reduced-motiondisables smooth scroll, dot spring, and crossfade (instant fallbacks)Key Decisions
cubic-bezier(0.34, 1.56, 0.64, 1)gives physical character.behavior: 'smooth'is 500ms+ and browser-dependent; custom gives snappy control.:+ any contentConstraints & Invariants
item.startto the last AssistantPart row'sitem.start + item.size. HarmonicDot inside it isposition: sticky; top: 12px.topandheightonly update when new rows appear or rows resize (step boundaries) — never per-fragment.shouldAdjustScrollPositionOnItemSizeChangeor prepend-anchor logic.Files
packages/app/src/pages/session/timeline/message-timeline.tsxpackages/app/src/pages/session/timeline/thought-rail.tsxdotCentreForGrouphelper, keep done-dots per-rowpackages/app/src/design-polish.csspackages/app/src/index.csspackages/session-ui/src/components/message-part-text.ts:) inchunkBoundaries, tail trimpackages/session-ui/src/components/message-part.tsxChunkedStreamMarkdownPrior Art
Stepcomponent (harmoniqs-aidemo/parts.jsx) — same per-row rail geometry