Skip to content

Commit 1c84d3f

Browse files
adamdotdevinengineer
authored andcommitted
fix(app): startup efficiency (anomalyco#18854)
1 parent 8dd12f2 commit 1c84d3f

33 files changed

Lines changed: 1073 additions & 649 deletions

packages/app/src/app.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
66
import { File } from "@opencode-ai/ui/file"
77
import { Font } from "@opencode-ai/ui/font"
88
import { Splash } from "@opencode-ai/ui/logo"
9-
import { ThemeProvider } from "@opencode-ai/ui/theme"
9+
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
1010
import { MetaProvider } from "@solidjs/meta"
1111
import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
1212
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
@@ -32,7 +32,7 @@ import { FileProvider } from "@/context/file"
3232
import { GlobalSDKProvider } from "@/context/global-sdk"
3333
import { GlobalSyncProvider } from "@/context/global-sync"
3434
import { HighlightsProvider } from "@/context/highlights"
35-
import { LanguageProvider, useLanguage } from "@/context/language"
35+
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
3636
import { LayoutProvider } from "@/context/layout"
3737
import { ModelsProvider } from "@/context/models"
3838
import { NotificationProvider } from "@/context/notification"
@@ -137,7 +137,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
137137
)
138138
}
139139

140-
export function AppBaseProviders(props: ParentProps) {
140+
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
141141
return (
142142
<MetaProvider>
143143
<Font />
@@ -146,7 +146,7 @@ export function AppBaseProviders(props: ParentProps) {
146146
void window.api?.setTitlebar?.({ mode })
147147
}}
148148
>
149-
<LanguageProvider>
149+
<LanguageProvider locale={props.locale}>
150150
<UiI18nBridge>
151151
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
152152
<QueryProvider>

packages/app/src/components/dialog-connect-provider.tsx

Lines changed: 172 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
1+
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
22
import { Button } from "@opencode-ai/ui/button"
33
import { useDialog } from "@opencode-ai/ui/context/dialog"
44
import { Dialog } from "@opencode-ai/ui/dialog"
@@ -9,21 +9,18 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
99
import { Spinner } from "@opencode-ai/ui/spinner"
1010
import { TextField } from "@opencode-ai/ui/text-field"
1111
import { 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"
1313
import { createStore, produce } from "solid-js/store"
1414
import { Link } from "@/components/link"
15-
import { useLanguage } from "@/context/language"
1615
import { useGlobalSDK } from "@/context/global-sdk"
1716
import { useGlobalSync } from "@/context/global-sync"
18-
import { usePlatform } from "@/context/platform"
19-
import { DialogSelectModel } from "./dialog-select-model"
17+
import { useLanguage } from "@/context/language"
2018
import { DialogSelectProvider } from "./dialog-select-provider"
2119

2220
export 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

Comments
 (0)