diff options
| author | Adam <[email protected]> | 2026-01-08 07:41:20 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-08 17:48:15 -0600 |
| commit | be9b2bab156d3eccaf1c8ea4fce2523407833fdd (patch) | |
| tree | 0fddff15d81bfd0e5dd8c88d55ac2948f56878d8 /packages/app/src/context/terminal.tsx | |
| parent | c949e5b390814348a2a86802d4c350e964864da6 (diff) | |
| download | opencode-be9b2bab156d3eccaf1c8ea4fce2523407833fdd.tar.gz opencode-be9b2bab156d3eccaf1c8ea4fce2523407833fdd.zip | |
feat(app): cache session-scoped stores, optional context gating
Diffstat (limited to 'packages/app/src/context/terminal.tsx')
| -rw-r--r-- | packages/app/src/context/terminal.tsx | 259 |
1 files changed, 163 insertions, 96 deletions
diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 2ee0d137e..43672fcb3 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -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 } -export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({ - name: "Terminal", - init: () => { - const sdk = useSDK() - const params = useParams() - const legacy = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`) - - const [store, setStore, _, ready] = persisted( - Persist.scoped(params.dir!, params.id, "terminal", [legacy()]), - createStore<{ - active?: string - all: LocalPTY[] - }>({ - all: [], - }), - ) +const WORKSPACE_KEY = "__workspace__" +const MAX_TERMINAL_SESSIONS = 20 - 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, +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) }) - 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) - } + .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) }) - await sdk.client.pty.remove({ ptyID: id }).catch((e) => { - console.error("Failed to close 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, }) - }, - move(id: string, to: number) { - const index = store.all.findIndex((f) => f.id === id) - if (index === -1) return + .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", - produce((all) => { - all.splice(to, 0, all.splice(index, 1)[0]) - }), + 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 cache = new Map<string, TerminalCacheEntry>() + + 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: () => 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), } }, }) |
