summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-06 21:38:08 -0600
committerAdam <[email protected]>2026-01-06 21:42:03 -0600
commit761863ae355b3ca1e606ea5eb2106772fa763c19 (patch)
treef8fef26687875e75f4279b6695e35a33977b728c /packages/app/src/context
parentdadc08ddc7f3c9b1a34e6f401a99406b6714b965 (diff)
downloadopencode-761863ae355b3ca1e606ea5eb2106772fa763c19.tar.gz
opencode-761863ae355b3ca1e606ea5eb2106772fa763c19.zip
chore(app): rework storage approach
Diffstat (limited to 'packages/app/src/context')
-rw-r--r--packages/app/src/context/file.tsx37
-rw-r--r--packages/app/src/context/layout.tsx28
-rw-r--r--packages/app/src/context/local.tsx4
-rw-r--r--packages/app/src/context/notification.tsx33
-rw-r--r--packages/app/src/context/permission.tsx52
-rw-r--r--packages/app/src/context/prompt.tsx6
-rw-r--r--packages/app/src/context/server.tsx4
-rw-r--r--packages/app/src/context/terminal.tsx6
8 files changed, 132 insertions, 38 deletions
diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx
index a26f97c2a..ff36919a1 100644
--- a/packages/app/src/context/file.tsx
+++ b/packages/app/src/context/file.tsx
@@ -1,4 +1,4 @@
-import { createMemo, onCleanup } from "solid-js"
+import { createEffect, createMemo, 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"
@@ -7,7 +7,7 @@ import { useParams } from "@solidjs/router"
import { getFilename } from "@opencode-ai/util/path"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
export type FileSelection = {
startLine: number
@@ -134,10 +134,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
file: {},
})
- const viewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
+ const legacyViewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
const [view, setView, _, ready] = persisted(
- viewKey(),
+ Persist.scoped(params.dir, params.id, "file-view", [legacyViewKey()]),
createStore<{
file: Record<string, FileViewState>
}>({
@@ -145,6 +145,32 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
}),
)
+ const MAX_VIEW_FILES = 500
+ const viewMeta = { 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 (viewMeta.pruned) return
+ viewMeta.pruned = true
+ pruneView()
+ })
+
function ensure(path: string) {
if (!path) return
if (store.file[path]) return
@@ -233,6 +259,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
scrollTop: top,
}
})
+ pruneView(path)
}
const setScrollLeft = (input: string, left: number) => {
@@ -244,6 +271,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
scrollLeft: left,
}
})
+ pruneView(path)
}
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
@@ -256,6 +284,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
selectedLines: next,
}
})
+ pruneView(path)
}
onCleanup(() => stop())
diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx
index def933c39..2d92409f0 100644
--- a/packages/app/src/context/layout.tsx
+++ b/packages/app/src/context/layout.tsx
@@ -5,7 +5,7 @@ import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server"
import { Project } from "@opencode-ai/sdk/v2"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted, removePersisted } from "@/utils/persist"
import { same } from "@/utils/same"
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
@@ -46,7 +46,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const globalSync = useGlobalSync()
const server = useServer()
const [store, setStore, _, ready] = persisted(
- "layout.v6",
+ Persist.global("layout", ["layout.v6"]),
createStore({
sidebar: {
opened: false,
@@ -75,6 +75,29 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const meta = { active: undefined as string | undefined, pruned: false }
const used = new Map<string, number>()
+ const SESSION_STATE_KEYS = [
+ { key: "prompt", legacy: "prompt", version: "v2" },
+ { key: "terminal", legacy: "terminal", version: "v1" },
+ { key: "file-view", legacy: "file", version: "v1" },
+ ] as const
+
+ const dropSessionState = (keys: string[]) => {
+ for (const key of keys) {
+ const parts = key.split("/")
+ const dir = parts[0]
+ const session = parts[1]
+ if (!dir) continue
+
+ for (const entry of SESSION_STATE_KEYS) {
+ const target = session ? Persist.session(dir, session, entry.key) : Persist.workspace(dir, entry.key)
+ void removePersisted(target)
+
+ const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}`
+ void removePersisted({ key: legacyKey })
+ }
+ }
+ }
+
function prune(keep?: string) {
if (!keep) return
@@ -102,6 +125,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
)
scroll.drop(drop)
+ dropSessionState(drop)
for (const key of drop) {
used.delete(key)
diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx
index 3af840556..ea71ec499 100644
--- a/packages/app/src/context/local.tsx
+++ b/packages/app/src/context/local.tsx
@@ -8,7 +8,7 @@ import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { useProviders } from "@/hooks/use-providers"
import { DateTime } from "luxon"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
import { showToast } from "@opencode-ai/ui/toast"
export type LocalFile = FileNode &
@@ -111,7 +111,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const model = (() => {
const [store, setStore, _, modelReady] = persisted(
- "model.v1",
+ Persist.global("model", ["model.v1"]),
createStore<{
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
recent: ModelKey[]
diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx
index 09f32a3c4..16b3d306c 100644
--- a/packages/app/src/context/notification.tsx
+++ b/packages/app/src/context/notification.tsx
@@ -1,5 +1,5 @@
import { createStore } from "solid-js/store"
-import { onCleanup } from "solid-js"
+import { createEffect, onCleanup } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
@@ -10,7 +10,7 @@ import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
type NotificationBase = {
directory?: string
@@ -31,6 +31,16 @@ type ErrorNotification = NotificationBase & {
export type Notification = TurnCompleteNotification | ErrorNotification
+const MAX_NOTIFICATIONS = 500
+const NOTIFICATION_TTL_MS = 1000 * 60 * 60 * 24 * 30
+
+function pruneNotifications(list: Notification[]) {
+ const cutoff = Date.now() - NOTIFICATION_TTL_MS
+ const pruned = list.filter((n) => n.time >= cutoff)
+ if (pruned.length <= MAX_NOTIFICATIONS) return pruned
+ return pruned.slice(pruned.length - MAX_NOTIFICATIONS)
+}
+
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
name: "Notification",
init: () => {
@@ -49,12 +59,25 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
- "notification.v1",
+ Persist.global("notification", ["notification.v1"]),
createStore({
list: [] as Notification[],
}),
)
+ const meta = { pruned: false }
+
+ createEffect(() => {
+ if (!ready()) return
+ if (meta.pruned) return
+ meta.pruned = true
+ setStore("list", pruneNotifications(store.list))
+ })
+
+ const append = (notification: Notification) => {
+ setStore("list", (list) => pruneNotifications([...list, notification]))
+ }
+
const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -73,7 +96,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
try {
idlePlayer?.play()
} catch {}
- setStore("list", store.list.length, {
+ append({
...base,
type: "turn-complete",
session: sessionID,
@@ -92,7 +115,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
errorPlayer?.play()
} catch {}
const error = "error" in event.properties ? event.properties.error : undefined
- setStore("list", store.list.length, {
+ append({
...base,
type: "error",
session: sessionID ?? "global",
diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx
index dc75553c7..52878ba8f 100644
--- a/packages/app/src/context/permission.tsx
+++ b/packages/app/src/context/permission.tsx
@@ -1,12 +1,12 @@
import { createMemo, onCleanup } from "solid-js"
-import { createStore } from "solid-js/store"
+import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "./global-sync"
import { useParams } from "@solidjs/router"
-import { base64Decode } from "@opencode-ai/util/encode"
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
type PermissionRespondFn = (input: {
sessionID: string
@@ -60,7 +60,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
})
const [store, setStore, _, ready] = persisted(
- "permission.v3",
+ Persist.global("permission", ["permission.v3"]),
createStore({
autoAcceptEdits: {} as Record<string, boolean>,
}),
@@ -85,8 +85,14 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
})
}
- function isAutoAccepting(sessionID: string) {
- return store.autoAcceptEdits[sessionID] ?? false
+ function acceptKey(sessionID: string, directory?: string) {
+ if (!directory) return sessionID
+ return `${base64Encode(directory)}/${sessionID}`
+ }
+
+ function isAutoAccepting(sessionID: string, directory?: string) {
+ const key = acceptKey(sessionID, directory)
+ return store.autoAcceptEdits[key] ?? store.autoAcceptEdits[sessionID] ?? false
}
const unsubscribe = globalSDK.event.listen((e) => {
@@ -94,7 +100,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
if (event?.type !== "permission.asked") return
const perm = event.properties
- if (!isAutoAccepting(perm.sessionID)) return
+ if (!isAutoAccepting(perm.sessionID, e.name)) return
if (!shouldAutoAccept(perm)) return
respondOnce(perm, e.name)
@@ -102,7 +108,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
onCleanup(unsubscribe)
function enable(sessionID: string, directory: string) {
- setStore("autoAcceptEdits", sessionID, true)
+ const key = acceptKey(sessionID, directory)
+ setStore(
+ produce((draft) => {
+ draft.autoAcceptEdits[key] = true
+ delete draft.autoAcceptEdits[sessionID]
+ }),
+ )
globalSDK.client.permission
.list({ directory })
@@ -117,31 +129,37 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
.catch(() => undefined)
}
- function disable(sessionID: string) {
- setStore("autoAcceptEdits", sessionID, false)
+ function disable(sessionID: string, directory?: string) {
+ const key = directory ? acceptKey(sessionID, directory) : undefined
+ setStore(
+ produce((draft) => {
+ if (key) delete draft.autoAcceptEdits[key]
+ delete draft.autoAcceptEdits[sessionID]
+ }),
+ )
}
return {
ready,
respond,
- autoResponds(permission: PermissionRequest) {
- return isAutoAccepting(permission.sessionID) && shouldAutoAccept(permission)
+ autoResponds(permission: PermissionRequest, directory?: string) {
+ return isAutoAccepting(permission.sessionID, directory) && shouldAutoAccept(permission)
},
isAutoAccepting,
toggleAutoAccept(sessionID: string, directory: string) {
- if (isAutoAccepting(sessionID)) {
- disable(sessionID)
+ if (isAutoAccepting(sessionID, directory)) {
+ disable(sessionID, directory)
return
}
enable(sessionID, directory)
},
enableAutoAccept(sessionID: string, directory: string) {
- if (isAutoAccepting(sessionID)) return
+ if (isAutoAccepting(sessionID, directory)) return
enable(sessionID, directory)
},
- disableAutoAccept(sessionID: string) {
- disable(sessionID)
+ disableAutoAccept(sessionID: string, directory?: string) {
+ disable(sessionID, directory)
},
permissionsEnabled,
}
diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx
index f77f62e3c..ee62938d9 100644
--- a/packages/app/src/context/prompt.tsx
+++ b/packages/app/src/context/prompt.tsx
@@ -3,7 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { useParams } from "@solidjs/router"
import type { FileSelection } from "@/context/file"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
interface PartBase {
content: string
@@ -103,10 +103,10 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
name: "Prompt",
init: () => {
const params = useParams()
- const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
+ const legacy = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
const [store, setStore, _, ready] = persisted(
- name(),
+ Persist.scoped(params.dir, params.id, "prompt", [legacy()]),
createStore<{
prompt: Prompt
cursor?: number
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 48e7e99cc..06c37b592 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -3,7 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
type StoredProject = { worktree: string; expanded: boolean }
@@ -35,7 +35,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
- "server.v3",
+ Persist.global("server", ["server.v3"]),
createStore({
list: [] as string[],
projects: {} as Record<string, StoredProject[]>,
diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx
index e9a07077c..6188772f0 100644
--- a/packages/app/src/context/terminal.tsx
+++ b/packages/app/src/context/terminal.tsx
@@ -3,7 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
-import { persisted } from "@/utils/persist"
+import { Persist, persisted } from "@/utils/persist"
export type LocalPTY = {
id: string
@@ -19,10 +19,10 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
init: () => {
const sdk = useSDK()
const params = useParams()
- const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
+ const legacy = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
const [store, setStore, _, ready] = persisted(
- name(),
+ Persist.scoped(params.dir, params.id, "terminal", [legacy()]),
createStore<{
active?: string
all: LocalPTY[]