Compare commits

...
6 changed files with 68 additions and 52 deletions
@@ -64,6 +64,25 @@ describe("query keys", () => {
expect(calls.toSorted()).toEqual(["a", "b"])
})
test("refreshes shared project metadata after invalidation", async () => {
let name = "Before"
const projects = {
list: async () => [{ id: "a", canonical: "/a", name, time: { created: 1, updated: 1 }, sandboxes: [] }],
} as unknown as ProjectApi
const worktrees = {
list: async () => [{ directory: "/a" }],
} as unknown as WorktreeApi
const queryClient = new QueryClient()
const query = loadProjectsQuery(ServerScope.local, projects, worktrees)
expect((await queryClient.fetchQuery(query))[0]?.name).toBe("Before")
name = "After"
await queryClient.invalidateQueries({ queryKey: query.queryKey, exact: true })
expect((await queryClient.fetchQuery(query))[0]?.name).toBe("After")
})
test("keeps projects whose directory inventory cannot load", async () => {
const projects = {
list: async () => [
@@ -1,4 +1,4 @@
import type { Config, Path, Project, ProviderAuthResponse } from "@/runtime/server/types"
import type { Config, Path, Project } from "@/runtime/server/types"
import type {
LocationGetInput,
LocationGetOutput,
@@ -18,14 +18,6 @@ import type { ServerScope } from "@/runtime/server/scope"
import type { ServerApi } from "@/runtime/server/api"
import { sameDirectory } from "@/workspaces/paths"
type GlobalStore = {
path: Path
project: Project[]
provider_auth: ProviderAuthResponse
config: Config
reload: undefined | "pending" | "complete"
}
function waitForPaint() {
return new Promise<void>((resolve) => {
let done = false
@@ -110,16 +102,13 @@ export async function bootstrapGlobal(input: {
readonly worktree: WorktreeApi
}
scope: ServerScope
setGlobalStore: SetStoreFunction<GlobalStore>
queryClient: QueryClient
}) {
const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)),
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
() =>
input.queryClient
.fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project, input.serverAPI.worktree))
.then((data) => input.setGlobalStore("project", data)),
input.queryClient.fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project, input.serverAPI.worktree)),
]
await runAll(slow)
}
+15 -23
View File
@@ -5,7 +5,13 @@ import { getOwner, onCleanup, untrack } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/runtime/i18n/language"
import { type ServerSDK } from "./client"
import { bootstrapDirectory, bootstrapGlobal, loadGlobalConfigQuery, loadPathQuery } from "./global-sync/bootstrap"
import {
bootstrapDirectory,
bootstrapGlobal,
loadGlobalConfigQuery,
loadPathQuery,
loadProjectsQuery,
} from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import type { ProjectMeta } from "./global-sync/types"
import { formatServerError } from "@/runtime/server/errors"
@@ -61,8 +67,14 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
{ ...queryOptionsApi.path(), enabled: connected() },
],
}))
const projectQuery = useQuery(() => ({
...loadProjectsQuery(serverSDK.scope, serverSDK.api.project, serverSDK.api.worktree),
enabled: connected(),
}))
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
project: [],
get project() {
return projectQuery.data ?? []
},
provider_auth: {},
get path() {
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
@@ -79,17 +91,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
})
const queryClient = useQueryClient()
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
setGlobalStore("project", next)
}
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 bootstrap = useQuery(() => ({
queryKey: [serverSDK.scope, "bootstrap"],
@@ -97,7 +98,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
await bootstrapGlobal({
serverAPI: serverSDK.api,
scope: serverSDK.scope,
setGlobalStore: setBootStore,
queryClient,
})
return Date.now()
@@ -105,14 +105,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
enabled: connected(),
}))
const set = ((...input: unknown[]) => {
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
return input[1]
}
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
}) as typeof setGlobalStore
const paused = () => untrack(() => globalStore.reload) !== undefined
const queue = createRefreshQueue({
@@ -269,7 +261,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) {
return {
data: globalStore,
set,
set: setGlobalStore,
child: children.child,
disableMcp: children.disableMcp,
// bootstrap,
@@ -1,6 +1,6 @@
import { getFilename } from "@opencode-ai/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useMutation } from "@tanstack/solid-query"
import { useMutation, useQueryClient } from "@tanstack/solid-query"
import { createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobal } from "@/runtime/server/runtime"
@@ -10,6 +10,7 @@ import { ServerConnection } from "@/runtime/server/registry"
export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) {
const dialog = useDialog()
const global = useGlobal()
const queryClient = useQueryClient()
const serverCtx = createMemo(() => global.ensureServerCtx(props.server))
const folderName = createMemo(() => getFilename(props.project.worktree))
const defaultName = createMemo(() => props.project.name || folderName())
@@ -70,13 +71,19 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
const start = store.startup.trim()
if (props.project.id && props.project.id !== "global") {
const color = store.color ?? ""
const override = store.iconOverride ?? ""
const colorChanged = color !== (props.project.icon?.color ?? "")
const overrideChanged = override !== (props.project.icon?.override ?? "")
await serverCtx().sdk.api.project.update({
projectID: props.project.id,
name,
icon: { color: store.color ?? "", override: store.iconOverride ?? "" },
commands: { start },
...(name !== (props.project.name ?? "") ? { name } : {}),
...(colorChanged || overrideChanged
? { icon: { ...(colorChanged ? { color } : {}), ...(overrideChanged ? { override } : {}) } }
: {}),
...(start !== (props.project.commands?.start ?? "") ? { commands: { start } } : {}),
})
dialog.close()
return
}
@@ -85,6 +92,11 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
commands: { start: start || undefined },
})
},
onSuccess: async () => {
if (props.project.id && props.project.id !== "global") {
await queryClient.invalidateQueries({ queryKey: [serverCtx().sdk.scope, "project"], exact: true })
}
dialog.close()
},
}))
@@ -38,7 +38,7 @@ import {
} from "@/workspaces/paths"
import { listAllSessions } from "@/session/list"
import type { ServerScope } from "@/runtime/server/scope"
import { normalizeProjectInfo } from "@/runtime/server/global-sync/utils"
import { loadProjectsQuery } from "@/runtime/server/global-sync/bootstrap"
import "@/settings/settings.css"
type Workspace = {
@@ -59,16 +59,7 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string }> = (prop
})
const projectQuery = useQuery(() => ({
queryKey: [serverSDK.scope, "settings-workspace-projects"] as const,
queryFn: async () =>
Promise.all(
(await serverSDK.api.project.list()).map(async (project) => {
const worktrees = await serverSDK.api.worktree
.list({ projectID: project.id })
.catch(() => [{ directory: project.canonical }, ...project.sandboxes.map((directory) => ({ directory }))])
return normalizeProjectInfo({ ...project, worktrees })
}),
),
...loadProjectsQuery(serverSDK.scope, serverSDK.api.project, serverSDK.api.worktree),
refetchOnMount: "always",
}))
const workspaces = createMemo(() => workspaceInventory(projectQuery.data ?? []))
+13
View File
@@ -100,6 +100,19 @@ describe("Project.update", () => {
commands: { start: "bun install" },
})
expect(yield* project.update({ projectID: id, name: "Renamed" })).toMatchObject({
id,
name: "Renamed",
icon: { color: "blue", override: "data:image/png;base64,test" },
commands: { start: "bun install" },
})
expect(yield* project.update({ projectID: id, icon: { color: "green" } })).toMatchObject({
id,
name: "Renamed",
icon: { color: "green", override: "data:image/png;base64,test" },
commands: { start: "bun install" },
})
expect(
yield* project.update({
projectID: id,