From 1f0390cfbbd931297faa4dbc6d297ef2665a4a84 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Fri, 22 May 2026 12:23:16 +0800 Subject: [PATCH] app: wrap provider data in Map to avoid store (#28765) --- .../components/dialog-connect-provider.tsx | 4 +-- .../src/components/dialog-custom-provider.tsx | 2 +- .../components/dialog-select-model-unpaid.tsx | 2 +- .../src/components/dialog-select-provider.tsx | 2 +- .../src/components/session-context-usage.tsx | 2 +- .../session/session-context-tab.tsx | 2 +- packages/app/src/context/global-sync.tsx | 15 +++-------- .../app/src/context/global-sync/bootstrap.ts | 9 +++---- .../src/context/global-sync/child-store.ts | 12 ++++----- packages/app/src/context/global-sync/types.ts | 4 +-- packages/app/src/context/global-sync/utils.ts | 21 +++++++++++---- packages/app/src/context/local.tsx | 2 +- packages/app/src/hooks/use-providers.ts | 27 +++++++++++++++---- packages/app/src/pages/layout.tsx | 2 +- packages/app/src/pages/session.tsx | 9 ++++--- .../src/pages/session/session-side-panel.tsx | 4 +-- packages/ui/src/components/message-part.tsx | 4 +-- packages/ui/src/context/data.tsx | 12 +++++++-- 18 files changed, 80 insertions(+), 55 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index e305743799..9086c10826 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -41,9 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) { }) const provider = createMemo( - () => - providers.all().find((x) => x.id === props.provider) ?? - globalSync.data.provider.all.find((x) => x.id === props.provider)!, + () => providers.all().get(props.provider) ?? globalSync.data.provider.all.get(props.provider)!, ) const fallback = createMemo(() => [ { diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 53b66fb451..7d449849eb 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -106,7 +106,7 @@ export function DialogCustomProvider(props: Props) { form, t: language.t, disabledProviders: globalSync.data.config.disabled_providers ?? [], - existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)), + existingProviderIDs: new Set(globalSync.data.provider.all.keys()), }) batch(() => { setForm("err", output.err) diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index e25e8f0c17..f916ef6230 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -91,7 +91,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
x?.id} + key={(p) => p.id} items={providers.popular} activeIcon="plus-small" sortBy={(a, b) => { diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index e53738399a..1273db596f 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -35,7 +35,7 @@ export const DialogSelectProvider: Component = () => { key={(x) => x?.id} items={() => { language.locale() - return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()] + return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] }} filterKeys={["id", "name"]} groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 6b7fe4ef7d..1f65e9adb3 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -52,7 +52,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) + const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()])) const context = createMemo(() => metrics().context) const cost = createMemo(() => { return usd().format(metrics().totalCost) diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 43741bd3fc..88c3889858 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -132,7 +132,7 @@ export function SessionContextTab() { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) + const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()])) const ctx = createMemo(() => metrics().context) const formatter = createMemo(() => createSessionContextFormatter(language.intl())) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index c418420da5..2e9ac17071 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -1,12 +1,4 @@ -import type { - Config, - OpencodeClient, - Path, - Project, - ProviderAuthResponse, - ProviderListResponse, - Todo, -} from "@opencode-ai/sdk/v2/client" +import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/core/util/path" import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" @@ -37,6 +29,7 @@ import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" import { PathKey } from "@/utils/path-key" import { createDirSyncContext } from "./directory-sync" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" type GlobalStore = { ready: boolean @@ -46,7 +39,7 @@ type GlobalStore = { session_todo: { [sessionID: string]: Todo[] } - provider: ProviderListResponse + provider: NormalizedProviderListResponse provider_auth: ProviderAuthResponse config: Config reload: undefined | "pending" | "complete" @@ -121,7 +114,7 @@ function createGlobalSync() { return pathQuery.data ?? EMPTY }, get provider() { - const EMPTY = { all: [], connected: [], default: {} } + const EMPTY = { all: new Map(), connected: [], default: {} } if (providerQuery.isLoading) return EMPTY return providerQuery.data ?? EMPTY }, diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 531917bde6..655f65a676 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -5,7 +5,6 @@ import type { PermissionRequest, Project, ProviderAuthResponse, - ProviderListResponse, QuestionRequest, Session, Todo, @@ -20,6 +19,7 @@ import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions } from "@tanstack/solid-query" import { loadMcpQuery } from "../global-sync" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" type GlobalStore = { ready: boolean @@ -28,7 +28,7 @@ type GlobalStore = { session_todo: { [sessionID: string]: Todo[] } - provider: ProviderListResponse + provider: NormalizedProviderListResponse provider_auth: ProviderAuthResponse config: Config reload: undefined | "pending" | "complete" @@ -208,7 +208,7 @@ export async function bootstrapDirectory(input: { config: Config path: Path project: Project[] - provider: ProviderListResponse + provider: NormalizedProviderListResponse } queryClient: QueryClient }) { @@ -220,7 +220,6 @@ export async function bootstrapDirectory(input: { if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { input.setStore("config", reconcile(input.global.config, { merge: false })) } - if (loading) input.setStore("status", "partial") const rev = (providerRev.get(input.directory) ?? 0) + 1 providerRev.set(input.directory, rev) @@ -327,7 +326,5 @@ export async function bootstrapDirectory(input: { description: formatServerError(slowErrs[0], input.translate), }) } - - if (loading && slowErrs.length === 0) input.setStore("status", "complete") })() } diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 08299b3017..56935ccc99 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -1,7 +1,7 @@ import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js" import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" -import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client" +import type { VcsInfo } from "@opencode-ai/sdk/v2/client" import { DIR_IDLE_TTL_MS, MAX_DIR_STORES, @@ -17,6 +17,7 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" import { useQueries } from "@tanstack/solid-query" import { QueryOptionsApi } from "../global-sync" import { directoryKey, type DirectoryKey } from "./utils" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" export function createChildStoreManager(input: { owner: Owner @@ -27,7 +28,7 @@ export function createChildStoreManager(input: { translate: (key: string, vars?: Record) => string queryOptions: QueryOptionsApi global: { - provider: ProviderListResponse + provider: NormalizedProviderListResponse } }) { const children: Record, SetStoreFunction]> = {} @@ -190,10 +191,9 @@ export function createChildStoreManager(input: { return !providerQuery.isLoading }, get provider() { - const EMPTY = { all: [], connected: [], default: {} } + const EMPTY = { all: new Map(), connected: [], default: {} } if (providerQuery.isLoading) return EMPTY - if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0) - return input.global.provider + if (providerQuery.data?.all.size === 0 && input.global.provider.all.size > 0) return input.global.provider return providerQuery.data ?? EMPTY }, config: {}, @@ -202,7 +202,7 @@ export function createChildStoreManager(input: { return { state: "", config: "", worktree: "", directory: "", home: "" } return pathQuery.data }, - status: "loading" as const, + status: "complete" as const, agent: [], command: [], session: [], diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 43837ac97f..77b5b0c78f 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -8,7 +8,6 @@ import type { Part, Path, PermissionRequest, - ProviderListResponse, QuestionRequest, Session, SessionStatus, @@ -16,6 +15,7 @@ import type { Todo, VcsInfo, } from "@opencode-ai/sdk/v2/client" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" import type { Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" @@ -38,7 +38,7 @@ export type State = { projectMeta: ProjectMeta | undefined icon: string | undefined provider_ready: boolean - provider: ProviderListResponse + provider: NormalizedProviderListResponse config: Config path: Path session: Session[] diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index b982990884..2515a593fb 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -1,4 +1,5 @@ import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) @@ -17,13 +18,23 @@ export function normalizeAgentList(input: unknown): Agent[] { return Object.values(input).filter(isAgent) } -export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { +export function normalizeProviderList(input: ProviderListResponse): NormalizedProviderListResponse { return { ...input, - all: input.all.map((provider) => ({ - ...provider, - models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")), - })), + all: new Map( + input.all.map( + (provider) => + [ + provider.id, + { + ...provider, + models: Object.fromEntries( + Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), + ), + }, + ] as const, + ), + ), } } diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 4465a0261d..c0ae79d695 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -90,7 +90,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const validModel = (model: ModelKey) => { - const provider = providers.all().find((item) => item.id === model.providerID) + const provider = providers.all().get(model.providerID) return !!provider?.models[model.modelID] && connected().has(model.providerID) } diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index f4ed359de3..45ad8b07d7 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -1,6 +1,7 @@ import { useGlobalSync } from "@/context/global-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" +import { Iterable, pipe } from "effect" import { createMemo } from "solid-js" export const popularProviders = [ @@ -29,16 +30,32 @@ export function useProviders() { return { all: () => providers().all, default: () => providers().default, - popular: () => providers().all.filter((p) => popularProviderSet.has(p.id)), + popular: () => + pipe( + providers().all, + Iterable.map(([, p]) => p), + Iterable.filter((p) => popularProviderSet.has(p.id)), + (v) => Array.from(v), + ), connected: () => { const connected = new Set(providers().connected) - return providers().all.filter((p) => connected.has(p.id)) + return pipe( + providers().all, + Iterable.map(([, p]) => p), + Iterable.filter((p) => connected.has(p.id)), + (v) => Array.from(v), + ) }, paid: () => { const connected = new Set(providers().connected) - return providers().all.filter( - (p) => connected.has(p.id) && (p.id !== "opencode" || Object.values(p.models).some((m) => m.cost?.input)), - ) + return [ + ...Iterable.filter( + providers().all, + ([id]) => + connected.has(id) && + (id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)), + ), + ] }, } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 44ab97ed6a..3166cc11f0 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2303,7 +2303,7 @@ export default function Layout(props: ParentProps) {
0 && providers.paid().length === 0), + hidden: store.gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0), }} >
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 52b6c5d66c..406ece23ce 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -262,8 +262,9 @@ export default function Page() { const isDesktop = createMediaQuery("(min-width: 768px)") const size = createSizing() - const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) - const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) + const isV2NewSessionPage = () => import.meta.env.VITE_OPENCODE_CHANNEL === "prod" || !params.id + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage()) + const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage()) const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) const sessionPanelWidth = createMemo(() => { if (!desktopSidePanelOpen()) return "100%" @@ -1733,12 +1734,12 @@ export default function Page() { - {/* Session panel */}
+