Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Brendan Allan
ea2db98dbe
Merge branch 'dev' into persisted-suspense 2026-07-07 14:08:32 +08:00
Brendan Allan
6910eee7fb
refactor(app): add suspense support to persisted 2026-06-29 08:51:29 +08:00
30 changed files with 269 additions and 269 deletions

View file

@ -72,7 +72,7 @@ const SessionRoute = () => {
return ( return (
<Show when={tabs.ready()}> <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)} /> return <Navigate href={sessionHref(legacySessionServer(persisted, sessionID, server.key), sessionID)} />
}} }}
</Show> </Show>
@ -140,7 +140,7 @@ function DraftRoute() {
return ( return (
<Show when={tabs.ready()}> <Show when={tabs.ready()}>
<Show <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 keyed
fallback={<Navigate href="/" />} fallback={<Navigate href="/" />}
> >
@ -568,7 +568,7 @@ function LegacyTargetSessionRoute() {
<Navigate <Navigate
href={sessionHref( href={sessionHref(
legacySessionServer( legacySessionServer(
tabs.store.filter((item) => item.type === "session"), tabs.store().filter((item) => item.type === "session"),
params.id, params.id,
server.key, server.key,
), ),

View file

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

View file

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

View file

@ -40,7 +40,7 @@ function SessionTabSlot(props: {
let ref!: HTMLDivElement let ref!: HTMLDivElement
const sdk = createMemo(() => props.serverCtx()?.sdk ?? null) const sdk = createMemo(() => props.serverCtx()?.sdk ?? null)
const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId)) const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId))
const persisted = createMemo(() => tabs.info[props.id]) const persisted = createMemo(() => tabs.info()[props.id])
const [loadedSession] = createResource( const [loadedSession] = createResource(
() => { () => {
const ctx = props.serverCtx() const ctx = props.serverCtx()

View file

@ -275,10 +275,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const matchRoute = (route: LayoutRoute) => { const matchRoute = (route: LayoutRoute) => {
if (route.type === "home") return if (route.type === "home") return
if (route.type === "draft") { 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") { if (route.type === "session") {
const main = tabsStore.find( const main = tabsStore().find(
(item) => (item) =>
item.type === "session" && item.server === route.server && item.sessionId === route.sessionId, item.type === "session" && item.server === route.server && item.sessionId === route.sessionId,
) )
@ -286,7 +286,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const s = session() const s = session()
if (s?.parentID) { if (s?.parentID) {
const parentID = 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, (item) => item.type === "session" && item.server === route.server && item.sessionId === parentID,
) )
if (parent) return parent if (parent) return parent
@ -395,7 +395,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: "mod+w", keybind: "mod+w",
hidden: true, hidden: true,
onSelect: () => { onSelect: () => {
tabsStoreActions.closeTab(tabsStore.findIndex((tab) => current === tab)) tabsStoreActions.closeTab(tabsStore().findIndex((tab) => current === tab))
}, },
}, },
{ {
@ -412,13 +412,13 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`, keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
hidden: true, hidden: true,
onSelect: () => { onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab()) let index = tabsStore().findIndex((tab) => tab === currentTab())
if (index === -1) return if (index === -1) return
index -= 1 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) if (next) tabs.select(next)
}, },
}, },
@ -429,13 +429,13 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
keybind: `mod+option+ArrowRight,ctrl+tab`, keybind: `mod+option+ArrowRight,ctrl+tab`,
hidden: true, hidden: true,
onSelect: () => { onSelect: () => {
let index = tabsStore.findIndex((tab) => tab === currentTab()) let index = tabsStore().findIndex((tab) => tab === currentTab())
if (index === -1) return if (index === -1) return
index += 1 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) if (next) tabs.select(next)
}, },
}, },
@ -482,7 +482,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
</TooltipV2> </TooltipV2>
<TitlebarTabStrip <TitlebarTabStrip
tabs={tabsStore} tabs={tabsStore()}
currentTab={currentTab} currentTab={currentTab}
forceTruncate={tabsAreOverflowing()} forceTruncate={tabsAreOverflowing()}
onOverflowChange={setTabsAreOverflowing} onOverflowChange={setTabsAreOverflowing}
@ -491,7 +491,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
el?.scrollIntoView({ behavior: "instant" }) el?.scrollIntoView({ behavior: "instant" })
}} }}
onClose={(tab) => { onClose={(tab) => {
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab)) const index = tabsStore().findIndex((item) => tabKey(item) === tabKey(tab))
if (index !== -1) tabsStoreActions.closeTab(index) if (index !== -1) tabsStoreActions.closeTab(index)
}} }}
onReorder={(keys) => tabsStoreActions.reorder(keys)} onReorder={(keys) => tabsStoreActions.reorder(keys)}

View file

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

View file

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

View file

@ -37,7 +37,7 @@ function equalSelectedLines(a: SelectedLineRange | null | undefined, b: Selected
function createViewSession(scope: ServerScope, dir: string, id: string | undefined) { function createViewSession(scope: ServerScope, dir: string, id: string | undefined) {
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1` 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]), Persist.serverScoped(scope, dir, id, "file-view", [legacyViewKey]),
createStore<{ createStore<{
file: Record<string, FileViewState> file: Record<string, FileViewState>
@ -49,7 +49,7 @@ function createViewSession(scope: ServerScope, dir: string, id: string | undefin
const meta = { pruned: false } const meta = { pruned: false }
const pruneView = (keep?: string) => { const pruneView = (keep?: string) => {
const keys = Object.keys(view.file) const keys = Object.keys(view().file)
if (keys.length <= MAX_VIEW_FILES) return if (keys.length <= MAX_VIEW_FILES) return
const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES) 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() pruneView()
}) })
const scrollTop = (path: string) => view.file[path]?.scrollTop const scrollTop = (path: string) => view().file[path]?.scrollTop
const scrollLeft = (path: string) => view.file[path]?.scrollLeft const scrollLeft = (path: string) => view().file[path]?.scrollLeft
const selectedLines = (path: string) => view.file[path]?.selectedLines const selectedLines = (path: string) => view().file[path]?.selectedLines
const setScrollTop = (path: string, top: number) => { const setScrollTop = (path: string, top: number) => {
setView( setView(

View file

@ -9,9 +9,8 @@ import { ServerScope } from "@/utils/server-scope"
let createChildStoreManager: typeof import("./child-store").createChildStoreManager let createChildStoreManager: typeof import("./child-store").createChildStoreManager
const querySingles: Array<() => { queryKey?: unknown[]; enabled?: boolean }> = [] const querySingles: Array<() => { queryKey?: unknown[]; enabled?: boolean }> = []
const persist: typeof import("@/utils/persist").persisted = (_target, store) => [ const persist: typeof import("@/utils/persist").persisted = (_target, store) => [
store[0], () => store[0],
store[1], store[1],
null,
Object.assign(() => true, { promise: undefined }), 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 { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import type { VcsInfo } from "@opencode-ai/sdk/v2/client" 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")) if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
const vcsStore = vcs[0] 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, () => const meta = runWithOwner(input.owner, () =>
input.persist( input.persist(
@ -166,7 +166,7 @@ export function createChildStoreManager(input: {
), ),
) )
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed")) 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, () => const icon = runWithOwner(input.owner, () =>
input.persist( input.persist(
@ -175,12 +175,12 @@ export function createChildStoreManager(input: {
), ),
) )
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed")) 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 = () => const init = () =>
createRoot((dispose) => { createRoot((dispose) => {
const initialMeta = meta[0].value const initialMeta = meta[0]().value
const initialIcon = icon[0].value const initialIcon = icon[0]().value
const [mcpEnabled, setMcpEnabled] = createSignal(false) const [mcpEnabled, setMcpEnabled] = createSignal(false)
const pathQuery = useQuery(() => input.queryOptions.path(key)) const pathQuery = useQuery(() => input.queryOptions.path(key))
@ -241,7 +241,7 @@ export function createChildStoreManager(input: {
get lsp() { get lsp() {
return lspQuery.isLoading ? [] : (lspQuery.data ?? []) return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
}, },
vcs: vcsStore.value, vcs: vcsStore().value,
limit: 5, limit: 5,
message: {}, message: {},
part: {}, part: {},
@ -251,28 +251,28 @@ export function createChildStoreManager(input: {
disposers.set(key, dispose) disposers.set(key, dispose)
mcpToggles.set(key, setMcpEnabled) mcpToggles.set(key, setMcpEnabled)
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => { const onPersistedInit = (ready: Accessor<boolean> & { promise: undefined | Promise<any> }, run: () => void) => {
if (!(init instanceof Promise)) return if (!ready.promise) return
void init.then(() => { void ready.promise.then(() => {
if (children[key] !== child) return if (children[key] !== child) return
run() run()
}) })
} }
onPersistedInit(vcs[2], () => { onPersistedInit(vcs[2], () => {
const cached = vcsStore.value const cached = vcsStore().value
if (!cached?.branch) return if (!cached?.branch) return
child[1]("vcs", (value) => value ?? cached) child[1]("vcs", (value) => value ?? cached)
}) })
onPersistedInit(meta[2], () => { onPersistedInit(meta[2], () => {
if (child[0].projectMeta !== initialMeta) return if (child[0].projectMeta !== initialMeta) return
child[1]("projectMeta", meta[0].value) child[1]("projectMeta", meta[0]().value)
}) })
onPersistedInit(icon[2], () => { onPersistedInit(icon[2], () => {
if (child[0].icon !== initialIcon) return 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", directory: "/tmp",
loadLsp() {}, loadLsp() {},
vcsCache: { vcsCache: {
store: cacheStore, store: () => cacheStore,
setStore: setCacheStore, setStore: setCacheStore,
ready: () => true, ready: () => true,
}, },

View file

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

View file

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

View file

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

View file

@ -171,7 +171,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
if (value.type === "home") return value if (value.type === "home") return value
if (value.server) return value if (value.server) return value
if (value.type === "draft") { 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 } if (draft) return { ...value, server: draft.server }
} }
return { ...value, server: server.key } return { ...value, server: server.key }
@ -268,7 +268,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
const target = Persist.serverGlobal(serverSdk().scope, "layout", ["layout.v6"]) const target = Persist.serverGlobal(serverSdk().scope, "layout", ["layout.v6"])
const [store, setStore, _, ready] = persisted( const [store, setStore, ready] = persisted(
{ ...target, migrate }, { ...target, migrate },
createStore({ createStore({
sidebar: { sidebar: {
@ -350,8 +350,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
keep, keep,
max: MAX_SESSION_KEYS, max: MAX_SESSION_KEYS,
used: usage.used, used: usage.used,
view: Object.keys(store.sessionView), view: Object.keys(store().sessionView),
tabs: Object.keys(store.sessionTabs), tabs: Object.keys(store().sessionTabs),
}) })
if (drop.length === 0) return if (drop.length === 0) return
@ -385,9 +385,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const scroll = createScrollPersistence({ const scroll = createScrollPersistence({
debounceMs: 250, debounceMs: 250,
getSnapshot: (sessionKey) => store.sessionView[sessionKey]?.scroll, getSnapshot: (sessionKey) => store().sessionView[sessionKey]?.scroll,
onFlush: (sessionKey, next) => { onFlush: (sessionKey, next) => {
const current = store.sessionView[sessionKey] const current = store().sessionView[sessionKey]
const keep = usage.active ?? sessionKey const keep = usage.active ?? sessionKey
if (!current) { if (!current) {
setStore("sessionView", sessionKey, { scroll: next }) setStore("sessionView", sessionKey, { scroll: next })
@ -597,18 +597,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
route, route,
ready, ready,
home: { home: {
selection: createMemo(() => store.home.selection), selection: createMemo(() => store().home.selection),
setSelection(selection: HomeProjectSelection) { setSelection(selection: HomeProjectSelection) {
setStore("home", "selection", reconcile(selection)) setStore("home", "selection", reconcile(selection))
}, },
}, },
handoff: { handoff: {
tabs: createMemo(() => store.handoff?.tabs), tabs: createMemo(() => store().handoff?.tabs),
setTabs(dir: string, id: string) { setTabs(dir: string, id: string) {
setStore("handoff", "tabs", { scope: serverSdk().scope, dir, id, at: Date.now() }) setStore("handoff", "tabs", { scope: serverSdk().scope, dir, id, at: Date.now() })
}, },
clearTabs() { clearTabs() {
if (!store.handoff?.tabs) return if (!store().handoff?.tabs) return
setStore("handoff", "tabs", undefined) setStore("handoff", "tabs", undefined)
}, },
}, },
@ -642,7 +642,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
}, },
sidebar: { sidebar: {
opened: createMemo(() => store.sidebar.opened), opened: createMemo(() => store().sidebar.opened),
open() { open() {
setStore("sidebar", "opened", true) setStore("sidebar", "opened", true)
}, },
@ -652,31 +652,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
toggle() { toggle() {
setStore("sidebar", "opened", (x) => !x) setStore("sidebar", "opened", (x) => !x)
}, },
width: createMemo(() => store.sidebar.width), width: createMemo(() => store().sidebar.width),
resize(width: number) { resize(width: number) {
setStore("sidebar", "width", width) setStore("sidebar", "width", width)
}, },
workspaces(directory: string) { 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) { setWorkspaces(directory: string, value: boolean) {
setStore("sidebar", "workspaces", directory, value) setStore("sidebar", "workspaces", directory, value)
}, },
toggleWorkspaces(directory: string) { 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) setStore("sidebar", "workspaces", directory, !current)
}, },
}, },
terminal: { terminal: {
height: createMemo(() => store.terminal.height), height: createMemo(() => store().terminal.height),
resize(height: number) { resize(height: number) {
setStore("terminal", "height", height) setStore("terminal", "height", height)
}, },
}, },
review: { review: {
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"), diffStyle: createMemo(() => store().review?.diffStyle ?? "split"),
setDiffStyle(diffStyle: ReviewDiffStyle) { setDiffStyle(diffStyle: ReviewDiffStyle) {
if (!store.review) { if (!store().review) {
setStore("review", { diffStyle, panelOpened: DEFAULT_REVIEW_PANEL_OPENED }) setStore("review", { diffStyle, panelOpened: DEFAULT_REVIEW_PANEL_OPENED })
return return
} }
@ -684,39 +684,39 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
}, },
fileTree: { fileTree: {
opened: createMemo(() => store.fileTree?.opened ?? true), opened: createMemo(() => store().fileTree?.opened ?? true),
width: createMemo(() => store.fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH), width: createMemo(() => store().fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH),
tab: createMemo(() => store.fileTree?.tab ?? "changes"), tab: createMemo(() => store().fileTree?.tab ?? "changes"),
setTab(tab: "changes" | "all") { setTab(tab: "changes" | "all") {
if (!store.fileTree) { if (!store().fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab }) setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab })
return return
} }
setStore("fileTree", "tab", tab) setStore("fileTree", "tab", tab)
}, },
open() { open() {
if (!store.fileTree) { if (!store().fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return return
} }
setStore("fileTree", "opened", true) setStore("fileTree", "opened", true)
}, },
close() { close() {
if (!store.fileTree) { if (!store().fileTree) {
setStore("fileTree", { opened: false, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) setStore("fileTree", { opened: false, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return return
} }
setStore("fileTree", "opened", false) setStore("fileTree", "opened", false)
}, },
toggle() { toggle() {
if (!store.fileTree) { if (!store().fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return return
} }
setStore("fileTree", "opened", (x) => !x) setStore("fileTree", "opened", (x) => !x)
}, },
resize(width: number) { resize(width: number) {
if (!store.fileTree) { if (!store().fileTree) {
setStore("fileTree", { opened: true, width, tab: "changes" }) setStore("fileTree", { opened: true, width, tab: "changes" })
return return
} }
@ -724,9 +724,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
}, },
session: { session: {
width: createMemo(() => store.session?.width ?? DEFAULT_SESSION_WIDTH), width: createMemo(() => store().session?.width ?? DEFAULT_SESSION_WIDTH),
resize(width: number) { resize(width: number) {
if (!store.session) { if (!store().session) {
setStore("session", { width }) setStore("session", { width })
return return
} }
@ -734,7 +734,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
}, },
mobileSidebar: { mobileSidebar: {
opened: createMemo(() => store.mobileSidebar?.opened ?? false), opened: createMemo(() => store().mobileSidebar?.opened ?? false),
show() { show() {
setStore("mobileSidebar", "opened", true) setStore("mobileSidebar", "opened", true)
}, },
@ -749,7 +749,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
set(sessionKey: string, messageID: string) { set(sessionKey: string, messageID: string) {
const at = Date.now() const at = Date.now()
touch(sessionKey) touch(sessionKey)
const current = store.sessionView[sessionKey] const current = store().sessionView[sessionKey]
if (!current) { if (!current) {
setStore("sessionView", sessionKey, { setStore("sessionView", sessionKey, {
scroll: {}, scroll: {},
@ -770,7 +770,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
) )
}, },
consume(sessionKey: string) { consume(sessionKey: string) {
const current = store.sessionView[sessionKey] const current = store().sessionView[sessionKey]
const message = current?.pendingMessage const message = current?.pendingMessage
const at = current?.pendingMessageAt const at = current?.pendingMessageAt
if (!message || !at) return if (!message || !at) return
@ -790,13 +790,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
view(sessionKey: string | Accessor<string>) { view(sessionKey: string | Accessor<string>) {
const key = createSessionKeyReader(sessionKey, ensureKey) const key = createSessionKeyReader(sessionKey, ensureKey)
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} }) const s = createMemo(() => store().sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false) const terminalOpened = createMemo(() => store().terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED) const reviewPanelOpened = createMemo(() => store().review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other")) const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))
function setTerminalOpened(next: boolean) { function setTerminalOpened(next: boolean) {
const current = store.terminal const current = store().terminal
if (!current) { if (!current) {
setStore("terminal", { height: DEFAULT_TERMINAL_HEIGHT, opened: next }) setStore("terminal", { height: DEFAULT_TERMINAL_HEIGHT, opened: next })
return return
@ -809,7 +809,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
function setReviewPanelOpened(next: boolean, source: ReviewPanelSource) { function setReviewPanelOpened(next: boolean, source: ReviewPanelSource) {
const nextSource = next ? source : "other" const nextSource = next ? source : "other"
const current = store.review const current = store().review
if (!current) { if (!current) {
batch(() => { batch(() => {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
@ -840,7 +840,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
get: () => s().todoCollapsed ?? false, get: () => s().todoCollapsed ?? false,
set(collapsed: boolean) { set(collapsed: boolean) {
const session = key() const session = key()
const current = store.sessionView[session] const current = store().sessionView[session]
if (!current) { if (!current) {
setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed }) setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed })
} else { } else {
@ -878,7 +878,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setOpen(open: string[]) { setOpen(open: string[]) {
const session = key() const session = key()
const next = Array.from(new Set(open)) const next = Array.from(new Set(open))
const current = store.sessionView[session] const current = store().sessionView[session]
if (!current) { if (!current) {
setStore("sessionView", session, { setStore("sessionView", session, {
scroll: {}, scroll: {},
@ -892,7 +892,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
openPath(path: string) { openPath(path: string) {
const session = key() const session = key()
const current = store.sessionView[session] const current = store().sessionView[session]
if (!current) { if (!current) {
setStore("sessionView", session, { setStore("sessionView", session, {
scroll: {}, scroll: {},
@ -911,7 +911,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
closePath(path: string) { closePath(path: string) {
const session = key() const session = key()
const current = store.sessionView[session]?.reviewOpen const current = store().sessionView[session]?.reviewOpen
if (!current) return if (!current) return
const index = current.indexOf(path) const index = current.indexOf(path)
@ -928,7 +928,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
togglePath(path: string) { togglePath(path: string) {
const session = key() const session = key()
const current = store.sessionView[session]?.reviewOpen const current = store().sessionView[session]?.reviewOpen
if (!current || !current.includes(path)) { if (!current || !current.includes(path)) {
this.openPath(path) this.openPath(path)
return return
@ -942,7 +942,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
tabs(sessionKey: string | Accessor<string>) { tabs(sessionKey: string | Accessor<string>) {
const key = createSessionKeyReader(sessionKey, ensureKey) const key = createSessionKeyReader(sessionKey, ensureKey)
const path = createMemo(() => sessionPath(key())) 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 normalize = (tab: string) => normalizeSessionTab(path(), tab)
const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all) const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all)
return { return {
@ -952,7 +952,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setActive(tab: string | undefined) { setActive(tab: string | undefined) {
const session = key() const session = key()
const next = tab ? normalize(tab) : tab const next = tab ? normalize(tab) : tab
if (!store.sessionTabs[session]) { if (!store().sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: next }) setStore("sessionTabs", session, { all: [], active: next })
} else { } else {
setStore("sessionTabs", session, "active", next) setStore("sessionTabs", session, "active", next)
@ -961,7 +961,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setAll(all: string[]) { setAll(all: string[]) {
const session = key() const session = key()
const next = normalizeAll(all).filter((tab) => tab !== "review") const next = normalizeAll(all).filter((tab) => tab !== "review")
if (!store.sessionTabs[session]) { if (!store().sessionTabs[session]) {
setStore("sessionTabs", session, { all: next, active: undefined }) setStore("sessionTabs", session, { all: next, active: undefined })
} else { } else {
setStore("sessionTabs", session, "all", next) setStore("sessionTabs", session, "all", next)
@ -969,12 +969,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
async open(tab: string) { async open(tab: string) {
const session = key() const session = key()
const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab)) const next = nextSessionTabsForOpen(store().sessionTabs[session], normalize(tab))
setStore("sessionTabs", session, next) setStore("sessionTabs", session, next)
}, },
close(tab: string) { close(tab: string) {
const session = key() const session = key()
const current = store.sessionTabs[session] const current = store().sessionTabs[session]
if (!current) return if (!current) return
if (tab === "review") { if (tab === "review") {
@ -998,7 +998,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}, },
move(tab: string, to: number) { move(tab: string, to: number) {
const session = key() const session = key()
const current = store.sessionTabs[session] const current = store().sessionTabs[session]
if (!current) return if (!current) return
const index = current.all.findIndex((f) => f === tab) const index = current.all.findIndex((f) => f === tab)
if (index === -1) return if (index === -1) return

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,7 +55,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const server = useServer() const server = useServer()
const platform = usePlatform() const platform = usePlatform()
const fallback = server.key const fallback = server.key
const [store, setStore, _, ready] = persisted( const [store, setStore, ready] = persisted(
{ {
...Persist.window("tabs"), ...Persist.window("tabs"),
migrate: (value: unknown) => { migrate: (value: unknown) => {
@ -68,9 +68,9 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}, },
createStore<Tab[]>([]), createStore<Tab[]>([]),
) )
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({})) const [recent, setRecent, recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
const [info, setInfo] = persisted(Persist.window("tabs.info"), createStore<Record<string, TabInfo>>({})) const [info, setInfo] = persisted(Persist.window("tabs.info"), createStore<Record<string, TabInfo>>({}))
const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore<ClosedTab[]>([])) const [closed, setClosed, closedReady] = persisted(Persist.window("tabs.closed"), createStore<ClosedTab[]>([]))
const params = useParams() const params = useParams()
const navigate = useNavigate() const navigate = useNavigate()
@ -81,7 +81,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
let recentWrite = 0 let recentWrite = 0
let recentValue: string | undefined let recentValue: string | undefined
const recentKey = () => (recentWrite ? recentValue : recent.key) const recentKey = () => (recentWrite ? recentValue : recent().key)
const setRecentKey = (key: string | undefined) => { const setRecentKey = (key: string | undefined) => {
const write = ++recentWrite const write = ++recentWrite
@ -109,7 +109,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
} }
const removeInfo = (key: string) => { const removeInfo = (key: string) => {
if (!info[key]) return if (!info()[key]) return
setInfo( setInfo(
produce((draft) => { produce((draft) => {
delete draft[key] delete draft[key]
@ -122,9 +122,9 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
createEffect(() => { createEffect(() => {
if (!ready() || !recentReady()) return if (!ready() || !recentReady()) return
const servers = new Set(server.list.map(ServerConnection.key)) const servers = new Set(server.list.map(ServerConnection.key))
const next = store.filter((tab) => servers.has(tab.server)) const next = store().filter((tab) => servers.has(tab.server))
if (next.length !== store.length) { if (next.length !== store().length) {
for (const tab of store) { for (const tab of store()) {
if (!servers.has(tab.server)) { if (!servers.has(tab.server)) {
const key = tabKey(tab) const key = tabKey(tab)
memory.remove(key) memory.remove(key)
@ -133,9 +133,9 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
} }
setStore(() => next) 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 keys = new Set(next.map(tabKey)) const keys = new Set(next.map(tabKey))
for (const key of Object.keys(info)) { for (const key of Object.keys(info())) {
if (!keys.has(key)) removeInfo(key) if (!keys.has(key)) removeInfo(key)
} }
}) })
@ -143,8 +143,8 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
createEffect(() => { createEffect(() => {
if (!closedReady()) return if (!closedReady()) return
const servers = new Set(server.list.map(ServerConnection.key)) const servers = new Set(server.list.map(ServerConnection.key))
const next = closed.filter((entry) => servers.has(entry.tab.server)) const next = closed().filter((entry) => servers.has(entry.tab.server))
if (next.length !== closed.length) setClosed(() => next) if (next.length !== closed().length) setClosed(() => next)
}) })
const navigateTab = (tab: Tab) => { const navigateTab = (tab: Tab) => {
@ -154,11 +154,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
} }
const removeTab = (index: number) => { const removeTab = (index: number) => {
const tab = store[index] const tab = store()[index]
if (!tab) return if (!tab) return
const key = tabKey(tab) const key = tabKey(tab)
const draftID = tab.type === "draft" ? tab.draftID : undefined const draftID = tab.type === "draft" ? tab.draftID : undefined
const nextTab = nextTabAfterClose(store, index, recentKey() === key && location.pathname !== "/") const nextTab = nextTabAfterClose(store(), index, recentKey() === key && location.pathname !== "/")
closing.add(key) closing.add(key)
void startTransition(() => { void startTransition(() => {
setStore( setStore(
@ -180,7 +180,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const actions = { const actions = {
addSessionTab: (tab: Omit<SessionTab, "type">) => { addSessionTab: (tab: Omit<SessionTab, "type">) => {
const next = { type: "session" as const, ...tab } 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 if (existing) return existing
void startTransition(() => { void startTransition(() => {
setStore( setStore(
@ -203,7 +203,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
) )
}, },
draft(draftID: string) { 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}`) if (!tab || tab.type !== "draft") throw new Error(`Draft not found: ${draftID}`)
return tab return tab
}, },
@ -228,7 +228,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}, },
promoteDraft(draftID: string, session: Omit<SessionTab, "type">) { promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
// Keep the replacement and navigation atomic so /new-session never renders // 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 active = location.pathname === "/new-session" && location.query.draftId === draftID
const next = { type: "session" as const, ...session } const next = { type: "session" as const, ...session }
void startTransition(() => { void startTransition(() => {
@ -238,7 +238,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
if (index !== -1) tabs[index] = next 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) if (active) navigateTab(next)
}) })
memory.remove(`draft:${draftID}`) memory.remove(`draft:${draftID}`)
@ -249,7 +249,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
// Cleanup paths (missing sessions, archive, server removal) go through // Cleanup paths (missing sessions, archive, server removal) go through
// removeTab and friends directly and are not recorded. // removeTab and friends directly and are not recorded.
closeTab(index: number) { closeTab(index: number) {
const tab = store[index] const tab = store()[index]
if (!tab) return if (!tab) return
if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index)) if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index))
removeTab(index) removeTab(index)
@ -259,12 +259,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
void closedReady.promise?.then(() => actions.reopenClosedTab()) void closedReady.promise?.then(() => actions.reopenClosedTab())
return return
} }
const result = takeClosedTab(closed, store) const result = takeClosedTab(closed(), store())
if (result.stack.length === closed.length) return if (result.stack.length === closed().length) return
setClosed(() => result.stack) setClosed(() => result.stack)
const entry = result.entry const entry = result.entry
if (!entry) return if (!entry) return
const index = Math.min(entry.index, store.length) const index = Math.min(entry.index, store().length)
void startTransition(() => { void startTransition(() => {
setStore( setStore(
produce((tabs) => { produce((tabs) => {
@ -277,26 +277,27 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}, },
removeSessionTab(input: Omit<SessionTab, "type">) { removeSessionTab(input: Omit<SessionTab, "type">) {
updateClosed((stack) => removeClosedTabs(stack, input.server, [input.sessionId])) updateClosed((stack) => removeClosedTabs(stack, input.server, [input.sessionId]))
const index = store.findIndex( const index = store().findIndex(
(tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId, (tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId,
) )
if (index !== -1) removeTab(index) if (index !== -1) removeTab(index)
}, },
removeServer(key: ServerConnection.Key) { removeServer(key: ServerConnection.Key) {
updateClosed((stack) => stack.filter((entry) => entry.tab.server !== key)) updateClosed((stack) => stack.filter((entry) => entry.tab.server !== key))
const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : [])) const drafts = store().flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : []))
const removed = store.filter((tab) => tab.server === key).map(tabKey) const removed = store().filter((tab) => tab.server === key).map(tabKey)
setStore((tabs) => tabs.filter((tab) => tab.server !== key)) setStore((tabs) => tabs.filter((tab) => tab.server !== key))
for (const key of removed) memory.remove(key) for (const key of removed) memory.remove(key)
for (const key of removed) removeInfo(key) for (const key of removed) removeInfo(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) for (const draftID of drafts) removeDraftPersisted(draftID)
if (server.key === key) navigate("/") if (server.key === key) navigate("/")
}, },
removeSessions: (input: SessionTabsRemovedDetail) => { removeSessions: (input: SessionTabsRemovedDetail) => {
const targetServer = input.server ?? server.key const targetServer = input.server ?? server.key
updateClosed((stack) => removeClosedTabs(stack, targetServer, input.sessionIDs)) updateClosed((stack) => removeClosedTabs(stack, targetServer, input.sessionIDs))
const removed = store const removed = store()
.filter( .filter(
(tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId), (tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
) )
@ -340,7 +341,8 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
else navigate("/") 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) for (const key of removed) memory.remove(key)
for (const key of removed) removeInfo(key) for (const key of removed) removeInfo(key)
@ -348,7 +350,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
rememberSessionInfo(tab: SessionTab, session: Session) { rememberSessionInfo(tab: SessionTab, session: Session) {
const key = tabKey(tab) const key = tabKey(tab)
const next = { title: session.title, directory: session.directory } const next = { title: session.title, directory: session.directory }
const current = info[key] const current = info()[key]
if (current?.title === next.title && current.directory === next.directory) return if (current?.title === next.title && current.directory === next.directory) return
setInfo(key, next) setInfo(key, next)
}, },
@ -359,7 +361,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}, },
toggleHome(input: { home: boolean; current?: Tab }) { toggleHome(input: { home: boolean; current?: Tab }) {
if (input.home) { if (input.home) {
const tab = store.find((tab) => tabKey(tab) === recentKey()) const tab = store().find((tab) => tabKey(tab) === recentKey())
if (tab) navigateTab(tab) if (tab) navigateTab(tab)
return return
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,11 +22,11 @@ export function createReviewPanelV2State() {
const [filter, setFilter] = createSignal("") const [filter, setFilter] = createSignal("")
return { return {
sidebarOpened: () => store.sidebarOpened, sidebarOpened: () => store().sidebarOpened,
sidebarWidth: () => store.sidebarWidth, sidebarWidth: () => store().sidebarWidth,
filter, filter,
setFilter, setFilter,
expandMode: () => store.expandMode, expandMode: () => store().expandMode,
setExpandMode: (mode: SessionReviewExpandMode) => setStore("expandMode", mode), setExpandMode: (mode: SessionReviewExpandMode) => setStore("expandMode", mode),
resizeSidebar: (width: number) => resizeSidebar: (width: number) =>
setStore( setStore(

View file

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