summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-03-18 22:47:51 -0500
committerGitHub <[email protected]>2026-03-19 13:47:51 +1000
commit8e09e8c6121f03244a1f25281b506a90bcb355d7 (patch)
tree6a23a3cba72743af22e19aae89272c7d0a924e6d /packages/app/src
parent84e62fc662c00ba87f30e1dddab0474a08db487c (diff)
downloadopencode-8e09e8c6121f03244a1f25281b506a90bcb355d7.tar.gz
opencode-8e09e8c6121f03244a1f25281b506a90bcb355d7.zip
feat: integrate multistep auth flows into desktop app (#18103)
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/components/dialog-connect-provider.tsx150
-rw-r--r--packages/app/src/i18n/ar.ts1
-rw-r--r--packages/app/src/i18n/br.ts1
-rw-r--r--packages/app/src/i18n/bs.ts1
-rw-r--r--packages/app/src/i18n/da.ts1
-rw-r--r--packages/app/src/i18n/de.ts1
-rw-r--r--packages/app/src/i18n/en.ts1
-rw-r--r--packages/app/src/i18n/es.ts1
-rw-r--r--packages/app/src/i18n/fr.ts1
-rw-r--r--packages/app/src/i18n/ja.ts1
-rw-r--r--packages/app/src/i18n/ko.ts1
-rw-r--r--packages/app/src/i18n/no.ts1
-rw-r--r--packages/app/src/i18n/pl.ts1
-rw-r--r--packages/app/src/i18n/ru.ts1
-rw-r--r--packages/app/src/i18n/th.ts1
-rw-r--r--packages/app/src/i18n/tr.ts1
-rw-r--r--packages/app/src/i18n/zh.ts1
-rw-r--r--packages/app/src/i18n/zht.ts1
18 files changed, 151 insertions, 16 deletions
diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx
index b042205cf..e4fe9e7c4 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 }
@@ -49,13 +47,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 +76,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 +124,7 @@ export function DialogConnectProvider(props: { provider: string }) {
return fallback
}
- async function selectMethod(index: number) {
+ async function selectMethod(index: number, inputs?: Record<string, string>) {
if (timer.current !== undefined) {
clearTimeout(timer.current)
timer.current = undefined
@@ -130,6 +134,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 +145,7 @@ export function DialogConnectProvider(props: { provider: string }) {
{
providerID: props.provider,
method: index,
+ inputs,
},
{ throwOnError: true },
)
@@ -163,6 +172,122 @@ export function DialogConnectProvider(props: { provider: string }) {
}
}
+ function OAuthPromptsView() {
+ const [formStore, setFormStore] = createStore({
+ value: {} as Record<string, string>,
+ index: 0,
+ })
+
+ const prompts = createMemo(() => method()?.prompts ?? [])
+ const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
+ 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<string, string>) {
+ 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
+ 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 (
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+ <Switch>
+ <Match when={item()?.prompt.type === "text"}>
+ <TextField
+ type="text"
+ label={text()?.message ?? ""}
+ placeholder={text()?.placeholder}
+ value={text() ? (formStore.value[text()!.key] ?? "") : ""}
+ onChange={(value) => {
+ const prompt = text()
+ if (!prompt) return
+ setFormStore("value", prompt.key, value)
+ }}
+ />
+ <Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
+ {language.t("common.continue")}
+ </Button>
+ </Match>
+ <Match when={item()?.prompt.type === "select"}>
+ <div class="w-full flex flex-col gap-1.5">
+ <div class="text-14-regular text-text-base">{select()?.message}</div>
+ <div>
+ <List
+ items={select()?.options ?? []}
+ key={(x) => x.value}
+ 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) => (
+ <div class="w-full flex items-center gap-x-2">
+ <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
+ <div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
+ </div>
+ <span>{option.label}</span>
+ <span class="text-14-regular text-text-weak">{option.hint}</span>
+ </div>
+ )}
+ </List>
+ </div>
+ </div>
+ </Match>
+ </Switch>
+ </form>
+ )
+ }
+
let listRef: ListRef | undefined
function handleKey(e: KeyboardEvent) {
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
@@ -301,7 +426,7 @@ export function DialogConnectProvider(props: { provider: string }) {
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
- {language.t("common.submit")}
+ {language.t("common.continue")}
</Button>
</form>
</div>
@@ -314,12 +439,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()
@@ -368,7 +487,7 @@ export function DialogConnectProvider(props: { provider: string }) {
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
- {language.t("common.submit")}
+ {language.t("common.continue")}
</Button>
</form>
</div>
@@ -386,10 +505,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,
@@ -470,6 +585,9 @@ export function DialogConnectProvider(props: { provider: string }) {
</div>
</div>
</Match>
+ <Match when={store.state === "prompt"}>
+ <OAuthPromptsView />
+ </Match>
<Match when={store.state === "error"}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-2">
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index 720045a4d..c8f58c796 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 a7d7433b0..3112e91bb 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 ccdf2b604..f2dbd8493 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 f1701094b..e90e1071a 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 2dfeed720..69658b29e 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 7f6816de9..72caed40a 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 1cd47dfc7..9e36e4de6 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 c7d89c325..f53b3882c 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 267411083..d66a7341d 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 bb57f9939..d534c27e8 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 83d6a9903..c23d0a279 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 db9ef1800..dac847b21 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 e1abb6e6c..684d5deec 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 b522e4631..80f0da94e 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 8542dff79..9041e0dd0 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 e762ba78d..cf64ca9b2 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 184c789ce..02c00d17a 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": "儲存中...",