summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/file.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-08 07:41:20 -0600
committerAdam <[email protected]>2026-01-08 17:48:15 -0600
commitbe9b2bab156d3eccaf1c8ea4fce2523407833fdd (patch)
tree0fddff15d81bfd0e5dd8c88d55ac2948f56878d8 /packages/app/src/context/file.tsx
parentc949e5b390814348a2a86802d4c350e964864da6 (diff)
downloadopencode-be9b2bab156d3eccaf1c8ea4fce2523407833fdd.tar.gz
opencode-be9b2bab156d3eccaf1c8ea4fce2523407833fdd.zip
feat(app): cache session-scoped stores, optional context gating
Diffstat (limited to 'packages/app/src/context/file.tsx')
-rw-r--r--packages/app/src/context/file.tsx202
1 files changed, 142 insertions, 60 deletions
diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx
index 050262ae6..2cc0d62de 100644
--- a/packages/app/src/context/file.tsx
+++ b/packages/app/src/context/file.tsx
@@ -1,4 +1,4 @@
-import { createEffect, createMemo, onCleanup } from "solid-js"
+import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { FileContent } from "@opencode-ai/sdk/v2"
@@ -82,8 +82,106 @@ function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
}
}
+const WORKSPACE_KEY = "__workspace__"
+const MAX_FILE_VIEW_SESSIONS = 20
+const MAX_VIEW_FILES = 500
+
+type ViewSession = ReturnType<typeof createViewSession>
+
+type ViewCacheEntry = {
+ value: ViewSession
+ dispose: VoidFunction
+}
+
+function createViewSession(dir: string, id: string | undefined) {
+ const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
+
+ const [view, setView, _, ready] = persisted(
+ Persist.scoped(dir, id, "file-view", [legacyViewKey]),
+ createStore<{
+ file: Record<string, FileViewState>
+ }>({
+ file: {},
+ }),
+ )
+
+ const meta = { pruned: false }
+
+ const pruneView = (keep?: string) => {
+ const keys = Object.keys(view.file)
+ if (keys.length <= MAX_VIEW_FILES) return
+
+ const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
+ if (drop.length === 0) return
+
+ setView(
+ produce((draft) => {
+ for (const key of drop) {
+ delete draft.file[key]
+ }
+ }),
+ )
+ }
+
+ createEffect(() => {
+ if (!ready()) return
+ if (meta.pruned) return
+ meta.pruned = true
+ pruneView()
+ })
+
+ const scrollTop = (path: string) => view.file[path]?.scrollTop
+ const scrollLeft = (path: string) => view.file[path]?.scrollLeft
+ const selectedLines = (path: string) => view.file[path]?.selectedLines
+
+ const setScrollTop = (path: string, top: number) => {
+ setView("file", path, (current) => {
+ if (current?.scrollTop === top) return current
+ return {
+ ...(current ?? {}),
+ scrollTop: top,
+ }
+ })
+ pruneView(path)
+ }
+
+ const setScrollLeft = (path: string, left: number) => {
+ setView("file", path, (current) => {
+ if (current?.scrollLeft === left) return current
+ return {
+ ...(current ?? {}),
+ scrollLeft: left,
+ }
+ })
+ pruneView(path)
+ }
+
+ const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
+ const next = range ? normalizeSelectedLines(range) : null
+ setView("file", path, (current) => {
+ if (current?.selectedLines === next) return current
+ return {
+ ...(current ?? {}),
+ selectedLines: next,
+ }
+ })
+ pruneView(path)
+ }
+
+ return {
+ ready,
+ scrollTop,
+ scrollLeft,
+ selectedLines,
+ setScrollTop,
+ setScrollLeft,
+ setSelectedLines,
+ }
+}
+
export const { use: useFile, provider: FileProvider } = createSimpleContext({
name: "File",
+ gate: false,
init: () => {
const sdk = useSDK()
const sync = useSync()
@@ -134,42 +232,45 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
file: {},
})
- const legacyViewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
+ const viewCache = new Map<string, ViewCacheEntry>()
- const [view, setView, _, ready] = persisted(
- Persist.scoped(params.dir!, params.id, "file-view", [legacyViewKey()]),
- createStore<{
- file: Record<string, FileViewState>
- }>({
- file: {},
- }),
- )
+ const disposeViews = () => {
+ for (const entry of viewCache.values()) {
+ entry.dispose()
+ }
+ viewCache.clear()
+ }
- const MAX_VIEW_FILES = 500
- const viewMeta = { pruned: false }
+ const pruneViews = () => {
+ while (viewCache.size > MAX_FILE_VIEW_SESSIONS) {
+ const first = viewCache.keys().next().value
+ if (!first) return
+ const entry = viewCache.get(first)
+ entry?.dispose()
+ viewCache.delete(first)
+ }
+ }
- const pruneView = (keep?: string) => {
- const keys = Object.keys(view.file)
- if (keys.length <= MAX_VIEW_FILES) return
+ const loadView = (dir: string, id: string | undefined) => {
+ const key = `${dir}:${id ?? WORKSPACE_KEY}`
+ const existing = viewCache.get(key)
+ if (existing) {
+ viewCache.delete(key)
+ viewCache.set(key, existing)
+ return existing.value
+ }
- const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
- if (drop.length === 0) return
+ const entry = createRoot((dispose) => ({
+ value: createViewSession(dir, id),
+ dispose,
+ }))
- setView(
- produce((draft) => {
- for (const key of drop) {
- delete draft.file[key]
- }
- }),
- )
+ viewCache.set(key, entry)
+ pruneViews()
+ return entry.value
}
- createEffect(() => {
- if (!ready()) return
- if (viewMeta.pruned) return
- viewMeta.pruned = true
- pruneView()
- })
+ const view = createMemo(() => loadView(params.dir!, params.id))
function ensure(path: string) {
if (!path) return
@@ -246,51 +347,32 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const get = (input: string) => store.file[normalize(input)]
- const scrollTop = (input: string) => view.file[normalize(input)]?.scrollTop
- const scrollLeft = (input: string) => view.file[normalize(input)]?.scrollLeft
- const selectedLines = (input: string) => view.file[normalize(input)]?.selectedLines
+ const scrollTop = (input: string) => view().scrollTop(normalize(input))
+ const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
+ const selectedLines = (input: string) => view().selectedLines(normalize(input))
const setScrollTop = (input: string, top: number) => {
const path = normalize(input)
- setView("file", path, (current) => {
- if (current?.scrollTop === top) return current
- return {
- ...(current ?? {}),
- scrollTop: top,
- }
- })
- pruneView(path)
+ view().setScrollTop(path, top)
}
const setScrollLeft = (input: string, left: number) => {
const path = normalize(input)
- setView("file", path, (current) => {
- if (current?.scrollLeft === left) return current
- return {
- ...(current ?? {}),
- scrollLeft: left,
- }
- })
- pruneView(path)
+ view().setScrollLeft(path, left)
}
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
const path = normalize(input)
- const next = range ? normalizeSelectedLines(range) : null
- setView("file", path, (current) => {
- if (current?.selectedLines === next) return current
- return {
- ...(current ?? {}),
- selectedLines: next,
- }
- })
- pruneView(path)
+ view().setSelectedLines(path, range)
}
- onCleanup(() => stop())
+ onCleanup(() => {
+ stop()
+ disposeViews()
+ })
return {
- ready,
+ ready: () => view().ready(),
normalize,
tab,
pathFromTab,