summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
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
parentc949e5b390814348a2a86802d4c350e964864da6 (diff)
downloadopencode-be9b2bab156d3eccaf1c8ea4fce2523407833fdd.tar.gz
opencode-be9b2bab156d3eccaf1c8ea4fce2523407833fdd.zip
feat(app): cache session-scoped stores, optional context gating
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/app.tsx22
-rw-r--r--packages/app/src/context/file.tsx202
-rw-r--r--packages/app/src/context/prompt.tsx192
-rw-r--r--packages/app/src/context/terminal.tsx259
-rw-r--r--packages/app/src/pages/session.tsx14
5 files changed, 460 insertions, 229 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 13d9d147e..3caaf99fe 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -108,18 +108,16 @@ export function AppInterface() {
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
- component={(p) => (
- <Show when={p.params.id ?? "new"} keyed>
- <TerminalProvider>
- <FileProvider>
- <PromptProvider>
- <Suspense fallback={<Loading />}>
- <Session />
- </Suspense>
- </PromptProvider>
- </FileProvider>
- </TerminalProvider>
- </Show>
+ component={() => (
+ <TerminalProvider>
+ <FileProvider>
+ <PromptProvider>
+ <Suspense fallback={<Loading />}>
+ <Session />
+ </Suspense>
+ </PromptProvider>
+ </FileProvider>
+ </TerminalProvider>
)}
/>
</Route>
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,
diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx
index 2fa4571e8..993d7e7a8 100644
--- a/packages/app/src/context/prompt.tsx
+++ b/packages/app/src/context/prompt.tsx
@@ -1,6 +1,6 @@
import { createStore } 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 type { FileSelection } from "@/context/file"
import { Persist, persisted } from "@/utils/persist"
@@ -99,74 +99,146 @@ function clonePrompt(prompt: Prompt): Prompt {
return prompt.map(clonePart)
}
+const WORKSPACE_KEY = "__workspace__"
+const MAX_PROMPT_SESSIONS = 20
+
+type PromptSession = ReturnType<typeof createPromptSession>
+
+type PromptCacheEntry = {
+ value: PromptSession
+ dispose: VoidFunction
+}
+
+function createPromptSession(dir: string, id: string | undefined) {
+ const legacy = `${dir}/prompt${id ? "/" + id : ""}.v2`
+
+ const [store, setStore, _, ready] = persisted(
+ Persist.scoped(dir, id, "prompt", [legacy]),
+ createStore<{
+ prompt: Prompt
+ cursor?: number
+ context: {
+ activeTab: boolean
+ items: (ContextItem & { key: string })[]
+ }
+ }>({
+ prompt: clonePrompt(DEFAULT_PROMPT),
+ cursor: undefined,
+ context: {
+ activeTab: true,
+ items: [],
+ },
+ }),
+ )
+
+ function keyForItem(item: ContextItem) {
+ if (item.type !== "file") return item.type
+ const start = item.selection?.startLine
+ const end = item.selection?.endLine
+ return `${item.type}:${item.path}:${start}:${end}`
+ }
+
+ return {
+ ready,
+ current: createMemo(() => store.prompt),
+ cursor: createMemo(() => store.cursor),
+ dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
+ context: {
+ activeTab: createMemo(() => store.context.activeTab),
+ items: createMemo(() => store.context.items),
+ addActive() {
+ setStore("context", "activeTab", true)
+ },
+ removeActive() {
+ setStore("context", "activeTab", false)
+ },
+ add(item: ContextItem) {
+ const key = keyForItem(item)
+ if (store.context.items.find((x) => x.key === key)) return
+ setStore("context", "items", (items) => [...items, { key, ...item }])
+ },
+ remove(key: string) {
+ setStore("context", "items", (items) => items.filter((x) => x.key !== key))
+ },
+ },
+ set(prompt: Prompt, cursorPosition?: number) {
+ const next = clonePrompt(prompt)
+ batch(() => {
+ setStore("prompt", next)
+ if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
+ })
+ },
+ reset() {
+ batch(() => {
+ setStore("prompt", clonePrompt(DEFAULT_PROMPT))
+ setStore("cursor", 0)
+ })
+ },
+ }
+}
+
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
name: "Prompt",
+ gate: false,
init: () => {
const params = useParams()
- const legacy = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
-
- const [store, setStore, _, ready] = persisted(
- Persist.scoped(params.dir!, params.id, "prompt", [legacy()]),
- createStore<{
- prompt: Prompt
- cursor?: number
- context: {
- activeTab: boolean
- items: (ContextItem & { key: string })[]
- }
- }>({
- prompt: clonePrompt(DEFAULT_PROMPT),
- cursor: undefined,
- context: {
- activeTab: true,
- items: [],
- },
- }),
- )
-
- function keyForItem(item: ContextItem) {
- if (item.type !== "file") return item.type
- const start = item.selection?.startLine
- const end = item.selection?.endLine
- return `${item.type}:${item.path}:${start}:${end}`
+ const cache = new Map<string, PromptCacheEntry>()
+
+ const disposeAll = () => {
+ for (const entry of cache.values()) {
+ entry.dispose()
+ }
+ cache.clear()
+ }
+
+ onCleanup(disposeAll)
+
+ const prune = () => {
+ while (cache.size > MAX_PROMPT_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: createPromptSession(dir, id),
+ dispose,
+ }))
+
+ cache.set(key, entry)
+ prune()
+ return entry.value
}
+ const session = createMemo(() => load(params.dir!, params.id))
+
return {
- ready,
- current: createMemo(() => store.prompt),
- cursor: createMemo(() => store.cursor),
- dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
+ ready: () => session().ready(),
+ current: () => session().current(),
+ cursor: () => session().cursor(),
+ dirty: () => session().dirty(),
context: {
- activeTab: createMemo(() => store.context.activeTab),
- items: createMemo(() => store.context.items),
- addActive() {
- setStore("context", "activeTab", true)
- },
- removeActive() {
- setStore("context", "activeTab", false)
- },
- add(item: ContextItem) {
- const key = keyForItem(item)
- if (store.context.items.find((x) => x.key === key)) return
- setStore("context", "items", (items) => [...items, { key, ...item }])
- },
- remove(key: string) {
- setStore("context", "items", (items) => items.filter((x) => x.key !== key))
- },
- },
- set(prompt: Prompt, cursorPosition?: number) {
- const next = clonePrompt(prompt)
- batch(() => {
- setStore("prompt", next)
- if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
- })
- },
- reset() {
- batch(() => {
- setStore("prompt", clonePrompt(DEFAULT_PROMPT))
- setStore("cursor", 0)
- })
+ activeTab: () => session().context.activeTab(),
+ items: () => session().context.items(),
+ addActive: () => session().context.addActive(),
+ removeActive: () => session().context.removeActive(),
+ add: (item: ContextItem) => session().context.add(item),
+ remove: (key: string) => session().context.remove(key),
},
+ set: (prompt: Prompt, cursorPosition?: number) => session().set(prompt, cursorPosition),
+ reset: () => session().reset(),
}
},
})
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),
}
},
})
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index e71177730..58f1e0a64 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -977,9 +977,18 @@ export default function Page() {
.join("")
.trim()
- onCleanup(() => {
+ createEffect(() => {
+ if (!prompt.ready()) return
handoff.prompt = previewPrompt()
+ })
+
+ createEffect(() => {
+ if (!terminal.ready()) return
handoff.terminals = terminal.all().map((t) => t.title)
+ })
+
+ createEffect(() => {
+ if (!file.ready()) return
handoff.files = Object.fromEntries(
tabs()
.all()
@@ -989,6 +998,9 @@ export default function Page() {
return [[path, file.selectedLines(path) ?? null] as const]
}),
)
+ })
+
+ onCleanup(() => {
cancelTurnBackfill()
document.removeEventListener("keydown", handleKeyDown)
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)