Skip to content

feat(ui-web): scroll the transcript under a frosted chat header - #1151

Open
LivXue wants to merge 8 commits into
mainfrom
feat/titlebar_frosted_glass
Open

LivXue wants to merge 8 commits into
mainfrom
feat/titlebar_frosted_glass

Conversation

@LivXue

@LivXue LivXue commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Summary

The chat header was a flex item above the transcript scroller, so a message scrolled to the top was clipped at the header's edge. The header is now an overlay over the whole chat column, and the transcript travels underneath it through a frost that ramps down instead of banding.

The frost paints past the header row by --topbar-fade-tail (56px), so the band is 52 + 56 = 108px. It is two layers. A blur layer with no ground of its own carries the backdrop-filter, and its mask walks the blur from full behind the controls down to none at the band's lower edge. A tint layer carries --topbar-tint, a third of --page at most, fading out below the controls. The band is glass: the bubble under it stays visible as colour and shape, the way a title bar shows the window behind it, and it is the blur that eats it, not a painted ground.

The split matters because an engine that draws an element's background over the filtered backdrop, the order the spec describes, would hide the blur entirely under an opaque ground and leave only a paler sheet of sharp text. Measured in Chrome, a mask of 0 leaves the backdrop untouched and a mask of 1 draws the full blur. Neither layer uses color-mix.

Legibility over a dark bubble is carried by the controls, not by the tint: a soft --topbar-halo shadow behind the name, the chip and the corner glyphs, and a page-coloured plate under each corner button, so the band can stay translucent instead of turning opaque.

The readable edge is --topbar-fade-h (row plus tail). scroll-padding-top, the up pill, the trajectory host and the turn nav read it, and headerBand() returns it by measuring the row and reading the tail token, so the desktop shell's measured title bar and a resize both reach it. Both jumps leave the same 16px lead, so the first line after a jump is clear of the frost; measured in the page, a turn-nav click lands the question at y=124 on the 108px band. The scroller's padding-top stays the row height, so the resting frame does not move.

Where backdrop-filter is unsupported the whole band is painted with the opaque --page ground and no mask, so content is hidden down to the same edge a jump clears rather than shown through a blurless tint.

The branch is the feature, the follow-up that moves the header-height helper into lib/dom.ts, and this round's translucent ramp, tokens and tests.

Type

  • Feature

Verification

  • npx vitest run src/chrome/ChatTop.test.tsx src/features/composer/userjump.test.ts src/features/transcript/TurnNav.test.tsx --maxWorkers=2 gives 3 files and 50 tests passed.

  • npx vitest run src/features/composer src/features/transcript --maxWorkers=2 gives 16 files and 495 tests passed; npx vitest run src/chrome --maxWorkers=2 gives 10 files and 170 tests passed.

  • npx vitest run --no-file-parallelism scripts/gates gives 54 files and 257 tests passed.

  • npx eslint over the five changed TypeScript files reports no issues; npm run type-check exits 0.

  • The up pill's landing assertion in ComposerPage.test.tsx moved from 400 to 384: the shared 16px lead applies to both jumps, and its expectation encoded the old landing.

  • Mutation proof, each mutant reverted after its run: the fade token at 0 fails the scroll-padding assertion; headerBand ignoring the token fails the jump test; the trajectory host padding back to the row height fails; the parked inset at 0 fails; the tint layer losing its ground fails; the blur layer gaining an opaque ground fails; the fallback ground at transparent fails; the tint alpha at 1 fails; the tint layer painting --page fails; the tail shrinking back to 36px fails; the blur ramp ending 20px above the edge fails; the halo token removed fails; the icon plate rule removed fails.

  • Chrome 151, real dev page: band 108px; the blur layer's mask is 0 / 44px / 68px at 0.8 / 88px at 0.45 / 100px at 0.18 / 108px at 0, with backdrop-filter blur(20px) saturate(1.4); the tint layer is rgba(250,250,248,.3) masked 0 / 48px / 68px at 0.35 / 108px at 0; scroll-padding-top 108px, trajectory host padding 108px, turn nav top 108px. Shell tokens give a 38px row, a 94px band and 94px scroll-padding.

  • Chrome 151 edge probe: a black step under the band measures 42px wide at the top, 39px at 55px, 29px at 78px, 17px at 88px and 1px from 96px down, so the blur thins out continuously and lands sharp at the edge. With the blur layer removed the same step is sharp throughout, and the tint alone leaves sharp glyphs visible behind the title, which the blur layer suppresses.

  • A turn-nav click in the real page lands the question at y=124 (band 108 plus the 16px lead); the first line is clear of the frost.

  • At scrollTop 0 the first turn starts at y=88, inside the band's lower fade: its top edge is slightly soft, its text at y=100 and below is essentially sharp.

  • The frost tokens are declared four times (light defaults, media dark, dark override, light override); the test pins the count and the shared 0.3 alpha, and a single drifted block turns it red.

  • check_source_language.py, check_large_files.py and check_commit_messages.py over the three-dot range exit 0.

  • CI: the py3.12 unit shard 1 job has a known time-window flake in tests/test_cron_service_wake.py (at time is in the past); the test schedules an at time 150ms ahead and the runner can miss it. This branch touches only ui-web files.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

