diff options
| author | Adam <[email protected]> | 2025-12-17 13:10:57 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-17 13:11:02 -0600 |
| commit | 4a3ba58f65d12b4f7c7a97b42a5bb3bc0eb5f88b (patch) | |
| tree | abae119fe310b1c73fc0d70d9d0d81985c5def42 /packages | |
| parent | 2a3a8a1ec2d71ae27730226cfac37830c7a5dfd7 (diff) | |
| download | opencode-4a3ba58f65d12b4f7c7a97b42a5bb3bc0eb5f88b.tar.gz opencode-4a3ba58f65d12b4f7c7a97b42a5bb3bc0eb5f88b.zip | |
chore: localStorage -> tauri store
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/desktop/package.json | 2 | ||||
| -rw-r--r-- | packages/desktop/src/app.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 8 | ||||
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 9 | ||||
| -rw-r--r-- | packages/desktop/src/context/local.tsx | 7 | ||||
| -rw-r--r-- | packages/desktop/src/context/notification.tsx | 9 | ||||
| -rw-r--r-- | packages/desktop/src/context/platform.tsx | 4 | ||||
| -rw-r--r-- | packages/desktop/src/context/prompt.tsx | 9 | ||||
| -rw-r--r-- | packages/desktop/src/context/terminal.tsx | 9 | ||||
| -rw-r--r-- | packages/desktop/src/utils/persist.ts | 26 | ||||
| -rw-r--r-- | packages/tauri/package.json | 1 | ||||
| -rw-r--r-- | packages/tauri/src/index.tsx | 18 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 4 | ||||
| -rw-r--r-- | packages/ui/src/context/helper.tsx | 11 |
14 files changed, 85 insertions, 34 deletions
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> ) |
