diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx index 943e91c6..7a43ead2 100644 --- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx +++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx @@ -843,6 +843,7 @@ const useMorphdomMarkdown = ({ // Reconcile per block: only re-morph blocks whose content changed, leaving // stable leading blocks untouched. Keeps per-stream-step DOM work bounded // to the trailing (growing) block instead of the whole message. + let enteredThisPass = 0; blocks.forEach((block, index) => { let el = existing[index]; let isNewBlock = false; @@ -863,9 +864,16 @@ const useMorphdomMarkdown = ({ // goes on the block's children — the wrapper is display:contents // and cannot animate — and the transform never changes layout, so // row measurement stays exact. Skipped for the first block so a - // full initial render does not shimmer. + // full initial render does not shimmer. Several blocks committed + // in one tick cascade with a small stagger instead of popping in + // together. + const delayMs = Math.min(enteredThisPass, 4) * 55; + enteredThisPass += 1; for (const child of Array.from(temp.children)) { child.classList.add('oc-md-block-enter'); + if (delayMs > 0 && child instanceof HTMLElement) { + child.style.setProperty('--oc-md-enter-delay', `${delayMs}ms`); + } } } const hadMermaidBlock = shouldRefreshMermaidViewers(el); diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index 4b631c6b..a9f2fdc6 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -1378,21 +1378,29 @@ html:not(.dark) .chat-scroll { } } -/* A block committed mid-stream enters with a short fade-and-rise; the - transform is paint-only, so virtualized row measurement is unaffected. */ +/* A block committed mid-stream materializes: fade, a gentle rise, and a + touch of blur resolving. Transform and filter are paint-only, so + virtualized row measurement is unaffected. 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(4px); + transform: translateY(10px); + filter: blur(2px); + } + 60% { + filter: blur(0); } to { opacity: 1; transform: none; + filter: none; } } .oc-md-block-enter { - animation: oc-md-block-enter 140ms ease-out both; + 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) {