From b57896afcb44232a34140d6d27c2be3e6bb28afd Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 18 Mar 2026 11:36:56 -0500 Subject: [PATCH 1/4] feat: integrate multistep auth flows into desktop app --- .../components/dialog-connect-provider.tsx | 92 ++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index b042205cf4d..05645f45bcd 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -9,7 +9,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" -import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" +import { createMemo, For, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useLanguage } from "@/context/language" @@ -49,13 +49,14 @@ export function DialogConnectProvider(props: { provider: string }) { const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | ProviderAuthAuthorization, - state: "pending" as undefined | "pending" | "complete" | "error", + state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", error: undefined as string | undefined, }) type Action = | { type: "method.select"; index: number } | { type: "method.reset" } + | { type: "auth.prompt" } | { type: "auth.pending" } | { type: "auth.complete"; authorization: ProviderAuthAuthorization } | { type: "auth.error"; error: string } @@ -77,6 +78,11 @@ export function DialogConnectProvider(props: { provider: string }) { draft.error = undefined return } + if (action.type === "auth.prompt") { + draft.state = "prompt" + draft.error = undefined + return + } if (action.type === "auth.pending") { draft.state = "pending" draft.error = undefined @@ -120,7 +126,7 @@ export function DialogConnectProvider(props: { provider: string }) { return fallback } - async function selectMethod(index: number) { + async function selectMethod(index: number, inputs?: Record) { if (timer.current !== undefined) { clearTimeout(timer.current) timer.current = undefined @@ -130,6 +136,10 @@ export function DialogConnectProvider(props: { provider: string }) { dispatch({ type: "method.select", index }) if (method.type === "oauth") { + if (method.prompts?.length && !inputs) { + dispatch({ type: "auth.prompt" }) + return + } dispatch({ type: "auth.pending" }) const start = Date.now() await globalSDK.client.provider.oauth @@ -137,6 +147,7 @@ export function DialogConnectProvider(props: { provider: string }) { { providerID: props.provider, method: index, + inputs, }, { throwOnError: true }, ) @@ -163,6 +174,78 @@ export function DialogConnectProvider(props: { provider: string }) { } } + function OAuthPromptsView() { + const [formStore, setFormStore] = createStore({ + value: {} as Record, + }) + + const prompts = createMemo(() => method()?.prompts ?? []) + const visible = createMemo(() => + prompts().filter((prompt) => { + if (!prompt.when) return true + const value = formStore.value[prompt.when.key] + if (value === undefined) return false + const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value + if (!matches) return false + return true + }), + ) + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + if (store.methodIndex === undefined) return + await selectMethod(store.methodIndex, formStore.value) + } + + return ( +
+ + {(prompt) => { + if (prompt.type === "text") { + return ( + setFormStore("value", prompt.key, value)} + /> + ) + } + + return ( +
+
{prompt.message}
+ x.value} + current={prompt.options.find((x) => x.value === formStore.value[prompt.key])} + onSelect={(value) => { + if (!value) return + setFormStore("value", prompt.key, value.value) + }} + > + {(option) => ( +
+
+ + {option.label} + {option.hint} +
+ )} + +
+ ) + }} + + + + ) + } + let listRef: ListRef | undefined function handleKey(e: KeyboardEvent) { if (e.key === "Enter" && e.target instanceof HTMLInputElement) { @@ -470,6 +553,9 @@ export function DialogConnectProvider(props: { provider: string }) {
+ + +
From bce460ef14138ca3da5543b1406df139a7eb7c18 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 18 Mar 2026 13:48:33 -0500 Subject: [PATCH 2/4] tweaks: rename Submit -> continue, stop auto navigating --- .../src/components/dialog-connect-provider.tsx | 18 +++--------------- packages/app/src/i18n/ar.ts | 1 + packages/app/src/i18n/br.ts | 1 + packages/app/src/i18n/bs.ts | 1 + packages/app/src/i18n/da.ts | 1 + packages/app/src/i18n/de.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 1 + packages/app/src/i18n/fr.ts | 1 + packages/app/src/i18n/ja.ts | 1 + packages/app/src/i18n/ko.ts | 1 + packages/app/src/i18n/no.ts | 1 + packages/app/src/i18n/pl.ts | 1 + packages/app/src/i18n/ru.ts | 1 + packages/app/src/i18n/th.ts | 1 + packages/app/src/i18n/tr.ts | 1 + packages/app/src/i18n/zh.ts | 1 + packages/app/src/i18n/zht.ts | 1 + 18 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 05645f45bcd..653851dee72 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -15,7 +15,6 @@ import { Link } from "@/components/link" import { useLanguage } from "@/context/language" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" -import { usePlatform } from "@/context/platform" import { DialogSelectModel } from "./dialog-select-model" import { DialogSelectProvider } from "./dialog-select-provider" @@ -23,7 +22,6 @@ export function DialogConnectProvider(props: { provider: string }) { const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() - const platform = usePlatform() const language = useLanguage() const alive = { value: true } @@ -240,7 +238,7 @@ export function DialogConnectProvider(props: { provider: string }) { }} ) @@ -384,7 +382,7 @@ export function DialogConnectProvider(props: { provider: string }) { error={formStore.error} />
@@ -397,12 +395,6 @@ export function DialogConnectProvider(props: { provider: string }) { error: undefined as string | undefined, }) - onMount(() => { - if (store.authorization?.method === "code" && store.authorization?.url) { - platform.openLink(store.authorization.url) - } - }) - async function handleSubmit(e: SubmitEvent) { e.preventDefault() @@ -451,7 +443,7 @@ export function DialogConnectProvider(props: { provider: string }) { error={formStore.error} />
@@ -469,10 +461,6 @@ export function DialogConnectProvider(props: { provider: string }) { onMount(() => { void (async () => { - if (store.authorization?.url) { - platform.openLink(store.authorization.url) - } - const result = await globalSDK.client.provider.oauth .callback({ providerID: props.provider, diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 720045a4d1c..c8f58c796e6 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -204,6 +204,7 @@ export const dict = { "common.cancel": "إلغاء", "common.connect": "اتصال", "common.disconnect": "قطع الاتصال", + "common.continue": "إرسال", "common.submit": "إرسال", "common.save": "حفظ", "common.saving": "جارٍ الحفظ...", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index a7d7433b02c..3112e91bbea 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -204,6 +204,7 @@ export const dict = { "common.cancel": "Cancelar", "common.connect": "Conectar", "common.disconnect": "Desconectar", + "common.continue": "Enviar", "common.submit": "Enviar", "common.save": "Salvar", "common.saving": "Salvando...", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index ccdf2b6044d..f2dbd8493c6 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -221,6 +221,7 @@ export const dict = { "common.cancel": "Otkaži", "common.connect": "Poveži", "common.disconnect": "Prekini vezu", + "common.continue": "Pošalji", "common.submit": "Pošalji", "common.save": "Sačuvaj", "common.saving": "Čuvanje...", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index f1701094b56..e90e1071ad5 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -219,6 +219,7 @@ export const dict = { "common.cancel": "Annuller", "common.connect": "Forbind", "common.disconnect": "Frakobl", + "common.continue": "Indsend", "common.submit": "Indsend", "common.save": "Gem", "common.saving": "Gemmer...", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 2dfeed72032..69658b29e9a 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -209,6 +209,7 @@ export const dict = { "common.cancel": "Abbrechen", "common.connect": "Verbinden", "common.disconnect": "Trennen", + "common.continue": "Absenden", "common.submit": "Absenden", "common.save": "Speichern", "common.saving": "Speichert...", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 7f6816de9e3..72caed40ad9 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -221,6 +221,7 @@ export const dict = { "common.open": "Open", "common.connect": "Connect", "common.disconnect": "Disconnect", + "common.continue": "Continue", "common.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 1cd47dfc796..9e36e4de6db 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "Cancelar", "common.connect": "Conectar", "common.disconnect": "Desconectar", + "common.continue": "Enviar", "common.submit": "Enviar", "common.save": "Guardar", "common.saving": "Guardando...", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index c7d89c3251b..f53b3882c6d 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -204,6 +204,7 @@ export const dict = { "common.cancel": "Annuler", "common.connect": "Connecter", "common.disconnect": "Déconnecter", + "common.continue": "Soumettre", "common.submit": "Soumettre", "common.save": "Enregistrer", "common.saving": "Enregistrement...", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 267411083f4..d66a7341d5a 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -203,6 +203,7 @@ export const dict = { "common.cancel": "キャンセル", "common.connect": "接続", "common.disconnect": "切断", + "common.continue": "送信", "common.submit": "送信", "common.save": "保存", "common.saving": "保存中...", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index bb57f99396b..d534c27e8fb 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -207,6 +207,7 @@ export const dict = { "common.cancel": "취소", "common.connect": "연결", "common.disconnect": "연결 해제", + "common.continue": "제출", "common.submit": "제출", "common.save": "저장", "common.saving": "저장 중...", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 83d6a9903b6..c23d0a27927 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -223,6 +223,7 @@ export const dict = { "common.cancel": "Avbryt", "common.connect": "Koble til", "common.disconnect": "Koble fra", + "common.continue": "Send inn", "common.submit": "Send inn", "common.save": "Lagre", "common.saving": "Lagrer...", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index db9ef18003e..dac847b217f 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -205,6 +205,7 @@ export const dict = { "common.cancel": "Anuluj", "common.connect": "Połącz", "common.disconnect": "Rozłącz", + "common.continue": "Prześlij", "common.submit": "Prześlij", "common.save": "Zapisz", "common.saving": "Zapisywanie...", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index e1abb6e6cf6..684d5deecd0 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "Отмена", "common.connect": "Подключить", "common.disconnect": "Отключить", + "common.continue": "Отправить", "common.submit": "Отправить", "common.save": "Сохранить", "common.saving": "Сохранение...", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index b522e4631b9..80f0da94ec6 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "ยกเลิก", "common.connect": "เชื่อมต่อ", "common.disconnect": "ยกเลิกการเชื่อมต่อ", + "common.continue": "ส่ง", "common.submit": "ส่ง", "common.save": "บันทึก", "common.saving": "กำลังบันทึก...", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 8542dff799b..9041e0dd07f 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -225,6 +225,7 @@ export const dict = { "common.cancel": "İptal", "common.connect": "Bağlan", "common.disconnect": "Bağlantı Kes", + "common.continue": "Gönder", "common.submit": "Gönder", "common.save": "Kaydet", "common.saving": "Kaydediliyor...", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index e762ba78d9c..cf64ca9b2c5 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -242,6 +242,7 @@ export const dict = { "common.cancel": "取消", "common.connect": "连接", "common.disconnect": "断开连接", + "common.continue": "提交", "common.submit": "提交", "common.save": "保存", "common.saving": "保存中...", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 184c789ce36..02c00d17a22 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -220,6 +220,7 @@ export const dict = { "common.cancel": "取消", "common.connect": "連線", "common.disconnect": "中斷連線", + "common.continue": "提交", "common.submit": "提交", "common.save": "儲存", "common.saving": "儲存中...", From c9d02071ddfd5f9399182258b1a4ae83c9a83b4f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 18 Mar 2026 14:03:25 -0500 Subject: [PATCH 3/4] tweaks: better continue enable validation, etc --- .../src/components/dialog-connect-provider.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 653851dee72..363f77abea9 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -188,9 +188,17 @@ export function DialogConnectProvider(props: { provider: string }) { return true }), ) + const valid = createMemo(() => + visible().every((prompt) => { + const value = formStore.value[prompt.key] ?? "" + if (prompt.type === "text") return value.trim().length > 0 + return value.length > 0 + }), + ) async function handleSubmit(e: SubmitEvent) { e.preventDefault() + if (!valid()) return if (store.methodIndex === undefined) return await selectMethod(store.methodIndex, formStore.value) } @@ -224,10 +232,7 @@ export function DialogConnectProvider(props: { provider: string }) { }} > {(option) => ( -
-
- +
{option.label} {option.hint}
@@ -237,7 +242,7 @@ export function DialogConnectProvider(props: { provider: string }) { ) }} - From dee2d94a60089b7c904dace9fa6a799646f8fc5a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 18 Mar 2026 14:20:31 -0500 Subject: [PATCH 4/4] tweaks: adjust select styling -- maybe incomplete --- .../components/dialog-connect-provider.tsx | 131 ++++++++++++------ 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 363f77abea9..e4fe9e7c4ed 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -9,7 +9,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" -import { createMemo, For, Match, onCleanup, onMount, Switch } from "solid-js" +import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useLanguage } from "@/context/language" @@ -175,76 +175,115 @@ export function DialogConnectProvider(props: { provider: string }) { function OAuthPromptsView() { const [formStore, setFormStore] = createStore({ value: {} as Record, + index: 0, }) const prompts = createMemo(() => method()?.prompts ?? []) - const visible = createMemo(() => - prompts().filter((prompt) => { - if (!prompt.when) return true - const value = formStore.value[prompt.when.key] - if (value === undefined) return false - const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value - if (!matches) return false - return true - }), - ) - const valid = createMemo(() => - visible().every((prompt) => { - const value = formStore.value[prompt.key] ?? "" - if (prompt.type === "text") return value.trim().length > 0 - return value.length > 0 - }), - ) + const matches = (prompt: NonNullable[number]>, value: Record) => { + if (!prompt.when) return true + const actual = value[prompt.when.key] + if (actual === undefined) return false + return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value + } + const current = createMemo(() => { + const all = prompts() + const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value)) + if (index === -1) return + return { + index, + prompt: all[index], + } + }) + const valid = createMemo(() => { + const item = current() + if (!item || item.prompt.type !== "text") return false + const value = formStore.value[item.prompt.key] ?? "" + return value.trim().length > 0 + }) + + async function next(index: number, value: Record) { + if (store.methodIndex === undefined) return + const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value)) + if (next !== -1) { + setFormStore("index", next) + return + } + await selectMethod(store.methodIndex, value) + } async function handleSubmit(e: SubmitEvent) { e.preventDefault() + const item = current() + if (!item || item.prompt.type !== "text") return if (!valid()) return - if (store.methodIndex === undefined) return - await selectMethod(store.methodIndex, formStore.value) + await next(item.index, formStore.value) } + const item = () => current() + const text = createMemo(() => { + const prompt = item()?.prompt + if (!prompt || prompt.type !== "text") return + return prompt + }) + const select = createMemo(() => { + const prompt = item()?.prompt + if (!prompt || prompt.type !== "select") return + return prompt + }) + return (
- - {(prompt) => { - if (prompt.type === "text") { - return ( - setFormStore("value", prompt.key, value)} - /> - ) - } - - return ( -
-
{prompt.message}
+ + + { + const prompt = text() + if (!prompt) return + setFormStore("value", prompt.key, value) + }} + /> + + + +
+
{select()?.message}
+
x.value} - current={prompt.options.find((x) => x.value === formStore.value[prompt.key])} + current={select()?.options.find((x) => x.value === formStore.value[select()!.key])} onSelect={(value) => { if (!value) return + const prompt = select() + if (!prompt) return + const nextValue = { + ...formStore.value, + [prompt.key]: value.value, + } setFormStore("value", prompt.key, value.value) + void next(item()!.index, nextValue) }} > {(option) => ( -
+
+
+ {option.label} {option.hint}
)}
- ) - }} - - +
+ + ) }