Skip to content

Commit 9e8b217

Browse files
fix(app): refresh V1 providers after auth (anomalyco#38786)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev>
1 parent 5e2a625 commit 9e8b217

10 files changed

Lines changed: 107 additions & 39 deletions

File tree

packages/app/src/components/dialog-connect-provider.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ function ProviderPicker(props: {
159159
const settings = useSettings()
160160
if (settings.general.newLayoutDesigns())
161161
return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
162-
const providers = useProviders(props.directory)
162+
const providers = useProviders(() => props.directory?.())
163163
const language = useLanguage()
164164
const popularGroup = () => language.t("dialog.provider.group.popular")
165165
const otherGroup = () => language.t("dialog.provider.group.other")
@@ -231,7 +231,7 @@ function ProviderPickerV2(props: {
231231
onSelect: (provider: string) => void
232232
onPrepare?: () => void
233233
}) {
234-
const providers = useProviders(props.directory)
234+
const providers = useProviders(() => props.directory?.())
235235
const language = useLanguage()
236236
const [store, setStore] = createStore({
237237
filter: "",
@@ -391,7 +391,7 @@ function ProviderConnection(props: {
391391
const language = useLanguage()
392392
const settings = useSettings()
393393
const newLayout = settings.general.newLayoutDesigns
394-
const providers = useProviders(props.directory)
394+
const providers = useProviders(() => props.directory?.())
395395
const directory = () => props.directory?.() ?? decode64(params.dir)
396396
const location = () => {
397397
const value = directory()

packages/app/src/components/settings-providers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
4141
const serverSDK = useServerSDK()
4242
const protocol = useServerProtocol()
4343
const serverSync = useServerSync()
44-
const providers = useProviders()
44+
const providers = useProviders(() => undefined)
4545
const providerConnect = useProviderConnectController({ onBack: props.onBack })
4646

4747
const connect = (provider?: string) => {

packages/app/src/components/settings-v2/dialog-settings-v2.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, createSignal, startTransition } from "solid-js"
1+
import { Component, createMemo, createSignal, startTransition } from "solid-js"
22
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
33
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
44
import { Icon } from "@opencode-ai/ui/icon"
@@ -11,6 +11,9 @@ import { SettingsModelsV2 } from "./models"
1111
import "./settings-v2.css"
1212
import { SettingsServersV2 } from "./servers"
1313
import { useDialog } from "@opencode-ai/ui/context/dialog"
14+
import { useLayout } from "@/context/layout"
15+
import { useTabs } from "@/context/tabs"
16+
import { useServerSync } from "@/context/server-sync"
1417

1518
export const DialogSettings: Component<{
1619
sessionID?: string
@@ -19,7 +22,20 @@ export const DialogSettings: Component<{
1922
const language = useLanguage()
2023
const platform = usePlatform()
2124
const dialog = useDialog()
25+
const layout = useLayout()
26+
const tabs = useTabs()
27+
const serverSync = useServerSync()
2228
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
29+
const directory = createMemo(() => {
30+
const route = layout.route()
31+
if (route.type === "dir-new-sesssion") return route.dir
32+
if (route.type === "draft") {
33+
const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID)
34+
return draft?.type === "draft" ? draft.directory : undefined
35+
}
36+
if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory
37+
return undefined
38+
})
2339

2440
const showProviders = () => {
2541
void dialog.show(() => <DialogSettings sessionID={props.sessionID} defaultValue="providers" />)
@@ -87,7 +103,7 @@ export const DialogSettings: Component<{
87103
<SettingsServersV2 />
88104
</TabsV2.Content>
89105
<TabsV2.Content value="providers" class="settings-v2-panel">
90-
<SettingsProvidersV2 onBack={showProviders} />
106+
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
91107
</TabsV2.Content>
92108
<TabsV2.Content value="models" class="settings-v2-panel">
93109
<SettingsModelsV2 />

packages/app/src/components/settings-v2/providers.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
44
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
55
import { showToast } from "@/utils/toast"
66
import { popularProviders, useProviders } from "@/hooks/use-providers"
7-
import { createMemo, type Component, For, Show } from "solid-js"
7+
import { createMemo, type Accessor, type Component, For, Show } from "solid-js"
88
import { useLanguage } from "@/context/language"
99
import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
1010
import { useServerSync } from "@/context/server-sync"
@@ -29,18 +29,21 @@ const PROVIDER_NOTES = [
2929

3030
const PROVIDER_ICON_SIZE = 16
3131

32-
export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => {
32+
export const SettingsProvidersV2: Component<{
33+
directory: Accessor<string | undefined>
34+
onBack?: () => void
35+
}> = (props) => {
3336
const dialog = useDialog()
3437
const language = useLanguage()
3538
const serverSdk = useServerSDK()
3639
const protocol = useServerProtocol()
3740
const serverSync = useServerSync()
38-
const providers = useProviders()
41+
const providers = useProviders(props.directory)
3942
const providerConnect = useProviderConnectController({ onBack: props.onBack })
4043

4144
const connect = (provider?: string) => {
4245
providerConnect.select(provider)
43-
void dialog.show(() => <DialogConnectProvider controller={providerConnect} />)
46+
void dialog.show(() => <DialogConnectProvider directory={props.directory} controller={providerConnect} />)
4447
}
4548

4649
const connected = createMemo(() => {

packages/app/src/context/models.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
2626
name: "Models",
2727
gate: false,
2828
init: (props: { directory?: Accessor<string | undefined> } = {}) => {
29-
const providers = useProviders(props.directory)
29+
const providers = useProviders(() => props.directory?.())
3030

3131
const [store, setStore, _, ready] = persisted(
3232
Persist.global("model", ["model.v1"]),

packages/app/src/hooks/use-providers.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useServerSync } from "@/context/server-sync"
22
import { decode64 } from "@/utils/base64"
33
import { useParams } from "@solidjs/router"
44
import { Iterable, pipe } from "effect"
5-
import type { Accessor } from "solid-js"
5+
import { createEffect, createMemo, type Accessor } from "solid-js"
66
import { selectProviderCatalog } from "./provider-catalog"
77

88
export const popularProviders = [
@@ -17,14 +17,14 @@ export const popularProviders = [
1717
]
1818
const popularProviderSet = new Set(popularProviders)
1919

20-
export function useProviders(directory?: Accessor<string | undefined>) {
20+
export function useProviders(directory: Accessor<string | undefined>) {
2121
const serverSync = useServerSync()
2222
const params = useParams()
2323
const dir = () => (directory ? directory() : decode64(params.dir))
2424
const providers = () => {
2525
const value = dir()
2626
const projectStore = value ? serverSync().child(value)[0] : undefined
27-
if (directory)
27+
if (value)
2828
return selectProviderCatalog({
2929
explicit: true,
3030
directory: value,
@@ -37,6 +37,7 @@ export function useProviders(directory?: Accessor<string | undefined>) {
3737
global: serverSync().data.provider,
3838
})
3939
}
40+
4041
return {
4142
all: () => providers().all,
4243
default: () => providers().default,
@@ -58,14 +59,15 @@ export function useProviders(directory?: Accessor<string | undefined>) {
5859
},
5960
paid: () => {
6061
const connected = new Set(providers().connected)
61-
return [
62+
const paid = [
6263
...Iterable.filter(
6364
providers().all,
6465
([id]) =>
6566
connected.has(id) &&
6667
(id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)),
6768
),
6869
]
70+
return paid
6971
},
7072
}
7173
}

packages/app/src/pages/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default function LegacyLayout(props: ParentProps) {
119119
const permission = usePermission()
120120
const navigate = useNavigate()
121121
setNavigate(navigate)
122-
const providers = useProviders()
122+
const providers = useProviders(() => undefined)
123123
const dialog = useDialog()
124124
const command = useCommand()
125125
const theme = useTheme()

packages/app/src/pages/session/composer/session-composer-controls.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,39 @@ export function createPromptInputController(input: {
2525
}) {
2626
const layout = useLayout()
2727
const local = useLocal()
28-
const providers = useProviders()
29-
const sync = useSync()
3028
const sdk = useSDK()
29+
const sync = useSync()
30+
const providers = useProviders(() => sdk().directory)
3131
const view = layout.view(input.sessionKey)
3232
const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory)))
3333
const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null))
3434
const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory)))
3535

36-
return createMemo<PromptInputControls>(() => ({
37-
agents: {
38-
available: sync().data.agent,
39-
options: local.agent.list().map((agent) => agent.name),
40-
current: local.agent.current()?.name ?? "",
41-
loading: agentsQuery.isLoading,
42-
visible: local.agent.visible(),
43-
select: local.agent.set,
44-
},
45-
model: {
46-
selection: input.model ?? local.model,
47-
paid: providers.paid().length > 0,
48-
loading:
49-
(local.agent.visible() && agentsQuery.isLoading) || providersQuery.isLoading || globalProvidersQuery.isLoading,
50-
},
51-
session: {
52-
id: input.sessionID(),
53-
tabs: layout.tabs(input.sessionKey),
54-
reviewPanel: view.reviewPanel,
55-
},
56-
}))
36+
return createMemo<PromptInputControls>(() => {
37+
return {
38+
agents: {
39+
available: sync().data.agent,
40+
options: local.agent.list().map((agent) => agent.name),
41+
current: local.agent.current()?.name ?? "",
42+
loading: agentsQuery.isLoading,
43+
visible: local.agent.visible(),
44+
select: local.agent.set,
45+
},
46+
model: {
47+
selection: input.model ?? local.model,
48+
paid: providers.paid().length > 0,
49+
loading:
50+
(local.agent.visible() && agentsQuery.isLoading) ||
51+
providersQuery.isLoading ||
52+
globalProvidersQuery.isLoading,
53+
},
54+
session: {
55+
id: input.sessionID(),
56+
tabs: layout.tabs(input.sessionKey),
57+
reviewPanel: view.reviewPanel,
58+
},
59+
}
60+
})
5761
}
5862

5963
export function createPromptProjectControls() {

packages/app/src/utils/server-compat.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,41 @@ describe("createCompatibleApi", () => {
190190
expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1")
191191
expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other")
192192
})
193+
194+
test("disposes the V1 instance after connecting a provider", async () => {
195+
const { api, requests } = setup("v1")
196+
197+
await api.integration.connect.key({
198+
integrationID: "openrouter",
199+
key: "secret",
200+
location: { directory: "/repo" },
201+
})
202+
203+
expect(requests.map((request) => new URL(request.url).pathname)).toEqual([
204+
"/auth/openrouter",
205+
"/instance/dispose",
206+
"/instance/dispose",
207+
])
208+
expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo")
209+
expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull()
210+
})
211+
212+
test("disposes the V1 instance after completing provider OAuth", async () => {
213+
const { api, requests } = setup("v1")
214+
215+
await api.integration.oauth.complete({
216+
integrationID: "openrouter",
217+
attemptID: "openrouter:0",
218+
code: "code",
219+
location: { directory: "/repo" },
220+
})
221+
222+
expect(requests.map((request) => new URL(request.url).pathname)).toEqual([
223+
"/provider/openrouter/oauth/callback",
224+
"/instance/dispose",
225+
"/instance/dispose",
226+
])
227+
expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo")
228+
expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull()
229+
})
193230
})

packages/app/src/utils/server-compat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
401401
providerID: value.integrationID,
402402
auth: { type: "api", key: value.key },
403403
})
404+
await legacy(value.location).instance.dispose()
405+
await input.legacy().instance.dispose()
404406
},
405407
},
406408
oauth: {
@@ -429,13 +431,17 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
429431
{ providerID: value.integrationID, method, code: value.code },
430432
{ throwOnError: true },
431433
)
434+
await legacy(value.location).instance.dispose()
435+
await input.legacy().instance.dispose()
432436
},
433437
status: async (value: Parameters<ServerApi["integration"]["oauth"]["status"]>[0]) => {
434438
const method = Number(value.attemptID.split(":").at(-1))
435439
await legacy(value.location).provider.oauth.callback(
436440
{ providerID: value.integrationID, method },
437441
{ throwOnError: true },
438442
)
443+
await legacy(value.location).instance.dispose()
444+
await input.legacy().instance.dispose()
439445
return located(
440446
{ status: "complete" as const, time: { created: Date.now(), expires: Date.now() } },
441447
value.location,

0 commit comments

Comments
 (0)