fix(app): refresh V1 providers after auth (#38786)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
parent
5e2a6257b2
commit
9e8b2171a5
10 changed files with 107 additions and 39 deletions
|
|
@ -159,7 +159,7 @@ function ProviderPicker(props: {
|
|||
const settings = useSettings()
|
||||
if (settings.general.newLayoutDesigns())
|
||||
return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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(() => <DialogSettings sessionID={props.sessionID} defaultValue="providers" />)
|
||||
|
|
@ -87,7 +103,7 @@ export const DialogSettings: Component<{
|
|||
<SettingsServersV2 />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||
<SettingsProvidersV2 onBack={showProviders} />
|
||||
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||
<SettingsModelsV2 />
|
||||
|
|
|
|||
|
|
@ -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<string | undefined>
|
||||
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(() => <DialogConnectProvider controller={providerConnect} />)
|
||||
void dialog.show(() => <DialogConnectProvider directory={props.directory} controller={providerConnect} />)
|
||||
}
|
||||
|
||||
const connected = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext(
|
|||
name: "Models",
|
||||
gate: false,
|
||||
init: (props: { directory?: Accessor<string | undefined> } = {}) => {
|
||||
const providers = useProviders(props.directory)
|
||||
const providers = useProviders(() => props.directory?.())
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("model", ["model.v1"]),
|
||||
|
|
|
|||
|
|
@ -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<string | undefined>) {
|
||||
export function useProviders(directory: Accessor<string | undefined>) {
|
||||
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<string | undefined>) {
|
|||
global: serverSync().data.provider,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
all: () => providers().all,
|
||||
default: () => providers().default,
|
||||
|
|
@ -58,7 +59,7 @@ export function useProviders(directory?: Accessor<string | undefined>) {
|
|||
},
|
||||
paid: () => {
|
||||
const connected = new Set(providers().connected)
|
||||
return [
|
||||
const paid = [
|
||||
...Iterable.filter(
|
||||
providers().all,
|
||||
([id]) =>
|
||||
|
|
@ -66,6 +67,7 @@ export function useProviders(directory?: Accessor<string | undefined>) {
|
|||
(id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)),
|
||||
),
|
||||
]
|
||||
return paid
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<PromptInputControls>(() => ({
|
||||
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<PromptInputControls>(() => {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<ServerApi["integration"]["oauth"]["status"]>[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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue