diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 9ad389317b..744bc79227 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -159,7 +159,7 @@ function ProviderPicker(props: { const settings = useSettings() if (settings.general.newLayoutDesigns()) return - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -231,7 +231,7 @@ function ProviderPickerV2(props: { onSelect: (provider: string) => void onPrepare?: () => void }) { - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const language = useLanguage() const [store, setStore] = createStore({ filter: "", @@ -391,7 +391,7 @@ function ProviderConnection(props: { const language = useLanguage() const settings = useSettings() const newLayout = settings.general.newLayoutDesigns - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const directory = () => props.directory?.() ?? decode64(params.dir) const location = () => { const value = directory() diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7a15d82eaf..080e6c517b 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -41,7 +41,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => const serverSDK = useServerSDK() const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders() + const providers = useProviders(() => undefined) const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index af24a47274..4116f4a620 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, startTransition } from "solid-js" +import { Component, createMemo, createSignal, startTransition } from "solid-js" import { Dialog } from "@opencode-ai/ui/v2/dialog-v2" import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2" import { Icon } from "@opencode-ai/ui/icon" @@ -11,6 +11,9 @@ import { SettingsModelsV2 } from "./models" import "./settings-v2.css" import { SettingsServersV2 } from "./servers" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLayout } from "@/context/layout" +import { useTabs } from "@/context/tabs" +import { useServerSync } from "@/context/server-sync" export const DialogSettings: Component<{ sessionID?: string @@ -19,7 +22,20 @@ export const DialogSettings: Component<{ const language = useLanguage() const platform = usePlatform() const dialog = useDialog() + const layout = useLayout() + const tabs = useTabs() + const serverSync = useServerSync() const [tab, setTab] = createSignal(props.defaultValue ?? "general") + const directory = createMemo(() => { + const route = layout.route() + if (route.type === "dir-new-sesssion") return route.dir + if (route.type === "draft") { + const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID) + return draft?.type === "draft" ? draft.directory : undefined + } + if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory + return undefined + }) const showProviders = () => { void dialog.show(() => ) @@ -87,7 +103,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 29192114f0..acd73ddbe5 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -4,7 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Component, For, Show } from "solid-js" +import { createMemo, type Accessor, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerProtocol, useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" @@ -29,18 +29,21 @@ const PROVIDER_NOTES = [ const PROVIDER_ICON_SIZE = 16 -export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => { +export const SettingsProvidersV2: Component<{ + directory: Accessor + onBack?: () => void +}> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders() + const providers = useProviders(props.directory) const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { providerConnect.select(provider) - void dialog.show(() => ) + void dialog.show(() => ) } const connected = createMemo(() => { diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx index 736dc3a7c5..a80cf2e580 100644 --- a/packages/app/src/context/models.tsx +++ b/packages/app/src/context/models.tsx @@ -26,7 +26,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( name: "Models", gate: false, init: (props: { directory?: Accessor } = {}) => { - const providers = useProviders(props.directory) + const providers = useProviders(() => props.directory?.()) const [store, setStore, _, ready] = persisted( Persist.global("model", ["model.v1"]), diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 982c26ca97..60ad01c867 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -2,7 +2,7 @@ import { useServerSync } from "@/context/server-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { Iterable, pipe } from "effect" -import type { Accessor } from "solid-js" +import { createEffect, createMemo, type Accessor } from "solid-js" import { selectProviderCatalog } from "./provider-catalog" export const popularProviders = [ @@ -17,14 +17,14 @@ export const popularProviders = [ ] const popularProviderSet = new Set(popularProviders) -export function useProviders(directory?: Accessor) { +export function useProviders(directory: Accessor) { const serverSync = useServerSync() const params = useParams() const dir = () => (directory ? directory() : decode64(params.dir)) const providers = () => { const value = dir() const projectStore = value ? serverSync().child(value)[0] : undefined - if (directory) + if (value) return selectProviderCatalog({ explicit: true, directory: value, @@ -37,6 +37,7 @@ export function useProviders(directory?: Accessor) { global: serverSync().data.provider, }) } + return { all: () => providers().all, default: () => providers().default, @@ -58,7 +59,7 @@ export function useProviders(directory?: Accessor) { }, paid: () => { const connected = new Set(providers().connected) - return [ + const paid = [ ...Iterable.filter( providers().all, ([id]) => @@ -66,6 +67,7 @@ export function useProviders(directory?: Accessor) { (id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)), ), ] + return paid }, } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5947442318..96ed022373 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -119,7 +119,7 @@ export default function LegacyLayout(props: ParentProps) { const permission = usePermission() const navigate = useNavigate() setNavigate(navigate) - const providers = useProviders() + const providers = useProviders(() => undefined) const dialog = useDialog() const command = useCommand() const theme = useTheme() diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index 4ae7827e21..a9b0070bc0 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -25,35 +25,39 @@ export function createPromptInputController(input: { }) { const layout = useLayout() const local = useLocal() - const providers = useProviders() - const sync = useSync() const sdk = useSDK() + const sync = useSync() + const providers = useProviders(() => sdk().directory) const view = layout.view(input.sessionKey) const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory))) const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null)) const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory))) - return createMemo(() => ({ - agents: { - available: sync().data.agent, - options: local.agent.list().map((agent) => agent.name), - current: local.agent.current()?.name ?? "", - loading: agentsQuery.isLoading, - visible: local.agent.visible(), - select: local.agent.set, - }, - model: { - selection: input.model ?? local.model, - paid: providers.paid().length > 0, - loading: - (local.agent.visible() && agentsQuery.isLoading) || providersQuery.isLoading || globalProvidersQuery.isLoading, - }, - session: { - id: input.sessionID(), - tabs: layout.tabs(input.sessionKey), - reviewPanel: view.reviewPanel, - }, - })) + return createMemo(() => { + return { + agents: { + available: sync().data.agent, + options: local.agent.list().map((agent) => agent.name), + current: local.agent.current()?.name ?? "", + loading: agentsQuery.isLoading, + visible: local.agent.visible(), + select: local.agent.set, + }, + model: { + selection: input.model ?? local.model, + paid: providers.paid().length > 0, + loading: + (local.agent.visible() && agentsQuery.isLoading) || + providersQuery.isLoading || + globalProvidersQuery.isLoading, + }, + session: { + id: input.sessionID(), + tabs: layout.tabs(input.sessionKey), + reviewPanel: view.reviewPanel, + }, + } + }) } export function createPromptProjectControls() { diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts index 3f4b8f2205..605fdaace8 100644 --- a/packages/app/src/utils/server-compat.test.ts +++ b/packages/app/src/utils/server-compat.test.ts @@ -190,4 +190,41 @@ describe("createCompatibleApi", () => { expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1") expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other") }) + + test("disposes the V1 instance after connecting a provider", async () => { + const { api, requests } = setup("v1") + + await api.integration.connect.key({ + integrationID: "openrouter", + key: "secret", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/auth/openrouter", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) + + test("disposes the V1 instance after completing provider OAuth", async () => { + const { api, requests } = setup("v1") + + await api.integration.oauth.complete({ + integrationID: "openrouter", + attemptID: "openrouter:0", + code: "code", + location: { directory: "/repo" }, + }) + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/provider/openrouter/oauth/callback", + "/instance/dispose", + "/instance/dispose", + ]) + expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") + expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() + }) }) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts index 72e74438ed..94374aa9f9 100644 --- a/packages/app/src/utils/server-compat.ts +++ b/packages/app/src/utils/server-compat.ts @@ -401,6 +401,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { providerID: value.integrationID, auth: { type: "api", key: value.key }, }) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() }, }, oauth: { @@ -429,6 +431,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { { providerID: value.integrationID, method, code: value.code }, { throwOnError: true }, ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() }, status: async (value: Parameters[0]) => { const method = Number(value.attemptID.split(":").at(-1)) @@ -436,6 +440,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi { { providerID: value.integrationID, method }, { throwOnError: true }, ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() return located( { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, value.location,