diff options
| author | Adam <[email protected]> | 2025-12-14 06:39:08 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-14 21:38:58 -0600 |
| commit | 2613f44961a73bc57db6662bfa1d0407515c497a (patch) | |
| tree | 8f8de20b6b35c1678c2ff2f567ff2204050c3cbf | |
| parent | 62ffeb3987ad1188e37141513bee7d1f3ce0dcd8 (diff) | |
| download | opencode-2613f44961a73bc57db6662bfa1d0407515c497a.tar.gz opencode-2613f44961a73bc57db6662bfa1d0407515c497a.zip | |
wip(desktop): progress
| -rw-r--r-- | packages/desktop/src/components/dialog-connect.tsx | 74 | ||||
| -rw-r--r-- | packages/desktop/src/components/dialog-model.tsx | 32 | ||||
| -rw-r--r-- | packages/desktop/src/components/dialog-select-provider.tsx (renamed from packages/desktop/src/components/dialog-provider.tsx) | 15 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 9 | ||||
| -rw-r--r-- | packages/desktop/src/context/dialog.tsx | 80 | ||||
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 69 | ||||
| -rw-r--r-- | packages/desktop/src/pages/directory-layout.tsx | 7 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 16 |
8 files changed, 151 insertions, 151 deletions
diff --git a/packages/desktop/src/components/dialog-connect.tsx b/packages/desktop/src/components/dialog-connect.tsx index a44365069..d482b3f50 100644 --- a/packages/desktop/src/components/dialog-connect.tsx +++ b/packages/desktop/src/components/dialog-connect.tsx @@ -1,6 +1,6 @@ import { Component, createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" -import { useLayout } from "@/context/layout" +import { useDialog } from "@/context/dialog" import { useGlobalSync } from "@/context/global-sync" import { useGlobalSDK } from "@/context/global-sdk" import { usePlatform } from "@/context/platform" @@ -17,18 +17,19 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconName } from "@opencode-ai/ui/icons/provider" import { iife } from "@opencode-ai/util/iife" import { Link } from "@/components/link" +import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogModel } from "./dialog-model" -export const DialogConnect: Component = () => { - const layout = useLayout() +export const DialogConnect: Component<{ provider: string }> = (props) => { + const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const platform = usePlatform() - const providerID = createMemo(() => layout.connect.provider()!) - const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!) + const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const methods = createMemo( () => - globalSync.data.provider_auth[providerID()] ?? [ + globalSync.data.provider_auth[props.provider] ?? [ { type: "api", label: "API key", @@ -61,7 +62,7 @@ export const DialogConnect: Component = () => { await globalSDK.client.provider.oauth .authorize( { - providerID: providerID(), + providerID: props.provider, method: index, }, { throwOnError: true }, @@ -116,55 +117,50 @@ export const DialogConnect: Component = () => { title: `${provider().name} connected`, description: `${provider().name} models are now available to use.`, }) - layout.connect.complete() + dialog.replace(() => <DialogModel connectedProvider={props.provider} />) }, 500) } + function goBack() { + if (methods().length === 1) { + dialog.replace(() => <DialogSelectProvider />) + return + } + if (store.authorization) { + setStore("authorization", undefined) + setStore("method", undefined) + return + } + if (store.method) { + setStore("method", undefined) + return + } + dialog.replace(() => <DialogSelectProvider />) + } + return ( <Dialog modal defaultOpen onOpenChange={(open) => { - if (open) { - layout.dialog.open("connect") - } else { - layout.dialog.close("connect") + if (!open) { + dialog.clear() } }} > <Dialog.Header class="px-4.5"> <Dialog.Title class="flex items-center"> - <IconButton - tabIndex={-1} - icon="arrow-left" - variant="ghost" - onClick={() => { - if (methods().length === 1) { - layout.dialog.open("provider") - return - } - if (store.authorization) { - setStore("authorization", undefined) - setStore("method", undefined) - return - } - if (store.method) { - setStore("method", undefined) - return - } - layout.dialog.open("provider") - }} - /> + <IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} /> </Dialog.Title> <Dialog.CloseButton tabIndex={-1} /> </Dialog.Header> <Dialog.Body> <div class="flex flex-col gap-6 px-2.5 pb-3"> <div class="px-2.5 flex gap-4 items-center"> - <ProviderIcon id={providerID() as IconName} class="size-5 shrink-0 icon-strong-base" /> + <ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" /> <div class="text-16-medium text-text-strong"> <Switch> - <Match when={providerID() === "anthropic" && store.method?.label?.toLowerCase().includes("max")}> + <Match when={props.provider === "anthropic" && store.method?.label?.toLowerCase().includes("max")}> Login with Claude Pro/Max </Match> <Match when={true}>Connect {provider().name}</Match> @@ -233,7 +229,7 @@ export const DialogConnect: Component = () => { setFormStore("error", undefined) await globalSDK.client.auth.set({ - providerID: providerID(), + providerID: props.provider, auth: { type: "api", key: apiKey, @@ -320,7 +316,7 @@ export const DialogConnect: Component = () => { setFormStore("error", undefined) const { error } = await globalSDK.client.provider.oauth.callback({ - providerID: providerID(), + providerID: props.provider, method: methodIndex(), code, }) @@ -369,12 +365,12 @@ export const DialogConnect: Component = () => { onMount(async () => { const result = await globalSDK.client.provider.oauth.callback({ - providerID: providerID(), + providerID: props.provider, method: methodIndex(), }) if (result.error) { // TODO: show error - layout.dialog.close("connect") + dialog.clear() return } await complete() diff --git a/packages/desktop/src/components/dialog-model.tsx b/packages/desktop/src/components/dialog-model.tsx index 9d36e0797..7f90e1a78 100644 --- a/packages/desktop/src/components/dialog-model.tsx +++ b/packages/desktop/src/components/dialog-model.tsx @@ -1,6 +1,6 @@ import { Component, createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js" import { useLocal } from "@/context/local" -import { useLayout } from "@/context/layout" +import { useDialog } from "@/context/dialog" import { popularProviders, useProviders } from "@/hooks/use-providers" import { SelectDialog } from "@opencode-ai/ui/select-dialog" import { Button } from "@opencode-ai/ui/button" @@ -10,10 +10,12 @@ import { List, ListRef } from "@opencode-ai/ui/list" import { iife } from "@opencode-ai/util/iife" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconName } from "@opencode-ai/ui/icons/provider" +import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogConnect } from "./dialog-connect" -export const DialogModel: Component = () => { +export const DialogModel: Component<{ connectedProvider?: string }> = (props) => { const local = useLocal() - const layout = useLayout() + const dialog = useDialog() const providers = useProviders() return ( @@ -24,18 +26,14 @@ export const DialogModel: Component = () => { local.model .list() .filter((m) => m.visible) - .filter((m) => - layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true, - ), + .filter((m) => (props.connectedProvider ? m.provider.id === props.connectedProvider : true)), ) return ( <SelectDialog defaultOpen onOpenChange={(open) => { - if (open) { - layout.dialog.open("model") - } else { - layout.dialog.close("model") + if (!open) { + dialog.clear() } }} title="Select model" @@ -66,7 +64,7 @@ export const DialogModel: Component = () => { class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} - onClick={() => layout.dialog.open("provider")} + onClick={() => dialog.replace(() => <DialogSelectProvider />)} > Connect provider </Button> @@ -107,10 +105,8 @@ export const DialogModel: Component = () => { modal defaultOpen onOpenChange={(open) => { - if (open) { - layout.dialog.open("model") - } else { - layout.dialog.close("model") + if (!open) { + dialog.clear() } }} > @@ -130,7 +126,7 @@ export const DialogModel: Component = () => { local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, }) - layout.dialog.close("model") + dialog.clear() }} > {(i) => ( @@ -163,7 +159,7 @@ export const DialogModel: Component = () => { }} onSelect={(x) => { if (!x) return - layout.dialog.connect(x.id) + dialog.replace(() => <DialogConnect provider={x.id} />) }} > {(i) => ( @@ -193,7 +189,7 @@ export const DialogModel: Component = () => { class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium" icon="dot-grid" onClick={() => { - layout.dialog.open("provider") + dialog.replace(() => <DialogSelectProvider />) }} > View all providers diff --git a/packages/desktop/src/components/dialog-provider.tsx b/packages/desktop/src/components/dialog-select-provider.tsx index 56c791479..6dabdb8b4 100644 --- a/packages/desktop/src/components/dialog-provider.tsx +++ b/packages/desktop/src/components/dialog-select-provider.tsx @@ -1,13 +1,14 @@ import { Component, Show } from "solid-js" -import { useLayout } from "@/context/layout" +import { useDialog } from "@/context/dialog" import { popularProviders, useProviders } from "@/hooks/use-providers" import { SelectDialog } from "@opencode-ai/ui/select-dialog" import { Tag } from "@opencode-ai/ui/tag" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconName } from "@opencode-ai/ui/icons/provider" +import { DialogConnect } from "./dialog-connect" -export const DialogProvider: Component = () => { - const layout = useLayout() +export const DialogSelectProvider: Component = () => { + const dialog = useDialog() const providers = useProviders() return ( @@ -32,13 +33,11 @@ export const DialogProvider: Component = () => { }} onSelect={(x) => { if (!x) return - layout.dialog.connect(x.id) + dialog.replace(() => <DialogConnect provider={x.id} />) }} onOpenChange={(open) => { - if (open) { - layout.dialog.open("provider") - } else { - layout.dialog.close("provider") + if (!open) { + dialog.clear() } }} > diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 7c60a6d01..ca0ccf96a 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -15,7 +15,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { useLayout } from "@/context/layout" +import { useDialog } from "@/context/dialog" import { DialogModel } from "@/components/dialog-model" interface PromptInputProps { @@ -57,7 +57,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const sync = useSync() const local = useLocal() const session = useSession() - const layout = useLayout() + const dialog = useDialog() let editorRef!: HTMLDivElement const [store, setStore] = createStore<{ @@ -610,14 +610,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => { class="capitalize" variant="ghost" /> - <Button as="div" variant="ghost" onClick={() => layout.dialog.open("model")}> + <Button as="div" variant="ghost" onClick={() => dialog.push(() => <DialogModel />)}> {local.model.current()?.name ?? "Select model"} <span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span> <Icon name="chevron-down" size="small" /> </Button> - <Show when={layout.dialog.opened() === "model"}> - <DialogModel /> - </Show> </div> <Tooltip placement="top" diff --git a/packages/desktop/src/context/dialog.tsx b/packages/desktop/src/context/dialog.tsx new file mode 100644 index 000000000..cc49764fe --- /dev/null +++ b/packages/desktop/src/context/dialog.tsx @@ -0,0 +1,80 @@ +import { createEffect, For, onCleanup, Show, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { createSimpleContext } from "@opencode-ai/ui/context" + +type DialogElement = JSX.Element | (() => JSX.Element) + +export const { use: useDialog, provider: DialogProvider } = createSimpleContext({ + name: "Dialog", + init: () => { + const [store, setStore] = createStore({ + stack: [] as { + element: DialogElement + onClose?: () => void + }[], + }) + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape" && store.stack.length > 0) { + const current = store.stack.at(-1)! + current.onClose?.() + setStore("stack", store.stack.slice(0, -1)) + e.preventDefault() + e.stopPropagation() + } + } + + createEffect(() => { + document.addEventListener("keydown", handleKeyDown, true) + onCleanup(() => { + document.removeEventListener("keydown", handleKeyDown, true) + }) + }) + + return { + get stack() { + return store.stack + }, + push(element: DialogElement, onClose?: () => void) { + setStore("stack", (s) => [...s, { element, onClose }]) + }, + pop() { + const current = store.stack.at(-1) + current?.onClose?.() + setStore("stack", store.stack.slice(0, -1)) + }, + replace(element: DialogElement, onClose?: () => void) { + for (const item of store.stack) { + item.onClose?.() + } + setStore("stack", [{ element, onClose }]) + }, + clear() { + for (const item of store.stack) { + item.onClose?.() + } + setStore("stack", []) + }, + } + }, +}) + +export function DialogRoot(props: { children?: JSX.Element }) { + const dialog = useDialog() + return ( + <> + {props.children} + <Show when={dialog.stack.length > 0}> + <div data-component="dialog-stack"> + <For each={dialog.stack}> + {(item, index) => ( + <Show when={index() === dialog.stack.length - 1}> + {typeof item.element === "function" ? item.element() : item.element} + </Show> + )} + </For> + </div> + </Show> + </> + ) +} diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 587276c53..925bf4d4c 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -1,5 +1,5 @@ -import { createStore, produce } from "solid-js/store" -import { batch, createMemo, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { createMemo, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { makePersisted } from "@solid-primitives/storage" import { useGlobalSync } from "./global-sync" @@ -22,8 +22,6 @@ export function getAvatarColors(key?: string) { } } -type Dialog = "provider" | "model" | "connect" | "manage-models" - export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { @@ -45,22 +43,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, }), { - name: "layout.v1", + name: "layout.v2", }, ) - const [ephemeral, setEphemeral] = createStore<{ - connect: { - provider?: string - state?: "pending" | "complete" | "error" - error?: string - } - dialog: { - open?: Dialog - } - }>({ - connect: {}, - dialog: {}, - }) + const usedColors = new Set<AvatarColorKey>() function pickAvailableColor(): AvatarColorKey { @@ -169,53 +155,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("review", "state", "tab") }, }, - dialog: { - opened: createMemo(() => ephemeral.dialog?.open), - open(dialog: Dialog) { - setEphemeral("dialog", "open", dialog) - }, - close(dialog: Dialog) { - if (ephemeral.dialog.open === dialog) { - setEphemeral( - produce((state) => { - state.dialog.open = undefined - state.connect = {} - }), - ) - } - }, - connect(provider: string) { - setEphemeral( - produce((state) => { - state.dialog.open = "connect" - state.connect = { provider, state: "pending" } - }), - ) - }, - }, - connect: { - provider: createMemo(() => ephemeral.connect.provider), - state: createMemo(() => ephemeral.connect.state), - complete() { - setEphemeral( - produce((state) => { - state.dialog.open = "model" - state.connect.state = "complete" - }), - ) - }, - error(message: string) { - setEphemeral( - produce((state) => { - state.connect.state = "error" - state.connect.error = message - }), - ) - }, - clear() { - setEphemeral("connect", {}) - }, - }, } }, }) diff --git a/packages/desktop/src/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx index c909a373d..1349f6ec0 100644 --- a/packages/desktop/src/pages/directory-layout.tsx +++ b/packages/desktop/src/pages/directory-layout.tsx @@ -6,6 +6,7 @@ import { LocalProvider } from "@/context/local" import { base64Decode } from "@opencode-ai/util/encode" import { DataProvider } from "@opencode-ai/ui/context" import { iife } from "@opencode-ai/util/iife" +import { DialogProvider, DialogRoot } from "@/context/dialog" export default function Layout(props: ParentProps) { const params = useParams() @@ -20,7 +21,11 @@ export default function Layout(props: ParentProps) { const sync = useSync() return ( <DataProvider data={sync.data} directory={directory()}> - <LocalProvider>{props.children}</LocalProvider> + <LocalProvider> + <DialogProvider> + <DialogRoot>{props.children}</DialogRoot> + </DialogProvider> + </LocalProvider> </DataProvider> ) })} diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 63ee5b2aa..7b1d0e45a 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -33,8 +33,6 @@ import { useGlobalSDK } from "@/context/global-sdk" import { useNotification } from "@/context/notification" import { Binary } from "@opencode-ai/util/binary" import { Header } from "@/components/header" -import { DialogProvider } from "@/components/dialog-provider" -import { DialogConnect } from "@/components/dialog-connect" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -90,10 +88,6 @@ export default function Layout(props: ParentProps) { } } - async function connectProvider() { - layout.dialog.open("provider") - } - createEffect(() => { if (!params.dir || !params.id) return const directory = base64Decode(params.dir) @@ -494,7 +488,7 @@ export default function Layout(props: ParentProps) { class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px" size="large" icon="plus" - onClick={connectProvider} + // onClick={connectProvider} > <Show when={layout.sidebar.opened()}>Connect provider</Show> </Button> @@ -508,7 +502,7 @@ export default function Layout(props: ParentProps) { variant="ghost" size="large" icon="plus" - onClick={connectProvider} + // onClick={connectProvider} > <Show when={layout.sidebar.opened()}>Connect provider</Show> </Button> @@ -555,12 +549,6 @@ export default function Layout(props: ParentProps) { </div> </div> <main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main> - <Show when={layout.dialog.opened() === "provider"}> - <DialogProvider /> - </Show> - <Show when={layout.dialog.opened() === "connect"}> - <DialogConnect /> - </Show> </div> <Toast.Region /> </div> |
