fix(ui): cache reduced-motion query and clean up deferRender timers

- Add module-level prefersReducedMotion signal in message-part.tsx,
  replacing per-mount window.matchMedia calls in useToolFade
- Cancel pending rAF/setTimeout in session.tsx deferRender when session
  key changes rapidly, and clean up on unmount
This commit is contained in:
Kit Langton
2026-03-04 10:23:12 -05:00
parent 6455ccb609
commit 7fe615247b
2 changed files with 29 additions and 7 deletions

View File

@@ -38,6 +38,7 @@ import { createScrollSpy } from "@/pages/session/scroll-spy"
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { MessageTimeline } from "@/pages/session/message-timeline"
import { AnimationDebugPanel } from "@opencode-ai/ui/animation-debug-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
@@ -419,16 +420,28 @@ export default function Page() {
deferRender: false,
})
let deferFrame: number | undefined
let deferTimer: ReturnType<typeof setTimeout> | undefined
createComputed((prev) => {
const key = sessionKey()
if (key !== prev) {
if (deferFrame !== undefined) cancelAnimationFrame(deferFrame)
if (deferTimer !== undefined) clearTimeout(deferTimer)
setStore("deferRender", true)
requestAnimationFrame(() => {
setTimeout(() => setStore("deferRender", false), 0)
deferFrame = requestAnimationFrame(() => {
deferFrame = undefined
deferTimer = setTimeout(() => {
deferTimer = undefined
setStore("deferRender", false)
}, 0)
})
}
return key
}, sessionKey())
onCleanup(() => {
if (deferFrame !== undefined) cancelAnimationFrame(deferFrame)
if (deferTimer !== undefined) clearTimeout(deferTimer)
})
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
@@ -1162,6 +1175,7 @@ export default function Page() {
return (
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
{import.meta.env.DEV && <AnimationDebugPanel />}
<SessionHeader />
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
<SessionMobileTabs

View File

@@ -50,7 +50,7 @@ import { list } from "./text-utils"
import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title"
import { GrowBox } from "./grow-box"
import { animate, type AnimationPlaybackControls, clearFadeStyles, clearMaskStyles, FADE_SPRING, WIPE_MASK } from "./motion"
import { animate, type AnimationPlaybackControls, clearFadeStyles, clearMaskStyles, GROW_SPRING, WIPE_MASK } from "./motion"
interface Diagnostic {
range: {
@@ -293,6 +293,14 @@ const pageVisible = /* @__PURE__ */ (() => {
return visible
})()
const prefersReducedMotion = /* @__PURE__ */ (() => {
if (typeof window === "undefined") return () => false
const mql = window.matchMedia("(prefers-reduced-motion: reduce)")
const [reduced, setReduced] = createSignal(mql.matches)
mql.addEventListener("change", () => setReduced(mql.matches))
return reduced
})()
function createGroupOpenState() {
const [state, setState] = createSignal<Record<string, boolean>>({})
const read = (key?: string, collapse?: boolean) => {
@@ -1496,7 +1504,7 @@ function useToolFade(
const el = ref()
if (!el || typeof window === "undefined") return
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
if (prefersReducedMotion()) return
const mask =
wipe &&
@@ -1529,10 +1537,10 @@ function useToolFade(
? animate(
node,
{ opacity: 1, filter: "blur(0px)", transform: "translateX(0)", maskPosition: "0% 0%" },
{ ...FADE_SPRING, delay },
{ ...GROW_SPRING, delay },
)
: animate(node, { opacity: 1, filter: "blur(0px)", transform: "translateX(0)" }, { ...FADE_SPRING, delay })
: animate(node, { opacity: 1, filter: "blur(0px)", transform: "translateY(0)" }, { ...FADE_SPRING, delay })
: animate(node, { opacity: 1, filter: "blur(0px)", transform: "translateX(0)" }, { ...GROW_SPRING, delay })
: animate(node, { opacity: 1, filter: "blur(0px)", transform: "translateY(0)" }, { ...GROW_SPRING, delay })
anim?.finished.then(() => {
const value = ref()