Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions packages/app/src/components/dialog-edit-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ import { getFilename } from "@opencode-ai/core/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
import { useLanguage } from "@/context/language"
import { getProjectAvatarSource } from "@/pages/layout/helpers"
import { ServerConnection } from "@/context/server"
import { useGlobal } from "@/context/global"

const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const

export function DialogEditProject(props: { project: LocalProject }) {
export function DialogEditProject(props: { project: LocalProject; server?: ServerConnection.Any }) {
const dialog = useDialog()
const serverSDK = useServerSDK()
const serverSync = useServerSync()
const activeServerSDK = useServerSDK()
const activeServerSync = useServerSync()
const global = useGlobal()
const language = useLanguage()
const serverCtx = createMemo(() => (props.server ? global.createServerCtx(props.server) : undefined))
const serverSDK = () => serverCtx()?.sdk ?? activeServerSDK
const serverSync = () => serverCtx()?.sync ?? activeServerSync

const folderName = createMemo(() => getFilename(props.project.worktree))
const defaultName = createMemo(() => props.project.name || folderName())
Expand Down Expand Up @@ -78,19 +84,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
const start = store.startup.trim()

if (props.project.id && props.project.id !== "global") {
await serverSDK.client.project.update({
await serverSDK().client.project.update({
projectID: props.project.id,
directory: props.project.worktree,
name,
icon: { color: store.color || "", override: store.iconOverride || "" },
commands: { start },
})
serverSync.project.icon(props.project.worktree, store.iconOverride || undefined)
serverSync().project.icon(props.project.worktree, store.iconOverride || undefined)
dialog.close()
return
}

serverSync.project.meta(props.project.worktree, {
serverSync().project.meta(props.project.worktree, {
name,
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
commands: { start: start || undefined },
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/components/prompt-input/submit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ beforeAll(async () => {
}),
}))

mock.module("@/context/server-sdk", () => ({
useServerSDK: () => ({ scope: "local" }),
}))

mock.module("@/context/platform", () => ({
usePlatform: () => ({
fetch: fetch,
Expand Down
21 changes: 13 additions & 8 deletions packages/app/src/components/prompt-input/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { Worktree as WorktreeState } from "@/utils/worktree"
import { buildRequestParts } from "./build-request-parts"
import { setCursorPosition } from "./editor-dom"
import { formatServerError } from "@/utils/server-errors"
import { useServerSDK } from "@/context/server-sdk"
import { ScopedKey } from "@/utils/server-scope"

type PendingPrompt = {
abort: AbortController
Expand Down Expand Up @@ -206,12 +208,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const sdk = useSDK()
const sync = useSync()
const serverSync = useServerSync()
const serverSDK = useServerSDK()
const local = useLocal()
const permission = usePermission()
const prompt = usePrompt()
const layout = useLayout()
const language = useLanguage()
const params = useParams()
const pendingKey = (sessionID: string) => ScopedKey.from(serverSDK.scope, sessionID)

const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "data" in err) {
Expand All @@ -232,11 +236,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {

input.onAbort?.()

const queued = pending.get(sessionID)
const key = pendingKey(sessionID)
const queued = pending.get(key)
if (queued) {
queued.abort.abort()
queued.cleanup()
pending.delete(sessionID)
pending.delete(key)
return Promise.resolve()
}
return sdk.client.session
Expand Down Expand Up @@ -341,7 +346,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
})
return
}
WorktreeState.pending(createdWorktree.directory)
WorktreeState.pending(serverSDK.scope, createdWorktree.directory)
sessionDirectory = createdWorktree.directory
}

Expand Down Expand Up @@ -500,7 +505,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
clearInput()

const waitForWorktree = async () => {
const worktree = WorktreeState.get(sessionDirectory)
const worktree = WorktreeState.get(serverSDK.scope, sessionDirectory)
if (!worktree || worktree.status !== "pending") return true

if (sessionDirectory === projectDirectory) {
Expand All @@ -517,7 +522,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
restoreInput()
}

pending.set(session.id, { abort: controller, cleanup })
pending.set(pendingKey(session.id), { abort: controller, cleanup })

const abortWait = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
if (controller.signal.aborted) {
Expand All @@ -544,11 +549,11 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}, timeoutMs)
})

const result = await Promise.race([WorktreeState.wait(sessionDirectory), abortWait, timeout]).finally(() => {
const result = await Promise.race([WorktreeState.wait(serverSDK.scope, sessionDirectory), abortWait, timeout]).finally(() => {
if (timer.id === undefined) return
clearTimeout(timer.id)
})
pending.delete(session.id)
pending.delete(pendingKey(session.id))
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)
return true
Expand All @@ -563,7 +568,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
optimisticBusy: sessionDirectory === projectDirectory,
before: waitForWorktree,
}).catch((err) => {
pending.delete(session.id)
pending.delete(pendingKey(session.id))
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" })
}
Expand Down
9 changes: 6 additions & 3 deletions packages/app/src/context/comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { createStore, reconcile, type SetStoreFunction, type Store } from "solid
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useParams } from "@solidjs/router"
import { Persist, persisted } from "@/utils/persist"
import { useServerSDK } from "./server-sdk"
import type { ServerScope } from "@/utils/server-scope"
import { createScopedCache } from "@/utils/scoped-cache"
import { uuid } from "@/utils/uuid"
import type { SelectedLineRange } from "@/context/file"
Expand Down Expand Up @@ -166,11 +168,11 @@ export function createCommentSessionForTest(comments: Record<string, LineComment
return createCommentSessionState(store, setStore)
}

function createCommentSession(dir: string, id: string | undefined) {
function createCommentSession(scope: ServerScope, dir: string, id: string | undefined) {
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`

const [store, setStore, _, ready] = persisted(
Persist.scoped(dir, id, "comments", [legacy]),
Persist.serverScoped(scope, dir, id, "comments", [legacy]),
createStore<CommentStore>({
comments: {},
}),
Expand Down Expand Up @@ -200,11 +202,12 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
gate: false,
init: () => {
const params = useParams()
const serverSDK = useServerSDK()
const cache = createScopedCache(
(key) => {
const decoded = decodeSessionKey(key)
return createRoot((dispose) => ({
value: createCommentSession(decoded.dir, decoded.id === WORKSPACE_KEY ? undefined : decoded.id),
value: createCommentSession(serverSDK.scope, decoded.dir, decoded.id === WORKSPACE_KEY ? undefined : decoded.id),
dispose,
}))
},
Expand Down
9 changes: 5 additions & 4 deletions packages/app/src/context/directory-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t

const evict = (directory: string, setStore: Setter, sessionIDs: string[]) => {
if (sessionIDs.length === 0) return
clearSessionPrefetch(directory, sessionIDs)
clearSessionPrefetch(serverSDK.scope, directory, sessionIDs)
for (const sessionID of sessionIDs) {
serverSync.todo.set(sessionID, undefined)
}
Expand Down Expand Up @@ -345,6 +345,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
setMeta("cursor", key, next.cursor)
setMeta("complete", key, next.complete)
setSessionPrefetch({
scope: serverSDK.scope,
directory: input.directory,
sessionID: input.sessionID,
limit: message.length,
Expand Down Expand Up @@ -435,7 +436,7 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t

touch(directory, setStore, sessionID)

const seeded = getSessionPrefetch(directory, sessionID)
const seeded = getSessionPrefetch(serverSDK.scope, directory, sessionID)
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
batch(() => {
setMeta("limit", key, seeded.limit)
Expand All @@ -446,10 +447,10 @@ export const createDirSyncContext = (directory: string, serverSync: ReturnType<t
}

return runInflight(inflight, key, async () => {
const pending = getSessionPrefetchPromise(directory, sessionID)
const pending = getSessionPrefetchPromise(serverSDK.scope, directory, sessionID)
if (pending) {
await pending
const seeded = getSessionPrefetch(directory, sessionID)
const seeded = getSessionPrefetch(serverSDK.scope, directory, sessionID)
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
batch(() => {
setMeta("limit", key, seeded.limit)
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/context/file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
touchFileContent,
} from "./file/content-cache"
import { createFileViewCache } from "./file/view-cache"
import { useServerSDK } from "./server-sdk"
import { createFileTreeStore } from "./file/tree-store"
import { invalidateFromWatcher } from "./file/watcher"
import {
Expand Down Expand Up @@ -56,6 +57,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const sdk = useSDK()
useSync()
const params = useParams()
const serverSDK = useServerSDK()
const language = useLanguage()
const layout = useLayout()

Expand Down Expand Up @@ -107,7 +109,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
})
})

const viewCache = createFileViewCache()
const viewCache = createFileViewCache(serverSDK.scope)
const view = createMemo(() => viewCache.load(scope(), params.id))

const ensure = (file: string) => {
Expand Down
9 changes: 5 additions & 4 deletions packages/app/src/context/file/view-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStore, produce } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import { createScopedCache } from "@/utils/scoped-cache"
import type { FileViewState, SelectedLineRange } from "./types"
import type { ServerScope } from "@/utils/server-scope"

const WORKSPACE_KEY = "__workspace__"
const MAX_FILE_VIEW_SESSIONS = 20
Expand Down Expand Up @@ -33,11 +34,11 @@ function equalSelectedLines(a: SelectedLineRange | null | undefined, b: Selected
)
}

function createViewSession(dir: string, id: string | undefined) {
function createViewSession(scope: ServerScope, dir: string, id: string | undefined) {
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`

const [view, setView, _, ready] = persisted(
Persist.scoped(dir, id, "file-view", [legacyViewKey]),
Persist.serverScoped(scope, dir, id, "file-view", [legacyViewKey]),
createStore<{
file: Record<string, FileViewState>
}>({
Expand Down Expand Up @@ -119,14 +120,14 @@ function createViewSession(dir: string, id: string | undefined) {
}
}

export function createFileViewCache() {
export function createFileViewCache(scope: ServerScope) {
const cache = createScopedCache(
(key) => {
const split = key.lastIndexOf("\n")
const dir = split >= 0 ? key.slice(0, split) : key
const id = split >= 0 ? key.slice(split + 1) : WORKSPACE_KEY
return createRoot((dispose) => ({
value: createViewSession(dir, id === WORKSPACE_KEY ? undefined : id),
value: createViewSession(scope, dir, id === WORKSPACE_KEY ? undefined : id),
dispose,
}))
},
Expand Down
15 changes: 14 additions & 1 deletion packages/app/src/context/global-sync/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { createStore } from "solid-js/store"
import { QueryClient } from "@tanstack/solid-query"
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { bootstrapDirectory } from "./bootstrap"
import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap"
import type { State, VcsCache } from "./types"
import { ServerScope } from "@/utils/server-scope"

const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse

Expand Down Expand Up @@ -45,6 +46,7 @@ describe("bootstrapDirectory", () => {

await bootstrapDirectory({
directory: "/project",
scope: ServerScope.local,
mcp: false,
global: {
config: {} satisfies Config,
Expand Down Expand Up @@ -89,3 +91,14 @@ describe("bootstrapDirectory", () => {
expect(mcpReads).toEqual([])
})
})

describe("query keys", () => {
test("partitions identical directories by server scope", () => {
const client = {} as OpencodeClient
const remote = "https://debian.example" as typeof ServerScope.local

expect([...loadPathQuery(ServerScope.local, "/repo", client).queryKey]).toEqual(["local", "/repo", "path"])
expect([...loadPathQuery(remote, "/repo", client).queryKey]).toEqual(["https://debian.example", "/repo", "path"])
expect([...loadProvidersQuery(remote, null, client).queryKey]).toEqual(["https://debian.example", null, "providers"])
})
})
Loading
Loading