diff options
| author | Adam <[email protected]> | 2026-03-02 10:50:50 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-02 10:50:50 -0600 |
| commit | 8176bafc555e562ade48a675dffa3f38751ed8c9 (patch) | |
| tree | 7d4b0f6e98f431999b89c1f24687f6f53bd0bc6b /packages/app/src/context | |
| parent | 0a3a3216db5974efd3edc9a213054fd97d8dbd34 (diff) | |
| download | opencode-8176bafc555e562ade48a675dffa3f38751ed8c9.tar.gz opencode-8176bafc555e562ade48a675dffa3f38751ed8c9.zip | |
chore(app): solidjs refactoring (#13399)
Diffstat (limited to 'packages/app/src/context')
| -rw-r--r-- | packages/app/src/context/global-sync.tsx | 96 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/child-store.ts | 30 | ||||
| -rw-r--r-- | packages/app/src/context/layout.tsx | 82 |
3 files changed, 156 insertions, 52 deletions
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 112bc9240..574929115 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -11,7 +11,6 @@ import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" import { createContext, - createEffect, getOwner, Match, onCleanup, @@ -35,7 +34,6 @@ import { trimSessions } from "./global-sync/session-trim" import type { ProjectMeta } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { sanitizeProject } from "./global-sync/utils" -import { usePlatform } from "./platform" import { formatServerError } from "@/utils/server-errors" type GlobalStore = { @@ -54,7 +52,6 @@ type GlobalStore = { function createGlobalSync() { const globalSDK = useGlobalSDK() - const platform = usePlatform() const language = useLanguage() const owner = getOwner() if (!owner) throw new Error("GlobalSync must be created within owner") @@ -64,7 +61,7 @@ function createGlobalSync() { const sessionLoads = new Map<string, Promise<void>>() const sessionMeta = new Map<string, { limit: number }>() - const [projectCache, setProjectCache, , projectCacheReady] = persisted( + const [projectCache, setProjectCache, projectInit] = persisted( Persist.global("globalSync.project", ["globalSync.project.v1"]), createStore({ value: [] as Project[] }), ) @@ -80,6 +77,57 @@ function createGlobalSync() { reload: undefined, }) + let active = true + let projectWritten = false + + onCleanup(() => { + active = false + }) + + const cacheProjects = () => { + setProjectCache( + "value", + untrack(() => globalStore.project.map(sanitizeProject)), + ) + } + + const setProjects = (next: Project[] | ((draft: Project[]) => void)) => { + projectWritten = true + if (typeof next === "function") { + setGlobalStore("project", produce(next)) + cacheProjects() + return + } + setGlobalStore("project", next) + cacheProjects() + } + + const setBootStore = ((...input: unknown[]) => { + if (input[0] === "project" && Array.isArray(input[1])) { + setProjects(input[1] as Project[]) + return input[1] + } + return (setGlobalStore as (...args: unknown[]) => unknown)(...input) + }) as typeof setGlobalStore + + const set = ((...input: unknown[]) => { + if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) { + setProjects(input[1] as Project[] | ((draft: Project[]) => void)) + return input[1] + } + return (setGlobalStore as (...args: unknown[]) => unknown)(...input) + }) as typeof setGlobalStore + + if (projectInit instanceof Promise) { + void projectInit.then(() => { + if (!active) return + if (projectWritten) return + const cached = projectCache.value + if (cached.length === 0) return + setGlobalStore("project", cached) + }) + } + const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => { if (!sessionID) return if (!todos) { @@ -127,30 +175,6 @@ function createGlobalSync() { return sdk } - createEffect(() => { - if (!projectCacheReady()) return - if (globalStore.project.length !== 0) return - const cached = projectCache.value - if (cached.length === 0) return - setGlobalStore("project", cached) - }) - - createEffect(() => { - if (!projectCacheReady()) return - const projects = globalStore.project - if (projects.length === 0) { - const cachedLength = untrack(() => projectCache.value.length) - if (cachedLength !== 0) return - } - setProjectCache("value", projects.map(sanitizeProject)) - }) - - createEffect(() => { - if (globalStore.reload !== "complete") return - setGlobalStore("reload", undefined) - queue.refresh() - }) - async function loadSessions(directory: string) { const pending = sessionLoads.get(directory) if (pending) return pending @@ -259,13 +283,7 @@ function createGlobalSync() { event, project: globalStore.project, refresh: queue.refresh, - setGlobalProject(next) { - if (typeof next === "function") { - setGlobalStore("project", produce(next)) - return - } - setGlobalStore("project", next) - }, + setGlobalProject: setProjects, }) if (event.type === "server.connected" || event.type === "global.disposed") { for (const directory of Object.keys(children.children)) { @@ -316,7 +334,7 @@ function createGlobalSync() { unknownError: language.t("error.chain.unknown"), invalidConfigurationError: language.t("error.server.invalidConfiguration"), formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), - setGlobalStore, + setGlobalStore: setBootStore, }) } @@ -340,7 +358,9 @@ function createGlobalSync() { .update({ config }) .then(bootstrap) .then(() => { - setGlobalStore("reload", "complete") + queue.refresh() + setGlobalStore("reload", undefined) + queue.refresh() }) .catch((error) => { setGlobalStore("reload", undefined) @@ -350,7 +370,7 @@ function createGlobalSync() { return { data: globalStore, - set: setGlobalStore, + set, get ready() { return globalStore.ready }, diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 2fe5b7830..e2ada244f 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -1,4 +1,4 @@ -import { createRoot, createEffect, getOwner, onCleanup, runWithOwner, type Accessor, type Owner } from "solid-js" +import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js" import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" import type { VcsInfo } from "@opencode-ai/sdk/v2/client" @@ -131,8 +131,7 @@ export function createChildStoreManager(input: { ) if (!vcs) throw new Error("Failed to create persisted cache") const vcsStore = vcs[0] - const vcsReady = vcs[3] - vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcsReady }) + vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] }) const meta = runWithOwner(input.owner, () => persisted( @@ -154,10 +153,12 @@ export function createChildStoreManager(input: { const init = () => createRoot((dispose) => { + const initialMeta = meta[0].value + const initialIcon = icon[0].value const child = createStore<State>({ project: "", - projectMeta: meta[0].value, - icon: icon[0].value, + projectMeta: initialMeta, + icon: initialIcon, provider: { all: [], connected: [], default: {} }, config: {}, path: { state: "", config: "", worktree: "", directory: "", home: "" }, @@ -181,16 +182,27 @@ export function createChildStoreManager(input: { children[directory] = child disposers.set(directory, dispose) - createEffect(() => { - if (!vcsReady()) return + const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => { + if (!(init instanceof Promise)) return + void init.then(() => { + if (children[directory] !== child) return + run() + }) + } + + onPersistedInit(vcs[2], () => { const cached = vcsStore.value if (!cached?.branch) return child[1]("vcs", (value) => value ?? cached) }) - createEffect(() => { + + onPersistedInit(meta[2], () => { + if (child[0].projectMeta !== initialMeta) return child[1]("projectMeta", meta[0].value) }) - createEffect(() => { + + onPersistedInit(icon[2], () => { + if (child[0].icon !== initialIcon) return child[1]("icon", icon[0].value) }) }) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 71f0294e7..5199e5a26 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -7,8 +7,10 @@ import { useServer } from "./server" import { usePlatform } from "./platform" import { Project } from "@opencode-ai/sdk/v2" import { Persist, persisted, removePersisted } from "@/utils/persist" +import { decode64 } from "@/utils/base64" import { same } from "@/utils/same" import { createScrollPersistence, type SessionScroll } from "./layout-scroll" +import { createPathHelpers } from "./file/path" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const const DEFAULT_PANEL_WIDTH = 344 @@ -96,6 +98,38 @@ function nextSessionTabsForOpen(current: SessionTabs | undefined, tab: string): return { all, active: tab } } +const sessionPath = (key: string) => { + const dir = key.split("/")[0] + if (!dir) return + const root = decode64(dir) + if (!root) return + return createPathHelpers(() => root) +} + +const normalizeSessionTab = (path: ReturnType<typeof createPathHelpers> | undefined, tab: string) => { + if (!tab.startsWith("file://")) return tab + if (!path) return tab + return path.tab(tab) +} + +const normalizeSessionTabList = (path: ReturnType<typeof createPathHelpers> | undefined, all: string[]) => { + const seen = new Set<string>() + return all.flatMap((tab) => { + const value = normalizeSessionTab(path, tab) + if (seen.has(value)) return [] + seen.add(value) + return [value] + }) +} + +const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { + const path = sessionPath(key) + return { + all: normalizeSessionTabList(path, tabs.all), + active: tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active, + } +} + export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { @@ -147,12 +181,46 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } })() - if (migratedSidebar === sidebar && migratedReview === review && migratedFileTree === fileTree) return value + const sessionTabs = value.sessionTabs + const migratedSessionTabs = (() => { + if (!isRecord(sessionTabs)) return sessionTabs + + let changed = false + const next = Object.fromEntries( + Object.entries(sessionTabs).map(([key, tabs]) => { + if (!isRecord(tabs) || !Array.isArray(tabs.all)) return [key, tabs] + + const current = { + all: tabs.all.filter((tab): tab is string => typeof tab === "string"), + active: typeof tabs.active === "string" ? tabs.active : undefined, + } + const normalized = normalizeStoredSessionTabs(key, current) + if (current.all.length !== tabs.all.length) changed = true + if (!same(current.all, normalized.all) || current.active !== normalized.active) changed = true + if (tabs.active !== undefined && typeof tabs.active !== "string") changed = true + return [key, normalized] + }), + ) + + if (!changed) return sessionTabs + return next + })() + + if ( + migratedSidebar === sidebar && + migratedReview === review && + migratedFileTree === fileTree && + migratedSessionTabs === sessionTabs + ) { + return value + } + return { ...value, sidebar: migratedSidebar, review: migratedReview, fileTree: migratedFileTree, + sessionTabs: migratedSessionTabs, } } @@ -745,22 +813,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, tabs(sessionKey: string | Accessor<string>) { const key = createSessionKeyReader(sessionKey, ensureKey) + const path = createMemo(() => sessionPath(key())) const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] }) + const normalize = (tab: string) => normalizeSessionTab(path(), tab) + const normalizeAll = (all: string[]) => normalizeSessionTabList(path(), all) return { tabs, active: createMemo(() => tabs().active), all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), setActive(tab: string | undefined) { const session = key() + const next = tab ? normalize(tab) : tab if (!store.sessionTabs[session]) { - setStore("sessionTabs", session, { all: [], active: tab }) + setStore("sessionTabs", session, { all: [], active: next }) } else { - setStore("sessionTabs", session, "active", tab) + setStore("sessionTabs", session, "active", next) } }, setAll(all: string[]) { const session = key() - const next = all.filter((tab) => tab !== "review") + const next = normalizeAll(all).filter((tab) => tab !== "review") if (!store.sessionTabs[session]) { setStore("sessionTabs", session, { all: next, active: undefined }) } else { @@ -769,7 +841,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, async open(tab: string) { const session = key() - const next = nextSessionTabsForOpen(store.sessionTabs[session], tab) + const next = nextSessionTabsForOpen(store.sessionTabs[session], normalize(tab)) setStore("sessionTabs", session, next) }, close(tab: string) { |
