From 8595dae1a47bff819bb507d77d0dddf14c335757 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:00:59 -0600 Subject: [PATCH] fix(app): session loading loop --- .../src/components/session/session-header.tsx | 30 ++- packages/app/src/context/global-sync.tsx | 218 ++++++++++-------- packages/app/src/pages/layout.tsx | 24 +- 3 files changed, 166 insertions(+), 106 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 7cded4bce..7214ac652 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -45,6 +45,8 @@ export function SessionHeader() { const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id)) const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") + const showReview = createMemo(() => !!currentSession()?.summary?.files) + const showShare = createMemo(() => shareEnabled() && !!currentSession()) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const view = createMemo(() => layout.view(sessionKey())) @@ -172,12 +174,14 @@ export function SessionHeader() { {/* */} {/* */}
- -
- -
+
-
- +
)} diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 69a2b8ce2..6a2102640 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -88,6 +88,10 @@ type VcsCache = { ready: Accessor } +type ChildOptions = { + bootstrap?: boolean +} + function createGlobalSync() { const globalSDK = useGlobalSDK() const platform = usePlatform() @@ -127,8 +131,10 @@ function createGlobalSync() { }) const children: Record, SetStoreFunction]> = {} + const booting = new Map>() + const sessionLoads = new Map>() - function child(directory: string) { + function ensureChild(directory: string) { if (!directory) console.error("No directory provided") if (!children[directory]) { const cache = runWithOwner(owner, () => @@ -163,7 +169,6 @@ function createGlobalSync() { message: {}, part: {}, }) - bootstrapInstance(directory) } runWithOwner(owner, init) @@ -173,11 +178,23 @@ function createGlobalSync() { return childStore } + function child(directory: string, options: ChildOptions = {}) { + const childStore = ensureChild(directory) + const shouldBootstrap = options.bootstrap ?? true + if (shouldBootstrap && childStore[0].status === "loading") { + void bootstrapInstance(directory) + } + return childStore + } + async function loadSessions(directory: string) { - const [store, setStore] = child(directory) + const pending = sessionLoads.get(directory) + if (pending) return pending + + const [store, setStore] = child(directory, { bootstrap: false }) const limit = store.limit - return globalSDK.client.session + const promise = globalSDK.client.session .list({ directory, roots: true }) .then((x) => { const nonArchived = (x.data ?? []) @@ -208,13 +225,23 @@ function createGlobalSync() { const project = getFilename(directory) showToast({ title: `Failed to load sessions for ${project}`, description: err.message }) }) + + sessionLoads.set(directory, promise) + promise.finally(() => { + sessionLoads.delete(directory) + }) + return promise } async function bootstrapInstance(directory: string) { if (!directory) return - const [store, setStore] = child(directory) - const cache = vcsCache.get(directory) - if (!cache) return + const pending = booting.get(directory) + if (pending) return pending + + const promise = (async () => { + const [store, setStore] = ensureChild(directory) + const cache = vcsCache.get(directory) + if (!cache) return const sdk = createOpencodeClient({ baseUrl: globalSDK.url, fetch: platform.fetch, @@ -250,98 +277,105 @@ function createGlobalSync() { config: () => sdk.config.get().then((x) => setStore("config", x.data!)), } - try { - await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) - } catch (err) { - console.error("Failed to bootstrap instance", err) - const project = getFilename(directory) - const message = err instanceof Error ? err.message : String(err) - showToast({ title: `Failed to reload ${project}`, description: message }) - setStore("status", "partial") - return - } + try { + await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) + } catch (err) { + console.error("Failed to bootstrap instance", err) + const project = getFilename(directory) + const message = err instanceof Error ? err.message : String(err) + showToast({ title: `Failed to reload ${project}`, description: message }) + setStore("status", "partial") + return + } - if (store.status !== "complete") setStore("status", "partial") + if (store.status !== "complete") setStore("status", "partial") - Promise.all([ - sdk.path.get().then((x) => setStore("path", x.data!)), - sdk.command.list().then((x) => setStore("command", x.data ?? [])), - sdk.session.status().then((x) => setStore("session_status", x.data!)), - loadSessions(directory), - sdk.mcp.status().then((x) => setStore("mcp", x.data!)), - sdk.lsp.status().then((x) => setStore("lsp", x.data!)), - sdk.vcs.get().then((x) => { - const next = x.data ?? store.vcs - setStore("vcs", next) - if (next?.branch) cache.setStore("value", next) - }), - sdk.permission.list().then((x) => { - const grouped: Record = {} - for (const perm of x.data ?? []) { - if (!perm?.id || !perm.sessionID) continue - const existing = grouped[perm.sessionID] - if (existing) { - existing.push(perm) - continue + Promise.all([ + sdk.path.get().then((x) => setStore("path", x.data!)), + sdk.command.list().then((x) => setStore("command", x.data ?? [])), + sdk.session.status().then((x) => setStore("session_status", x.data!)), + loadSessions(directory), + sdk.mcp.status().then((x) => setStore("mcp", x.data!)), + sdk.lsp.status().then((x) => setStore("lsp", x.data!)), + sdk.vcs.get().then((x) => { + const next = x.data ?? store.vcs + setStore("vcs", next) + if (next?.branch) cache.setStore("value", next) + }), + sdk.permission.list().then((x) => { + const grouped: Record = {} + for (const perm of x.data ?? []) { + if (!perm?.id || !perm.sessionID) continue + const existing = grouped[perm.sessionID] + if (existing) { + existing.push(perm) + continue + } + grouped[perm.sessionID] = [perm] } - grouped[perm.sessionID] = [perm] - } - batch(() => { - for (const sessionID of Object.keys(store.permission)) { - if (grouped[sessionID]) continue - setStore("permission", sessionID, []) + batch(() => { + for (const sessionID of Object.keys(store.permission)) { + if (grouped[sessionID]) continue + setStore("permission", sessionID, []) + } + for (const [sessionID, permissions] of Object.entries(grouped)) { + setStore( + "permission", + sessionID, + reconcile( + permissions + .filter((p) => !!p?.id) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)), + { key: "id" }, + ), + ) + } + }) + }), + sdk.question.list().then((x) => { + const grouped: Record = {} + for (const question of x.data ?? []) { + if (!question?.id || !question.sessionID) continue + const existing = grouped[question.sessionID] + if (existing) { + existing.push(question) + continue + } + grouped[question.sessionID] = [question] } - for (const [sessionID, permissions] of Object.entries(grouped)) { - setStore( - "permission", - sessionID, - reconcile( - permissions - .filter((p) => !!p?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) - } - }) - }), - sdk.question.list().then((x) => { - const grouped: Record = {} - for (const question of x.data ?? []) { - if (!question?.id || !question.sessionID) continue - const existing = grouped[question.sessionID] - if (existing) { - existing.push(question) - continue - } - grouped[question.sessionID] = [question] - } - batch(() => { - for (const sessionID of Object.keys(store.question)) { - if (grouped[sessionID]) continue - setStore("question", sessionID, []) - } - for (const [sessionID, questions] of Object.entries(grouped)) { - setStore( - "question", - sessionID, - reconcile( - questions - .filter((q) => !!q?.id) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)), - { key: "id" }, - ), - ) - } - }) - }), - ]).then(() => { - setStore("status", "complete") + batch(() => { + for (const sessionID of Object.keys(store.question)) { + if (grouped[sessionID]) continue + setStore("question", sessionID, []) + } + for (const [sessionID, questions] of Object.entries(grouped)) { + setStore( + "question", + sessionID, + reconcile( + questions + .filter((q) => !!q?.id) + .slice() + .sort((a, b) => a.id.localeCompare(b.id)), + { key: "id" }, + ), + ) + } + }) + }), + ]).then(() => { + setStore("status", "complete") + }) + })() + + booting.set(directory, promise) + promise.finally(() => { + booting.delete(directory) }) + return promise } const unsub = globalSDK.event.listen((e) => { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 8ea21bd34..64fc52739 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -563,9 +563,13 @@ export default function Layout(props: ParentProps) { if (!project) return [] as Session[] if (workspaceSetting()) { const dirs = workspaceIds(project) + const activeDir = params.dir ? base64Decode(params.dir) : "" const result: Session[] = [] for (const dir of dirs) { - const [dirStore] = globalSync.child(dir) + const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree + const active = dir === activeDir + if (!expanded && !active) continue + const [dirStore] = globalSync.child(dir, { bootstrap: true }) const dirSessions = dirStore.session .filter((session) => session.directory === dirStore.path.directory) .filter((session) => !session.parentID && !session.time?.archived) @@ -1238,8 +1242,12 @@ export default function Layout(props: ParentProps) { if (!project) return if (workspaceSetting()) { + const activeDir = params.dir ? base64Decode(params.dir) : "" const dirs = [project.worktree, ...(project.sandboxes ?? [])] for (const directory of dirs) { + const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree + const active = directory === activeDir + if (!expanded && !active) continue globalSync.project.loadSessions(directory) } return @@ -1558,7 +1566,7 @@ export default function Layout(props: ParentProps) { const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => { const sortable = createSortable(props.directory) - const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory) + const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) const [menuOpen, setMenuOpen] = createSignal(false) const [pendingRename, setPendingRename] = createSignal(false) const slug = createMemo(() => base64Encode(props.directory)) @@ -1569,12 +1577,17 @@ export default function Layout(props: ParentProps) { .toSorted(sortSessions), ) const local = createMemo(() => props.directory === props.project.worktree) + const active = createMemo(() => { + const current = params.dir ? base64Decode(params.dir) : "" + return current === props.directory + }) const workspaceValue = createMemo(() => { const branch = workspaceStore.vcs?.branch const name = branch ?? getFilename(props.directory) return workspaceName(props.directory, props.project.id, branch) ?? name }) - const open = createMemo(() => store.workspaceExpanded[props.directory] ?? true) + const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local()) + const boot = createMemo(() => open() || active()) const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0) const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length) const loadMore = async () => { @@ -1591,6 +1604,11 @@ export default function Layout(props: ParentProps) { if (editorOpen(`workspace:${props.directory}`)) closeEditor() } + createEffect(() => { + if (!boot()) return + globalSync.child(props.directory, { bootstrap: true }) + }) + const header = () => (