diff options
| author | Adam <[email protected]> | 2026-01-06 04:18:20 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-06 08:18:49 -0600 |
| commit | 3f463bc9168abd907be9ae582e161ff89c3a27c9 (patch) | |
| tree | 5b4aad62e698be847dccb6e93b80ac2c3f84844f /packages/app/src/context/layout.tsx | |
| parent | 0b02f6d22f793387322d96f937cfc1c8eee9bfbb (diff) | |
| download | opencode-3f463bc9168abd907be9ae582e161ff89c3a27c9.tar.gz opencode-3f463bc9168abd907be9ae582e161ff89c3a27c9.zip | |
fix(app): scroll store performance
Diffstat (limited to 'packages/app/src/context/layout.tsx')
| -rw-r--r-- | packages/app/src/context/layout.tsx | 114 |
1 files changed, 98 insertions, 16 deletions
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index e454f6cfa..def933c39 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -1,5 +1,5 @@ import { createStore, produce } from "solid-js/store" -import { batch, createEffect, createMemo, onMount } from "solid-js" +import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" @@ -7,6 +7,7 @@ import { useServer } from "./server" import { Project } from "@opencode-ai/sdk/v2" import { persisted } from "@/utils/persist" import { same } from "@/utils/same" +import { createScrollPersistence, type SessionScroll } from "./layout-scroll" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] @@ -29,11 +30,6 @@ type SessionTabs = { all: string[] } -type SessionScroll = { - x: number - y: number -} - type SessionView = { scroll: Record<string, SessionScroll> reviewOpen?: string[] @@ -75,6 +71,97 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }), ) + const MAX_SESSION_KEYS = 50 + const meta = { active: undefined as string | undefined, pruned: false } + const used = new Map<string, number>() + + function prune(keep?: string) { + if (!keep) return + + const keys = new Set<string>() + for (const key of Object.keys(store.sessionView)) keys.add(key) + for (const key of Object.keys(store.sessionTabs)) keys.add(key) + if (keys.size <= MAX_SESSION_KEYS) return + + const score = (key: string) => { + if (key === keep) return Number.MAX_SAFE_INTEGER + return used.get(key) ?? 0 + } + + const ordered = Array.from(keys).sort((a, b) => score(b) - score(a)) + const drop = ordered.slice(MAX_SESSION_KEYS) + if (drop.length === 0) return + + setStore( + produce((draft) => { + for (const key of drop) { + delete draft.sessionView[key] + delete draft.sessionTabs[key] + } + }), + ) + + scroll.drop(drop) + + for (const key of drop) { + used.delete(key) + } + } + + function touch(sessionKey: string) { + meta.active = sessionKey + used.set(sessionKey, Date.now()) + + if (!ready()) return + if (meta.pruned) return + + meta.pruned = true + prune(sessionKey) + } + + const scroll = createScrollPersistence({ + debounceMs: 250, + getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll, + onFlush: (sessionKey, next) => { + const current = store.sessionView[sessionKey] + const keep = meta.active ?? sessionKey + if (!current) { + setStore("sessionView", sessionKey, { scroll: next }) + prune(keep) + return + } + + setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...(prev ?? {}), ...next })) + prune(keep) + }, + }) + + createEffect(() => { + if (!ready()) return + if (meta.pruned) return + const active = meta.active + if (!active) return + meta.pruned = true + prune(active) + }) + + onMount(() => { + const flush = () => batch(() => scroll.flushAll()) + const handleVisibility = () => { + if (document.visibilityState !== "hidden") return + flush() + } + + window.addEventListener("pagehide", flush) + document.addEventListener("visibilitychange", handleVisibility) + + onCleanup(() => { + window.removeEventListener("pagehide", flush) + document.removeEventListener("visibilitychange", handleVisibility) + scroll.dispose() + }) + }) + const usedColors = new Set<AvatarColorKey>() function pickAvailableColor(): AvatarColorKey { @@ -253,21 +340,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }, view(sessionKey: string) { + touch(sessionKey) + scroll.seed(sessionKey) const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) return { scroll(tab: string) { - return s().scroll?.[tab] + return scroll.scroll(sessionKey, tab) }, setScroll(tab: string, pos: SessionScroll) { - const current = store.sessionView[sessionKey] - if (!current) { - setStore("sessionView", sessionKey, { scroll: { [tab]: pos } }) - return - } - - const prev = current.scroll?.[tab] - if (prev?.x === pos.x && prev?.y === pos.y) return - setStore("sessionView", sessionKey, "scroll", tab, pos) + scroll.setScroll(sessionKey, tab, pos) }, review: { open: createMemo(() => s().reviewOpen), @@ -285,6 +366,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } }, tabs(sessionKey: string) { + touch(sessionKey) const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] }) return { tabs, |
