feat(app): chat UI polish — segmentation, harmonic dot, rail, scroll - #269
Merged
Conversation
|
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 |
jack-champagne
force-pushed
the
local/amicode
branch
from
August 28, 2026 20:19
88d63c9 to
3779ab8
Compare
jeonghun-jj-lee
force-pushed
the
chat-ui-polish-628
branch
2 times, most recently
from
August 31, 2026 00:00
0ef3613 to
3684a8e
Compare
A short label (<40 chars ending with ':') no longer splits from its content — the blank line after it is not treated as a chunk boundary. This prevents orphaned single-line cards like 'Results:' appearing alone above their list. The whitespace-only chunk guard was already in splitSettledChunks; tests now explicitly verify it. Closes #632
…l, deterministic offsets Hoist the running HarmonicDot from per-row inline rendering into a single persistent overlay element inside the virtual content container. The dot never remounts while a turn is running — one DOM element throughout. Key changes: - TurnOverlay in message-timeline.tsx: positioned absolutely in virtual space, top computed from virtualizer measurements of the last running row. Updates only on step boundaries (row count changes for the active turn), not per prose-fragment chunk. - dotCentreForGroup(): deterministic offsets (prose=21, tool_group=11, single_tool=16, thinking=11) replace the old ResizeObserver + TreeWalker measurement in TimelineRowFrame. - ThoughtRail renders null for the running dot path; done-dots remain per-row with deterministic centres. - CSS transitions updated to 250ms spring cubic-bezier(0.34, 1.56, 0.64, 1) with slight overshoot. prefers-reduced-motion disables the spring. Closes #630
… teleport New content arriving while anchored to bottom now animates with a custom 180ms ease-out cubic RAF loop instead of instant scrollToEnd(). Cancel on user scroll gesture (wheel, touchstart, pointerdown). prefers-reduced-motion disables smooth scroll (instant fallback). The smoothScrollToEnd helper is used for: - maybeAnchorBottom (new rows append while following) - anchorResizedBottom (row grows while following) - Jump to latest button Instant scroll preserved for: initial mount, scrollToIndex (reveal/history), and prepend-anchor (direct scrollTop writes). The pure interpolation function (smoothScrollInterpolate) is unit-tested. Closes #631
Adds the visual polish layer on top of the persistent dot overlay: - Continuous rail line: a single absolutely-positioned <span> in the overlay spans from the turn's first row (Thinking) to the dot position. No per-row seams during streaming. - Rail inset: md:pl-3 on the outer TimelineRowFrame wrapper for assistant rows, shifting the rail inside the composer's visible left boundary at md+ breakpoints. - Crossfade on turn completion: when session status transitions to idle, the overlay and its rail line fade to opacity:0 over 150ms (CSS transition on data-state='completed'). After the transition, the overlay DOM is removed and per-row done-dots (always rendered underneath) become visible. Rapid turn succession cancels any pending fade. - prefers-reduced-motion disables crossfade (instant removal). Closes #633
…sticky at 50vh Two fixes: 1. The overlay dot was not rendering because measurementsCache is not reactive in SolidJS — the memo could read stale values before the virtualizer updated. Now uses the reactive virtualItemByKey() map first, falls back to measurementsCache, and retries on next frame if measurements aren't available yet. 2. The dot uses position: sticky with top: calc(50vh - 6.5px) inside the turn-spanning overlay, so it sticks at the viewport midpoint during long streaming responses rather than scrolling off.
…pl-3 The overlay dot and rail line were at the left edge of the full-width virtual content, but the actual per-row rail sits inside a centered, padded column (md:max-w-200 md:mx-auto md:pl-3). Fix: wrap the overlay contents in a centering div that mirrors TimelineRowFrame's classList, so LINE_X inside the overlay aligns with LINE_X inside the rows. Also: the dot uses flex-1 spacer + sticky to sit at its natural position (bottom of the turn span) and stick at 50vh when scrolling through a long response.
The overlay dot position was computed from virtualizer measurements with fragile math (pt-3, headerOffset, centering) that didn't match the actual rendered layout. Now reads getBoundingClientRect() of the target row's session-turn element relative to virtualContent — this guarantees both horizontal and vertical alignment with per-row dots. Also removed the overlay rail line entirely — per-row ThoughtRail segments already bridge their gaps with negative margins and look continuous. The overlay only provides the persistent HarmonicDot.
getBoundingClientRect-based measurement was unreliable — scroll position and timing could produce wrong results. Now reads the VirtualTimelineRow's inline style.top (which is item.start - headerOffset, set by the virtualizer) plus turnEl.offsetTop (accounts for pt-3 padding). This is deterministic and doesn't depend on scroll state or measurement timing. Horizontal left still uses getBoundingClientRect difference (scroll- independent for horizontal, and centering makes absolute offsetLeft unreliable).
…w dot - Restore margin-top: 12px on [data-component="text-part"] — gives 24px above text boxes (12px frame pt-3 + 12px margin), 12px below. Status items stay at 12px among themselves. The asymmetry encodes reading direction: text introduces, actions follow tightly. - Revert overlay dot back to per-row DotWithTooltip in ThoughtRail. The overlay approach (absolute positioning, sticky) was fragile across scroll/resize. Per-row rendering with DOM-measured dotCentre (TreeWalker + ResizeObserver) is self-correcting. - Remove all overlay infrastructure: signals, effects, JSX, CSS (.turn-overlay-dot--settled, [data-turn-overlay]). - Extract pure functions (selectUserMessages, isTimelineReady, selectVisibleUserMessages, loadOlderTimeline) into model-pure.ts to fix pre-existing model.test.ts SolidJS SSR resolution failure. - Update dotCentreForGroup: tool_group 11→16 (matches measured value for 32px collapsible triggers with 16px text). 73/73 timeline tests pass, typecheck clean, build 10.66s.
ThinkingMeta (time + tokens) now renders before the 'Interrupted' divider, attached to the last pre-interrupt output. Previously it rendered at the very end of the turn, below the divider and any post-interrupt parts. Non-interrupted turns unchanged — ThinkingMeta still renders last.
Remove the snappy 150ms prose-fragment-enter override. All streaming fragments now use the general timeline-enter animation (300ms, cubic-bezier(0.2,0,0.2,1), 8px rise + 8px blur) — the same grammar Kate's website-chat-style branch uses. The faster animation was jarring when multiple boxes arrive in rapid succession; the unified 300ms + blur provides a softer leading edge that reads as a gentle fade rather than staccato popping.
Replace blank-line chunk splitting with heading-anchored sections. Boundaries fire only at markdown headings (# through ######) outside code fences. Each card is now a complete section: heading + all its body paragraphs, code, math, and horizontal rules. Fixes: - Empty bordered cards from `---` separators - Orphan heading cards (heading alone in a tiny box) - Isolated display-math cards - Overly aggressive fragmentation on rapid streaming output The intro (text before the first heading) is its own card. Text past the last heading stays withheld as the tail during streaming.
The running dot now sits at the bottom edge of the last row instead of at the first-line dotCentre. As content streams, the row grows and the dot rides down passively — no position transitions, no re-renders, no spring animation. Cards appear above it, pushing the column up while the dot stays as the insertion cursor. Removes: - .thought-rail-dot--settled class and its top transition - [data-slot=thought-rail-line] height transition - settled prop from ThoughtRail and DotWithTooltip - dotSettled signal from TimelineRowFrame Done dots on completed rows stay at first-line centre (unchanged). Rail line for the running row extends to bottom (where dot sits).
The harmonic dot now has two positioning modes: - Thinking row (isLoneRunning): centred at dotCentre, aligned with the 'Thinking' text — it's a status signal, not growing content. - AssistantPart content rows: bottom-anchored, rides down as content grows above it. Fixes the dot sitting below the Thinking label instead of beside it.
The bottomAnchored mode now keys off a 'prose' prop (true when the row has no railLabel — i.e. it's a growing text content row). Tool rows (Shell, Edit, Read) and the Thinking row all get dotCentre positioning so the dot aligns with their label text. Only prose rows (the actively-growing text cards) get bottom-anchored.
The bottom-anchored dot on prose rows now sits 21px from the row's bottom edge (card padding 10px + half line-height 11px), aligning its centre with the last line of body text. Rail line terminates at the same inset. This is a fixed offset — the dot never jumps, it rides down as the row grows, always aligned with the last visible text line.
railLabel is only set for 'reasoning' parts — shell/edit/context groups had undefined railLabel and were incorrectly treated as prose (bottom-anchored). Now checks group.type === 'part' for prose vs shell/edit/context for tool rows. Tool rows get dotCentre positioning.
The dotCentre signal previously started at DEFAULT_DOT_CENTRE (11px) for all rows, waiting for a ResizeObserver measurement to correct it. This caused a few-pixel misalignment on tool rows where the actual first-text centre is 16px. Now initializes from dotCentreForGroup(row.group.type): - part (prose): 21px - shell/edit/context: 16px - thinking: 11px Also wires dotCentreForGroup to accept PartGroup.type values directly so the mapping is consistent.
The ResizeObserver measurement was overriding the correct dotCentreForGroup value on running tool rows. The TreeWalker found a slightly different first-text position than the calibrated 16px, causing the harmonic dot to sit a few pixels off from where the done dot would land. Now skips measurement when the row is running+last — the running dot uses either bottom-anchoring (prose) or the deterministic initial value (tools). Measurement only fires for done dots that need to align with settled content.
The previous commit skipped measurement for ALL running rows, which broke the Thinking row (it needs measurement to align with the 'Thinking' label). Now only skips for running shell/edit/context rows where the TreeWalker produces wrong results. Thinking and prose rows continue to measure normally.
An inline SVG inside a span gets vertical-align: baseline treatment, adding a ~2-3px descender gap that pushed the dot above its intended centre. display:block makes the SVG fill its wrapper exactly — no inline formatting, no gap. Also reverts the measurement-skip workaround (no longer needed — the real issue was the SVG baseline offset, not the measurement).
Move display:block from inline style (unreliable in SolidJS style merge) to the .thought-rail-dot--harmonic CSS class. This kills the inline SVG baseline gap that shifted the dot above its centre. Revert the inline style change on the component.
Force the wrapper span to exactly HARMONIC_SIZE dimensions so it can't grow beyond the SVG's bounds. This eliminates any possible sizing discrepancy between the positioned wrapper and its content.
The harmonic dot's top value wasn't updating when dotCentre changed
because the style object used a conditional spread:
...( condition ? {top: x} : {bottom: y} )
SolidJS can't trace reactivity through object spreads. Replaced with
direct property assignment (top: condition ? undefined : value).
Also removes the dead thought-rail-grow animation (was already
disconnected) and simplifies dotCentreForGroup to always return
DEFAULT_DOT_CENTRE since measurement is the source of truth.
A single shell/edit/read command has group.type 'part' (same as text), so the previous check treated it as prose and bottom-anchored it. Now looks up the actual message part: only part.type === 'text' is prose. Single tool parts get dotCentre positioning like grouped tools.
Replace the hand-rolled tooltip with the standard TooltipV2 component (portalled, themed, animated) positioned below the dot so it never occludes the rail or chat content above. Adds the ↑ arrow before token count to match the thinking-meta printout. Extract pure functions (shouldRenderRail, dotCentreForGroup) into thought-rail-pure.ts to isolate tests from Kobalte's SSR error.
Replace the randomized pulse sequence with a fixed, strictly ascending progression through quantum numbers: l=2 (m=0,1,2) → l=3 (m=0,1,2,3) → l=4 (m=0,1,2,3,4). Each level starts with its pill (m=0), then builds lobes as m increases. Higher l for the same lobe count uses a lower base offset (deeper valleys = sharper shape). - Add 6 new harmonicRadius modes (8-13) for intermediate (l,m) pairs - MODE_COUNT 8→14, PULSE_COUNT 10→12, cycle 12s→14.4s - Fixed rotation: m=2 (4-lobe) shapes at 45° (X not +), all else at 0° - Remove randomPulseSequence — HarmonicDot now uses pre-computed SMIL - Update tests to verify level ordering and base-offset progression
…uence Remove mode 5 '(3,0) trefoil' — its formula (1+cos3θ)/2 is a cardioid, not a spherical harmonic (no Y_l^m has 3-fold symmetry; lobe count is always 2m). Remove mode 11 '(4,1) deep pill' — indistinguishable from (3,1) at 13px. Sequence now: 10 genuine harmonics, 12s cycle. l=2: (2,0) pinched, (2,1) pill, (2,2) clover l=3: (3,1) pill, (3,2) clover, (3,3) rosette l=4: (4,0) double-pinch, (4,2) clover, (4,3) rosette, (4,4) star
Replace duplicate-topology modes with 10 shapes that each look different at 13px. No two share the same angular function — every pulse in the cycle is unmistakably new. Azimuthal |cos(mθ)|: pill(2), clover(4), rosette(6), star-8, star-10, star-12 Zonal P_l^0: pinched(P₂⁰), peanut(P₃⁰), double-pinch(P₄⁰), hedgehog(P₅⁰) MODE_COUNT 14→11, all modes renumbered 0-10.
P₃⁰ (peanut) and P₅⁰ (hedgehog) look like bumpy pills at 13px. Keep only the 2 most readable Legendres (P₂⁰ pinched, P₄⁰ double-pinch) alongside the 6-step lobe ladder (2→4→6→8→10→12).
Sections without headings that exceed 40 lines now sub-split at double-newline paragraph breaks. Paragraph gaps inside code fences are ignored. Short sections (<40 lines) are unaffected. Fixes wall-of-text rendering where a long analysis without ## headings produced one massive card.
A skill-only turn could render an empty bordered box below the skill chip — the text-part wrapper or prose fragment div had border + padding but no content. Add :empty guards on [data-prose-fragment], [data-component='text-part'], and [data-slot='session-turn-assistant-content'] so empty wrappers collapse to display:none.
SolidJS <Show> leaves comment nodes when falsy, so :empty never matches. Use :not(:has(*)) to detect when the wrapper contains no element children and collapse it — prevents the empty bordered box below a skill-only turn.
BasicTool's hasChildren() was truthy because the SolidJS <Show> node counted as a child even when body() was empty. The Collapsible.Content then rendered an empty bordered box below the skill chip. Pass undefined children when there's no body so hasChildren() is false and the collapsible content section doesn't render at all.
Line count was meaningless for prose — a single paragraph can be 500 characters on one raw line. Switch to character-count threshold so the fallback fires on actual text volume, matching what the user sees.
jeonghun-jj-lee
force-pushed
the
chat-ui-polish-628
branch
from
August 31, 2026 12:03
05077b9 to
39dfd9b
Compare
jeonghun-jj-lee
marked this pull request as ready for review
August 31, 2026 12:07
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
Polishes the chat timeline across four areas: prose segmentation, harmonic dot animation, thought rail, and scroll behavior.
Prose segmentation
Harmonic dot animation
Thought rail and dot
Scroll and misc
Test plan