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,