mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(app): fit connect dialog steps to their content under a fixed top edge (#57006)
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
b9ed73b24f
commit
48dcd2fa40
@@ -2,7 +2,7 @@ import { expect, test, type Page } from "@playwright/test"
|
||||
import type { ConfigEntry, OpenCodeEvent, WorktreeDirectory } from "@opencode/client/promise"
|
||||
import { NO_PROVIDER, REMOTE_SERVER, SERVER, holdRoute, project, session } from "../utils/app"
|
||||
import { mockMissingLocation, mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { mockRemoteServer, mockWorkspace, openSettings, type WorkspaceInput } from "../utils/workspace"
|
||||
import { mockRemoteServer, mockWorkspace, openSession, openSettings, type WorkspaceInput } from "../utils/workspace"
|
||||
|
||||
const directory = "C:/Projects/settings-demo"
|
||||
|
||||
@@ -138,6 +138,7 @@ test("project list menus rename, edit, add, and stay inside the scrollport", asy
|
||||
const projects = ["rebase", "dinocms", "opencode", "Playground"].map((name, index) =>
|
||||
project({ id: `project-${index}`, directory: `/projects/${name}`, name }),
|
||||
)
|
||||
|
||||
const added = project({ id: "project-added", directory: "/projects/added", name: "added" })
|
||||
|
||||
const { settings } = await open(page, {
|
||||
@@ -842,6 +843,42 @@ test("providers: OpenCode Go connects with its own methods, not the Console sign
|
||||
expect(attempts).toEqual([])
|
||||
})
|
||||
|
||||
test("providers: the free tier upsell opens Go on its own, sized to its content", async ({ page }) => {
|
||||
const view = await openSession(page, { name: "FreeTier", integrations: consoleIntegrations })
|
||||
await view.push([
|
||||
{
|
||||
id: "evt_free_tier_limit",
|
||||
created: 1700000000000,
|
||||
type: "session.status",
|
||||
location: { directory: view.directory },
|
||||
data: {
|
||||
sessionID: view.session.id,
|
||||
status: {
|
||||
type: "retry",
|
||||
attempt: 1,
|
||||
message: "Free tier limit reached",
|
||||
next: 1700000060000,
|
||||
action: {
|
||||
reason: "free_tier_limit",
|
||||
provider: "opencode",
|
||||
title: "Free tier limit reached",
|
||||
message: "Subscribe to OpenCode Go to keep going.",
|
||||
label: "Continue with OpenCode Go",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
await page.getByRole("button", { name: "Continue with OpenCode Go", exact: true }).click()
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Connect OpenCode Go", exact: true })
|
||||
const container = page.locator('[data-slot="dialog-container"]')
|
||||
await expect(dialog.getByPlaceholder("API key")).toBeVisible()
|
||||
// Nothing to go back to: the upsell opened the dialog straight on Go.
|
||||
await expect(dialog.getByRole("button", { name: "Navigate back", exact: true })).toHaveCount(0)
|
||||
await expect.poll(async () => (await container.boundingBox())?.height).toBeLessThan(512)
|
||||
})
|
||||
|
||||
test("providers: the connect dialog lists connected providers and manages their accounts", async ({ page }) => {
|
||||
const accounts = [
|
||||
{ type: "credential", id: "cred_work", label: "Work", method: "key" },
|
||||
@@ -880,9 +917,15 @@ test("providers: the connect dialog lists connected providers and manages their
|
||||
await expect(
|
||||
dialog.locator('[data-provider-id="anthropic"]').locator('[data-slot="provider-connected"]'),
|
||||
).toHaveCount(0)
|
||||
const container = page.locator('[data-slot="dialog-container"]')
|
||||
await expect.poll(async () => (await container.boundingBox())?.height).toBe(512)
|
||||
const picker = await container.boundingBox()
|
||||
await openai.click()
|
||||
|
||||
await expect(page.getByRole("dialog", { name: "OpenAI", exact: true })).toBeVisible()
|
||||
// The provider step fits its content below the list's top edge.
|
||||
await expect.poll(async () => (await container.boundingBox())?.height).toBeLessThan(picker!.height)
|
||||
expect((await container.boundingBox())?.y).toBe(picker!.y)
|
||||
const list = dialog.getByRole("radiogroup", { name: "Connected accounts", exact: true })
|
||||
await expect(list.getByRole("radio", { name: "Work", exact: true })).toHaveAttribute("aria-checked", "true")
|
||||
// Rows keep their order when the active account changes.
|
||||
|
||||
@@ -74,6 +74,8 @@ export const DialogConnectProvider: Component<{
|
||||
/** Connects at the server's default Location instead of the current route's directory. */
|
||||
defaultLocation?: boolean
|
||||
controller?: ReturnType<typeof useProviderConnectController>
|
||||
/** Opened for one provider from another flow, with no way back to the provider list. */
|
||||
standalone?: boolean
|
||||
selection?: ModelSelection
|
||||
onDone?: () => void
|
||||
onConnected?: (provider: string) => void
|
||||
@@ -87,11 +89,13 @@ export const DialogConnectProvider: Component<{
|
||||
authorization: boolean
|
||||
chatgptWelcome: boolean
|
||||
picked: boolean
|
||||
stepBack: boolean
|
||||
}>({
|
||||
completed: false,
|
||||
authorization: false,
|
||||
chatgptWelcome: false,
|
||||
picked: false,
|
||||
stepBack: false,
|
||||
})
|
||||
|
||||
const language = useLanguage()
|
||||
@@ -136,6 +140,7 @@ export const DialogConnectProvider: Component<{
|
||||
}}
|
||||
onFirstConnection={(provider) => setState("modelProvider", provider)}
|
||||
onAuthorization={(authorization) => setState("authorization", authorization)}
|
||||
onStepBack={(stepBack) => setState("stepBack", stepBack)}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
@@ -149,13 +154,14 @@ export const DialogConnectProvider: Component<{
|
||||
return (
|
||||
<Dialog
|
||||
preventBackdropDismiss={state.authorization}
|
||||
containerClass={
|
||||
// The top edge stays where the provider list puts it, so a step that fits its content only moves the bottom edge.
|
||||
containerClass={`!self-start [--connect-top:max(8px,calc((100vh_-_512px)/2))] !mt-[var(--connect-top)] !w-[min(calc(100vw_-_16px),640px)] ${
|
||||
state.modelProvider
|
||||
? "!h-[min(calc(100vh_-_16px),560px)] !w-[min(calc(100vw_-_16px),640px)]"
|
||||
: consoleSelected() && state.authorization
|
||||
? "!h-auto !max-h-[min(calc(100vh_-_16px),560px)] !w-[min(calc(100vw_-_16px),640px)]"
|
||||
: "!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
|
||||
}
|
||||
? "!h-[min(calc(100vh_-_8px_-_var(--connect-top)),560px)]"
|
||||
: controller.selected()
|
||||
? "!h-auto !max-h-[min(calc(100vh_-_8px_-_var(--connect-top)),560px)]"
|
||||
: "!h-[min(calc(100vh_-_16px),512px)]"
|
||||
}`}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (state.completed && props.onDone) {
|
||||
event.preventDefault()
|
||||
@@ -186,7 +192,7 @@ export const DialogConnectProvider: Component<{
|
||||
</div>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={controller.selected()}>
|
||||
<Match when={controller.selected() && (!props.standalone || state.stepBack)}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||
@@ -196,9 +202,13 @@ export const DialogConnectProvider: Component<{
|
||||
<Icon name="arrow-left" size="small" />
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Match when={!props.standalone}>
|
||||
<DialogTitle>{language.t("command.provider.connect")}</DialogTitle>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
{/* Holds the back button's space so the content doesn't shift when it appears. */}
|
||||
<span aria-hidden="true" class="size-5" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</DialogHeader>
|
||||
<DialogBody
|
||||
@@ -425,6 +435,7 @@ function ProviderConnection(props: {
|
||||
onConnected?: (methodID?: string) => void
|
||||
onFirstConnection: (provider: { id: string; name: string }) => void
|
||||
onAuthorization: (authorization: boolean) => void
|
||||
onStepBack: (stepBack: boolean) => void
|
||||
accounts?: boolean
|
||||
}) {
|
||||
const dialog = useDialog()
|
||||
@@ -1037,20 +1048,25 @@ function ProviderConnection(props: {
|
||||
)
|
||||
}
|
||||
|
||||
const backToAccounts = () =>
|
||||
!!props.accounts &&
|
||||
state.adding &&
|
||||
accounts().length > 0 &&
|
||||
(state.direct || controller.methodIndex() === undefined)
|
||||
|
||||
const backToMethods = () => (isConsole || controller.methods().length > 1) && controller.methodIndex() !== undefined
|
||||
|
||||
createEffect(() => props.onStepBack(backToAccounts() || backToMethods()))
|
||||
|
||||
function goBack() {
|
||||
if (
|
||||
props.accounts &&
|
||||
state.adding &&
|
||||
accounts().length > 0 &&
|
||||
(state.direct || controller.methodIndex() === undefined)
|
||||
) {
|
||||
if (backToAccounts()) {
|
||||
controller.auth.reset()
|
||||
setState({ adding: false, direct: false })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ((isConsole || controller.methods().length > 1) && controller.methodIndex() !== undefined) {
|
||||
if (backToMethods()) {
|
||||
controller.auth.reset()
|
||||
|
||||
return
|
||||
|
||||
@@ -128,7 +128,7 @@ export function useUsageExceededDialogs() {
|
||||
void import("@/providers/connect/dialog").then((x) => {
|
||||
const controller = x.useProviderConnectController()
|
||||
controller.select("opencode-go")
|
||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} />)
|
||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} standalone />)
|
||||
})
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user