User-visible: the header band is 56px taller than the row and its frost fades to clear, so a scrolled message is progressively eaten instead of meeting a hard edge. The band is translucent: a bubble under it stays visible as colour and shape. The corner buttons gained a page-coloured plate and the name a soft halo so they stay readable over a dark bubble. The up pill, the turn nav and the trajectory host moved down with the band. The resting layout is unchanged, though the first turn's top edge now sits in the band's lower fade.

Performance: one backdrop-filter over a 108px strip, recomputed while the transcript scrolls.

Known gaps: WebKit was not available on this machine, so the -webkit- path was not run; the mask's effect on backdrop-filter was measured in Chrome only. The overlay scrollbar thumb's track starts at the top of the chat column and can cross the band's right edge, as before.

Rollback: revert the commits; the change is confined to styles/page.css, one feature stylesheet, two jump helpers and one lib helper, with no data or wire changes.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

@LivXue
LivXue requested review from 0xKT and gloryfromca October 30, 2026 08:16

@gloryfromca gloryfromca left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

I reviewed the full diff and surrounding chat-column layout, the turn-nav and previous-user jump callers, desktop-shell and trajectory variants, and the two commits' history. I also checked the repository rules and Web UI context, import/DOM-touch architecture, backward behavior when the header is absent, and that the added tests preserve the existing cases rather than weakening them.

Verification: npm test (261 files, 4,512 tests passed), npm run type-check (passed), npm run lint (passed with four pre-existing warnings in untouched files), npm run gen:check (passed), npm run build plus uv run python ui-web/build.py and node ui-web/scripts/check-page.mjs (passed, including both boot snapshots). git diff --check and the merge-tree check against github/main also passed.

@LivXue

LivXue commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review — PR #1151 @ 6a829312605ed0c8a31af96eafc64df1018e1218

Scope: the full diff at this head against base 11ca10d8 (7 files, +207/−18): the chat header is taken out of flow and floats over the transcript with a frosted band (styles/page.css), the band's height becomes the --topbar-h token, lib/dom.ts::headerBand() measures it for the turn nav and the up pill, and tests/goldens are updated. Reviewed on native Windows.

