chore: localStorage -> tauri store

This commit is contained in:
Adam
2025-12-17 13:10:57 -06:00
parent 2a3a8a1ec2
commit 4a3ba58f65
16 changed files with 89 additions and 35 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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:",

View File

@@ -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"

View File

@@ -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 =>

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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({

View File

@@ -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)),

View File

@@ -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() {

View File

@@ -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]
}

View File

@@ -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",

View File

@@ -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()

View File

@@ -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)
})
})
}

View File

@@ -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>
)