summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-14 06:39:08 -0600
committerAdam <[email protected]>2025-12-14 21:38:58 -0600
commit2613f44961a73bc57db6662bfa1d0407515c497a (patch)
tree8f8de20b6b35c1678c2ff2f567ff2204050c3cbf
parent62ffeb3987ad1188e37141513bee7d1f3ce0dcd8 (diff)
downloadopencode-2613f44961a73bc57db6662bfa1d0407515c497a.tar.gz
opencode-2613f44961a73bc57db6662bfa1d0407515c497a.zip
wip(desktop): progress
-rw-r--r--packages/desktop/src/components/dialog-connect.tsx74
-rw-r--r--packages/desktop/src/components/dialog-model.tsx32
-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.tsx9
-rw-r--r--packages/desktop/src/context/dialog.tsx80
-rw-r--r--packages/desktop/src/context/layout.tsx69
-rw-r--r--packages/desktop/src/pages/directory-layout.tsx7
-rw-r--r--packages/desktop/src/pages/layout.tsx16
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>