Skip to content

Commit 8595dae

Browse files
committed
fix(app): session loading loop
1 parent c365f0a commit 8595dae

File tree

3 files changed

+167
-107
lines changed

3 files changed

+167
-107
lines changed

packages/app/src/components/session/session-header.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export function SessionHeader() {
4545

4646
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
4747
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
48+
const showReview = createMemo(() => !!currentSession()?.summary?.files)
49+
const showShare = createMemo(() => shareEnabled() && !!currentSession())
4850
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
4951
const view = createMemo(() => layout.view(sessionKey()))
5052

@@ -172,12 +174,14 @@ export function SessionHeader() {
172174
{/* <SessionMcpIndicator /> */}
173175
{/* </div> */}
174176
<div class="flex items-center gap-1">
175-
<Show when={currentSession()?.summary?.files}>
176-
<TooltipKeybind
177-
class="hidden md:block shrink-0"
178-
title="Toggle review"
179-
keybind={command.keybind("review.toggle")}
180-
>
177+
<div
178+
class="hidden md:block shrink-0"
179+
classList={{
180+
"opacity-0 pointer-events-none": !showReview(),
181+
}}
182+
aria-hidden={!showReview()}
183+
>
184+
<TooltipKeybind title="Toggle review" keybind={command.keybind("review.toggle")}>
181185
<Button
182186
variant="ghost"
183187
class="group/review-toggle size-6 p-0"
@@ -202,7 +206,7 @@ export function SessionHeader() {
202206
</div>
203207
</Button>
204208
</TooltipKeybind>
205-
</Show>
209+
</div>
206210
<TooltipKeybind
207211
class="hidden md:block shrink-0"
208212
title="Toggle terminal"
@@ -233,8 +237,13 @@ export function SessionHeader() {
233237
</Button>
234238
</TooltipKeybind>
235239
</div>
236-
<Show when={shareEnabled() && currentSession()}>
237-
<div class="flex items-center">
240+
<div
241+
class="flex items-center"
242+
classList={{
243+
"opacity-0 pointer-events-none": !showShare(),
244+
}}
245+
aria-hidden={!showShare()}
246+
>
238247
<Popover
239248
title="Publish on web"
240249
description={
@@ -308,8 +317,7 @@ export function SessionHeader() {
308317
/>
309318
</Tooltip>
310319
</Show>
311-
</div>
312-
</Show>
320+
</div>
313321
</div>
314322
</Portal>
315323
)}

packages/app/src/context/global-sync.tsx

Lines changed: 127 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ type VcsCache = {
8888
ready: Accessor<boolean>
8989
}
9090

91+
type ChildOptions = {
92+
bootstrap?: boolean
93+
}
94+
9195
function createGlobalSync() {
9296
const globalSDK = useGlobalSDK()
9397
const platform = usePlatform()
@@ -127,8 +131,10 @@ function createGlobalSync() {
127131
})
128132

129133
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
134+
const booting = new Map<string, Promise<void>>()
135+
const sessionLoads = new Map<string, Promise<void>>()
130136

131-
function child(directory: string) {
137+
function ensureChild(directory: string) {
132138
if (!directory) console.error("No directory provided")
133139
if (!children[directory]) {
134140
const cache = runWithOwner(owner, () =>
@@ -163,7 +169,6 @@ function createGlobalSync() {
163169
message: {},
164170
part: {},
165171
})
166-
bootstrapInstance(directory)
167172
}
168173

169174
runWithOwner(owner, init)
@@ -173,11 +178,23 @@ function createGlobalSync() {
173178
return childStore
174179
}
175180

181+
function child(directory: string, options: ChildOptions = {}) {
182+
const childStore = ensureChild(directory)
183+
const shouldBootstrap = options.bootstrap ?? true
184+
if (shouldBootstrap && childStore[0].status === "loading") {
185+
void bootstrapInstance(directory)
186+
}
187+
return childStore
188+
}
189+
176190
async function loadSessions(directory: string) {
177-
const [store, setStore] = child(directory)
191+
const pending = sessionLoads.get(directory)
192+
if (pending) return pending
193+
194+
const [store, setStore] = child(directory, { bootstrap: false })
178195
const limit = store.limit
179196

180-
return globalSDK.client.session
197+
const promise = globalSDK.client.session
181198
.list({ directory, roots: true })
182199
.then((x) => {
183200
const nonArchived = (x.data ?? [])
@@ -208,13 +225,23 @@ function createGlobalSync() {
208225
const project = getFilename(directory)
209226
showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
210227
})
228+
229+
sessionLoads.set(directory, promise)
230+
promise.finally(() => {
231+
sessionLoads.delete(directory)
232+
})
233+
return promise
211234
}
212235

213236
async function bootstrapInstance(directory: string) {
214237
if (!directory) return
215-
const [store, setStore] = child(directory)
216-
const cache = vcsCache.get(directory)
217-
if (!cache) return
238+
const pending = booting.get(directory)
239+
if (pending) return pending
240+
241+
const promise = (async () => {
242+
const [store, setStore] = ensureChild(directory)
243+
const cache = vcsCache.get(directory)
244+
if (!cache) return
218245
const sdk = createOpencodeClient({
219246
baseUrl: globalSDK.url,
220247
fetch: platform.fetch,
@@ -250,98 +277,105 @@ function createGlobalSync() {
250277
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
251278
}
252279

253-
try {
254-
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
255-
} catch (err) {
256-
console.error("Failed to bootstrap instance", err)
257-
const project = getFilename(directory)
258-
const message = err instanceof Error ? err.message : String(err)
259-
showToast({ title: `Failed to reload ${project}`, description: message })
260-
setStore("status", "partial")
261-
return
262-
}
280+
try {
281+
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
282+
} catch (err) {
283+
console.error("Failed to bootstrap instance", err)
284+
const project = getFilename(directory)
285+
const message = err instanceof Error ? err.message : String(err)
286+
showToast({ title: `Failed to reload ${project}`, description: message })
287+
setStore("status", "partial")
288+
return
289+
}
263290

264-
if (store.status !== "complete") setStore("status", "partial")
265-
266-
Promise.all([
267-
sdk.path.get().then((x) => setStore("path", x.data!)),
268-
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
269-
sdk.session.status().then((x) => setStore("session_status", x.data!)),
270-
loadSessions(directory),
271-
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
272-
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
273-
sdk.vcs.get().then((x) => {
274-
const next = x.data ?? store.vcs
275-
setStore("vcs", next)
276-
if (next?.branch) cache.setStore("value", next)
277-
}),
278-
sdk.permission.list().then((x) => {
279-
const grouped: Record<string, PermissionRequest[]> = {}
280-
for (const perm of x.data ?? []) {
281-
if (!perm?.id || !perm.sessionID) continue
282-
const existing = grouped[perm.sessionID]
283-
if (existing) {
284-
existing.push(perm)
285-
continue
291+
if (store.status !== "complete") setStore("status", "partial")
292+
293+
Promise.all([
294+
sdk.path.get().then((x) => setStore("path", x.data!)),
295+
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
296+
sdk.session.status().then((x) => setStore("session_status", x.data!)),
297+
loadSessions(directory),
298+
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
299+
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
300+
sdk.vcs.get().then((x) => {
301+
const next = x.data ?? store.vcs
302+
setStore("vcs", next)
303+
if (next?.branch) cache.setStore("value", next)
304+
}),
305+
sdk.permission.list().then((x) => {
306+
const grouped: Record<string, PermissionRequest[]> = {}
307+
for (const perm of x.data ?? []) {
308+
if (!perm?.id || !perm.sessionID) continue
309+
const existing = grouped[perm.sessionID]
310+
if (existing) {
311+
existing.push(perm)
312+
continue
313+
}
314+
grouped[perm.sessionID] = [perm]
286315
}
287-
grouped[perm.sessionID] = [perm]
288-
}
289316

290-
batch(() => {
291-
for (const sessionID of Object.keys(store.permission)) {
292-
if (grouped[sessionID]) continue
293-
setStore("permission", sessionID, [])
294-
}
295-
for (const [sessionID, permissions] of Object.entries(grouped)) {
296-
setStore(
297-
"permission",
298-
sessionID,
299-
reconcile(
300-
permissions
301-
.filter((p) => !!p?.id)
302-
.slice()
303-
.sort((a, b) => a.id.localeCompare(b.id)),
304-
{ key: "id" },
305-
),
306-
)
307-
}
308-
})
309-
}),
310-
sdk.question.list().then((x) => {
311-
const grouped: Record<string, QuestionRequest[]> = {}
312-
for (const question of x.data ?? []) {
313-
if (!question?.id || !question.sessionID) continue
314-
const existing = grouped[question.sessionID]
315-
if (existing) {
316-
existing.push(question)
317-
continue
317+
batch(() => {
318+
for (const sessionID of Object.keys(store.permission)) {
319+
if (grouped[sessionID]) continue
320+
setStore("permission", sessionID, [])
321+
}
322+
for (const [sessionID, permissions] of Object.entries(grouped)) {
323+
setStore(
324+
"permission",
325+
sessionID,
326+
reconcile(
327+
permissions
328+
.filter((p) => !!p?.id)
329+
.slice()
330+
.sort((a, b) => a.id.localeCompare(b.id)),
331+
{ key: "id" },
332+
),
333+
)
334+
}
335+
})
336+
}),
337+
sdk.question.list().then((x) => {
338+
const grouped: Record<string, QuestionRequest[]> = {}
339+
for (const question of x.data ?? []) {
340+
if (!question?.id || !question.sessionID) continue
341+
const existing = grouped[question.sessionID]
342+
if (existing) {
343+
existing.push(question)
344+
continue
345+
}
346+
grouped[question.sessionID] = [question]
318347
}
319-
grouped[question.sessionID] = [question]
320-
}
321348

322-
batch(() => {
323-
for (const sessionID of Object.keys(store.question)) {
324-
if (grouped[sessionID]) continue
325-
setStore("question", sessionID, [])
326-
}
327-
for (const [sessionID, questions] of Object.entries(grouped)) {
328-
setStore(
329-
"question",
330-
sessionID,
331-
reconcile(
332-
questions
333-
.filter((q) => !!q?.id)
334-
.slice()
335-
.sort((a, b) => a.id.localeCompare(b.id)),
336-
{ key: "id" },
337-
),
338-
)
339-
}
340-
})
341-
}),
342-
]).then(() => {
343-
setStore("status", "complete")
349+
batch(() => {
350+
for (const sessionID of Object.keys(store.question)) {
351+
if (grouped[sessionID]) continue
352+
setStore("question", sessionID, [])
353+
}
354+
for (const [sessionID, questions] of Object.entries(grouped)) {
355+
setStore(
356+
"question",
357+
sessionID,
358+
reconcile(
359+
questions
360+
.filter((q) => !!q?.id)
361+
.slice()
362+
.sort((a, b) => a.id.localeCompare(b.id)),
363+
{ key: "id" },
364+
),
365+
)
366+
}
367+
})
368+
}),
369+
]).then(() => {
370+
setStore("status", "complete")
371+
})
372+
})()
373+
374+
booting.set(directory, promise)
375+
promise.finally(() => {
376+
booting.delete(directory)
344377
})
378+
return promise
345379
}
346380

347381
const unsub = globalSDK.event.listen((e) => {

0 commit comments

Comments
 (0)