summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--bun.lock4
-rw-r--r--package.json1
-rw-r--r--packages/desktop/package.json2
-rw-r--r--packages/desktop/src/app.tsx2
-rw-r--r--packages/desktop/src/components/prompt-input.tsx8
-rw-r--r--packages/desktop/src/context/layout.tsx9
-rw-r--r--packages/desktop/src/context/local.tsx7
-rw-r--r--packages/desktop/src/context/notification.tsx9
-rw-r--r--packages/desktop/src/context/platform.tsx4
-rw-r--r--packages/desktop/src/context/prompt.tsx9
-rw-r--r--packages/desktop/src/context/terminal.tsx9
-rw-r--r--packages/desktop/src/utils/persist.ts26
-rw-r--r--packages/tauri/package.json1
-rw-r--r--packages/tauri/src/index.tsx18
-rw-r--r--packages/ui/src/components/session-turn.tsx4
-rw-r--r--packages/ui/src/context/helper.tsx11
16 files changed, 89 insertions, 35 deletions
diff --git a/bun.lock b/bun.lock
index 6ce8fd8b1..958287b6c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -137,7 +137,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
- "@solid-primitives/storage": "4.3.3",
+ "@solid-primitives/storage": "catalog:",
"@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
@@ -355,6 +355,7 @@
"version": "1.0.164",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
+ "@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
@@ -474,6 +475,7 @@
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.0.0-beta.3",
+ "@solid-primitives/storage": "4.3.3",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
diff --git a/package.json b/package.json
index 86f1aca39..930ab9acc 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.0.0-beta.3",
+ "@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 7f28ecc10..56eef913e 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -40,7 +40,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
- "@solid-primitives/storage": "4.3.3",
+ "@solid-primitives/storage": "catalog:",
"@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx
index be31a594e..13ef6833b 100644
--- a/packages/desktop/src/app.tsx
+++ b/packages/desktop/src/app.tsx
@@ -1,5 +1,5 @@
import "@/index.css"
-import { Show } from "solid-js"
+import { Show, Suspense } from "solid-js"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index aea9f4e23..a8f0736f8 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -1,7 +1,6 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
import { createStore, produce } from "solid-js/store"
-import { makePersisted } from "@solid-primitives/storage"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, usePrompt, ImageAttachmentPart } from "@/context/prompt"
@@ -21,6 +20,7 @@ import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
import { useCommand, formatKeybind } from "@/context/command"
+import { persisted } from "@/utils/persist"
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
@@ -109,15 +109,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const MAX_HISTORY = 100
- const [history, setHistory] = makePersisted(
+ const [history, setHistory] = persisted(
+ "prompt-history.v1",
createStore<{
entries: Prompt[]
}>({
entries: [],
}),
- {
- name: "prompt-history.v1",
- },
)
const clonePromptParts = (prompt: Prompt): Prompt =>
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index af71c6a00..01e0bdf52 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -1,10 +1,10 @@
import { createStore, produce } from "solid-js/store"
import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
-import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { Project } from "@opencode-ai/sdk/v2"
+import { persisted } from "@/utils/persist"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
@@ -32,7 +32,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
- const [store, setStore] = makePersisted(
+ const [store, setStore, _, ready] = persisted(
+ "layout.v3",
createStore({
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
@@ -48,9 +49,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
sessionTabs: {} as Record<string, SessionTabs>,
}),
- {
- name: "layout.v3",
- },
)
const usedColors = new Set<AvatarColorKey>()
@@ -93,6 +91,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
return {
+ ready,
projects: {
list,
open(directory: string) {
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index b12679210..2ea4de524 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -7,8 +7,8 @@ import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { useProviders } from "@/hooks/use-providers"
-import { makePersisted } from "@solid-primitives/storage"
import { DateTime } from "luxon"
+import { persisted } from "@/utils/persist"
export type LocalFile = FileNode &
Partial<{
@@ -110,7 +110,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
const model = (() => {
- const [store, setStore] = makePersisted(
+ const [store, setStore, _, modelReady] = persisted(
+ "model.v1",
createStore<{
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
recent: ModelKey[]
@@ -118,7 +119,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
user: [],
recent: [],
}),
- { name: "model.v1" },
)
const [ephemeral, setEphemeral] = createStore<{
@@ -242,6 +242,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
return {
+ ready: modelReady,
current,
recent,
list,
diff --git a/packages/desktop/src/context/notification.tsx b/packages/desktop/src/context/notification.tsx
index 5ca813448..2b258ebd6 100644
--- a/packages/desktop/src/context/notification.tsx
+++ b/packages/desktop/src/context/notification.tsx
@@ -1,6 +1,5 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
-import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
import { Binary } from "@opencode-ai/util/binary"
@@ -8,6 +7,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"
type NotificationBase = {
directory?: string
@@ -44,13 +44,11 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
- const [store, setStore] = makePersisted(
+ const [store, setStore, _, ready] = persisted(
+ "notification.v1",
createStore({
list: [] as Notification[],
}),
- {
- name: "notification.v1",
- },
)
globalSDK.event.listen((e) => {
@@ -101,6 +99,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
})
return {
+ ready,
session: {
all(session: string) {
return store.list.filter((n) => n.session === session)
diff --git a/packages/desktop/src/context/platform.tsx b/packages/desktop/src/context/platform.tsx
index 21be49cbd..92bb2ba15 100644
--- a/packages/desktop/src/context/platform.tsx
+++ b/packages/desktop/src/context/platform.tsx
@@ -1,4 +1,5 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
+import { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
export type Platform = {
/** Platform discriminator */
@@ -15,6 +16,9 @@ export type Platform = {
/** Open a URL in the default browser */
openLink(url: string): void
+
+ /** Storage mechanism, defaults to localStorage */
+ storage?: (name?: string) => SyncStorage | AsyncStorage
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
diff --git a/packages/desktop/src/context/prompt.tsx b/packages/desktop/src/context/prompt.tsx
index 2da0a08d5..8d3590cd9 100644
--- a/packages/desktop/src/context/prompt.tsx
+++ b/packages/desktop/src/context/prompt.tsx
@@ -1,9 +1,9 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
-import { makePersisted } from "@solid-primitives/storage"
import { useParams } from "@solidjs/router"
import { TextSelection } from "./local"
+import { persisted } from "@/utils/persist"
interface PartBase {
content: string
@@ -77,7 +77,8 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
const params = useParams()
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)
- const [store, setStore] = makePersisted(
+ const [store, setStore, _, ready] = persisted(
+ name(),
createStore<{
prompt: Prompt
cursor?: number
@@ -85,12 +86,10 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
}),
- {
- name: name(),
- },
)
return {
+ ready,
current: createMemo(() => store.prompt),
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
diff --git a/packages/desktop/src/context/terminal.tsx b/packages/desktop/src/context/terminal.tsx
index cf9b5a5b9..6f7c11dea 100644
--- a/packages/desktop/src/context/terminal.tsx
+++ b/packages/desktop/src/context/terminal.tsx
@@ -1,9 +1,9 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
-import { makePersisted } from "@solid-primitives/storage"
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
+import { persisted } from "@/utils/persist"
export type LocalPTY = {
id: string
@@ -21,19 +21,18 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
const params = useParams()
const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
- const [store, setStore] = makePersisted(
+ const [store, setStore, _, ready] = persisted(
+ name(),
createStore<{
active?: string
all: LocalPTY[]
}>({
all: [],
}),
- {
- name: name(),
- },
)
return {
+ ready,
all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active),
new() {
diff --git a/packages/desktop/src/utils/persist.ts b/packages/desktop/src/utils/persist.ts
new file mode 100644
index 000000000..12b334f9f
--- /dev/null
+++ b/packages/desktop/src/utils/persist.ts
@@ -0,0 +1,26 @@
+import { usePlatform } from "@/context/platform"
+import { makePersisted } from "@solid-primitives/storage"
+import { createResource, type Accessor } from "solid-js"
+import type { SetStoreFunction, Store } from "solid-js/store"
+
+type InitType = Promise<string> | string | null
+type PersistedWithReady<T> = [Store<T>, SetStoreFunction<T>, InitType, Accessor<boolean>]
+
+export function persisted<T>(key: string, store: [Store<T>, SetStoreFunction<T>]): PersistedWithReady<T> {
+ const platform = usePlatform()
+ const [state, setState, init] = makePersisted(store, { name: key, storage: platform.storage?.() ?? localStorage })
+
+ // Create a resource that resolves when the store is initialized
+ // This integrates with Suspense and provides a ready signal
+ const isAsync = init instanceof Promise
+ const [ready] = createResource(
+ () => init,
+ async (initValue) => {
+ if (initValue instanceof Promise) await initValue
+ return true
+ },
+ { initialValue: !isAsync },
+ )
+
+ return [state, setState, init, () => ready() === true]
+}
diff --git a/packages/tauri/package.json b/packages/tauri/package.json
index e0c6b177a..57761996a 100644
--- a/packages/tauri/package.json
+++ b/packages/tauri/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
+ "@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
diff --git a/packages/tauri/src/index.tsx b/packages/tauri/src/index.tsx
index 01df8166a..c77058eb9 100644
--- a/packages/tauri/src/index.tsx
+++ b/packages/tauri/src/index.tsx
@@ -5,6 +5,7 @@ import { onMount } from "solid-js"
import { open, save } from "@tauri-apps/plugin-dialog"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
+import { AsyncStorage } from "@solid-primitives/storage"
import { runUpdater, UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
@@ -48,6 +49,23 @@ const platform: Platform = {
openLink(url: string) {
shellOpen(url)
},
+
+ storage: (name = "default.dat") => {
+ const api: AsyncStorage = {
+ _store: null,
+ _getStore: async () => api._store || (api._store = (await import("@tauri-apps/plugin-store")).Store.load(name)),
+ getItem: async (key: string) => (await (await api._getStore()).get(key)) ?? null,
+ setItem: async (key: string, value: string) => await (await api._getStore()).set(key, value),
+ removeItem: async (key: string) => await (await api._getStore()).delete(key),
+ clear: async () => await (await api._getStore()).clear(),
+ key: async (index: number) => (await (await api._getStore()).keys())[index],
+ getLength: async () => (await api._getStore()).length(),
+ get length() {
+ return api.getLength()
+ },
+ }
+ return api
+ },
}
createMenu()
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 722b02492..79b3b7d47 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -102,7 +102,9 @@ export function SessionTurn(
setState("autoScrolled", true)
requestAnimationFrame(() => {
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "smooth" })
- setState("autoScrolled", false)
+ requestAnimationFrame(() => {
+ setState("autoScrolled", false)
+ })
})
}
diff --git a/packages/ui/src/context/helper.tsx b/packages/ui/src/context/helper.tsx
index 6be88e775..53f987945 100644
--- a/packages/ui/src/context/helper.tsx
+++ b/packages/ui/src/context/helper.tsx
@@ -1,4 +1,4 @@
-import { createContext, Show, useContext, type ParentProps } from "solid-js"
+import { createContext, createMemo, Show, useContext, type ParentProps, type Accessor } from "solid-js"
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
name: string
@@ -9,9 +9,14 @@ export function createSimpleContext<T, Props extends Record<string, any>>(input:
return {
provider: (props: ParentProps<Props>) => {
const init = input.init(props)
- return (
+ // Access init.ready inside the memo to make it reactive for getter properties
+ const isReady = createMemo(() => {
// @ts-expect-error
- <Show when={init.ready === undefined || init.ready === true}>
+ const ready = init.ready as Accessor<boolean> | boolean | undefined
+ return ready === undefined || (typeof ready === "function" ? ready() : ready)
+ })
+ return (
+ <Show when={isReady()}>
<ctx.Provider value={init}>{props.children}</ctx.Provider>
</Show>
)