Result: no Windows-specific defect found by reading. The change is layout, offsetHeight measurement, and backdrop-filter/mask-image CSS. Chromium on Windows (and the product's WebView2 shell) supports backdrop-filter: blur(...); where it does not, the @supports not (...) block falls back to an opaque var(--page) ground with the mask removed, so the transcript is hidden rather than rendered washed-out. No path, filesystem, subprocess, or encoding surface is involved.

Verification: no executable check was possible for the UI — the shared D:/projects/Raven/ui-web/node_modules is empty (0 entries), so vitest (ChatTop, TurnNav, userjump tests) cannot run. The diff contains no Python.

Coverage gaps: the visual result (frost over a scrolled line, the fade at the band's lower edge, the shell's --tl-row title bar swap) was not rendered in a browser here; the review rests on the CSS and code reading. Real Chromium/WebView2 on this host was not exercised.

The chat header used to be a flex item above the scroller, so a message
scrolled to the top was clipped at the header's edge. It is an overlay over
the whole column now: the transcript passes underneath and is read through a
semi-transparent ink tint with a 20px backdrop blur, and the band's last
20px fade out so the boundary is a gradient rather than a cut. The scroller
gives the band's height back as top padding and scroll-padding, so the
resting frame does not move and only a scrolled one passes under.

The header height moves to --topbar-h (52px, or the shell's measured
--tl-row), which the parked scroller, the up pill and the trajectory box
also read. The up pill and the turn nav land a question below the band
instead of at the scroller's edge, so a jump cannot hide what it brought
into view.

Where backdrop-filter is unsupported the band falls back to the opaque page
ground the header had before.
The import-direction gate holds chrome below features, and the
state-dom-touch gate caps how many modules may reach for the document, so a
helper both islands import could not live under chrome/behaviour. It sits
beside $ in lib/dom.ts: the page's one sanctioned selector, and the file the
touch gate exempts.
The band used to be the header's own height at one tint with a 20px
tail, so a scrolled message met a hard edge and only the last 20px
softened. It now paints past the row (--topbar-fade-tail below it) and
its mask runs from opaque behind the controls to nothing at the lower
edge. Because the mask fades the filtered backdrop too, the blur itself
thins out down the ramp: a bubble is eaten a little more per pixel
instead of emerging at a cut.

The ground is plain --page instead of a color-mix, which removes the
window where backdrop-filter was supported but color-mix was not, the
background was dropped and the title sat on a bare blur.

The readable edge is --topbar-fade-h (row plus tail). scroll-padding,
the up pill, the trajectory host and the turn nav use it; headerBand()
returns it by measuring the row and reading the tail token, and both
jumps leave the same 16px lead. The rules are scoped to .chat > .top,
the selector headerBand() uses.

Tests pin the fade token, the mask shape, the fallback body, the
trajectory host's padding and the parked copy's inset; each was watched
failing on its own mutation.
@LivXue
LivXue force-pushed the feat/titlebar_frosted_glass branch from 6a82931 to f35ecf4 Compare October 31, 2026 14:40

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the UI test suite must pass with the new quick-jump landing position.

The rebased first two commits are patch-identical to the revision I previously reviewed. I reviewed the new frost-ramp commit, the final combined diff, and the latest comment-only follow-up. I covered the repository and Web UI rules, layout and import/DOM-touch architecture, affected jump callers, desktop-shell and trajectory variants, backward behavior without a mounted header, history/range comparison, and whether tests were weakened.

Verification: the full npm test run failed with 1 failure and 4,524 passes; on this exact head, npm test -- src/features/composer/ComposerPage.test.tsx reproduced the failure (1 failed, 69 passed). npm run type-check, npm run gen:check, and npm run build passed. npm run lint passed with four pre-existing warnings in untouched files. git diff --check and the merge-tree check against github/main passed.

HEADER_LEAD is shared by both jumps now, so the pill lands 16px lower
than the old expectation. The turn nav's test already asserts the same
number.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The only delta updates the existing quick-jump integration assertion to the intentional 16px lead, resolving the failing gate without weakening or replacing coverage. I reviewed the delta against the full diff, caller behavior, history, and the repository/Web UI test and architecture rules. My prior blocker is fixed, replied to, and resolved; no other thread I opened remains outstanding.

Verification: npm test passed all 262 files and 4,525 tests. npm run type-check passed. npm run lint passed with four pre-existing warnings in untouched files. git diff --check passed.

An opaque ground on the same element as the backdrop-filter hides the
blur on engines that paint the element's background over the filtered
backdrop, which is the order the spec describes; the band there would
be a paler sheet of sharp text rather than frost. The blur now has its
own transparent layer, masked away over the band's last 22px, and the
tint keeps the --page ground on a second layer behind the controls.

Tests pin the split: the blur layer must carry no background, the tint
layer must carry --page, and the fallback turns the tint opaque and
drops the blur.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the new split-layer frost delta at 160983f against the full PR and prior head. The blur-only ::before and tint-only ::after remain inside the header stacking context, preserve the existing geometry, and keep the opaque fallback when backdrop-filter is unavailable. The updated ChatTop tests strengthen coverage for both layers rather than weakening an assertion.

Covered the repository and Web UI rules, full diff and range history, affected callers/stacking context, backward compatibility and fallback behavior, test integrity, and the existing architecture constraints. Verification: git diff --check passed; npm test passed 262 files / 4,525 tests; npm run type-check passed; npm run lint passed with 0 errors and 4 pre-existing warnings outside this diff; npm run build passed. The head still matches 160983f, and no settled thread opened by me remains unresolved.

The band painted an almost opaque --page ground, so a bubble under it
was washed out instead of frosted. It is now glass: a weak --topbar-tint
(0.3 of --page) that unifies the tone and fades out below the controls,
over a blur layer whose mask walks from full behind the controls to none
at the lower edge. The colour and shape of a bubble stay visible through
it, the way a title bar shows the window behind it.

The legibility an opaque ground used to buy is now carried by the
controls: a --topbar-halo shadow behind the name and the chip, and a
page-coloured plate under each corner button. The plates ride on a
--ghost-bg variable so :hover and the pressed toggle still win.

The tail grows to 56px so the sharpening is gradual rather than a step:
the band is 108px, with the blur full to 44px and then 0.8 at 68, 0.45
at 88, 0.18 at 100 and 0 at 108. The jumps keep landing 16px under it.

Tests pin the low tint alpha, both layers, the halo and plate rules and
the fallback; each was watched failing on its own mutation.
Each token is declared in the light defaults, the media dark block and
both viewer overrides; a value that drifts in one of them is invisible
until someone opens that theme. The test pins the declaration count and
the shared alpha.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the frost-ramp revision and the test-only follow-up that landed while the review was running, through current head 530ec59. The translucent theme tokens, blur/tint ramps, header geometry consumers, ghost-button state specificity, and unsupported-backdrop fallback are internally consistent. The follow-up pins all four light/dark theme declarations and strengthens rather than weakens coverage.

Covered the repository and Web UI rules, full diff and range history, affected callers and selector interactions, backward compatibility/fallback behavior, test integrity, and architecture constraints. Verification on 6a955e2: git diff --check; focused tests 93/93; full UI suite 262 files / 4,525 tests; type-check; lint with 0 errors and 4 pre-existing warnings outside the diff; production build. Verification after the test-only 530ec59 follow-up: git diff --check; ChatTop tests 23/23; type-check; lint with the same 0 errors and 4 warnings. No settled thread opened by me remains unresolved.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants