Skip to content

Commit 8bed2f0

Browse files
jayairopencode-agent[bot]
authored andcommitted
fix(app): preserve provider dialog backdrop
1 parent b44bc0a commit 8bed2f0

5 files changed

Lines changed: 429 additions & 273 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expect, test } from "@playwright/test"
2+
import { mockOpenCodeServer } from "../utils/mock-server"
3+
import { expectAppVisible } from "../utils/waits"
4+
5+
const directory = "C:/OpenCode/ProviderDialogStack"
6+
7+
test("keeps the modal layer mounted when choosing a provider", async ({ page }) => {
8+
await mockOpenCodeServer(page, {
9+
directory,
10+
project: {
11+
id: "proj_provider_dialog_stack",
12+
worktree: directory,
13+
vcs: "git",
14+
name: "provider-dialog-stack",
15+
time: { created: 1700000000000, updated: 1700000000000 },
16+
sandboxes: [],
17+
},
18+
provider: {
19+
all: [
20+
{ id: "anthropic", name: "Anthropic", models: {} },
21+
{ id: "openai", name: "OpenAI", models: {} },
22+
],
23+
connected: [],
24+
default: {},
25+
},
26+
sessions: [],
27+
pageMessages: () => ({ items: [] }),
28+
})
29+
await page.route("**/provider/auth*", (route) =>
30+
route.fulfill({
31+
status: 200,
32+
contentType: "application/json",
33+
body: JSON.stringify({
34+
anthropic: [
35+
{ type: "api", label: "API key" },
36+
{ type: "oauth", label: "OAuth" },
37+
],
38+
}),
39+
}),
40+
)
41+
await page.goto("/")
42+
await expectAppVisible(page.getByPlaceholder("Search sessions"))
43+
await page.getByRole("button", { name: "Settings" }).click()
44+
await page.getByRole("tab", { name: "Providers" }).click()
45+
await page.getByRole("button", { name: "Show more providers" }).click()
46+
47+
const picker = page.getByPlaceholder("Search providers")
48+
await expect(picker).toBeVisible()
49+
await expect.poll(async () => (await page.locator('[data-slot="dialog-content"]').boundingBox())?.height).toBe(512)
50+
const overlay = await page.locator('[data-component="dialog-overlay"]').elementHandle()
51+
const content = await page.locator('[data-slot="dialog-content"]').elementHandle()
52+
const box = await page.locator('[data-slot="dialog-content"]').boundingBox()
53+
await page.getByRole("dialog").getByText("Anthropic", { exact: true }).click()
54+
55+
await expect(page.getByText("Connect Anthropic", { exact: true })).toBeVisible()
56+
expect(await overlay?.evaluate((element) => element.isConnected)).toBe(true)
57+
expect(await content?.evaluate((element) => element.isConnected)).toBe(true)
58+
expect((await page.locator('[data-slot="dialog-content"]').boundingBox())?.height).toBe(box?.height)
59+
60+
await page.getByRole("button", { name: "API key" }).click()
61+
await expect(page.getByRole("textbox", { name: "Anthropic API key" })).toBeVisible()
62+
await page.getByRole("button", { name: "Navigate back" }).click()
63+
await expect(page.getByRole("button", { name: "API key" })).toBeVisible()
64+
65+
await page.getByRole("button", { name: "Navigate back" }).click()
66+
await expect(page.getByText("Connect Anthropic", { exact: true })).not.toBeVisible()
67+
await expect(picker).toBeVisible()
68+
expect(await overlay?.evaluate((element) => element.isConnected)).toBe(true)
69+
expect(await content?.evaluate((element) => element.isConnected)).toBe(true)
70+
71+
await page.getByRole("dialog").getByRole("button", { name: "Custom Custom" }).click()
72+
await expect(page.getByText("Custom provider", { exact: true })).toBeVisible()
73+
expect(await overlay?.evaluate((element) => element.isConnected)).toBe(true)
74+
expect(await content?.evaluate((element) => element.isConnected)).toBe(true)
75+
expect((await page.locator('[data-slot="dialog-content"]').boundingBox())?.height).toBe(box?.height)
76+
77+
await page.getByRole("button", { name: "Navigate back" }).click()
78+
await expect(picker).toBeVisible()
79+
})

packages/app/e2e/utils/mock-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Page, Route } from "@playwright/test"
22

3-
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
3+
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff", "/pty/shells"])
44
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp"])
55

