1- import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
1+ import type { ProviderAuthAuthorization , ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
22import { Button } from "@opencode-ai/ui/button"
33import { useDialog } from "@opencode-ai/ui/context/dialog"
44import { Dialog } from "@opencode-ai/ui/dialog"
@@ -9,21 +9,18 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
99import { Spinner } from "@opencode-ai/ui/spinner"
1010import { TextField } from "@opencode-ai/ui/text-field"
1111import { showToast } from "@opencode-ai/ui/toast"
12- import { createMemo , Match , onCleanup , onMount , Switch } from "solid-js"
12+ import { createEffect , createMemo , createResource , Match , onCleanup , onMount , Switch } from "solid-js"
1313import { createStore , produce } from "solid-js/store"
1414import { Link } from "@/components/link"
15- import { useLanguage } from "@/context/language"
1615import { useGlobalSDK } from "@/context/global-sdk"
1716import { useGlobalSync } from "@/context/global-sync"
18- import { usePlatform } from "@/context/platform"
19- import { DialogSelectModel } from "./dialog-select-model"
17+ import { useLanguage } from "@/context/language"
2018import { DialogSelectProvider } from "./dialog-select-provider"
2119
2220export function DialogConnectProvider ( props : { provider : string } ) {
2321 const dialog = useDialog ( )
2422 const globalSync = useGlobalSync ( )
2523 const globalSDK = useGlobalSDK ( )
26- const platform = usePlatform ( )
2724 const language = useLanguage ( )
2825
2926 const alive = { value : true }
@@ -37,25 +34,36 @@ export function DialogConnectProvider(props: { provider: string }) {
3734 } )
3835
3936 const provider = createMemo ( ( ) => globalSync . data . provider . all . find ( ( x ) => x . id === props . provider ) ! )
40- const methods = createMemo (
41- ( ) =>
42- globalSync . data . provider_auth [ props . provider ] ?? [
43- {
44- type : "api" ,
45- label : language . t ( "provider.connect.method.apiKey" ) ,
46- } ,
47- ] ,
37+ const fallback = createMemo < ProviderAuthMethod [ ] > ( ( ) => [
38+ {
39+ type : "api" as const ,
40+ label : language . t ( "provider.connect.method.apiKey" ) ,
41+ } ,
42+ ] )
43+ const [ auth ] = createResource (
44+ ( ) => props . provider ,
45+ async ( ) => {
46+ const cached = globalSync . data . provider_auth [ props . provider ]
47+ if ( cached ) return cached
48+ const res = await globalSDK . client . provider . auth ( )
49+ if ( ! alive . value ) return fallback ( )
50+ globalSync . set ( "provider_auth" , res . data ?? { } )
51+ return res . data ?. [ props . provider ] ?? fallback ( )
52+ } ,
4853 )
54+ const loading = createMemo ( ( ) => auth . loading && ! globalSync . data . provider_auth [ props . provider ] )
55+ const methods = createMemo ( ( ) => auth . latest ?? globalSync . data . provider_auth [ props . provider ] ?? fallback ( ) )
4956 const [ store , setStore ] = createStore ( {
5057 methodIndex : undefined as undefined | number ,
5158 authorization : undefined as undefined | ProviderAuthAuthorization ,
52- state : "pending" as undefined | "pending" | "complete" | "error" ,
59+ state : "pending" as undefined | "pending" | "complete" | "error" | "prompt" ,
5360 error : undefined as string | undefined ,
5461 } )
5562
5663 type Action =
5764 | { type : "method.select" ; index : number }
5865 | { type : "method.reset" }
66+ | { type : "auth.prompt" }
5967 | { type : "auth.pending" }
6068 | { type : "auth.complete" ; authorization : ProviderAuthAuthorization }
6169 | { type : "auth.error" ; error : string }
@@ -77,6 +85,11 @@ export function DialogConnectProvider(props: { provider: string }) {
7785 draft . error = undefined
7886 return
7987 }
88+ if ( action . type === "auth.prompt" ) {
89+ draft . state = "prompt"
90+ draft . error = undefined
91+ return
92+ }
8093 if ( action . type === "auth.pending" ) {
8194 draft . state = "pending"
8295 draft . error = undefined
@@ -120,7 +133,7 @@ export function DialogConnectProvider(props: { provider: string }) {
120133 return fallback
121134 }
122135
123- async function selectMethod ( index : number ) {
136+ async function selectMethod ( index : number , inputs ?: Record < string , string > ) {
124137 if ( timer . current !== undefined ) {
125138 clearTimeout ( timer . current )
126139 timer . current = undefined
@@ -130,13 +143,18 @@ export function DialogConnectProvider(props: { provider: string }) {
130143 dispatch ( { type : "method.select" , index } )
131144
132145 if ( method . type === "oauth" ) {
146+ if ( method . prompts ?. length && ! inputs ) {
147+ dispatch ( { type : "auth.prompt" } )
148+ return
149+ }
133150 dispatch ( { type : "auth.pending" } )
134151 const start = Date . now ( )
135152 await globalSDK . client . provider . oauth
136153 . authorize (
137154 {
138155 providerID : props . provider ,
139156 method : index ,
157+ inputs,
140158 } ,
141159 { throwOnError : true } ,
142160 )
@@ -163,6 +181,126 @@ export function DialogConnectProvider(props: { provider: string }) {
163181 }
164182 }
165183
184+ function OAuthPromptsView ( ) {
185+ const [ formStore , setFormStore ] = createStore ( {
186+ value : { } as Record < string , string > ,
187+ index : 0 ,
188+ } )
189+
190+ const prompts = createMemo < NonNullable < ProviderAuthMethod [ "prompts" ] > > ( ( ) => {
191+ const value = method ( )
192+ if ( value ?. type !== "oauth" ) return [ ]
193+ return value . prompts ?? [ ]
194+ } )
195+ const matches = ( prompt : NonNullable < ReturnType < typeof prompts > [ number ] > , value : Record < string , string > ) => {
196+ if ( ! prompt . when ) return true
197+ const actual = value [ prompt . when . key ]
198+ if ( actual === undefined ) return false
199+ return prompt . when . op === "eq" ? actual === prompt . when . value : actual !== prompt . when . value
200+ }
201+ const current = createMemo ( ( ) => {
202+ const all = prompts ( )
203+ const index = all . findIndex ( ( prompt , index ) => index >= formStore . index && matches ( prompt , formStore . value ) )
204+ if ( index === - 1 ) return
205+ return {
206+ index,
207+ prompt : all [ index ] ,
208+ }
209+ } )
210+ const valid = createMemo ( ( ) => {
211+ const item = current ( )
212+ if ( ! item || item . prompt . type !== "text" ) return false
213+ const value = formStore . value [ item . prompt . key ] ?? ""
214+ return value . trim ( ) . length > 0
215+ } )
216+
217+ async function next ( index : number , value : Record < string , string > ) {
218+ if ( store . methodIndex === undefined ) return
219+ const next = prompts ( ) . findIndex ( ( prompt , i ) => i > index && matches ( prompt , value ) )
220+ if ( next !== - 1 ) {
221+ setFormStore ( "index" , next )
222+ return
223+ }
224+ await selectMethod ( store . methodIndex , value )
225+ }
226+
227+ async function handleSubmit ( e : SubmitEvent ) {
228+ e . preventDefault ( )
229+ const item = current ( )
230+ if ( ! item || item . prompt . type !== "text" ) return
231+ if ( ! valid ( ) ) return
232+ await next ( item . index , formStore . value )
233+ }
234+
235+ const item = ( ) => current ( )
236+ const text = createMemo ( ( ) => {
237+ const prompt = item ( ) ?. prompt
238+ if ( ! prompt || prompt . type !== "text" ) return
239+ return prompt
240+ } )
241+ const select = createMemo ( ( ) => {
242+ const prompt = item ( ) ?. prompt
243+ if ( ! prompt || prompt . type !== "select" ) return
244+ return prompt
245+ } )
246+
247+ return (
248+ < form onSubmit = { handleSubmit } class = "flex flex-col items-start gap-4" >
249+ < Switch >
250+ < Match when = { item ( ) ?. prompt . type === "text" } >
251+ < TextField
252+ type = "text"
253+ label = { text ( ) ?. message ?? "" }
254+ placeholder = { text ( ) ?. placeholder }
255+ value = { text ( ) ? ( formStore . value [ text ( ) ! . key ] ?? "" ) : "" }
256+ onChange = { ( value ) => {
257+ const prompt = text ( )
258+ if ( ! prompt ) return
259+ setFormStore ( "value" , prompt . key , value )
260+ } }
261+ />
262+ < Button class = "w-auto" type = "submit" size = "large" variant = "primary" disabled = { ! valid ( ) } >
263+ { language . t ( "common.continue" ) }
264+ </ Button >
265+ </ Match >
266+ < Match when = { item ( ) ?. prompt . type === "select" } >
267+ < div class = "w-full flex flex-col gap-1.5" >
268+ < div class = "text-14-regular text-text-base" > { select ( ) ?. message } </ div >
269+ < div >
270+ < List
271+ items = { select ( ) ?. options ?? [ ] }
272+ key = { ( x ) => x . value }
273+ current = { select ( ) ?. options . find ( ( x ) => x . value === formStore . value [ select ( ) ! . key ] ) }
274+ onSelect = { ( value ) => {
275+ if ( ! value ) return
276+ const prompt = select ( )
277+ if ( ! prompt ) return
278+ const nextValue = {
279+ ...formStore . value ,
280+ [ prompt . key ] : value . value ,
281+ }
282+ setFormStore ( "value" , prompt . key , value . value )
283+ void next ( item ( ) ! . index , nextValue )
284+ } }
285+ >
286+ { ( option ) => (
287+ < div class = "w-full flex items-center gap-x-2" >
288+ < div class = "w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center" >
289+ < div class = "w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot = "list-item-extra-icon" />
290+ </ div >
291+ < span > { option . label } </ span >
292+ < span class = "text-14-regular text-text-weak" > { option . hint } </ span >
293+ </ div >
294+ ) }
295+ </ List >
296+ </ div >
297+ </ div >
298+ </ Match >
299+ </ Switch >
300+ </ form >
301+ )
302+ }
303+
166304 let listRef : ListRef | undefined
167305 function handleKey ( e : KeyboardEvent ) {
168306 if ( e . key === "Enter" && e . target instanceof HTMLInputElement ) {
@@ -172,8 +310,12 @@ export function DialogConnectProvider(props: { provider: string }) {
172310 listRef ?. onKeyDown ( e )
173311 }
174312
175- onMount ( ( ) => {
313+ let auto = false
314+ createEffect ( ( ) => {
315+ if ( auto ) return
316+ if ( loading ( ) ) return
176317 if ( methods ( ) . length === 1 ) {
318+ auto = true
177319 selectMethod ( 0 )
178320 }
179321 } )
@@ -301,7 +443,7 @@ export function DialogConnectProvider(props: { provider: string }) {
301443 error = { formStore . error }
302444 />
303445 < Button class = "w-auto" type = "submit" size = "large" variant = "primary" >
304- { language . t ( "common.submit " ) }
446+ { language . t ( "common.continue " ) }
305447 </ Button >
306448 </ form >
307449 </ div >
@@ -314,12 +456,6 @@ export function DialogConnectProvider(props: { provider: string }) {
314456 error : undefined as string | undefined ,
315457 } )
316458
317- onMount ( ( ) => {
318- if ( store . authorization ?. method === "code" && store . authorization ?. url ) {
319- platform . openLink ( store . authorization . url )
320- }
321- } )
322-
323459 async function handleSubmit ( e : SubmitEvent ) {
324460 e . preventDefault ( )
325461
@@ -368,7 +504,7 @@ export function DialogConnectProvider(props: { provider: string }) {
368504 error = { formStore . error }
369505 />
370506 < Button class = "w-auto" type = "submit" size = "large" variant = "primary" >
371- { language . t ( "common.submit " ) }
507+ { language . t ( "common.continue " ) }
372508 </ Button >
373509 </ form >
374510 </ div >
@@ -386,10 +522,6 @@ export function DialogConnectProvider(props: { provider: string }) {
386522
387523 onMount ( ( ) => {
388524 void ( async ( ) => {
389- if ( store . authorization ?. url ) {
390- platform . openLink ( store . authorization . url )
391- }
392-
393525 const result = await globalSDK . client . provider . oauth
394526 . callback ( {
395527 providerID : props . provider ,
@@ -459,6 +591,14 @@ export function DialogConnectProvider(props: { provider: string }) {
459591 < div class = "px-2.5 pb-10 flex flex-col gap-6" >
460592 < div onKeyDown = { handleKey } tabIndex = { 0 } autofocus = { store . methodIndex === undefined ? true : undefined } >
461593 < Switch >
594+ < Match when = { loading ( ) } >
595+ < div class = "text-14-regular text-text-base" >
596+ < div class = "flex items-center gap-x-2" >
597+ < Spinner />
598+ < span > { language . t ( "provider.connect.status.inProgress" ) } </ span >
599+ </ div >
600+ </ div >
601+ </ Match >
462602 < Match when = { store . methodIndex === undefined } >
463603 < MethodSelection />
464604 </ Match >
@@ -470,6 +610,9 @@ export function DialogConnectProvider(props: { provider: string }) {
470610 </ div >
471611 </ div >
472612 </ Match >
613+ < Match when = { store . state === "prompt" } >
614+ < OAuthPromptsView />
615+ </ Match >
473616 < Match when = { store . state === "error" } >
474617 < div class = "text-14-regular text-text-base" >
475618 < div class = "flex items-center gap-x-2" >
0 commit comments