fix(site): fix sticky user message clipping and fade-in behavior (#23928)

The sticky user message in the chat timeline had two visual issues:

1. **Dead space during scroll** — the clipping calculation subtracted
48px prematurely (`fullHeight - scrolledPast - 48`), causing the message
to shrink before its content had actually left the viewport. Removed the
offset so clipping begins exactly when content scrolls out of view.

2. **Blur/gradient popping in abruptly** — the `--fade-opacity` variable
was a binary 0/1 toggle. Now it ramps 0→1 over the last 40px before
`MIN_HEIGHT`, so the blur and bottom gradient only appear when the
message is fully compressed.

Also added a longer (~25 line) user message to the `WithMessageHistory`
story to make the sticky behavior easier to test visually.
This commit is contained in:
Kyle Carberry
2026-04-01 16:33:51 +00:00
committed by GitHub
parent ee855f9618
commit 7dc81bdef1
2 changed files with 39 additions and 9 deletions
@@ -351,7 +351,7 @@ export const WithMessageHistory: Story = {
},
],
},
// -- Turn 3: user follow-up --
// -- Turn 3: user follow-up (long message) --
{
id: 3,
chat_id: CHAT_ID,
@@ -360,7 +360,33 @@ export const WithMessageHistory: Story = {
content: [
{
type: "text",
text: "Can you show me the token validation code and a comparison of the old vs new approach?",
text: [
"Can you show me the token validation code and a comparison of the old vs new approach?",
"",
"I have a lot of context I want to share so you can give me the best possible answer.",
"The current token validation is scattered across multiple files and it is really hard",
"to follow the flow from HTTP request to database lookup to response. The middleware in",
"coderd/httpmw/apikey.go does way too much - it parses the token, validates the signature,",
"checks expiration, looks up the user, checks if the user is suspended, and then sets up",
"the context. That is at least 6 different responsibilities in a single middleware function.",
"",
"Here are the specific files I have been looking at:",
"- coderd/httpmw/apikey.go (main middleware, ~400 lines)",
"- coderd/httpmw/oauth2.go (OAuth2 token handling)",
"- coderd/httpmw/session.go (session cookie management)",
"- coderd/userauth.go (login/logout handlers)",
"- coderd/apikey.go (API key CRUD operations)",
"- enterprise/coderd/proxyhealth.go (proxy authentication)",
"",
"The problem is that ExtractAPIKeyMW is doing too many things at once:",
"1. Extracting the token from the request (cookie or header)",
"2. Splitting the token into key ID and secret",
"3. Looking up the API key in the database",
"4. Hashing the secret and comparing it",
"5. Checking if the key is expired",
"",
"Can you incorporate all of this into your comparison of the old vs new approach?",
].join("\n"),
},
],
},
@@ -602,6 +602,7 @@ const ChatMessageItem = memo<{
<div
className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 max-h-12"
style={{
opacity: "var(--fade-opacity, 0)",
background:
"linear-gradient(to top, hsl(var(--surface-secondary)), transparent)",
}}
@@ -793,15 +794,17 @@ const StickyUserMessage = memo<{
container.style.top = "0px";
return;
}
const visible = Math.max(fullHeight - scrolledPast - 48, MIN_HEIGHT);
const visible = Math.max(fullHeight - scrolledPast, MIN_HEIGHT);
container.style.setProperty("--clip-h", `${visible}px`);
// Only show the fade gradient once enough content is
// clipped to be visually meaningful.
container.style.setProperty(
"--fade-opacity",
visible < fullHeight - 8 ? "1" : "0",
// Only show the blur and gradient once the message
// is near its minimum compressed height. Ramp over
// the last 40px before MIN_HEIGHT so it doesn't pop.
const FADE_RANGE = 40;
const fade = Math.max(
0,
Math.min((MIN_HEIGHT + FADE_RANGE - visible) / FADE_RANGE, 1),
);
container.style.setProperty("--fade-opacity", String(fade));
// Push-up effect: when the next user message's sentinel
// approaches the bottom of this sticky container, shift
// this container upward so it slides out of view — the
@@ -960,6 +963,7 @@ const StickyUserMessage = memo<{
<div
className="absolute inset-0 backdrop-blur-[1px] bg-surface-primary/15"
style={{
opacity: "var(--fade-opacity, 0)",
maxHeight: "calc(var(--clip-h, 100%) + 48px)",
willChange: "max-height, mask-image",
maskImage: