app: start migrating bootstrap data fetching to TanStack Query (#22756)
This commit is contained in:
@@ -75,6 +75,7 @@
|
|||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"tw-animate-css": "1.4.0",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
@@ -597,6 +598,7 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "catalog:",
|
"solid-list": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
|
"tw-animate-css": "1.4.0",
|
||||||
"virtua": "catalog:",
|
"virtua": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4859,6 +4861,8 @@
|
|||||||
|
|
||||||
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
||||||
|
|
||||||
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
"tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="],
|
"tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
|
|||||||
@@ -121,10 +121,10 @@ function SessionProviders(props: ParentProps) {
|
|||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
<Suspense fallback={<Loading />}>
|
{/*<Suspense fallback={<Loading />}>*/}
|
||||||
{props.appChildren}
|
{props.appChildren}
|
||||||
{props.children}
|
{props.children}
|
||||||
</Suspense>
|
{/*</Suspense>*/}
|
||||||
</AppShellProviders>
|
</AppShellProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,14 +184,22 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Suspense
|
||||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/*<Show
|
||||||
|
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||||
|
fallback={
|
||||||
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>*/}
|
||||||
|
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||||
<Show
|
<Show
|
||||||
when={startupHealthCheck()}
|
when={startupHealthCheck()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -209,7 +217,8 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
{/*</Show>*/}
|
||||||
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
|||||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
|
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
@@ -100,6 +102,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
|||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const files = useFile()
|
const files = useFile()
|
||||||
@@ -1249,6 +1252,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||||
|
const agentsLoading = () => agentsQuery.isLoading
|
||||||
|
|
||||||
|
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||||
|
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||||
|
|
||||||
|
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||||
<PromptPopover
|
<PromptPopover
|
||||||
@@ -1444,7 +1455,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||||
<div class="size-4 shrink-0" />
|
<div class="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||||
|
<Show when={!agentsLoading()}>
|
||||||
<div data-component="prompt-agent-control">
|
<div data-component="prompt-agent-control">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -1468,6 +1480,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={!providersLoading()}>
|
||||||
<Show when={store.mode !== "shell"}>
|
<Show when={store.mode !== "shell"}>
|
||||||
<div data-component="prompt-model-control">
|
<div data-component="prompt-model-control">
|
||||||
<Show
|
<Show
|
||||||
@@ -1565,6 +1579,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { ProjectMeta } from "./global-sync/types"
|
|||||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||||
import { sanitizeProject } from "./global-sync/utils"
|
import { sanitizeProject } from "./global-sync/utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -41,6 +42,9 @@ type GlobalStore = {
|
|||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadSessionsQuery = (directory: string) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -67,6 +71,7 @@ function createGlobalSync() {
|
|||||||
config: {},
|
config: {},
|
||||||
reload: undefined,
|
reload: undefined,
|
||||||
})
|
})
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
let active = true
|
let active = true
|
||||||
let projectWritten = false
|
let projectWritten = false
|
||||||
@@ -198,7 +203,11 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||||
const promise = loadRootSessionsWithFallback({
|
const promise = queryClient
|
||||||
|
.ensureQueryData({
|
||||||
|
...loadSessionsQuery(directory),
|
||||||
|
queryFn: () =>
|
||||||
|
loadRootSessionsWithFallback({
|
||||||
directory,
|
directory,
|
||||||
limit,
|
limit,
|
||||||
list: (query) => globalSDK.client.session.list(query),
|
list: (query) => globalSDK.client.session.list(query),
|
||||||
@@ -235,6 +244,9 @@ function createGlobalSync() {
|
|||||||
description: formatServerError(err, language.t),
|
description: formatServerError(err, language.t),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.then(() => null),
|
||||||
|
})
|
||||||
|
.then(() => {})
|
||||||
|
|
||||||
sessionLoads.set(directory, promise)
|
sessionLoads.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
@@ -250,8 +262,9 @@ function createGlobalSync() {
|
|||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(directory)
|
children.pin(directory)
|
||||||
const promise = (async () => {
|
const promise = Promise.resolve().then(async () => {
|
||||||
const child = children.ensureChild(directory)
|
const child = children.ensureChild(directory)
|
||||||
|
child[1]("bootstrapPromise", promise!)
|
||||||
const cache = children.vcsCache.get(directory)
|
const cache = children.vcsCache.get(directory)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
@@ -269,8 +282,9 @@ function createGlobalSync() {
|
|||||||
vcsCache: cache,
|
vcsCache: cache,
|
||||||
loadSessions,
|
loadSessions,
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
|
queryClient,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})()
|
|
||||||
|
|
||||||
booting.set(directory, promise)
|
booting.set(directory, promise)
|
||||||
void promise.finally(() => {
|
void promise.finally(() => {
|
||||||
@@ -346,6 +360,7 @@ function createGlobalSync() {
|
|||||||
translate: language.t,
|
translate: language.t,
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
setGlobalStore: setBootStore,
|
setGlobalStore: setBootStore,
|
||||||
|
queryClient,
|
||||||
})
|
})
|
||||||
bootedAt = Date.now()
|
bootedAt = Date.now()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
|||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||||
|
import { loadSessionsQuery } from "../global-sync"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -71,6 +73,7 @@ export async function bootstrapGlobal(input: {
|
|||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
formatMoreCount: (count: number) => string
|
formatMoreCount: (count: number) => string
|
||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const fast = [
|
const fast = [
|
||||||
() =>
|
() =>
|
||||||
@@ -80,11 +83,16 @@ export async function bootstrapGlobal(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() =>
|
() =>
|
||||||
|
input.queryClient.fetchQuery({
|
||||||
|
...loadProvidersQuery(null),
|
||||||
|
queryFn: () =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.globalSDK.provider.list().then((x) => {
|
input.globalSDK.provider.list().then((x) => {
|
||||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
|
return null
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
const slow = [
|
const slow = [
|
||||||
@@ -172,6 +180,12 @@ function warmSessions(input: {
|
|||||||
).then(() => undefined)
|
).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadProvidersQuery = (directory: string | null) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||||
|
|
||||||
|
export const loadAgentsQuery = (directory: string | null) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
sdk: OpencodeClient
|
sdk: OpencodeClient
|
||||||
@@ -186,6 +200,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
project: Project[]
|
project: Project[]
|
||||||
provider: ProviderListResponse
|
provider: ProviderListResponse
|
||||||
}
|
}
|
||||||
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const loading = input.store.status !== "complete"
|
const loading = input.store.status !== "complete"
|
||||||
const seededProject = projectID(input.directory, input.global.project)
|
const seededProject = projectID(input.directory, input.global.project)
|
||||||
@@ -207,13 +222,31 @@ export async function bootstrapDirectory(input: {
|
|||||||
input.setStore("lsp", [])
|
input.setStore("lsp", [])
|
||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const fast = [
|
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
|
||||||
|
const errs = errors(await runAll(fast))
|
||||||
|
if (errs.length > 0) {
|
||||||
|
console.error("Failed to bootstrap instance", errs[0])
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(errs[0], input.translate),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
const slow = [
|
||||||
|
() =>
|
||||||
|
input.queryClient.ensureQueryData({
|
||||||
|
...loadAgentsQuery(input.directory),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||||
|
() => null,
|
||||||
|
),
|
||||||
|
}),
|
||||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
]
|
|
||||||
|
|
||||||
const slow = [
|
|
||||||
() =>
|
() =>
|
||||||
seededProject
|
seededProject
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
@@ -299,17 +332,6 @@ export async function bootstrapDirectory(input: {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
const errs = errors(await runAll(fast))
|
|
||||||
if (errs.length > 0) {
|
|
||||||
console.error("Failed to bootstrap instance", errs[0])
|
|
||||||
const project = getFilename(input.directory)
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
|
||||||
description: formatServerError(errs[0], input.translate),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitForPaint()
|
await waitForPaint()
|
||||||
const slowErrs = errors(await runAll(slow))
|
const slowErrs = errors(await runAll(slow))
|
||||||
if (slowErrs.length > 0) {
|
if (slowErrs.length > 0) {
|
||||||
@@ -326,15 +348,17 @@ export async function bootstrapDirectory(input: {
|
|||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
providerRev.set(input.directory, rev)
|
providerRev.set(input.directory, rev)
|
||||||
void retry(() => input.sdk.provider.list())
|
void input.queryClient.ensureQueryData({
|
||||||
|
...loadSessionsQuery(input.directory),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() => input.sdk.provider.list())
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
if (providerRev.get(input.directory) !== rev) return
|
if (providerRev.get(input.directory) !== rev) return
|
||||||
input.setStore("provider", normalizeProviderList(x.data!))
|
input.setStore("provider", normalizeProviderList(x.data!))
|
||||||
input.setStore("provider_ready", true)
|
input.setStore("provider_ready", true)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (providerRev.get(input.directory) !== rev) return
|
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||||
console.error("Failed to refresh provider list", err)
|
|
||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -342,4 +366,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
description: formatServerError(err, input.translate),
|
description: formatServerError(err, input.translate),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.then(() => null),
|
||||||
|
})
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export function createChildStoreManager(input: {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
|
bootstrapPromise: Promise.resolve(),
|
||||||
})
|
})
|
||||||
children[directory] = child
|
children[directory] = child
|
||||||
disposers.set(directory, dispose)
|
disposers.set(directory, dispose)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export type State = {
|
|||||||
part: {
|
part: {
|
||||||
[messageID: string]: Part[]
|
[messageID: string]: Part[]
|
||||||
}
|
}
|
||||||
|
bootstrapPromise: Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VcsCache = {
|
export type VcsCache = {
|
||||||
|
|||||||
@@ -132,9 +132,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!slug) return { slug, dir: "" }
|
if (!slug) return { slug, dir: "" }
|
||||||
const dir = decode64(slug)
|
const dir = decode64(slug)
|
||||||
if (!dir) return { slug, dir: "" }
|
if (!dir) return { slug, dir: "" }
|
||||||
|
const store = globalSync.peek(dir, { bootstrap: false })
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
|
store,
|
||||||
|
dir: store[0].path.directory || dir,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
||||||
@@ -2353,8 +2355,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [loading] = createResource(
|
||||||
|
() => route()?.store?.[0]?.bootstrapPromise,
|
||||||
|
(p) => p,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
|
{(autoselecting(), loading()) ?? ""}
|
||||||
<Titlebar />
|
<Titlebar />
|
||||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { type LocalProject } from "@/context/layout"
|
import { type LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||||
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
|
|
||||||
type InlineEditorComponent = (props: {
|
type InlineEditorComponent = (props: {
|
||||||
id: string
|
id: string
|
||||||
@@ -454,7 +455,8 @@ export const LocalWorkspace = (props: {
|
|||||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||||
const count = createMemo(() => sessions()?.length ?? 0)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
const loading = createMemo(() => !booted() && count() === 0)
|
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||||
|
const loading = createMemo(() => query.isPending && count() === 0)
|
||||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
@@ -471,7 +473,7 @@ export const LocalWorkspace = (props: {
|
|||||||
mobile={props.mobile}
|
mobile={props.mobile}
|
||||||
ctx={props.ctx}
|
ctx={props.ctx}
|
||||||
showNew={() => false}
|
showNew={() => false}
|
||||||
loading={loading}
|
loading={() => query.isLoading}
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
untrack,
|
untrack,
|
||||||
|
createResource,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
@@ -804,8 +805,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||||
|
|
||||||
createEffect(
|
const [sessionSync] = createResource(
|
||||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
() => [sdk.directory, params.id] as const,
|
||||||
|
([directory, id]) => {
|
||||||
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
||||||
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
@@ -816,13 +818,10 @@ export default function Page() {
|
|||||||
const stale = !cached
|
const stale = !cached
|
||||||
? false
|
? false
|
||||||
: (() => {
|
: (() => {
|
||||||
const info = getSessionPrefetch(sdk.directory, id)
|
const info = getSessionPrefetch(directory, id)
|
||||||
if (!info) return true
|
if (!info) return true
|
||||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||||
})()
|
})()
|
||||||
untrack(() => {
|
|
||||||
void sync.session.sync(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
refreshFrame = requestAnimationFrame(() => {
|
refreshFrame = requestAnimationFrame(() => {
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
@@ -834,7 +833,9 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
|
return sync.session.sync(id)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -1881,6 +1882,7 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||||
|
{sessionSync() ?? ""}
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||||
<Show when={!isDesktop() && !!params.id}>
|
<Show when={!isDesktop() && !!params.id}>
|
||||||
|
|||||||
Reference in New Issue
Block a user