summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app/src/context')
-rw-r--r--packages/app/src/context/global-sync.tsx9
-rw-r--r--packages/app/src/context/global-sync/bootstrap.ts165
-rw-r--r--packages/app/src/context/global-sync/child-store.ts3
-rw-r--r--packages/app/src/context/global-sync/types.ts3
-rw-r--r--packages/app/src/context/local.tsx22
5 files changed, 154 insertions, 48 deletions
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index cbd08e99f..86ac9b45a 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -15,7 +15,7 @@ import { useLanguage } from "@/context/language"
import { Persist, persisted } from "@/utils/persist"
import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk"
-import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
+import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
import { createRefreshQueue } from "./global-sync/queue"
@@ -154,6 +154,7 @@ function createGlobalSync() {
queue.clear(directory)
sessionMeta.delete(directory)
sdkCache.delete(directory)
+ clearProviderRev(directory)
clearSessionPrefetchDirectory(directory)
},
translate: language.t,
@@ -252,6 +253,7 @@ function createGlobalSync() {
directory,
global: {
config: globalStore.config,
+ path: globalStore.path,
project: globalStore.project,
provider: globalStore.provider,
},
@@ -311,7 +313,10 @@ function createGlobalSync() {
loadLsp: () => {
sdkFor(directory)
.lsp.status()
- .then((x) => setStore("lsp", x.data ?? []))
+ .then((x) => {
+ setStore("lsp", x.data ?? [])
+ setStore("lsp_ready", true)
+ })
},
})
})
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index 6eec688b7..869f8b7ea 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -7,6 +7,7 @@ import type {
ProviderAuthResponse,
ProviderListResponse,
QuestionRequest,
+ Session,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
@@ -52,6 +53,12 @@ function errors(list: PromiseSettledResult<unknown>[]) {
return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason)
}
+const providerRev = new Map<string, number>()
+
+export function clearProviderRev(directory: string) {
+ providerRev.delete(directory)
+}
+
function runAll(list: Array<() => Promise<unknown>>) {
return Promise.allSettled(list.map((item) => item()))
}
@@ -144,6 +151,40 @@ function projectID(directory: string, projects: Project[]) {
return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id
}
+function mergeSession(setStore: SetStoreFunction<State>, session: Session) {
+ setStore("session", (list) => {
+ const next = list.slice()
+ const idx = next.findIndex((item) => item.id >= session.id)
+ if (idx === -1) return [...next, session]
+ if (next[idx]?.id === session.id) {
+ next[idx] = session
+ return next
+ }
+ next.splice(idx, 0, session)
+ return next
+ })
+}
+
+function warmSessions(input: {
+ ids: string[]
+ store: Store<State>
+ setStore: SetStoreFunction<State>
+ sdk: OpencodeClient
+}) {
+ const known = new Set(input.store.session.map((item) => item.id))
+ const ids = [...new Set(input.ids)].filter((id) => !!id && !known.has(id))
+ if (ids.length === 0) return Promise.resolve()
+ return Promise.all(
+ ids.map((sessionID) =>
+ retry(() => input.sdk.session.get({ sessionID })).then((x) => {
+ const session = x.data
+ if (!session?.id) return
+ mergeSession(input.setStore, session)
+ }),
+ ),
+ ).then(() => undefined)
+}
+
export async function bootstrapDirectory(input: {
directory: string
sdk: OpencodeClient
@@ -154,19 +195,29 @@ export async function bootstrapDirectory(input: {
translate: (key: string, vars?: Record<string, string | number>) => string
global: {
config: Config
+ path: Path
project: Project[]
provider: ProviderListResponse
}
}) {
const loading = input.store.status !== "complete"
const seededProject = projectID(input.directory, input.global.project)
+ const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
if (seededProject) input.setStore("project", seededProject)
+ if (seededPath) input.setStore("path", seededPath)
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
input.setStore("provider", input.global.provider)
}
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
input.setStore("config", input.global.config)
}
+ if (loading || input.store.provider.all.length === 0) {
+ input.setStore("provider_ready", false)
+ }
+ input.setStore("mcp_ready", false)
+ input.setStore("mcp", {})
+ input.setStore("lsp_ready", false)
+ input.setStore("lsp", [])
if (loading) input.setStore("status", "partial")
const fast = [
@@ -177,13 +228,15 @@ export async function bootstrapDirectory(input: {
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
() =>
- retry(() =>
- input.sdk.path.get().then((x) => {
- input.setStore("path", x.data!)
- const next = projectID(x.data?.directory ?? input.directory, input.global.project)
- if (next) input.setStore("project", next)
- }),
- ),
+ seededPath
+ ? Promise.resolve()
+ : retry(() =>
+ input.sdk.path.get().then((x) => {
+ input.setStore("path", x.data!)
+ const next = projectID(x.data?.directory ?? input.directory, input.global.project)
+ if (next) input.setStore("project", next)
+ }),
+ ),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
() =>
retry(() =>
@@ -197,61 +250,66 @@ export async function bootstrapDirectory(input: {
() =>
retry(() =>
input.sdk.permission.list().then((x) => {
+ const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
const grouped = groupBySession(
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
)
- batch(() => {
- for (const sessionID of Object.keys(input.store.permission)) {
- if (grouped[sessionID]) continue
- input.setStore("permission", sessionID, [])
- }
- for (const [sessionID, permissions] of Object.entries(grouped)) {
- input.setStore(
- "permission",
- sessionID,
- reconcile(
- permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
- { key: "id" },
- ),
- )
- }
- })
+ return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
+ batch(() => {
+ for (const sessionID of Object.keys(input.store.permission)) {
+ if (grouped[sessionID]) continue
+ input.setStore("permission", sessionID, [])
+ }
+ for (const [sessionID, permissions] of Object.entries(grouped)) {
+ input.setStore(
+ "permission",
+ sessionID,
+ reconcile(
+ permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
+ { key: "id" },
+ ),
+ )
+ }
+ }),
+ )
}),
),
() =>
retry(() =>
input.sdk.question.list().then((x) => {
+ const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
- batch(() => {
- for (const sessionID of Object.keys(input.store.question)) {
- if (grouped[sessionID]) continue
- input.setStore("question", sessionID, [])
- }
- for (const [sessionID, questions] of Object.entries(grouped)) {
- input.setStore(
- "question",
- sessionID,
- reconcile(
- questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
- { key: "id" },
- ),
- )
- }
- })
+ return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
+ batch(() => {
+ for (const sessionID of Object.keys(input.store.question)) {
+ if (grouped[sessionID]) continue
+ input.setStore("question", sessionID, [])
+ }
+ for (const [sessionID, questions] of Object.entries(grouped)) {
+ input.setStore(
+ "question",
+ sessionID,
+ reconcile(
+ questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
+ { key: "id" },
+ ),
+ )
+ }
+ }),
+ )
}),
),
]
const slow = [
+ () => Promise.resolve(input.loadSessions(input.directory)),
() =>
retry(() =>
- input.sdk.provider.list().then((x) => {
- input.setStore("provider", normalizeProviderList(x.data!))
+ input.sdk.mcp.status().then((x) => {
+ input.setStore("mcp", x.data!)
+ input.setStore("mcp_ready", true)
}),
),
- () => Promise.resolve(input.loadSessions(input.directory)),
- () => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
- () => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
]
const errs = errors(await runAll(fast))
@@ -278,4 +336,23 @@ export async function bootstrapDirectory(input: {
}
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
+
+ const rev = (providerRev.get(input.directory) ?? 0) + 1
+ providerRev.set(input.directory, rev)
+ void retry(() => input.sdk.provider.list())
+ .then((x) => {
+ if (providerRev.get(input.directory) !== rev) return
+ input.setStore("provider", normalizeProviderList(x.data!))
+ input.setStore("provider_ready", true)
+ })
+ .catch((err) => {
+ if (providerRev.get(input.directory) !== rev) return
+ console.error("Failed to refresh provider list", err)
+ const project = getFilename(input.directory)
+ showToast({
+ variant: "error",
+ title: input.translate("toast.project.reloadFailed.title", { project }),
+ description: formatServerError(err, input.translate),
+ })
+ })
}
diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts
index 70668350e..5678491f8 100644
--- a/packages/app/src/context/global-sync/child-store.ts
+++ b/packages/app/src/context/global-sync/child-store.ts
@@ -160,6 +160,7 @@ export function createChildStoreManager(input: {
project: "",
projectMeta: initialMeta,
icon: initialIcon,
+ provider_ready: false,
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
@@ -173,7 +174,9 @@ export function createChildStoreManager(input: {
todo: {},
permission: {},
question: {},
+ mcp_ready: false,
mcp: {},
+ lsp_ready: false,
lsp: [],
vcs: vcsStore.value,
limit: 5,
diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts
index c61dc337d..1d6e550f8 100644
--- a/packages/app/src/context/global-sync/types.ts
+++ b/packages/app/src/context/global-sync/types.ts
@@ -38,6 +38,7 @@ export type State = {
project: string
projectMeta: ProjectMeta | undefined
icon: string | undefined
+ provider_ready: boolean
provider: ProviderListResponse
config: Config
path: Path
@@ -58,9 +59,11 @@ export type State = {
question: {
[sessionID: string]: QuestionRequest[]
}
+ mcp_ready: boolean
mcp: {
[name: string]: McpStatus
}
+ lsp_ready: boolean
lsp: LspStatus[]
vcs: VcsInfo | undefined
limit: number
diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx
index 76d337c82..84a613c0d 100644
--- a/packages/app/src/context/local.tsx
+++ b/packages/app/src/context/local.tsx
@@ -390,10 +390,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
if (modelEnabled()) {
+ const probe = Symbol("model-probe")
+
+ modelProbe.bind(probe, {
+ setAgent: agent.set,
+ setModel: model.set,
+ setVariant: model.variant.set,
+ })
+
createEffect(() => {
const agent = result.agent.current()
const model = result.model.current()
- modelProbe.set({
+ modelProbe.set(probe, {
dir: sdk.directory,
sessionID: id(),
last: store.last,
@@ -411,10 +419,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
pick: scope(),
base: undefined,
current: store.current,
+ variants: result.model.variant.list(),
+ models: result.model
+ .list()
+ .filter((item) => result.model.visible({ providerID: item.provider.id, modelID: item.id }))
+ .map((item) => ({
+ providerID: item.provider.id,
+ modelID: item.id,
+ name: item.name,
+ })),
+ agents: result.agent.list().map((item) => ({ name: item.name })),
})
})
- onCleanup(() => modelProbe.clear())
+ onCleanup(() => modelProbe.clear(probe))
}
return result