66
export interface MockServerConfig {

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

Lines changed: 80 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ import { useServerSync } from "@/context/server-sync"
1717
import { useLanguage } from "@/context/language"
1818
import { useProviders } from "@/hooks/use-providers"
1919

20-
export function DialogConnectProvider(props: { provider: string; directory?: Accessor<string | undefined> }) {
20+
export function DialogConnectProvider(props: {
21+
provider: string
22+
directory?: Accessor<string | undefined>
23+
onBack?: () => void
24+
content?: boolean
25+
bind?: (value: { back: () => void }) => void
26+
}) {
2127
const dialog = useDialog()
2228
const serverSync = useServerSync()
2329
const serverSDK = useServerSDK()
2430
const language = useLanguage()
2531
const providers = useProviders(props.directory)
2632

2733
const all = () => {
34+
if (props.onBack) {
35+
props.onBack()
36+
return
37+
}
2838
void import("./dialog-select-provider").then((x) => {
2939
dialog.show(() => <x.DialogSelectProvider directory={props.directory} />)
3040
})
@@ -598,6 +608,74 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
598608
)
599609
}
600610

611+
const content = (
612+
<div class="flex flex-col gap-6 px-2.5 pb-3">
613+
<div class="px-2.5 flex gap-4 items-center">
614+
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
615+
<div class="text-16-medium text-text-strong">
616+
<Switch>
617+
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
618+
{language.t("provider.connect.title.anthropicProMax")}
619+
</Match>
620+
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
621+
</Switch>
622+
</div>
623+
</div>
624+
<div class="px-2.5 pb-10 flex flex-col gap-6">
625+
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
626+
<Switch>
627+
<Match when={loading()}>
628+
<div class="text-14-regular text-text-base">
629+
<div class="flex items-center gap-x-2">
630+
<Spinner />
631+
<span>{language.t("provider.connect.status.inProgress")}</span>
632+
</div>
633+
</div>
634+
</Match>
635+
<Match when={store.methodIndex === undefined}>
636+
<MethodSelection />
637+
</Match>
638+
<Match when={store.state === "pending"}>
639+
<div class="text-14-regular text-text-base">
640+
<div class="flex items-center gap-x-2">
641+
<Spinner />
642+
<span>{language.t("provider.connect.status.inProgress")}</span>
643+
</div>
644+
</div>
645+
</Match>
646+
<Match when={store.state === "prompt"}>
647+
<AuthPromptsView />
648+
</Match>
649+
<Match when={store.state === "error"}>
650+
<div class="text-14-regular text-text-base">
651+
<div class="flex items-center gap-x-2">
652+
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
653+
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
654+
</div>
655+
</div>
656+
</Match>
657+
<Match when={method()?.type === "api"}>
658+
<ApiAuthView />
659+
</Match>
660+
<Match when={method()?.type === "oauth"}>
661+
<Switch>
662+
<Match when={store.authorization?.method === "code"}>
663+
<OAuthCodeView />
664+
</Match>
665+
<Match when={store.authorization?.method === "auto"}>
666+
<OAuthAutoView />
667+
</Match>
668+
</Switch>
669+
</Match>
670+
</Switch>
671+
</div>
672+
</div>
673+
</div>
674+
)
675+
676+
props.bind?.({ back: goBack })
677+
if (props.content) return content
678+
601679
return (
602680
<Dialog
603681
title={
@@ -610,68 +688,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
610688
/>
611689
}
612690
>
613-
<div class="flex flex-col gap-6 px-2.5 pb-3">
614-
<div class="px-2.5 flex gap-4 items-center">
615-
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
616-
<div class="text-16-medium text-text-strong">
617-
<Switch>
618-
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
619-
{language.t("provider.connect.title.anthropicProMax")}
620-
</Match>
621-
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
622-
</Switch>
623-
</div>
624-
</div>
625-
<div class="px-2.5 pb-10 flex flex-col gap-6">
626-
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
627-
<Switch>
628-
<Match when={loading()}>
629-
<div class="text-14-regular text-text-base">
630-
<div class="flex items-center gap-x-2">
631-
<Spinner />
632-
<span>{language.t("provider.connect.status.inProgress")}</span>
633-
</div>
634-
</div>
635-
</Match>
636-
<Match when={store.methodIndex === undefined}>
637-
<MethodSelection />
638-
</Match>
639-
<Match when={store.state === "pending"}>
640-
<div class="text-14-regular text-text-base">
641-
<div class="flex items-center gap-x-2">
642-
<Spinner />
643-
<span>{language.t("provider.connect.status.inProgress")}</span>
644-
</div>
645-
</div>
646-
</Match>
647-
<Match when={store.state === "prompt"}>
648-
<AuthPromptsView />
649-
</Match>
650-
<Match when={store.state === "error"}>
651-
<div class="text-14-regular text-text-base">
652-
<div class="flex items-center gap-x-2">
653-
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
654-
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
655-
</div>
656-
</div>
657-
</Match>
658-
<Match when={method()?.type === "api"}>
659-
<ApiAuthView />
660-
</Match>
661-
<Match when={method()?.type === "oauth"}>
662-
<Switch>
663-
<Match when={store.authorization?.method === "code"}>
664-
<OAuthCodeView />
665-
</Match>
666-
<Match when={store.authorization?.method === "auto"}>
667-
<OAuthAutoView />
668-
</Match>
669-
</Switch>
670-
</Match>
671-
</Switch>
672-
</div>
673-
</div>
674-
</div>
691+
{content}
675692
</Dialog>
676693
)
677694
}

0 commit comments

Comments
 (0)