refactor(app): add suspense support to persisted

This commit is contained in:
Brendan Allan 2026-06-29 08:51:29 +08:00
commit 6910eee7fb
No known key found for this signature in database
GPG key ID: 41E835AEA046A32E
28 changed files with 256 additions and 256 deletions

View file

@ -79,7 +79,7 @@ const SessionRoute = () => {
return (
<Show when={tabs.ready()}>
{(_) => {
const persisted = tabs.store.filter((item) => item.type === "session")
const persisted = tabs.store().filter((item) => item.type === "session")
return <Navigate href={sessionHref(legacySessionServer(persisted, sessionID, server.key), sessionID)} />
}}
</Show>
@ -211,7 +211,7 @@ function DraftRoute() {
return (
<Show when={tabs.ready()}>
<Show
when={tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)}
when={tabs.store().find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)}
keyed
fallback={<Navigate href="/" />}
>
@ -620,7 +620,7 @@ function LegacyTargetSessionRoute() {
<Navigate
href={sessionHref(
legacySessionServer(
tabs.store.filter((item) => item.type === "session"),
tabs.store().filter((item) => item.type === "session"),
params.id,
server.key,
),

View file

@ -11,6 +11,7 @@ import {
createResource,
Switch,
Match,
type Accessor,
type JSX,
} from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
@ -114,21 +115,21 @@ export type PromptInputControls = {
export function createPromptInputHistory(): PromptInputHistory {
const [normal, setNormal] = createStore<PromptHistoryState>({ entries: [] })
const [shell, setShell] = createStore<PromptHistoryState>({ entries: [] })
return createPromptInputHistoryStore(normal, setNormal, shell, setShell)
return createPromptInputHistoryStore(() => normal, setNormal, () => shell, setShell)
}
type PromptHistoryState = { entries: PromptHistoryStoredEntry[] }
function createPromptInputHistoryStore(
normal: Store<PromptHistoryState>,
normal: Accessor<PromptHistoryState>,
setNormal: SetStoreFunction<PromptHistoryState>,
shell: Store<PromptHistoryState>,
shell: Accessor<PromptHistoryState>,
setShell: SetStoreFunction<PromptHistoryState>,
): PromptInputHistory {
return {
entries: (mode) => (mode === "shell" ? shell.entries : normal.entries),
entries: (mode) => (mode === "shell" ? shell().entries : normal().entries),
add(prompt, mode, comments) {
const current = mode === "shell" ? shell : normal
const current = mode === "shell" ? shell() : normal()
const setCurrent = mode === "shell" ? setShell : setNormal
const next = prependHistoryEntry(current.entries, prompt, comments)
if (next === current.entries) return

View file

@ -228,7 +228,7 @@ export function SessionHeader() {
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
const current = createMemo(
() =>
options().find((o) => o.id === prefs.app) ??
options().find((o) => o.id === prefs().app) ??
options()[0] ??
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
)

View file

@ -268,10 +268,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const matchRoute = (route: LayoutRoute) => {
if (route.type === "home") return
if (route.type === "draft") {
return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID)
return tabsStore().find((item) => item.type === "draft" && item.draftID === route.draftID)
}
if (route.type === "session") {
const main = tabsStore.find(
const main = tabsStore().find(
(item) =>
item.type === "session" && item.server === route.server && item.sessionId === route.sessionId,
)
@ -279,7 +279,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const s = session()
if (s?.parentID) {
const parentID = s.parentID
const parent = tabsStore.find(
const parent = tabsStore().find(
(item) => item.type === "session" && item.server === route.server && item.sessionId === parentID,
)
if (parent) return parent
@ -373,7 +373,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: "mod+w",
hidden: true,
onSelect: () => {
tabsStoreActions.removeTab(tabsStore.findIndex((tab) => current === tab))
tabsStoreActions.removeTab(tabsStore().findIndex((tab) => current === tab))
},
},
{
@ -383,13 +383,13 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab())
let index = tabsStore().findIndex((tab) => tab === currentTab())
if (index === -1) return
index -= 1
if (index === -1) index = tabsStore.length - 1
if (index === -1) index = tabsStore().length - 1
const next = tabsStore[index]
const next = tabsStore()[index]
if (next) tabs.select(next)
},
},
@ -400,13 +400,13 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: `mod+option+ArrowRight,ctrl+tab`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab())
let index = tabsStore().findIndex((tab) => tab === currentTab())
if (index === -1) return
index += 1
if (index === tabsStore.length) index = 0
if (index === tabsStore().length) index = 0
const next = tabsStore[index]
const next = tabsStore()[index]
if (next) tabs.select(next)
},
},
@ -453,7 +453,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
</TooltipV2>
<TitlebarTabStrip
tabs={tabsStore}
tabs={tabsStore()}
currentTab={currentTab}
activeServerKey={server.key}
forceTruncate={tabsAreOverflowing()}
@ -463,7 +463,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
el?.scrollIntoView({ behavior: "instant" })
}}
onClose={(tab) => {
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
const index = tabsStore().findIndex((item) => tabKey(item) === tabKey(tab))
if (index !== -1) tabsStoreActions.removeTab(index)
}}
onReorder={(keys) => tabsStoreActions.reorder(keys)}

View file

@ -253,8 +253,8 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
const warnedDuplicates = new Set<string>()
type CommandCatalog = Record<string, CommandCatalogItem>
const [catalog, setCatalog, _, catalogReady] = persisted(
Persist.global("command.catalog.v1"),
const [catalog, setCatalog, catalogReady] = persisted(
Persist.global("command.catalog().v1"),
createStore<CommandCatalog>({}),
)
@ -416,7 +416,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
const keybindConfig = (id: string) => {
if (id === PALETTE_ID) return settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
const base = actionId(id)
return options().find((x) => actionId(x.id) === base)?.keybind ?? bind(base, catalog[base]?.keybind)
return options().find((x) => actionId(x.id) === base)?.keybind ?? bind(base, catalog()[base]?.keybind)
}
return {

View file

@ -173,13 +173,13 @@ export function createCommentSessionForTest(comments: Record<string, LineComment
function createCommentSession(scope: ServerScope, dir: string, id: string | undefined) {
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
Persist.serverScoped(scope, dir, id, "comments", [legacy]),
createStore<CommentStore>({
comments: {},
}),
)
const session = createCommentSessionState(store, setStore)
const session = createCommentSessionState(store(), setStore)
return {
ready,

View file

@ -37,7 +37,7 @@ function equalSelectedLines(a: SelectedLineRange | null | undefined, b: Selected
function createViewSession(scope: ServerScope, dir: string, id: string | undefined) {
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
const [view, setView, _, ready] = persisted(
const [view, setView, ready] = persisted(
Persist.serverScoped(scope, dir, id, "file-view", [legacyViewKey]),
createStore<{
file: Record<string, FileViewState>
@ -49,7 +49,7 @@ function createViewSession(scope: ServerScope, dir: string, id: string | undefin
const meta = { pruned: false }
const pruneView = (keep?: string) => {
const keys = Object.keys(view.file)
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)
@ -71,9 +71,9 @@ function createViewSession(scope: ServerScope, dir: string, id: string | undefin
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 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(

View file

@ -9,9 +9,8 @@ import { ServerScope } from "@/utils/server-scope"
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
const querySingles: Array<() => { queryKey?: unknown[]; enabled?: boolean }> = []
const persist: typeof import("@/utils/persist").persisted = (_target, store) => [
store[0],
() => store[0],
store[1],
null,
Object.assign(() => true, { promise: undefined }),
]

View file

@ -1,4 +1,4 @@
import { createRoot, createSignal, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createRoot, createSignal, getOwner, onCleanup, runWithOwner, type Accessor, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
@ -157,7 +157,7 @@ export function createChildStoreManager(input: {
)
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
const vcsStore = vcs[0]
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[2] })
const meta = runWithOwner(input.owner, () =>
input.persist(
@ -166,7 +166,7 @@ export function createChildStoreManager(input: {
),
)
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] })
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[2] })
const icon = runWithOwner(input.owner, () =>
input.persist(
@ -175,12 +175,12 @@ export function createChildStoreManager(input: {
),
)
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] })
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[2] })
const init = () =>
createRoot((dispose) => {
const initialMeta = meta[0].value
const initialIcon = icon[0].value
const initialMeta = meta[0]().value
const initialIcon = icon[0]().value
const [mcpEnabled, setMcpEnabled] = createSignal(false)
const pathQuery = useQuery(() => input.queryOptions.path(key))
@ -233,7 +233,7 @@ export function createChildStoreManager(input: {
get lsp() {
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
},
vcs: vcsStore.value,
vcs: vcsStore().value,
limit: 5,
message: {},
part: {},
@ -243,28 +243,28 @@ export function createChildStoreManager(input: {
disposers.set(key, dispose)
mcpToggles.set(key, setMcpEnabled)
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
if (!(init instanceof Promise)) return
void init.then(() => {
const onPersistedInit = (ready: Accessor<boolean> & { promise: undefined | Promise<any> }, run: () => void) => {
if (!ready.promise) return
void ready.promise.then(() => {
if (children[key] !== child) return
run()
})
}
onPersistedInit(vcs[2], () => {
const cached = vcsStore.value
const cached = vcsStore().value
if (!cached?.branch) return
child[1]("vcs", (value) => value ?? cached)
})
onPersistedInit(meta[2], () => {
if (child[0].projectMeta !== initialMeta) return
child[1]("projectMeta", meta[0].value)
child[1]("projectMeta", meta[0]().value)
})
onPersistedInit(icon[2], () => {
if (child[0].icon !== initialIcon) return
child[1]("icon", icon[0].value)
child[1]("icon", icon[0]().value)
})
})

View file

@ -549,7 +549,7 @@ describe("applyDirectoryEvent", () => {
directory: "/tmp",
loadLsp() {},
vcsCache: {
store: cacheStore,
store: () => cacheStore,
setStore: setCacheStore,
ready: () => true,
},

View file

@ -17,7 +17,7 @@ import type {
} from "@opencode-ai/sdk/v2/client"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import type { Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
import type { SetStoreFunction } from "solid-js/store"
export type ProjectMeta = {
name?: string
@ -79,19 +79,19 @@ export type State = {
}
export type VcsCache = {
store: Store<{ value: VcsInfo | undefined }>
store: Accessor<{ value: VcsInfo | undefined }>
setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
ready: Accessor<boolean>
}
export type MetaCache = {
store: Store<{ value: ProjectMeta | undefined }>
store: Accessor<{ value: ProjectMeta | undefined }>
setStore: SetStoreFunction<{ value: ProjectMeta | undefined }>
ready: Accessor<boolean>
}
export type IconCache = {
store: Store<{ value: string | undefined }>
store: Accessor<{ value: string | undefined }>
setStore: SetStoreFunction<{ value: string | undefined }>
ready: Accessor<boolean>
}

View file

@ -144,7 +144,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
const platform = usePlatform()
const dialog = useDialog()
const settings = useSettings()
const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
const [store, setStore, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
const [range, setRange] = createStore({
from: undefined as string | undefined,
@ -208,7 +208,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
if (!platform.version) return
state.started = true
const previous = store.version
const previous = store().version
if (!previous) {
setStore("version", platform.version)
return
@ -225,7 +225,7 @@ export const { use: useHighlights, provider: HighlightsProvider } = createSimple
from: () => range.from,
to: () => range.to,
get last() {
return store.version
return store().version
},
markSeen,
}

View file

@ -201,14 +201,14 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
gate: false,
init: (props: { locale?: Locale }) => {
const initial = props.locale ?? readStoredLocale() ?? detectLocale()
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
Persist.global("language", ["language.v1"]),
createStore({
locale: initial,
}),
)
const locale = createMemo<Locale>(() => normalizeLocale(store.locale))
const locale = createMemo<Locale>(() => normalizeLocale(store().locale))
const intl = createMemo(() => INTL[locale()])
const [dict] = createResource(locale, loadDict, {

View file

@ -168,7 +168,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
if (value.type === "home") return value
if (value.server) return value
if (value.type === "draft") {
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === value.draftID)
const draft = tabs.store().find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === value.draftID)
if (draft) return { ...value, server: draft.server }
}
return { ...value, server: server.key }
@ -264,7 +264,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
const target = Persist.serverGlobal(serverSdk().scope, "layout", ["layout.v6"])
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
{ ...target, migrate },
createStore({
sidebar: {
@ -343,8 +343,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
keep,
max: MAX_SESSION_KEYS,
used: usage.used,
view: Object.keys(store.sessionView),
tabs: Object.keys(store.sessionTabs),
view: Object.keys(store().sessionView),
tabs: Object.keys(store().sessionTabs),
})
if (drop.length === 0) return
@ -378,9 +378,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const scroll = createScrollPersistence({
debounceMs: 250,
getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll,
getSnapshot: (sessionKey) => store().sessionView[sessionKey]?.scroll,
onFlush: (sessionKey, next) => {
const current = store.sessionView[sessionKey]
const current = store().sessionView[sessionKey]
const keep = usage.active ?? sessionKey
if (!current) {
setStore("sessionView", sessionKey, { scroll: next })
@ -590,18 +590,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
route,
ready,
home: {
selection: createMemo(() => store.home.selection),
selection: createMemo(() => store().home.selection),
setSelection(selection: HomeProjectSelection) {
setStore("home", "selection", reconcile(selection))
},
},
handoff: {
tabs: createMemo(() => store.handoff?.tabs),
tabs: createMemo(() => store().handoff?.tabs),
setTabs(dir: string, id: string) {
setStore("handoff", "tabs", { scope: serverSdk().scope, dir, id, at: Date.now() })
},
clearTabs() {
if (!store.handoff?.tabs) return
if (!store().handoff?.tabs) return
setStore("handoff", "tabs", undefined)
},
},
@ -627,7 +627,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
},
sidebar: {
opened: createMemo(() => store.sidebar.opened),
opened: createMemo(() => store().sidebar.opened),
open() {
setStore("sidebar", "opened", true)
},
@ -637,31 +637,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
toggle() {
setStore("sidebar", "opened", (x) => !x)
},
width: createMemo(() => store.sidebar.width),
width: createMemo(() => store().sidebar.width),
resize(width: number) {
setStore("sidebar", "width", width)
},
workspaces(directory: string) {
return () => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
return () => store().sidebar.workspaces[directory] ?? store().sidebar.workspacesDefault ?? false
},
setWorkspaces(directory: string, value: boolean) {
setStore("sidebar", "workspaces", directory, value)
},
toggleWorkspaces(directory: string) {
const current = store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
const current = store().sidebar.workspaces[directory] ?? store().sidebar.workspacesDefault ?? false
setStore("sidebar", "workspaces", directory, !current)
},
},
terminal: {
height: createMemo(() => store.terminal.height),
height: createMemo(() => store().terminal.height),
resize(height: number) {
setStore("terminal", "height", height)
},
},
review: {
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
diffStyle: createMemo(() => store().review?.diffStyle ?? "split"),
setDiffStyle(diffStyle: ReviewDiffStyle) {
if (!store.review) {
if (!store().review) {
setStore("review", { diffStyle, panelOpened: true })
return
}
@ -669,39 +669,39 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
},
fileTree: {
opened: createMemo(() => store.fileTree?.opened ?? true),
width: createMemo(() => store.fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH),
tab: createMemo(() => store.fileTree?.tab ?? "changes"),
opened: createMemo(() => store().fileTree?.opened ?? true),
width: createMemo(() => store().fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH),
tab: createMemo(() => store().fileTree?.tab ?? "changes"),
setTab(tab: "changes" | "all") {
if (!store.fileTree) {
if (!store().fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab })
return
}
setStore("fileTree", "tab", tab)
},
open() {
if (!store.fileTree) {
if (!store().fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return
}
setStore("fileTree", "opened", true)
},
close() {
if (!store.fileTree) {
if (!store().fileTree) {
setStore("fileTree", { opened: false, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return
}
setStore("fileTree", "opened", false)
},
toggle() {
if (!store.fileTree) {
if (!store().fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return
}
setStore("fileTree", "opened", (x) => !x)
},
resize(width: number) {
if (!store.fileTree) {
if (!store().fileTree) {
setStore("fileTree", { opened: true, width, tab: "changes" })
return
}
@ -709,9 +709,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
},
session: {
width: createMemo(() => store.session?.width ?? DEFAULT_SESSION_WIDTH),
width: createMemo(() => store().session?.width ?? DEFAULT_SESSION_WIDTH),
resize(width: number) {
if (!store.session) {
if (!store().session) {
setStore("session", { width })
return
}
@ -719,7 +719,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
},
mobileSidebar: {
opened: createMemo(() => store.mobileSidebar?.opened ?? false),
opened: createMemo(() => store().mobileSidebar?.opened ?? false),
show() {
setStore("mobileSidebar", "opened", true)
},
@ -734,7 +734,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
set(sessionKey: string, messageID: string) {
const at = Date.now()
touch(sessionKey)
const current = store.sessionView[sessionKey]
const current = store().sessionView[sessionKey]
if (!current) {
setStore("sessionView", sessionKey, {
scroll: {},
@ -755,7 +755,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
)
},
consume(sessionKey: string) {
const current = store.sessionView[sessionKey]
const current = store().sessionView[sessionKey]
const message = current?.pendingMessage
const at = current?.pendingMessageAt
if (!message || !at) return
@ -775,12 +775,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
view(sessionKey: string | Accessor<string>) {
const key = createSessionKeyReader(sessionKey, ensureKey)
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
const s = createMemo(() => store().sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store().terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store().review?.panelOpened ?? true)
function setTerminalOpened(next: boolean) {
const current = store.terminal
const current = store().terminal
if (!current) {
setStore("terminal", { height: DEFAULT_TERMINAL_HEIGHT, opened: next })
return
@ -792,7 +792,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
function setReviewPanelOpened(next: boolean) {
const current = store.review
const current = store().review
if (!current) {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
return
@ -814,7 +814,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
get: () => s().todoCollapsed ?? false,
set(collapsed: boolean) {
const session = key()
const current = store.sessionView[session]
const current = store().sessionView[session]
if (!current) {
setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed })
} else {
@ -851,7 +851,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setOpen(open: string[]) {
const session = key()
const next = Array.from(new Set(open))
const current = store.sessionView[session]
const current = store().sessionView[session]
if (!current) {
setStore("sessionView", session, {
scroll: {},
@ -865,7 +865,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
openPath(path: string) {
const session = key()
const current = store.sessionView[session]
const current = store().sessionView[session]
if (!current) {
setStore("sessionView", session, {
scroll: {},
@ -884,7 +884,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
closePath(path: string) {
const session = key()
const current = store.sessionView[session]?.reviewOpen
const current = store().sessionView[session]?.reviewOpen
if (!current) return
const index = current.indexOf(path)
@ -901,7 +901,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
togglePath(path: string) {
const session = key()
const current = store.sessionView[session]?.reviewOpen
const current = store().sessionView[session]?.reviewOpen
if (!current || !current.includes(path)) {
this.openPath(path)
return
@ -915,7 +915,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
tabs(sessionKey: string | Accessor<string>) {
const key = createSessionKeyReader(sessionKey, ensureKey)
const path = createMemo(() => sessionPath(key()))
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
const tabs = createMemo(() => store().sessionTabs[key()] ?? { all: [] })
const normalize = (tab: string) => normalizeSessionTab(path(), tab)
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
return {
@ -925,7 +925,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setActive(tab: string | undefined) {
const session = key()
const next = tab ? normalize(tab) : tab
if (!store.sessionTabs[session]) {
if (!store().sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: next })
} else {
setStore("sessionTabs", session, "active", next)
@ -934,7 +934,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setAll(all: string[]) {
const session = key()
const next = normalizeAll(all).filter((tab) => tab !== "review")
if (!store.sessionTabs[session]) {
if (!store().sessionTabs[session]) {
setStore("sessionTabs", session, { all: next, active: undefined })
} else {
setStore("sessionTabs", session, "all", next)
@ -942,12 +942,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
async open(tab: string) {
const session = key()
const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab))
const next = nextSessionTabsForOpen(store().sessionTabs[session], normalize(tab))
setStore("sessionTabs", session, next)
},
close(tab: string) {
const session = key()
const current = store.sessionTabs[session]
const current = store().sessionTabs[session]
if (!current) return
if (tab === "review") {
@ -971,7 +971,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
move(tab: string, to: number) {
const session = key()
const current = store.sessionTabs[session]
const current = store().sessionTabs[session]
if (!current) return
const index = current.all.findIndex((f) => f === tab)
if (index === -1) return

View file

@ -124,7 +124,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const scope = createMemo<State | undefined>(() => {
const session = id()
if (!session) return store.draft
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
return saved().session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
})
createEffect(() => {
@ -134,7 +134,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const key = handoffKey(serverSDK().scope, sdk().directory, session)
const next = handoff.get(key)
if (!next) return
if (saved.session[session] !== undefined) {
if (saved().session[session] !== undefined) {
handoff.delete(key)
return
}
@ -392,7 +392,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const session = id()
if (!session) return
if (msg.sessionID !== session) return
if (saved.session[session] !== undefined) return
if (saved().session[session] !== undefined) return
if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return
setSaved("session", session, {

View file

@ -28,7 +28,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
init: (props: { directory?: Accessor<string | undefined> } = {}) => {
const providers = useProviders(props.directory)
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
Persist.global("model", ["model.v1"]),
createStore<Store>({
user: [],
@ -89,7 +89,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
const visibility = createMemo(() => {
const map = new Map<string, Visibility>()
for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility)
for (const item of store().user) map.set(`${item.providerID}:${item.modelID}`, item.visibility)
return map
})
@ -104,12 +104,12 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID)
function update(model: ModelKey, state: Visibility) {
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
const index = store().user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
if (index >= 0) {
setStore("user", index, (current) => ({ ...current, visibility: state }))
return
}
setStore("user", store.user.length, { ...model, visibility: state })
setStore("user", store().user.length, { ...model, visibility: state })
}
const visible = (model: ModelKey) => {
@ -128,17 +128,17 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
}
const push = (model: ModelKey) => {
const uniq = uniqueBy([model, ...store.recent], (x) => `${x.providerID}:${x.modelID}`)
const uniq = uniqueBy([model, ...store().recent], (x) => `${x.providerID}:${x.modelID}`)
if (uniq.length > RECENT_LIMIT) uniq.pop()
setStore("recent", uniq)
}
const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}`
const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)]
const getVariant = (model: ModelKey) => store().variant?.[variantKey(model)]
const setVariant = (model: ModelKey, value: string | undefined) => {
const key = variantKey(model)
if (!store.variant) {
if (!store().variant) {
setStore("variant", { [key]: value })
return
}
@ -147,7 +147,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
const [recentModels] = createResource(
async () => {
const recent = store.recent
const recent = store().recent
await ready.promise
return recent
},

View file

@ -127,7 +127,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const activeServer = createMemo(() => {
if (params.serverKey) return requireServerKey(params.serverKey)
if (search.draftId) {
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)
const draft = tabs.store().find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)
if (draft) return draft.server
}
return server.key
@ -222,13 +222,13 @@ function createServerNotificationState(input: {
const currentDirectory = input.directory
const currentSession = input.sessionID
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
createStore({
list: [] as Notification[],
}),
)
const [index, setIndex] = createStore<NotificationIndex>(buildNotificationIndex(store.list))
const [index, setIndex] = createStore<NotificationIndex>(buildNotificationIndex(store().list))
const meta = { pruned: false, disposed: false }
@ -285,7 +285,7 @@ function createServerNotificationState(input: {
if (!ready()) return
if (meta.pruned) return
meta.pruned = true
const list = pruneNotifications(store.list)
const list = pruneNotifications(store().list)
batch(() => {
setStore("list", list)
setIndex(reconcile(buildNotificationIndex(list), { merge: false }))
@ -293,9 +293,9 @@ function createServerNotificationState(input: {
})
const append = (notification: Notification) => {
const list = pruneNotifications([...store.list, notification])
const list = pruneNotifications([...store().list, notification])
const keep = new Set(list)
const removed = store.list.filter((n) => !keep.has(n))
const removed = store().list.filter((n) => !keep.has(n))
batch(() => {
if (keep.has(notification)) appendToIndex(notification)

View file

@ -59,7 +59,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
return hasPermissionPromptRules(store.config.permission)
})
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
{
...Persist.serverGlobal(serverSDK().scope, "permission", ["permission.v3"]),
migrate(value) {
@ -91,7 +91,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
const perm = childStore.config.permission
if (typeof perm === "string" && perm === "allow") {
const key = directoryAcceptKey(directory)
if (store.autoAccept[key] === undefined) {
if (store().autoAccept[key] === undefined) {
setStore(
produce((draft) => {
draft.autoAccept[key] = true
@ -143,16 +143,16 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
function isAutoAccepting(sessionID: string, directory?: string) {
const session = directory ? serverSync().child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
return autoRespondsPermission(store().autoAccept, session, { sessionID }, directory)
}
function isAutoAcceptingDirectory(directory: string) {
return isDirectoryAutoAccepting(store.autoAccept, directory)
return isDirectoryAutoAccepting(store().autoAccept, directory)
}
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
const session = directory ? serverSync().child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, permission, directory)
return autoRespondsPermission(store().autoAccept, session, permission, directory)
}
function bumpEnableVersion(sessionID: string, directory?: string) {

View file

@ -195,12 +195,12 @@ function promptTarget(serverScope: ServerScope, scope: Scope) {
}
export function createPromptSession(serverScope: ServerScope, scope: Scope) {
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
promptTarget(serverScope, scope),
createStore<PromptStore>(promptStore()),
)
return { ready, ...createPromptStateValue(store, setStore) }
return { ready, ...createPromptStateValue(store(), setStore) }
}
export function createPromptReady(session: Accessor<PromptSession>) {
@ -316,7 +316,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
const scope = () =>
search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id }
const load = (scope: Scope) => {
const current = settings.general.newLayoutDesigns() ? selectPromptTab(tabs.store, scope, serverKey()) : undefined
const current = settings.general.newLayoutDesigns() ? selectPromptTab(tabs.store(), scope, serverKey()) : undefined
if (current) {
return createTabPromptState(tabs, current, serverSDK().scope, scope)
}

View file

@ -226,7 +226,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
}) => {
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
{
...Persist.global("server", ["server.v3"]),
migrate: (value) => migrateCanonicalLocalServerState(value, props.canonicalLocalServer),
@ -241,7 +241,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
const allServers = createMemo((): Array<ServerConnection.Any> => {
return resolveServerList({ stored: store.list, props: props.servers })
return resolveServerList({ stored: store().list, props: props.servers })
})
const [state, setState] = createStore({
@ -257,11 +257,11 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
if (!url_) return
const conn: ServerConnection.Http = { ...input, authToken: undefined, http: { ...input.http, url: url_ } }
return batch(() => {
const existing = store.list.findIndex((x) => url(x) === url_)
const existing = store().list.findIndex((x) => url(x) === url_)
if (existing !== -1) {
setStore("list", existing, conn)
} else {
setStore("list", store.list.length, conn)
setStore("list", store().list.length, conn)
}
setState("active", ServerConnection.key(conn))
return conn
@ -270,7 +270,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
function remove(key: ServerConnection.Key) {
const next = nextServerAfterRemoval(allServers(), key, props.defaultServer)
const list = store.list.filter((x) => url(x) !== key)
const list = store().list.filter((x) => url(x) !== key)
batch(() => {
setStore("list", list)
if (state.active === key) setState("active", next)
@ -280,12 +280,12 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const isReady = createMemo(() => ready() && !!state.active)
const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer)
const projects = createServerProjects({ scope, store, setStore })
const projects = createServerProjects({ scope, store: store(), setStore })
const projectStores = new Map<ServerConnection.Key, ReturnType<typeof createServerProjects>>()
const projectsForServer = (key: ServerConnection.Key) => {
const existing = projectStores.get(key)
if (existing) return existing
const next = createServerProjects({ scope: () => scope(key), store, setStore })
const next = createServerProjects({ scope: () => scope(key), store: store(), setStore })
projectStores.set(key, next)
return next
}

View file

@ -152,45 +152,45 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
name: "Settings",
gate: false,
init: () => {
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree)
const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch)
const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus)
const [store, setStore, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
const showFileTree = withFallback(() => store().general?.showFileTree, defaultSettings.general.showFileTree)
const showSearch = withFallback(() => store().general?.showSearch, defaultSettings.general.showSearch)
const showStatus = withFallback(() => store().general?.showStatus, defaultSettings.general.showStatus)
const showCustomAgents = withFallback(
() => store.general?.showCustomAgents,
() => store().general?.showCustomAgents,
defaultSettings.general.showCustomAgents,
)
const newLayoutDesigns = withFallback(() => store.general?.newLayoutDesigns, newLayoutDesignsDefault)
const newLayoutDesigns = withFallback(() => store().general?.newLayoutDesigns, newLayoutDesignsDefault)
const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference())
createEffect(() => {
if (typeof document === "undefined") return
const root = document.documentElement
root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.mono))
root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans))
root.style.setProperty("--font-family-mono", monoFontFamily(store().appearance?.mono))
root.style.setProperty("--font-family-sans", sansFontFamily(store().appearance?.sans))
})
createEffect(() => {
if (store.general?.followup !== "queue") return
if (store().general?.followup !== "queue") return
setStore("general", "followup", "steer")
})
return {
ready,
get current() {
return store
return store()
},
general: {
autoSave: withFallback(() => store.general?.autoSave, defaultSettings.general.autoSave),
autoSave: withFallback(() => store().general?.autoSave, defaultSettings.general.autoSave),
setAutoSave(value: boolean) {
setStore("general", "autoSave", value)
},
releaseNotes: withFallback(() => store.general?.releaseNotes, defaultSettings.general.releaseNotes),
releaseNotes: withFallback(() => store().general?.releaseNotes, defaultSettings.general.releaseNotes),
setReleaseNotes(value: boolean) {
setStore("general", "releaseNotes", value)
},
followup: withFallback(
() => (store.general?.followup === "queue" ? "steer" : store.general?.followup),
() => (store().general?.followup === "queue" ? "steer" : store().general?.followup),
defaultSettings.general.followup,
),
setFollowup(value: "queue" | "steer") {
@ -200,7 +200,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setShowFileTree(value: boolean) {
setStore("general", "showFileTree", value)
},
showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
showNavigation: withFallback(() => store().general?.showNavigation, defaultSettings.general.showNavigation),
setShowNavigation(value: boolean) {
setStore("general", "showNavigation", value)
},
@ -212,26 +212,26 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setShowStatus(value: boolean) {
setStore("general", "showStatus", value)
},
showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
showTerminal: withFallback(() => store().general?.showTerminal, defaultSettings.general.showTerminal),
setShowTerminal(value: boolean) {
setStore("general", "showTerminal", value)
},
showReasoningSummaries: withFallback(
() => store.general?.showReasoningSummaries,
() => store().general?.showReasoningSummaries,
defaultSettings.general.showReasoningSummaries,
),
setShowReasoningSummaries(value: boolean) {
setStore("general", "showReasoningSummaries", value)
},
shellToolPartsExpanded: withFallback(
() => store.general?.shellToolPartsExpanded,
() => store().general?.shellToolPartsExpanded,
defaultSettings.general.shellToolPartsExpanded,
),
setShellToolPartsExpanded(value: boolean) {
setStore("general", "shellToolPartsExpanded", value)
},
editToolPartsExpanded: withFallback(
() => store.general?.editToolPartsExpanded,
() => store().general?.editToolPartsExpanded,
defaultSettings.general.editToolPartsExpanded,
),
setEditToolPartsExpanded(value: boolean) {
@ -242,7 +242,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setStore("general", "showCustomAgents", value)
},
mobileTitlebarPosition: withFallback(
() => store.general?.mobileTitlebarPosition,
() => store().general?.mobileTitlebarPosition,
defaultSettings.general.mobileTitlebarPosition,
),
setMobileTitlebarPosition(value: "top" | "bottom") {
@ -260,25 +260,25 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
customAgents: visible(showCustomAgents),
},
appearance: {
fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
fontSize: withFallback(() => store().appearance?.fontSize, defaultSettings.appearance.fontSize),
setFontSize(value: number) {
setStore("appearance", "fontSize", value)
},
font: withFallback(() => store.appearance?.mono, defaultSettings.appearance.mono),
font: withFallback(() => store().appearance?.mono, defaultSettings.appearance.mono),
setFont(value: string) {
setStore("appearance", "mono", value.trim() ? value : "")
},
uiFont: withFallback(() => store.appearance?.sans, defaultSettings.appearance.sans),
uiFont: withFallback(() => store().appearance?.sans, defaultSettings.appearance.sans),
setUIFont(value: string) {
setStore("appearance", "sans", value.trim() ? value : "")
},
terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
terminalFont: withFallback(() => store().appearance?.terminal, defaultSettings.appearance.terminal),
setTerminalFont(value: string) {
setStore("appearance", "terminal", value.trim() ? value : "")
},
},
keybinds: {
get: (action: string) => store.keybinds?.[action],
get: (action: string) => store().keybinds?.[action],
set(action: string, keybind: string) {
setStore("keybinds", action, keybind)
},
@ -295,50 +295,50 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
},
},
permissions: {
autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove),
autoApprove: withFallback(() => store().permissions?.autoApprove, defaultSettings.permissions.autoApprove),
setAutoApprove(value: boolean) {
setStore("permissions", "autoApprove", value)
},
},
notifications: {
agent: withFallback(() => store.notifications?.agent, defaultSettings.notifications.agent),
agent: withFallback(() => store().notifications?.agent, defaultSettings.notifications.agent),
setAgent(value: boolean) {
setStore("notifications", "agent", value)
},
permissions: withFallback(() => store.notifications?.permissions, defaultSettings.notifications.permissions),
permissions: withFallback(() => store().notifications?.permissions, defaultSettings.notifications.permissions),
setPermissions(value: boolean) {
setStore("notifications", "permissions", value)
},
errors: withFallback(() => store.notifications?.errors, defaultSettings.notifications.errors),
errors: withFallback(() => store().notifications?.errors, defaultSettings.notifications.errors),
setErrors(value: boolean) {
setStore("notifications", "errors", value)
},
},
sounds: {
agentEnabled: withFallback(() => store.sounds?.agentEnabled, defaultSettings.sounds.agentEnabled),
agentEnabled: withFallback(() => store().sounds?.agentEnabled, defaultSettings.sounds.agentEnabled),
setAgentEnabled(value: boolean) {
setStore("sounds", "agentEnabled", value)
},
agent: withFallback(() => store.sounds?.agent, defaultSettings.sounds.agent),
agent: withFallback(() => store().sounds?.agent, defaultSettings.sounds.agent),
setAgent(value: string) {
setStore("sounds", "agent", value)
},
permissionsEnabled: withFallback(
() => store.sounds?.permissionsEnabled,
() => store().sounds?.permissionsEnabled,
defaultSettings.sounds.permissionsEnabled,
),
setPermissionsEnabled(value: boolean) {
setStore("sounds", "permissionsEnabled", value)
},
permissions: withFallback(() => store.sounds?.permissions, defaultSettings.sounds.permissions),
permissions: withFallback(() => store().sounds?.permissions, defaultSettings.sounds.permissions),
setPermissions(value: string) {
setStore("sounds", "permissions", value)
},
errorsEnabled: withFallback(() => store.sounds?.errorsEnabled, defaultSettings.sounds.errorsEnabled),
errorsEnabled: withFallback(() => store().sounds?.errorsEnabled, defaultSettings.sounds.errorsEnabled),
setErrorsEnabled(value: boolean) {
setStore("sounds", "errorsEnabled", value)
},
errors: withFallback(() => store.sounds?.errors, defaultSettings.sounds.errors),
errors: withFallback(() => store().sounds?.errors, defaultSettings.sounds.errors),
setErrors(value: string) {
setStore("sounds", "errors", value)
},

View file

@ -49,7 +49,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const server = useServer()
const platform = usePlatform()
const fallback = server.key
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
{
...Persist.global("tabs"),
migrate: (value: unknown) => {
@ -62,7 +62,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
createStore<Tab[]>([]),
)
const [recent, setRecent, , recentReady] = persisted(Persist.global("tabs.recent"), createStore<RecentTab>({}))
const [recent, setRecent, recentReady] = persisted(Persist.global("tabs.recent"), createStore<RecentTab>({}))
const params = useParams()
const navigate = useNavigate()
@ -73,7 +73,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
let recentWrite = 0
let recentValue: string | undefined
const recentKey = () => (recentWrite ? recentValue : recent.key)
const recentKey = () => (recentWrite ? recentValue : recent().key)
const setRecentKey = (key: string | undefined) => {
const write = ++recentWrite
@ -96,14 +96,14 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
createEffect(() => {
if (!ready() || !recentReady()) return
const servers = new Set(server.list.map(ServerConnection.key))
const next = store.filter((tab) => servers.has(tab.server))
if (next.length !== store.length) {
for (const tab of store) {
const next = store().filter((tab) => servers.has(tab.server))
if (next.length !== store().length) {
for (const tab of store()) {
if (!servers.has(tab.server)) memory.remove(tabKey(tab))
}
setStore(() => next)
}
if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined)
if (recent().key && !next.some((tab) => tabKey(tab) === recent().key)) setRecentKey(undefined)
})
const navigateTab = (tab: Tab) => {
@ -113,11 +113,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}
const removeTab = (index: number) => {
const tab = store[index]
const tab = store()[index]
if (!tab) return
const key = tabKey(tab)
const draftID = tab.type === "draft" ? tab.draftID : undefined
const nextTab = store[index + 1] ?? store[index - 1]
const nextTab = store()[index + 1] ?? store()[index - 1]
closing.add(key)
void startTransition(() => {
setStore(
@ -125,7 +125,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
tabs.splice(index, 1)
}),
)
if (recent.key === key) setRecentKey(nextTab && tabKey(nextTab))
if (recent().key === key) setRecentKey(nextTab && tabKey(nextTab))
if (nextTab) navigateTab(nextTab)
else navigate("/")
}).finally(() => closing.delete(key))
@ -136,7 +136,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const actions = {
addSessionTab: (tab: Omit<SessionTab, "type">) => {
const next = { type: "session" as const, ...tab }
const existing = store.find((item) => tabKey(item) === tabKey(next))
const existing = store().find((item) => tabKey(item) === tabKey(next))
if (existing) return existing
void startTransition(() => {
setStore(
@ -159,7 +159,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
)
},
draft(draftID: string) {
const tab = store.find((item) => item.type === "draft" && item.draftID === draftID)
const tab = store().find((item) => item.type === "draft" && item.draftID === draftID)
if (!tab || tab.type !== "draft") throw new Error(`Draft not found: ${draftID}`)
return tab
},
@ -184,7 +184,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
// Keep the replacement and navigation atomic so /new-session never renders
// after its backing draft tab has been removed from the store.
// after its backing draft tab has been removed from the store().
const active = location.pathname === "/new-session" && location.query.draftId === draftID
const next = { type: "session" as const, ...session }
void startTransition(() => {
@ -194,7 +194,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
if (index !== -1) tabs[index] = next
}),
)
if (recent.key === `draft:${draftID}`) setRecentKey(tabKey(next))
if (recent().key === `draft:${draftID}`) setRecentKey(tabKey(next))
if (active) navigateTab(next)
})
memory.remove(`draft:${draftID}`)
@ -202,23 +202,24 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
removeTab,
removeSessionTab(input: Omit<SessionTab, "type">) {
const index = store.findIndex(
const index = store().findIndex(
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
)
if (index !== -1) removeTab(index)
},
removeServer(key: ServerConnection.Key) {
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
const removed = store.filter((tab) => tab.server === key).map(tabKey)
const drafts = store().flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
const removed = store().filter((tab) => tab.server === key).map(tabKey)
setStore((tabs) => tabs.filter((tab) => tab.server !== key))
for (const key of removed) memory.remove(key)
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
const recentKey = recent().key
if (recentKey && removed.includes(recentKey)) setRecentKey(undefined)
for (const draftID of drafts) removeDraftPersisted(draftID)
if (server.key === key) navigate("/")
},
removeSessions: (input: SessionTabsRemovedDetail) => {
const targetServer = input.server ?? server.key
const removed = store
const removed = store()
.filter(
(tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
)
@ -262,7 +263,8 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
else navigate("/")
}),
)
if (recent.key && removed.includes(recent.key)) setRecentKey(undefined)
const recentKey = recent().key
if (recentKey && removed.includes(recentKey)) setRecentKey(undefined)
})
for (const key of removed) memory.remove(key)
},
@ -273,7 +275,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
toggleHome(input: { home: boolean; current?: Tab }) {
if (input.home) {
const tab = store.find((tab) => tabKey(tab) === recentKey())
const tab = store().find((tab) => tabKey(tab) === recentKey())
if (tab) navigateTab(tab)
return
}

View file

@ -151,7 +151,7 @@ function createWorkspaceTerminalSession(
) {
const legacy = scope === ServerScope.local ? getLegacyTerminalStorageKeys(dir, legacySessionID) : []
const [store, setStore, _, ready] = persisted(
const [store, setStore, ready] = persisted(
{
...terminalPersistTarget(scope, dir, legacy),
migrate: migrateTerminalState,
@ -166,7 +166,7 @@ function createWorkspaceTerminalSession(
const pickNextTerminalNumber = () => {
const existingTitleNumbers = new Set(
store.all.flatMap((pty) => {
store().all.flatMap((pty) => {
const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
if (direct !== undefined) return [direct]
const parsed = numberFromTitle(pty.title)
@ -183,10 +183,10 @@ function createWorkspaceTerminalSession(
}
const removeExited = (id: string) => {
const all = store.all
const all = store().all
const index = all.findIndex((x) => x.id === id)
if (index === -1) return
const active = store.active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store.active
const active = store().active === id ? (index === 0 ? all[1]?.id : all[0]?.id) : store().active
batch(() => {
setStore("active", active)
setStore(
@ -204,8 +204,8 @@ function createWorkspaceTerminalSession(
onCleanup(unsub)
const update = (client: DirectorySDK["client"], pty: Partial<LocalPTY> & { id: string }) => {
const index = store.all.findIndex((x) => x.id === pty.id)
const previous = index >= 0 ? store.all[index] : undefined
const index = store().all.findIndex((x) => x.id === pty.id)
const previous = index >= 0 ? store().all[index] : undefined
if (index >= 0) {
setStore("all", index, (item) => ({ ...item, ...pty }))
}
@ -217,7 +217,7 @@ function createWorkspaceTerminalSession(
})
.catch((error: unknown) => {
if (previous) {
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
const currentIndex = store().all.findIndex((item) => item.id === pty.id)
if (currentIndex >= 0) setStore("all", currentIndex, previous)
}
console.error("Failed to update terminal", error)
@ -225,8 +225,8 @@ function createWorkspaceTerminalSession(
}
const clone = async (client: DirectorySDK["client"], id: string) => {
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
const index = store().all.findIndex((x) => x.id === id)
const pty = store().all[index]
if (!pty) return
const next = await client.pty
.create({
@ -238,7 +238,7 @@ function createWorkspaceTerminalSession(
})
if (!next?.data) return
const active = store.active === pty.id
const active = store().active === pty.id
batch(() => {
setStore("all", index, {
@ -259,8 +259,8 @@ function createWorkspaceTerminalSession(
return {
ready,
all: createMemo(() => store.all),
active: createMemo(() => store.active),
all: createMemo(() => store().all),
active: createMemo(() => store().active),
clear() {
batch(() => {
setStore("active", undefined)
@ -280,7 +280,7 @@ function createWorkspaceTerminalSession(
title: pty.data?.title ?? defaultTitle(nextNumber),
titleNumber: nextNumber,
}
setStore("all", store.all.length, newTerminal)
setStore("all", store().all.length, newTerminal)
setStore("active", id)
})
.catch((error: unknown) => {
@ -291,7 +291,7 @@ function createWorkspaceTerminalSession(
update(sdk.client, pty)
},
trim(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const index = store().all.findIndex((x) => x.id === id)
if (index === -1) return
setStore("all", index, (pty) => trimTerminal(pty))
},
@ -309,7 +309,7 @@ function createWorkspaceTerminalSession(
const client = sdk.client
return {
trim(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const index = store().all.findIndex((x) => x.id === id)
if (index === -1) return
setStore("all", index, (pty) => trimTerminal(pty))
},
@ -325,23 +325,23 @@ function createWorkspaceTerminalSession(
setStore("active", id)
},
next() {
const index = store.all.findIndex((x) => x.id === store.active)
const index = store().all.findIndex((x) => x.id === store().active)
if (index === -1) return
const nextIndex = (index + 1) % store.all.length
setStore("active", store.all[nextIndex]?.id)
const nextIndex = (index + 1) % store().all.length
setStore("active", store().all[nextIndex]?.id)
},
previous() {
const index = store.all.findIndex((x) => x.id === store.active)
const index = store().all.findIndex((x) => x.id === store().active)
if (index === -1) return
const prevIndex = index === 0 ? store.all.length - 1 : index - 1
setStore("active", store.all[prevIndex]?.id)
const prevIndex = index === 0 ? store().all.length - 1 : index - 1
setStore("active", store().all[prevIndex]?.id)
},
async close(id: string) {
const index = store.all.findIndex((f) => f.id === id)
const index = store().all.findIndex((f) => f.id === id)
if (index !== -1) {
batch(() => {
if (store.active === id) {
const next = index > 0 ? store.all[index - 1]?.id : store.all[1]?.id
if (store().active === id) {
const next = index > 0 ? store().all[index - 1]?.id : store().all[1]?.id
setStore("active", next)
}
setStore(
@ -358,7 +358,7 @@ function createWorkspaceTerminalSession(
})
},
move(id: string, to: number) {
const index = store.all.findIndex((f) => f.id === id)
const index = store().all.findIndex((f) => f.id === id)
if (index === -1) return
setStore(
"all",

View file

@ -528,14 +528,14 @@ function HomeProjectColumn(props: {
const global = useGlobal()
const dialog = useDialog()
const controller = useServerManagementController({ navigateOnAdd: false })
const [_state, setState, _, ready] = persisted(
const [_state, setState, ready] = persisted(
Persist.global("home.servers", ["home.servers.v1"]),
createStore({ collapsed: {} as Record<string, boolean> }),
)
const [state] = createResource(
() => ready.promise ?? Promise.resolve(),
(p) => p.then(() => _state),
{ initialValue: _state },
(p) => p.then(() => _state()),
{ initialValue: _state() },
)
return (
@ -856,7 +856,7 @@ function HomeSessionLeading(props: {
activeServer: boolean
}) {
const tabs = useTabs()
const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store, props.server, props.session))
const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store(), props.server, props.session))
return (
<div class="relative shrink-0">
<Show when={hasOpenTab()}>

View file

@ -85,7 +85,7 @@ import { SidebarContent } from "./layout/sidebar-shell"
export default function LegacyLayout(props: ParentProps) {
const serverSDK = useServerSDK()
const [store, setStore, , ready] = persisted(
const [store, setStore, ready] = persisted(
Persist.serverGlobal(serverSDK().scope, "layout.page", ["layout.page.v1"]),
createStore({
lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
@ -556,11 +556,11 @@ export default function LegacyLayout(props: ParentProps) {
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
const key = pathKey(directory)
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
const direct = store().workspaceName[key] ?? store().workspaceName[directory]
if (direct) return direct
if (!projectId) return
if (!branch) return
return store.workspaceBranchName[projectId]?.[branch]
return store().workspaceBranchName[projectId]?.[branch]
}
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
@ -568,7 +568,7 @@ export default function LegacyLayout(props: ParentProps) {
setStore("workspaceName", key, next)
if (!projectId) return
if (!branch) return
if (!store.workspaceBranchName[projectId]) {
if (!store().workspaceBranchName[projectId]) {
setStore("workspaceBranchName", projectId, {})
}
setStore("workspaceBranchName", projectId, branch, next)
@ -591,7 +591,7 @@ export default function LegacyLayout(props: ParentProps) {
const activeDir = currentDir()
return workspaceIds(project).filter((directory) => {
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
const expanded = store().workspaceExpanded[directory] ?? directory === project.worktree
const active = pathKey(directory) === pathKey(activeDir)
return expanded || active
})
@ -601,7 +601,7 @@ export default function LegacyLayout(props: ParentProps) {
if (!pageReady()) return
if (!layoutReady()) return
const projects = layout.projects.list()
for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) {
for (const [directory, expanded] of Object.entries(store().workspaceExpanded)) {
if (!expanded) continue
const key = pathKey(directory)
const project = projects.find(
@ -1139,7 +1139,7 @@ export default function LegacyLayout(props: ParentProps) {
.find((item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key))
if (project) return project.worktree
const known = Object.entries(store.workspaceOrder).find(
const known = Object.entries(store().workspaceOrder).find(
([root, dirs]) => pathKey(root) === key || dirs.some((item) => pathKey(item) === key),
)
if (known) return known[0]
@ -1162,7 +1162,7 @@ export default function LegacyLayout(props: ParentProps) {
}
function clearLastProjectSession(root: string) {
if (!store.lastProjectSession[root]) return
if (!store().lastProjectSession[root]) return
setStore(
"lastProjectSession",
produce((draft) => {
@ -1174,7 +1174,7 @@ export default function LegacyLayout(props: ParentProps) {
function syncSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
rememberSessionRoute(directory, id, root)
notification.session.markViewed(id)
const expanded = untrack(() => store.workspaceExpanded[directory])
const expanded = untrack(() => store().workspaceExpanded[directory])
if (expanded === false) {
setStore("workspaceExpanded", directory, true)
}
@ -1188,7 +1188,7 @@ export default function LegacyLayout(props: ParentProps) {
server.projects.touch(root)
const project = layout.projects.list().find((item) => item.worktree === root)
let dirs = project
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store().workspaceOrder[root])
: [root]
const canOpen = (value: string | undefined) => {
if (!value) return false
@ -1200,7 +1200,7 @@ export default function LegacyLayout(props: ParentProps) {
.client.worktree.list({ directory: root })
.then((x) => x.data ?? [])
.catch(() => [] as string[])
dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
dirs = effectiveWorkspaceOrder(root, [root, ...listed], store().workspaceOrder[root])
return canOpen(target)
}
const openSession = async (target: { directory: string; id: string }) => {
@ -1222,7 +1222,7 @@ export default function LegacyLayout(props: ParentProps) {
return true
}
const projectSession = store.lastProjectSession[root]
const projectSession = store().lastProjectSession[root]
if (projectSession?.id) {
await refreshDirs(projectSession.directory)
const opened = await openSession(projectSession)
@ -1413,7 +1413,7 @@ export default function LegacyLayout(props: ParentProps) {
if (!result) return
if (pathKey(store.lastProjectSession[root]?.directory ?? "") === pathKey(directory)) {
if (pathKey(store().lastProjectSession[root]?.directory ?? "") === pathKey(directory)) {
clearLastProjectSession(root)
}
@ -1436,7 +1436,7 @@ export default function LegacyLayout(props: ParentProps) {
const nextKey = pathKey(nextCurrent)
const project = layout.projects.list().find((item) => item.worktree === root)
const dirs = project
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store().workspaceOrder[root])
: [root]
const valid = dirs.some((item) => pathKey(item) === nextKey)
@ -1783,7 +1783,7 @@ export default function LegacyLayout(props: ParentProps) {
: undefined
const pending = extra ? WorktreeState.get(serverSDK().scope, extra)?.status === "pending" : false
const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
const ordered = effectiveWorkspaceOrder(local, dirs, store().workspaceOrder[project.worktree])
if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)]
if (!extra) return ordered
if (pending) return ordered
@ -1887,7 +1887,7 @@ export default function LegacyLayout(props: ParentProps) {
setEditor,
InlineEditor,
isBusy,
workspaceExpanded: (directory, local) => store.workspaceExpanded[directory] ?? local,
workspaceExpanded: (directory, local) => store().workspaceExpanded[directory] ?? local,
setWorkspaceExpanded: (directory, value) => setStore("workspaceExpanded", directory, value),
showResetWorkspaceDialog: (root, directory) =>
dialog.show(() => <DialogResetWorkspace root={root} directory={directory} />),
@ -2183,7 +2183,7 @@ export default function LegacyLayout(props: ParentProps) {
<DragOverlay>
<WorkspaceDragOverlay
sidebarProject={sidebarProject}
activeWorkspace={() => store.activeWorkspace}
activeWorkspace={() => store().activeWorkspace}
workspaceLabel={workspaceLabel}
/>
</DragOverlay>
@ -2199,7 +2199,7 @@ export default function LegacyLayout(props: ParentProps) {
<div
class="shrink-0 px-3 py-3"
classList={{
hidden: store.gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0),
hidden: store().gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0),
}}
>
<div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started">
@ -2229,7 +2229,7 @@ export default function LegacyLayout(props: ParentProps) {
}
const projects = () => layout.projects.list()
const projectOverlay = () => <ProjectDragOverlay projects={projects} activeProject={() => store.activeProject} />
const projectOverlay = () => <ProjectDragOverlay projects={projects} activeProject={() => store().activeProject} />
const sidebarContent = (mobile?: boolean) => (
<SidebarContent
mobile={mobile}

View file

@ -308,7 +308,7 @@ export default function Page() {
})
const [followup, setFollowup] = persisted(
Persist.serverWorkspace(serverSDK().scope, sdk().directory, "followup", ["followup.v1"]),
Persist.serverWorkspace(serverSDK().scope, sdk().directory, "followup", ["followup().v1"]),
createStore<{
items: Record<string, FollowupItem[] | undefined>
failed: Record<string, string | undefined>
@ -1293,19 +1293,19 @@ export default function Page() {
const queuedFollowups = createMemo(() => {
const id = params.id
if (!id) return emptyFollowups
return followup.items[id] ?? emptyFollowups
return followup().items[id] ?? emptyFollowups
})
const editingFollowup = createMemo(() => {
const id = params.id
if (!id) return
return followup.edit[id]
return followup().edit[id]
})
const followupMutation = useMutation(() => ({
mutationFn: async (input: { sessionID: string; id: string; manual?: boolean }) => {
const owner = sessionOwnership.capture()
const item = (followup.items[input.sessionID] ?? []).find((entry) => entry.id === input.id)
const item = (followup().items[input.sessionID] ?? []).find((entry) => entry.id === input.id)
if (!item) return
if (input.manual) setFollowup("paused", input.sessionID, undefined)
@ -1375,7 +1375,7 @@ export default function Page() {
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
if (sync().session.get(sessionID)?.parentID) return Promise.resolve()
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
const item = (followup().items[sessionID] ?? []).find((entry) => entry.id === id)
if (!item) return Promise.resolve()
if (followupBusy(sessionID)) return Promise.resolve()
@ -1497,8 +1497,8 @@ export default function Page() {
const item = queuedFollowups()[0]
if (!item) return
if (followupBusy(sessionID)) return
if (followup.failed[sessionID] === item.id) return
if (followup.paused[sessionID]) return
if (followup().failed[sessionID] === item.id) return
if (followup().paused[sessionID]) return
if (isChildSession()) return
if (composer.blocked()) return
if (busy(sessionID)) return

View file

@ -61,9 +61,9 @@ export function useUsageExceededDialogs() {
const keys = goUpsellKeys(evt.properties.status)
if (!keys) return
const seen = goUpsellState[keys.lastSeenAt]
const seen = goUpsellState()[keys.lastSeenAt]
if (seen && Date.now() - seen < GO_UPSELL_WINDOW) return
if (goUpsellState[keys.dontShow]) return
if (goUpsellState()[keys.dontShow]) return
if (action.reason === "free_tier_limit") {
dialog.show(() => (

View file

@ -6,11 +6,9 @@ import type { SetStoreFunction, Store } from "solid-js/store"
import { pathKey } from "@/utils/path-key"
import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope"
type InitType = Promise<string> | string | null
type PersistedWithReady<T> = [
Store<T>,
Accessor<T>,
SetStoreFunction<T>,
InitType,
Accessor<boolean> & { promise: undefined | Promise<any> },
]
@ -619,20 +617,20 @@ export function persisted<T>(
const [state, setState, init] = makePersisted(store, { name: config.key, storage })
const isAsync = init instanceof Promise
const [ready] = createResource(
const [storeResource] = createResource(
() => init,
async (initValue) => {
if (initValue instanceof Promise) await initValue
return true
async () => {
if (init instanceof Promise) await init
return state
},
{ initialValue: !isAsync },
{ initialValue: state },
)
return [
state,
() => storeResource() as T,
setState,
init,
Object.assign(() => (ready.loading ? false : ready.latest === true), {
Object.assign(() => !isAsync || !storeResource.loading, {
promise: init instanceof Promise ? init : undefined,
}),
]