summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-25 06:25:57 -0500
committerAdam <[email protected]>2026-03-25 06:25:57 -0500
commit1041ae91d1a39401fe099747e3bc093bdcdaa079 (patch)
treeca5515910ad01f76639577ef8e3a991b644a5ade /packages/app/src/components
parent898456a25cf2edbfc4ae4961b37424f633419dd6 (diff)
downloadopencode-1041ae91d1a39401fe099747e3bc093bdcdaa079.tar.gz
opencode-1041ae91d1a39401fe099747e3bc093bdcdaa079.zip
Reapply "fix(app): startup efficiency"
This reverts commit 898456a25cf2edbfc4ae4961b37424f633419dd6.
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/dialog-connect-provider.tsx50
-rw-r--r--packages/app/src/components/prompt-input.tsx1
-rw-r--r--packages/app/src/components/settings-general.tsx50
-rw-r--r--packages/app/src/components/status-popover.tsx23
-rw-r--r--packages/app/src/components/terminal.tsx5
-rw-r--r--packages/app/src/components/titlebar.tsx2
6 files changed, 101 insertions, 30 deletions
diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx
index 734958dd5..e7eaa1fb2 100644
--- a/packages/app/src/components/dialog-connect-provider.tsx
+++ b/packages/app/src/components/dialog-connect-provider.tsx
@@ -1,4 +1,4 @@
-import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
+import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
@@ -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 { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
@@ -34,15 +34,25 @@ export function DialogConnectProvider(props: { provider: string }) {
})
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
- const methods = createMemo(
- () =>
- globalSync.data.provider_auth[props.provider] ?? [
- {
- type: "api",
- label: language.t("provider.connect.method.apiKey"),
- },
- ],
+ const fallback = createMemo<ProviderAuthMethod[]>(() => [
+ {
+ type: "api" as const,
+ label: language.t("provider.connect.method.apiKey"),
+ },
+ ])
+ const [auth] = createResource(
+ () => props.provider,
+ async () => {
+ const cached = globalSync.data.provider_auth[props.provider]
+ if (cached) return cached
+ const res = await globalSDK.client.provider.auth()
+ if (!alive.value) return fallback()
+ globalSync.set("provider_auth", res.data ?? {})
+ return res.data?.[props.provider] ?? fallback()
+ },
)
+ const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider])
+ const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback())
const [store, setStore] = createStore({
methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization,
@@ -177,7 +187,11 @@ export function DialogConnectProvider(props: { provider: string }) {
index: 0,
})
- const prompts = createMemo(() => method()?.prompts ?? [])
+ const prompts = createMemo<NonNullable<ProviderAuthMethod["prompts"]>>(() => {
+ const value = method()
+ if (value?.type !== "oauth") return []
+ return value.prompts ?? []
+ })
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
if (!prompt.when) return true
const actual = value[prompt.when.key]
@@ -296,8 +310,12 @@ export function DialogConnectProvider(props: { provider: string }) {
listRef?.onKeyDown(e)
}
- onMount(() => {
+ let auto = false
+ createEffect(() => {
+ if (auto) return
+ if (loading()) return
if (methods().length === 1) {
+ auto = true
selectMethod(0)
}
})
@@ -573,6 +591,14 @@ export function DialogConnectProvider(props: { provider: string }) {
<div class="px-2.5 pb-10 flex flex-col gap-6">
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
<Switch>
+ <Match when={loading()}>
+ <div class="text-14-regular text-text-base">
+ <div class="flex items-center gap-x-2">
+ <Spinner />
+ <span>{language.t("provider.connect.status.inProgress")}</span>
+ </div>
+ </div>
+ </Match>
<Match when={store.methodIndex === undefined}>
<MethodSelection />
</Match>
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index f523671ec..ee98e68cd 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -572,6 +572,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const open = recent()
const seen = new Set(open)
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
+ if (!query.trim()) return [...agents, ...pinned]
const paths = await files.searchFilesAndDirectories(query)
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index b768bafcc..f4b8198e7 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -1,27 +1,41 @@
-import { Component, Show, createMemo, createResource, type JSX } from "solid-js"
+import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
+import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings"
-import { playSound, SOUND_OPTIONS } from "@/utils/sound"
+import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
import { SettingsList } from "./settings-list"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
timeout: undefined as NodeJS.Timeout | undefined,
+ run: 0,
+}
+
+type ThemeOption = {
+ id: string
+ name: string
+}
+
+let font: Promise<typeof import("@opencode-ai/ui/font-loader")> | undefined
+
+function loadFont() {
+ font ??= import("@opencode-ai/ui/font-loader")
+ return font
}
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
// delay the playback by 100ms during quick selection changes and pause existing sounds.
const stopDemoSound = () => {
+ demoSoundState.run += 1
if (demoSoundState.cleanup) {
demoSoundState.cleanup()
}
@@ -29,12 +43,19 @@ const stopDemoSound = () => {
demoSoundState.cleanup = undefined
}
-const playDemoSound = (src: string | undefined) => {
+const playDemoSound = (id: string | undefined) => {
stopDemoSound()
- if (!src) return
+ if (!id) return
+ const run = ++demoSoundState.run
demoSoundState.timeout = setTimeout(() => {
- demoSoundState.cleanup = playSound(src)
+ void playSoundById(id).then((cleanup) => {
+ if (demoSoundState.run !== run) {
+ cleanup?.()
+ return
+ }
+ demoSoundState.cleanup = cleanup
+ })
}, 100)
}
@@ -44,6 +65,10 @@ export const SettingsGeneral: Component = () => {
const platform = usePlatform()
const settings = useSettings()
+ onMount(() => {
+ void theme.loadThemes()
+ })
+
const [store, setStore] = createStore({
checking: false,
})
@@ -104,9 +129,7 @@ export const SettingsGeneral: Component = () => {
.finally(() => setStore("checking", false))
}
- const themeOptions = createMemo(() =>
- Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
- )
+ const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
{ value: "system", label: language.t("theme.scheme.system") },
@@ -143,7 +166,7 @@ export const SettingsGeneral: Component = () => {
] as const
const fontOptionsList = [...fontOptions]
- const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const
+ const noneSound = { id: "none", label: "sound.option.none" } as const
const soundOptions = [noneSound, ...SOUND_OPTIONS]
const soundSelectProps = (
@@ -158,7 +181,7 @@ export const SettingsGeneral: Component = () => {
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
- playDemoSound(option.src)
+ playDemoSound(option.id === "none" ? undefined : option.id)
},
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
@@ -169,7 +192,7 @@ export const SettingsGeneral: Component = () => {
}
setEnabled(true)
set(option.id)
- playDemoSound(option.src)
+ playDemoSound(option.id)
},
variant: "secondary" as const,
size: "small" as const,
@@ -321,6 +344,9 @@ export const SettingsGeneral: Component = () => {
current={fontOptionsList.find((o) => o.value === settings.appearance.font())}
value={(o) => o.value}
label={(o) => language.t(o.label)}
+ onHighlight={(option) => {
+ void loadFont().then((x) => x.ensureMonoFont(option?.value))
+ }}
onSelect={(option) => option && settings.appearance.setFont(option.value)}
variant="secondary"
size="small"
diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx
index 464522443..8d5ecac39 100644
--- a/packages/app/src/components/status-popover.tsx
+++ b/packages/app/src/components/status-popover.tsx
@@ -16,7 +16,6 @@ import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
-import { DialogSelectServer } from "./dialog-select-server"
const pollMs = 10_000
@@ -54,11 +53,15 @@ const listServersByHealth = (
})
}
-const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
+const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
const checkServerHealth = useCheckServerHealth()
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
createEffect(() => {
+ if (!enabled()) {
+ setStatus(reconcile({}))
+ return
+ }
const list = servers()
let dead = false
@@ -162,6 +165,12 @@ export function StatusPopover() {
const navigate = useNavigate()
const [shown, setShown] = createSignal(false)
+ let dialogRun = 0
+ let dialogDead = false
+ onCleanup(() => {
+ dialogDead = true
+ dialogRun += 1
+ })
const servers = createMemo(() => {
const current = server.current
const list = server.list
@@ -169,7 +178,7 @@ export function StatusPopover() {
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
})
- const health = useServerHealth(servers)
+ const health = useServerHealth(servers, shown)
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
const toggleMcp = useMcpToggleMutation()
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
@@ -300,7 +309,13 @@ export function StatusPopover() {
<Button
variant="secondary"
class="mt-3 self-start h-8 px-3 py-1.5"
- onClick={() => dialog.show(() => <DialogSelectServer />, defaultServer.refresh)}
+ onClick={() => {
+ const run = ++dialogRun
+ void import("./dialog-select-server").then((x) => {
+ if (dialogDead || dialogRun !== run) return
+ dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
+ })
+ }}
>
{language.t("status.popover.action.manageServers")}
</Button>
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index aed46f126..0a5a7d2d3 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -1,4 +1,7 @@
-import { type HexColor, resolveThemeVariant, useTheme, withAlpha } from "@opencode-ai/ui/theme"
+import { withAlpha } from "@opencode-ai/ui/theme/color"
+import { useTheme } from "@opencode-ai/ui/theme/context"
+import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
+import type { HexColor } from "@opencode-ai/ui/theme/types"
import { showToast } from "@opencode-ai/ui/toast"
import type { FitAddon, Ghostty, Terminal as Term } from "ghostty-web"
import { type ComponentProps, createEffect, createMemo, onCleanup, onMount, splitProps } from "solid-js"
diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx
index 77de1a73c..0a41f3119 100644
--- a/packages/app/src/components/titlebar.tsx
+++ b/packages/app/src/components/titlebar.tsx
@@ -5,7 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
-import { useTheme } from "@opencode-ai/ui/theme"
+import { useTheme } from "@opencode-ai/ui/theme/context"
import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"