feat(app): cache session-scoped stores, optional context gating

This commit is contained in:
Adam
2026-01-08 07:41:20 -06:00
parent c949e5b390
commit be9b2bab15
6 changed files with 469 additions and 231 deletions

View File

@@ -1,6 +1,6 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
import { Persist, persisted } from "@/utils/persist"
@@ -14,108 +14,175 @@ export type LocalPTY = {
scrollY?: number
}
const WORKSPACE_KEY = "__workspace__"
const MAX_TERMINAL_SESSIONS = 20
type TerminalSession = ReturnType<typeof createTerminalSession>
type TerminalCacheEntry = {
value: TerminalSession
dispose: VoidFunction
}
function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id: string | undefined) {
const legacy = `${dir}/terminal${id ? "/" + id : ""}.v1`
const [store, setStore, _, ready] = persisted(
Persist.scoped(dir, id, "terminal", [legacy]),
createStore<{
active?: string
all: LocalPTY[]
}>({
all: [],
}),
)
return {
ready,
all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active),
new() {
sdk.client.pty
.create({ title: `Terminal ${store.all.length + 1}` })
.then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("all", [
...store.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("active", id)
})
.catch((e) => {
console.error("Failed to create terminal", e)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty
.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
.catch((e) => {
console.error("Failed to update terminal", e)
})
},
async clone(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
if (!pty) return
const clone = await sdk.client.pty
.create({
title: pty.title,
})
.catch((e) => {
console.error("Failed to clone terminal", e)
return undefined
})
if (!clone?.data) return
setStore("all", index, {
...pty,
...clone.data,
})
if (store.active === pty.id) {
setStore("active", clone.data.id)
}
},
open(id: string) {
setStore("active", id)
},
async close(id: string) {
batch(() => {
setStore(
"all",
store.all.filter((x) => x.id !== id),
)
if (store.active === id) {
const index = store.all.findIndex((f) => f.id === id)
const previous = store.all[Math.max(0, index - 1)]
setStore("active", previous?.id)
}
})
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
console.error("Failed to close terminal", e)
})
},
move(id: string, to: number) {
const index = store.all.findIndex((f) => f.id === id)
if (index === -1) return
setStore(
"all",
produce((all) => {
all.splice(to, 0, all.splice(index, 1)[0])
}),
)
},
}
}
export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
name: "Terminal",
gate: false,
init: () => {
const sdk = useSDK()
const params = useParams()
const legacy = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
const cache = new Map<string, TerminalCacheEntry>()
const [store, setStore, _, ready] = persisted(
Persist.scoped(params.dir!, params.id, "terminal", [legacy()]),
createStore<{
active?: string
all: LocalPTY[]
}>({
all: [],
}),
)
const disposeAll = () => {
for (const entry of cache.values()) {
entry.dispose()
}
cache.clear()
}
onCleanup(disposeAll)
const prune = () => {
while (cache.size > MAX_TERMINAL_SESSIONS) {
const first = cache.keys().next().value
if (!first) return
const entry = cache.get(first)
entry?.dispose()
cache.delete(first)
}
}
const load = (dir: string, id: string | undefined) => {
const key = `${dir}:${id ?? WORKSPACE_KEY}`
const existing = cache.get(key)
if (existing) {
cache.delete(key)
cache.set(key, existing)
return existing.value
}
const entry = createRoot((dispose) => ({
value: createTerminalSession(sdk, dir, id),
dispose,
}))
cache.set(key, entry)
prune()
return entry.value
}
const session = createMemo(() => load(params.dir!, params.id))
return {
ready,
all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active),
new() {
sdk.client.pty
.create({ title: `Terminal ${store.all.length + 1}` })
.then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("all", [
...store.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("active", id)
})
.catch((e) => {
console.error("Failed to create terminal", e)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty
.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
.catch((e) => {
console.error("Failed to update terminal", e)
})
},
async clone(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
if (!pty) return
const clone = await sdk.client.pty
.create({
title: pty.title,
})
.catch((e) => {
console.error("Failed to clone terminal", e)
return undefined
})
if (!clone?.data) return
setStore("all", index, {
...pty,
...clone.data,
})
if (store.active === pty.id) {
setStore("active", clone.data.id)
}
},
open(id: string) {
setStore("active", id)
},
async close(id: string) {
batch(() => {
setStore(
"all",
store.all.filter((x) => x.id !== id),
)
if (store.active === id) {
const index = store.all.findIndex((f) => f.id === id)
const previous = store.all[Math.max(0, index - 1)]
setStore("active", previous?.id)
}
})
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
console.error("Failed to close terminal", e)
})
},
move(id: string, to: number) {
const index = store.all.findIndex((f) => f.id === id)
if (index === -1) return
setStore(
"all",
produce((all) => {
all.splice(to, 0, all.splice(index, 1)[0])
}),
)
},
ready: () => session().ready(),
all: () => session().all(),
active: () => session().active(),
new: () => session().new(),
update: (pty: Partial<LocalPTY> & { id: string }) => session().update(pty),
clone: (id: string) => session().clone(id),
open: (id: string) => session().open(id),
close: (id: string) => session().close(id),
move: (id: string, to: number) => session().move(id, to),
}
},
})