summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src/context')
-rw-r--r--packages/app/src/context/global-sync.tsx96
-rw-r--r--packages/app/src/context/global-sync/child-store.ts30
-rw-r--r--packages/app/src/context/layout.tsx82
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) {