fix(app): session loading loop
This commit is contained in:
@@ -45,6 +45,8 @@ export function SessionHeader() {
|
|||||||
|
|
||||||
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
|
||||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
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 sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const view = createMemo(() => layout.view(sessionKey()))
|
const view = createMemo(() => layout.view(sessionKey()))
|
||||||
|
|
||||||
@@ -172,12 +174,14 @@ export function SessionHeader() {
|
|||||||
{/* <SessionMcpIndicator /> */}
|
{/* <SessionMcpIndicator /> */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Show when={currentSession()?.summary?.files}>
|
<div
|
||||||
<TooltipKeybind
|
class="hidden md:block shrink-0"
|
||||||
class="hidden md:block shrink-0"
|
classList={{
|
||||||
title="Toggle review"
|
"opacity-0 pointer-events-none": !showReview(),
|
||||||
keybind={command.keybind("review.toggle")}
|
}}
|
||||||
>
|
aria-hidden={!showReview()}
|
||||||
|
>
|
||||||
|
<TooltipKeybind title="Toggle review" keybind={command.keybind("review.toggle")}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="group/review-toggle size-6 p-0"
|
class="group/review-toggle size-6 p-0"
|
||||||
@@ -202,7 +206,7 @@ export function SessionHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</div>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
class="hidden md:block shrink-0"
|
class="hidden md:block shrink-0"
|
||||||
title="Toggle terminal"
|
title="Toggle terminal"
|
||||||
@@ -233,8 +237,13 @@ export function SessionHeader() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
<Show when={shareEnabled() && currentSession()}>
|
<div
|
||||||
<div class="flex items-center">
|
class="flex items-center"
|
||||||
|
classList={{
|
||||||
|
"opacity-0 pointer-events-none": !showShare(),
|
||||||
|
}}
|
||||||
|
aria-hidden={!showShare()}
|
||||||
|
>
|
||||||
<Popover
|
<Popover
|
||||||
title="Publish on web"
|
title="Publish on web"
|
||||||
description={
|
description={
|
||||||
@@ -308,8 +317,7 @@ export function SessionHeader() {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ type VcsCache = {
|
|||||||
ready: Accessor<boolean>
|
ready: Accessor<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChildOptions = {
|
||||||
|
bootstrap?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
@@ -127,8 +131,10 @@ function createGlobalSync() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
|
const booting = new Map<string, Promise<void>>()
|
||||||
|
const sessionLoads = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
function child(directory: string) {
|
function ensureChild(directory: string) {
|
||||||
if (!directory) console.error("No directory provided")
|
if (!directory) console.error("No directory provided")
|
||||||
if (!children[directory]) {
|
if (!children[directory]) {
|
||||||
const cache = runWithOwner(owner, () =>
|
const cache = runWithOwner(owner, () =>
|
||||||
@@ -163,7 +169,6 @@ function createGlobalSync() {
|
|||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
})
|
})
|
||||||
bootstrapInstance(directory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runWithOwner(owner, init)
|
runWithOwner(owner, init)
|
||||||
@@ -173,11 +178,23 @@ function createGlobalSync() {
|
|||||||
return childStore
|
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) {
|
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
|
const limit = store.limit
|
||||||
|
|
||||||
return globalSDK.client.session
|
const promise = globalSDK.client.session
|
||||||
.list({ directory, roots: true })
|
.list({ directory, roots: true })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const nonArchived = (x.data ?? [])
|
const nonArchived = (x.data ?? [])
|
||||||
@@ -208,13 +225,23 @@ function createGlobalSync() {
|
|||||||
const project = getFilename(directory)
|
const project = getFilename(directory)
|
||||||
showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
|
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) {
|
async function bootstrapInstance(directory: string) {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const [store, setStore] = child(directory)
|
const pending = booting.get(directory)
|
||||||
const cache = vcsCache.get(directory)
|
if (pending) return pending
|
||||||
if (!cache) return
|
|
||||||
|
const promise = (async () => {
|
||||||
|
const [store, setStore] = ensureChild(directory)
|
||||||
|
const cache = vcsCache.get(directory)
|
||||||
|
if (!cache) return
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: globalSDK.url,
|
baseUrl: globalSDK.url,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
@@ -250,98 +277,105 @@ function createGlobalSync() {
|
|||||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
|
await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to bootstrap instance", err)
|
console.error("Failed to bootstrap instance", err)
|
||||||
const project = getFilename(directory)
|
const project = getFilename(directory)
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
showToast({ title: `Failed to reload ${project}`, description: message })
|
showToast({ title: `Failed to reload ${project}`, description: message })
|
||||||
setStore("status", "partial")
|
setStore("status", "partial")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.status !== "complete") setStore("status", "partial")
|
if (store.status !== "complete") setStore("status", "partial")
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
sdk.path.get().then((x) => setStore("path", x.data!)),
|
sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||||
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||||
sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||||
loadSessions(directory),
|
loadSessions(directory),
|
||||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||||
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||||
sdk.vcs.get().then((x) => {
|
sdk.vcs.get().then((x) => {
|
||||||
const next = x.data ?? store.vcs
|
const next = x.data ?? store.vcs
|
||||||
setStore("vcs", next)
|
setStore("vcs", next)
|
||||||
if (next?.branch) cache.setStore("value", next)
|
if (next?.branch) cache.setStore("value", next)
|
||||||
}),
|
}),
|
||||||
sdk.permission.list().then((x) => {
|
sdk.permission.list().then((x) => {
|
||||||
const grouped: Record<string, PermissionRequest[]> = {}
|
const grouped: Record<string, PermissionRequest[]> = {}
|
||||||
for (const perm of x.data ?? []) {
|
for (const perm of x.data ?? []) {
|
||||||
if (!perm?.id || !perm.sessionID) continue
|
if (!perm?.id || !perm.sessionID) continue
|
||||||
const existing = grouped[perm.sessionID]
|
const existing = grouped[perm.sessionID]
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.push(perm)
|
existing.push(perm)
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
grouped[perm.sessionID] = [perm]
|
||||||
}
|
}
|
||||||
grouped[perm.sessionID] = [perm]
|
|
||||||
}
|
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
for (const sessionID of Object.keys(store.permission)) {
|
for (const sessionID of Object.keys(store.permission)) {
|
||||||
if (grouped[sessionID]) continue
|
if (grouped[sessionID]) continue
|
||||||
setStore("permission", sessionID, [])
|
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<string, QuestionRequest[]> = {}
|
||||||
|
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<string, QuestionRequest[]> = {}
|
|
||||||
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(() => {
|
batch(() => {
|
||||||
for (const sessionID of Object.keys(store.question)) {
|
for (const sessionID of Object.keys(store.question)) {
|
||||||
if (grouped[sessionID]) continue
|
if (grouped[sessionID]) continue
|
||||||
setStore("question", sessionID, [])
|
setStore("question", sessionID, [])
|
||||||
}
|
}
|
||||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
setStore(
|
setStore(
|
||||||
"question",
|
"question",
|
||||||
sessionID,
|
sessionID,
|
||||||
reconcile(
|
reconcile(
|
||||||
questions
|
questions
|
||||||
.filter((q) => !!q?.id)
|
.filter((q) => !!q?.id)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
{ key: "id" },
|
{ key: "id" },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
booting.set(directory, promise)
|
||||||
|
promise.finally(() => {
|
||||||
|
booting.delete(directory)
|
||||||
})
|
})
|
||||||
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsub = globalSDK.event.listen((e) => {
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
|
|||||||
@@ -563,9 +563,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!project) return [] as Session[]
|
if (!project) return [] as Session[]
|
||||||
if (workspaceSetting()) {
|
if (workspaceSetting()) {
|
||||||
const dirs = workspaceIds(project)
|
const dirs = workspaceIds(project)
|
||||||
|
const activeDir = params.dir ? base64Decode(params.dir) : ""
|
||||||
const result: Session[] = []
|
const result: Session[] = []
|
||||||
for (const dir of dirs) {
|
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
|
const dirSessions = dirStore.session
|
||||||
.filter((session) => session.directory === dirStore.path.directory)
|
.filter((session) => session.directory === dirStore.path.directory)
|
||||||
.filter((session) => !session.parentID && !session.time?.archived)
|
.filter((session) => !session.parentID && !session.time?.archived)
|
||||||
@@ -1238,8 +1242,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!project) return
|
if (!project) return
|
||||||
|
|
||||||
if (workspaceSetting()) {
|
if (workspaceSetting()) {
|
||||||
|
const activeDir = params.dir ? base64Decode(params.dir) : ""
|
||||||
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
|
||||||
for (const directory of dirs) {
|
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)
|
globalSync.project.loadSessions(directory)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -1558,7 +1566,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||||
const sortable = createSortable(props.directory)
|
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 [menuOpen, setMenuOpen] = createSignal(false)
|
||||||
const [pendingRename, setPendingRename] = createSignal(false)
|
const [pendingRename, setPendingRename] = createSignal(false)
|
||||||
const slug = createMemo(() => base64Encode(props.directory))
|
const slug = createMemo(() => base64Encode(props.directory))
|
||||||
@@ -1569,12 +1577,17 @@ export default function Layout(props: ParentProps) {
|
|||||||
.toSorted(sortSessions),
|
.toSorted(sortSessions),
|
||||||
)
|
)
|
||||||
const local = createMemo(() => props.directory === props.project.worktree)
|
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 workspaceValue = createMemo(() => {
|
||||||
const branch = workspaceStore.vcs?.branch
|
const branch = workspaceStore.vcs?.branch
|
||||||
const name = branch ?? getFilename(props.directory)
|
const name = branch ?? getFilename(props.directory)
|
||||||
return workspaceName(props.directory, props.project.id, branch) ?? name
|
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 loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
|
||||||
const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length)
|
const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length)
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
@@ -1591,6 +1604,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (editorOpen(`workspace:${props.directory}`)) closeEditor()
|
if (editorOpen(`workspace:${props.directory}`)) closeEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!boot()) return
|
||||||
|
globalSync.child(props.directory, { bootstrap: true })
|
||||||
|
})
|
||||||
|
|
||||||
const header = () => (
|
const header = () => (
|
||||||
<div class="flex items-center gap-1 min-w-0 flex-1">
|
<div class="flex items-center gap-1 min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-center shrink-0 size-6">
|
<div class="flex items-center justify-center shrink-0 size-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user