Merge main (anchored-turn chat scrolling) into perf/switch-and-scroll
Main replaced the chat timeline scroll engine while this branch was in flight, which obsoletes two of its subareas and reshapes a third: - Chat timeline: main's LegendList-based MessageList/ChatContainer win; the activation-overscan staircase targeted the removed tanstack path (LegendList provides adaptive rendering natively) and is dropped along with its test. - Scroll shadows: main's hook-based masks stay (the virtualized list owns its scroll element — there is no wrapper to hand the styling to); the viewport-wrapper ScrollShadow rewrite, its index.css replacement, its test, and the call-site viewportClassName adaptations are reverted to main. The chat OverlayScrollbar keeps this branch's disableHorizontal. - OverlayScrollbar: the direct-DOM rewrite lands, but binding now follows the live container node instead of binding once per ref object — the chat scroller remounts on every session switch, and a bind-once contract left the scrollbar attached to a dead element. - Markdown renderer: the detached-DOM cache and warm-block fast path merge with main's block-commit reveal (enter cascade), streaming code highlighting, and gutter reservation; the per-block reconcile keeps both the decoration-refresh path and the reveal cascade.
This commit is contained in:
+126
-58
@@ -256,81 +256,91 @@ div[data-chat-input-footer="true"] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* Scroll edge fades live on the viewport, never on the scrolling content. */
|
||||
[data-scroll-shadow-viewport]::before,
|
||||
[data-scroll-shadow-viewport]::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
/* Scroll shadow (HeroUI) fallback styling to ensure visible gradients without the Tailwind plugin */
|
||||
[data-scroll-shadow="true"] {
|
||||
/* A concrete default, not var(--scroll-shadow-size, 48px): a custom
|
||||
property referencing itself is a cycle and computes to invalid, which
|
||||
silently killed every mask below for consumers that don't set the
|
||||
variable inline (the hook-based chat scroller). Inline styles from the
|
||||
ScrollShadow component still override this. */
|
||||
--scroll-shadow-size: 48px;
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-orientation="vertical"]::before,
|
||||
[data-scroll-shadow-viewport][data-orientation="vertical"]::after {
|
||||
inset-inline: 0;
|
||||
[data-scroll-shadow="true"][data-orientation="vertical"] {
|
||||
mask-mode: alpha;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-orientation="vertical"]::before {
|
||||
top: 0;
|
||||
height: var(--scroll-shadow-top-size, var(--scroll-shadow-size));
|
||||
background: linear-gradient(
|
||||
[data-scroll-shadow="true"][data-orientation="vertical"][data-top-bottom-scroll="true"] {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--scroll-shadow-color, var(--surface-background)) 0,
|
||||
var(--scroll-shadow-color, var(--surface-background)) var(--scroll-shadow-top-clear-size, 0px),
|
||||
transparent 0%,
|
||||
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||
#000 calc(100% - var(--scroll-shadow-size)),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||
#000 calc(100% - var(--scroll-shadow-size)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-orientation="vertical"]::after {
|
||||
bottom: 0;
|
||||
height: var(--scroll-shadow-size);
|
||||
background: linear-gradient(to top, var(--scroll-shadow-color, var(--surface-background)), transparent);
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-orientation="horizontal"]::before,
|
||||
[data-scroll-shadow-viewport][data-orientation="horizontal"]::after {
|
||||
inset-block: 0;
|
||||
width: var(--scroll-shadow-size);
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-orientation="horizontal"]::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, var(--scroll-shadow-color, var(--surface-background)), transparent);
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-orientation="horizontal"]::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, var(--scroll-shadow-color, var(--surface-background)), transparent);
|
||||
}
|
||||
|
||||
[data-scroll-shadow-viewport][data-top-scroll="true"]::before,
|
||||
[data-scroll-shadow-viewport][data-top-bottom-scroll="true"]::before,
|
||||
[data-scroll-shadow-viewport][data-bottom-scroll="true"]::after,
|
||||
[data-scroll-shadow-viewport][data-top-bottom-scroll="true"]::after,
|
||||
[data-scroll-shadow-viewport][data-left-scroll="true"]::before,
|
||||
[data-scroll-shadow-viewport][data-left-right-scroll="true"]::before,
|
||||
[data-scroll-shadow-viewport][data-right-scroll="true"]::after,
|
||||
[data-scroll-shadow-viewport][data-left-right-scroll="true"]::after {
|
||||
opacity: 1;
|
||||
[data-scroll-shadow="true"][data-orientation="vertical"][data-top-scroll="true"] {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||
#000 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
transparent var(--scroll-shadow-top-clear-size, 0px),
|
||||
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
|
||||
#000 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Sticky-fade overlay: the crisp duplicate identity stays
|
||||
mounted and its visibility is driven synchronously by the viewport's
|
||||
top-scroll data attribute, so the overlay appears in the exact frame the
|
||||
edge fade covers the real header (no blink).
|
||||
mounted and its visibility is driven synchronously by the scroller's own
|
||||
top-scroll data attribute — the same signal that engages the mask — so the
|
||||
overlay appears in the exact frame the mask hides the real header (no blink).
|
||||
ScrollShadow sets data-top-scroll when nothing is below the viewport and
|
||||
data-top-bottom-scroll when content is both above and below. */
|
||||
.oc-sticky-fade-overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.oc-sticky-fade-root:has(
|
||||
[data-scroll-shadow-viewport]:is([data-top-scroll="true"], [data-top-bottom-scroll="true"])
|
||||
) .oc-sticky-fade-overlay {
|
||||
.oc-sticky-fade-root:has(.oc-sticky-fade-scroller[data-top-scroll="true"]) .oc-sticky-fade-overlay,
|
||||
.oc-sticky-fade-root:has(.oc-sticky-fade-scroller[data-top-bottom-scroll="true"]) .oc-sticky-fade-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-scroll-shadow="true"][data-orientation="vertical"][data-bottom-scroll="true"] {
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
#000 0%,
|
||||
#000 calc(100% - var(--scroll-shadow-size)),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
#000 0%,
|
||||
#000 calc(100% - var(--scroll-shadow-size)),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@keyframes spin-once {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@@ -885,15 +895,15 @@ html:not(.dark) .chat-scroll {
|
||||
}
|
||||
|
||||
/* Hide the long active todo before it can collide with the changed-files summary. */
|
||||
@container status-row (max-width: 38rem) {
|
||||
.status-row__active-todo {
|
||||
@container composer-status-bar (max-width: 38rem) {
|
||||
.composer-status-bar__active-todo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide the secondary changed-files label on narrow mobile layouts. */
|
||||
@container status-row (max-width: 30rem) {
|
||||
.status-row__changed-label {
|
||||
@container composer-status-bar (max-width: 30rem) {
|
||||
.composer-status-bar__changed-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1353,6 +1363,64 @@ html:not(.dark) .chat-scroll {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* First uncached open of a session shows a hydration skeleton; the real
|
||||
timeline replacing it fades in once instead of popping. Cached session
|
||||
switches never carry this class and stay instant. */
|
||||
@keyframes oc-chat-hydration-reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.oc-chat-hydration-reveal {
|
||||
animation: oc-chat-hydration-reveal 180ms ease-out both;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.oc-chat-hydration-reveal {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* A block committed mid-stream enters with a fade and a gentle rise —
|
||||
compositor-only properties, deliberately nothing heavier: during a long
|
||||
stream the follow scroll masks the entrance anyway, so the effect only
|
||||
really shows on short replies, and those must not pay for a GPU filter.
|
||||
Blocks committed in the same tick cascade via --oc-md-enter-delay set
|
||||
inline by the renderer. */
|
||||
@keyframes oc-md-block-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.oc-md-block-enter {
|
||||
animation: oc-md-block-enter 320ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
animation-delay: var(--oc-md-enter-delay, 0ms);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.oc-md-block-enter {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* While streaming defers the per-line gutter markup, hold its horizontal
|
||||
footprint (2rem column + 0.75rem gap) so the finished pass only fills in
|
||||
the numbers instead of shifting every code line. */
|
||||
.markdown-content pre[data-md-gutter-reserved] > code {
|
||||
display: block;
|
||||
padding-left: 2.75rem;
|
||||
}
|
||||
|
||||
.markdown-content [data-md-code-line-number] {
|
||||
align-self: stretch;
|
||||
padding-right: 0.75rem;
|
||||
|
||||
Reference in New Issue
Block a user