summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/layout.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src/context/layout.tsx')
-rw-r--r--packages/app/src/context/layout.tsx114
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,