tui: return to model selection after OAuth or API key setup

Improves the provider auth flow by restoring the model selection screen after successful provider connection, allowing users to immediately select a model without seeing intermediate dialogs. Also removes redundant dialog close calls in the form component since navigation is now handled at the dialog provider level.
This commit is contained in:
Dax Raad 2025-11-21 00:11:47 -05:00
parent ffe42bdc8a
commit 8eedd6aa07
3 changed files with 51 additions and 17 deletions

View file

@ -2,11 +2,9 @@ import { createMemo, createSignal } from "solid-js"
import { useLocal } from "@tui/context/local"
import { useSync } from "@tui/context/sync"
import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy } from "remeda"
import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { useTheme } from "../context/theme"
import { DialogPrompt } from "../ui/dialog-prompt"
import { useSDK } from "../context/sdk"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
function Free() {
@ -27,7 +25,6 @@ export function DialogModel() {
const local = useLocal()
const sync = useSync()
const dialog = useDialog()
const sdk = useSDK()
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
const connected = createMemo(() =>

View file

@ -1,4 +1,4 @@
import { createMemo, onMount } from "solid-js"
import { createMemo, createSignal, onMount, Show } from "solid-js"
import { useSync } from "@tui/context/sync"
import { map, pipe, sortBy } from "remeda"
import { DialogSelect } from "@tui/ui/dialog-select"
@ -7,7 +7,8 @@ import { useSDK } from "../context/sdk"
import { DialogPrompt } from "../ui/dialog-prompt"
import { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk"
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk"
import { DialogModel } from "./dialog-model"
const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0,
@ -23,19 +24,16 @@ export function createDialogProviderOptions() {
const sync = useSync()
const dialog = useDialog()
const sdk = useSDK()
const { theme } = useTheme()
const options = createMemo(() => {
return pipe(
sync.data.provider_next.all,
map((provider) => ({
title: provider.name,
value: provider.id,
footer: sync.data.provider_next.connected.includes(provider.id)
? "Connected"
: {
opencode: "Recommended",
anthropic: "Claude Max or API key",
}[provider.id],
footer: {
opencode: "Recommended",
anthropic: "Claude Max or API key",
}[provider.id],
async onSelect() {
const methods = sync.data.provider_auth[provider.id] ?? [
{
@ -78,13 +76,15 @@ export function createDialogProviderOptions() {
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
))
}
if (result.data?.method === "auto") {
dialog.replace(() => (
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
))
}
}
if (method.type === "api") {
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
}
},
})),
sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
@ -125,7 +125,7 @@ function AutoMethod(props: AutoMethodProps) {
}
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.clear()
dialog.replace(() => <DialogModel />)
})
return (
@ -153,6 +153,8 @@ function CodeMethod(props: CodeMethodProps) {
const { theme } = useTheme()
const sdk = useSDK()
const sync = useSync()
const dialog = useDialog()
const [error, setError] = createSignal(false)
return (
<DialogPrompt
@ -171,15 +173,52 @@ function CodeMethod(props: CodeMethodProps) {
if (!error) {
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel />)
return
}
setError(true)
}}
description={() => (
<box gap={1}>
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
<text fg={theme.primary}>{props.authorization.url}</text>
<Show when={error()}>
<text fg={theme.error}>Invalid code</text>
</Show>
</box>
)}
/>
)
}
interface ApiMethodProps {
providerID: string
title: string
}
function ApiMethod(props: ApiMethodProps) {
const dialog = useDialog()
const sdk = useSDK()
const sync = useSync()
return (
<DialogPrompt
title={props.title}
placeholder="API key"
onConfirm={async (value) => {
if (!value) return
sdk.client.auth.set({
path: {
id: props.providerID,
},
body: {
type: "api",
key: value,
},
})
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel />)
}}
/>
)
}

View file

@ -21,7 +21,6 @@ export function DialogPrompt(props: DialogPromptProps) {
useKeyboard((evt) => {
if (evt.name === "return") {
props.onConfirm?.(textarea.plainText)
dialog.clear()
}
})
@ -44,7 +43,6 @@ export function DialogPrompt(props: DialogPromptProps) {
<textarea
onSubmit={() => {
props.onConfirm?.(textarea.plainText)
dialog.clear()
}}
height={3}
keyBindings={[{ name: "return", action: "submit" }]}