summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-14 20:32:14 -0600
committerAdam <[email protected]>2025-12-14 21:38:59 -0600
commitdda579c8ad30f81ade458769971d85ff7afee64c (patch)
tree343c82906735fcfc82a5b2a178660aaab5bccb45 /packages/desktop/src
parent4246cdb069502c96ab11e260eb36a07a0370b710 (diff)
downloadopencode-dda579c8ad30f81ade458769971d85ff7afee64c.tar.gz
opencode-dda579c8ad30f81ade458769971d85ff7afee64c.zip
wip(desktop): progress
Diffstat (limited to 'packages/desktop/src')
-rw-r--r--packages/desktop/src/app.tsx2
-rw-r--r--packages/desktop/src/components/dialog-connect.tsx477
-rw-r--r--packages/desktop/src/components/dialog-file-select.tsx52
-rw-r--r--packages/desktop/src/components/dialog-manage-models.tsx86
-rw-r--r--packages/desktop/src/components/dialog-model-unpaid.tsx133
-rw-r--r--packages/desktop/src/components/dialog-model.tsx95
-rw-r--r--packages/desktop/src/components/dialog-select-file.tsx44
-rw-r--r--packages/desktop/src/components/dialog-select-model-unpaid.tsx119
-rw-r--r--packages/desktop/src/components/dialog-select-model.tsx85
-rw-r--r--packages/desktop/src/components/dialog-select-provider.tsx108
-rw-r--r--packages/desktop/src/components/prompt-input.tsx10
-rw-r--r--packages/desktop/src/context/dialog.tsx80
-rw-r--r--packages/desktop/src/pages/directory-layout.tsx2
-rw-r--r--packages/desktop/src/pages/layout.tsx2
-rw-r--r--packages/desktop/src/pages/session.tsx15
15 files changed, 572 insertions, 738 deletions
diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx
index fd55b228e..a49dac9aa 100644
--- a/packages/desktop/src/app.tsx
+++ b/packages/desktop/src/app.tsx
@@ -11,7 +11,7 @@ import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
import { SessionProvider } from "@/context/session"
import { NotificationProvider } from "@/context/notification"
-import { DialogProvider } from "@/context/dialog"
+import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import Layout from "@/pages/layout"
import Home from "@/pages/home"
import DirectoryLayout from "@/pages/directory-layout"
diff --git a/packages/desktop/src/components/dialog-connect.tsx b/packages/desktop/src/components/dialog-connect.tsx
index 3a1e05f27..f61221f72 100644
--- a/packages/desktop/src/components/dialog-connect.tsx
+++ b/packages/desktop/src/components/dialog-connect.tsx
@@ -1,10 +1,10 @@
-import { Component, createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
+import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
import { createStore, produce } from "solid-js/store"
-import { useDialog } from "@/context/dialog"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useGlobalSync } from "@/context/global-sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { usePlatform } from "@/context/platform"
-import { ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
+import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List, ListRef } from "@opencode-ai/ui/list"
import { Button } from "@opencode-ai/ui/button"
@@ -18,14 +18,13 @@ 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"
+import { DialogSelectModel } from "./dialog-select-model"
-export const DialogConnect: Component<{ provider: string }> = (props) => {
+export function DialogConnect(props: { provider: string }) {
const dialog = useDialog()
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const platform = usePlatform()
-
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
const methods = createMemo(
() =>
@@ -37,19 +36,19 @@ export const DialogConnect: Component<{ provider: string }> = (props) => {
],
)
const [store, setStore] = createStore({
- method: undefined as undefined | ProviderAuthMethod,
+ methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization,
state: "pending" as undefined | "pending" | "complete" | "error",
error: undefined as string | undefined,
})
- const methodIndex = createMemo(() => methods().findIndex((x) => x.label === store.method?.label))
+ const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
async function selectMethod(index: number) {
const method = methods()[index]
setStore(
produce((draft) => {
- draft.method = method
+ draft.methodIndex = index
draft.authorization = undefined
draft.state = undefined
draft.error = undefined
@@ -101,7 +100,6 @@ export const DialogConnect: Component<{ provider: string }> = (props) => {
if (methods().length === 1) {
selectMethod(0)
}
-
document.addEventListener("keydown", handleKey)
onCleanup(() => {
document.removeEventListener("keydown", handleKey)
@@ -117,8 +115,8 @@ export const DialogConnect: Component<{ provider: string }> = (props) => {
title: `${provider().name} connected`,
description: `${provider().name} models are now available to use.`,
})
- dialog.replace(() => <DialogModel provider={props.provider} />)
- }, 500)
+ dialog.replace(() => <DialogSelectModel provider={props.provider} />)
+ }, 1000)
}
function goBack() {
@@ -128,275 +126,258 @@ export const DialogConnect: Component<{ provider: string }> = (props) => {
}
if (store.authorization) {
setStore("authorization", undefined)
- setStore("method", undefined)
+ setStore("methodIndex", undefined)
return
}
- if (store.method) {
- setStore("method", undefined)
+ if (store.methodIndex) {
+ setStore("methodIndex", undefined)
return
}
dialog.replace(() => <DialogSelectProvider />)
}
return (
- <Dialog
- modal
- defaultOpen
- onOpenChange={(open) => {
- 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={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={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
- <div class="text-16-medium text-text-strong">
- <Switch>
- <Match when={props.provider === "anthropic" && store.method?.label?.toLowerCase().includes("max")}>
- Login with Claude Pro/Max
- </Match>
- <Match when={true}>Connect {provider().name}</Match>
- </Switch>
- </div>
- </div>
- <div class="px-2.5 pb-10 flex flex-col gap-6">
+ <Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
+ <div class="flex flex-col gap-6 px-2.5 pb-3">
+ <div class="px-2.5 flex gap-4 items-center">
+ <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={store.method === undefined}>
- <div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
- <div class="">
- <List
- ref={(ref) => (listRef = ref)}
- items={methods}
- key={(m) => m?.label}
- onSelect={async (method, index) => {
- if (!method) return
- selectMethod(index)
- }}
- >
- {(i) => (
- <div class="w-full flex items-center gap-x-4">
- <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 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
- </div>
- <span>{i.label}</span>
+ <Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
+ Login with Claude Pro/Max
+ </Match>
+ <Match when={true}>Connect {provider().name}</Match>
+ </Switch>
+ </div>
+ </div>
+ <div class="px-2.5 pb-10 flex flex-col gap-6">
+ <Switch>
+ <Match when={store.methodIndex === undefined}>
+ <div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
+ <div class="">
+ <List
+ ref={(ref) => (listRef = ref)}
+ items={methods}
+ key={(m) => m?.label}
+ onSelect={async (method, index) => {
+ if (!method) return
+ selectMethod(index)
+ }}
+ >
+ {(i) => (
+ <div class="w-full flex items-center gap-x-4">
+ <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 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
</div>
- )}
- </List>
+ <span>{i.label}</span>
+ </div>
+ )}
+ </List>
+ </div>
+ </Match>
+ <Match when={store.state === "pending"}>
+ <div class="text-14-regular text-text-base">
+ <div class="flex items-center gap-x-4">
+ <Spinner />
+ <span>Authorization in progress...</span>
</div>
- </Match>
- <Match when={store.state === "pending"}>
- <div class="text-14-regular text-text-base">
- <div class="flex items-center gap-x-4">
- <Spinner />
- <span>Authorization in progress...</span>
- </div>
+ </div>
+ </Match>
+ <Match when={store.state === "error"}>
+ <div class="text-14-regular text-text-base">
+ <div class="flex items-center gap-x-4">
+ <Icon name="circle-ban-sign" class="text-icon-critical-base" />
+ <span>Authorization failed: {store.error}</span>
</div>
- </Match>
- <Match when={store.state === "error"}>
- <div class="text-14-regular text-text-base">
- <div class="flex items-center gap-x-4">
- <Icon name="circle-ban-sign" class="text-icon-critical-base" />
- <span>Authorization failed: {store.error}</span>
- </div>
- </div>
- </Match>
- <Match when={store.method?.type === "api"}>
- {iife(() => {
- const [formStore, setFormStore] = createStore({
- value: "",
- error: undefined as string | undefined,
- })
-
- async function handleSubmit(e: SubmitEvent) {
- e.preventDefault()
+ </div>
+ </Match>
+ <Match when={method()?.type === "api"}>
+ {iife(() => {
+ const [formStore, setFormStore] = createStore({
+ value: "",
+ error: undefined as string | undefined,
+ })
- const form = e.currentTarget as HTMLFormElement
- const formData = new FormData(form)
- const apiKey = formData.get("apiKey") as string
+ async function handleSubmit(e: SubmitEvent) {
+ e.preventDefault()
- if (!apiKey?.trim()) {
- setFormStore("error", "API key is required")
- return
- }
+ const form = e.currentTarget as HTMLFormElement
+ const formData = new FormData(form)
+ const apiKey = formData.get("apiKey") as string
- setFormStore("error", undefined)
- await globalSDK.client.auth.set({
- providerID: props.provider,
- auth: {
- type: "api",
- key: apiKey,
- },
- })
- await complete()
+ if (!apiKey?.trim()) {
+ setFormStore("error", "API key is required")
+ return
}
- return (
- <div class="flex flex-col gap-6">
- <Switch>
- <Match when={provider().id === "opencode"}>
- <div class="flex flex-col gap-4">
- <div class="text-14-regular text-text-base">
- OpenCode Zen gives you access to a curated set of reliable optimized models for coding
- agents.
- </div>
- <div class="text-14-regular text-text-base">
- With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and
- more.
- </div>
- <div class="text-14-regular text-text-base">
- Visit{" "}
- <Link href="https://opencode.ai/zen" tabIndex={-1}>
- opencode.ai/zen
- </Link>{" "}
- to collect your API key.
- </div>
+ setFormStore("error", undefined)
+ await globalSDK.client.auth.set({
+ providerID: props.provider,
+ auth: {
+ type: "api",
+ key: apiKey,
+ },
+ })
+ await complete()
+ }
+
+ return (
+ <div class="flex flex-col gap-6">
+ <Switch>
+ <Match when={provider().id === "opencode"}>
+ <div class="flex flex-col gap-4">
+ <div class="text-14-regular text-text-base">
+ OpenCode Zen gives you access to a curated set of reliable optimized models for coding
+ agents.
</div>
- </Match>
- <Match when={true}>
<div class="text-14-regular text-text-base">
- Enter your {provider().name} API key to connect your account and use {provider().name}{" "}
- models in OpenCode.
+ With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
</div>
- </Match>
- </Switch>
- <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
- <TextField
- autofocus
- type="text"
- label={`${provider().name} API key`}
- placeholder="API key"
- name="apiKey"
- value={formStore.value}
- onChange={setFormStore.bind(null, "value")}
- validationState={formStore.error ? "invalid" : undefined}
- error={formStore.error}
- />
- <Button class="w-auto" type="submit" size="large" variant="primary">
- Submit
- </Button>
- </form>
- </div>
- )
- })}
- </Match>
- <Match when={store.method?.type === "oauth"}>
- <Switch>
- <Match when={store.authorization?.method === "code"}>
- {iife(() => {
- const [formStore, setFormStore] = createStore({
- value: "",
- error: undefined as string | undefined,
- })
+ <div class="text-14-regular text-text-base">
+ Visit{" "}
+ <Link href="https://opencode.ai/zen" tabIndex={-1}>
+ opencode.ai/zen
+ </Link>{" "}
+ to collect your API key.
+ </div>
+ </div>
+ </Match>
+ <Match when={true}>
+ <div class="text-14-regular text-text-base">
+ Enter your {provider().name} API key to connect your account and use {provider().name} models
+ in OpenCode.
+ </div>
+ </Match>
+ </Switch>
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+ <TextField
+ autofocus
+ type="text"
+ label={`${provider().name} API key`}
+ placeholder="API key"
+ name="apiKey"
+ value={formStore.value}
+ onChange={setFormStore.bind(null, "value")}
+ validationState={formStore.error ? "invalid" : undefined}
+ error={formStore.error}
+ />
+ <Button class="w-auto" type="submit" size="large" variant="primary">
+ Submit
+ </Button>
+ </form>
+ </div>
+ )
+ })}
+ </Match>
+ <Match when={method()?.type === "oauth"}>
+ <Switch>
+ <Match when={store.authorization?.method === "code"}>
+ {iife(() => {
+ const [formStore, setFormStore] = createStore({
+ value: "",
+ error: undefined as string | undefined,
+ })
- onMount(() => {
- if (store.authorization?.method === "code" && store.authorization?.url) {
- platform.openLink(store.authorization.url)
- }
- })
+ onMount(() => {
+ if (store.authorization?.method === "code" && store.authorization?.url) {
+ platform.openLink(store.authorization.url)
+ }
+ })
- async function handleSubmit(e: SubmitEvent) {
- e.preventDefault()
+ async function handleSubmit(e: SubmitEvent) {
+ e.preventDefault()
- const form = e.currentTarget as HTMLFormElement
- const formData = new FormData(form)
- const code = formData.get("code") as string
+ const form = e.currentTarget as HTMLFormElement
+ const formData = new FormData(form)
+ const code = formData.get("code") as string
- if (!code?.trim()) {
- setFormStore("error", "Authorization code is required")
- return
- }
+ if (!code?.trim()) {
+ setFormStore("error", "Authorization code is required")
+ return
+ }
- setFormStore("error", undefined)
- const { error } = await globalSDK.client.provider.oauth.callback({
- providerID: props.provider,
- method: methodIndex(),
- code,
- })
- if (!error) {
- await complete()
- return
- }
- setFormStore("error", "Invalid authorization code")
+ setFormStore("error", undefined)
+ const { error } = await globalSDK.client.provider.oauth.callback({
+ providerID: props.provider,
+ method: store.methodIndex,
+ code,
+ })
+ if (!error) {
+ await complete()
+ return
}
+ setFormStore("error", "Invalid authorization code")
+ }
- return (
- <div class="flex flex-col gap-6">
- <div class="text-14-regular text-text-base">
- Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
- code to connect your account and use {provider().name} models in OpenCode.
- </div>
- <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
- <TextField
- autofocus
- type="text"
- label={`${store.method?.label} authorization code`}
- placeholder="Authorization code"
- name="code"
- value={formStore.value}
- onChange={setFormStore.bind(null, "value")}
- validationState={formStore.error ? "invalid" : undefined}
- error={formStore.error}
- />
- <Button class="w-auto" type="submit" size="large" variant="primary">
- Submit
- </Button>
- </form>
+ return (
+ <div class="flex flex-col gap-6">
+ <div class="text-14-regular text-text-base">
+ Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
+ code to connect your account and use {provider().name} models in OpenCode.
</div>
- )
- })}
- </Match>
- <Match when={store.authorization?.method === "auto"}>
- {iife(() => {
- const code = createMemo(() => {
- const instructions = store.authorization?.instructions
- if (instructions?.includes(":")) {
- return instructions?.split(":")[1]?.trim()
- }
- return instructions
- })
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+ <TextField
+ autofocus
+ type="text"
+ label={`${method()?.label} authorization code`}
+ placeholder="Authorization code"
+ name="code"
+ value={formStore.value}
+ onChange={setFormStore.bind(null, "value")}
+ validationState={formStore.error ? "invalid" : undefined}
+ error={formStore.error}
+ />
+ <Button class="w-auto" type="submit" size="large" variant="primary">
+ Submit
+ </Button>
+ </form>
+ </div>
+ )
+ })}
+ </Match>
+ <Match when={store.authorization?.method === "auto"}>
+ {iife(() => {
+ const code = createMemo(() => {
+ const instructions = store.authorization?.instructions
+ if (instructions?.includes(":")) {
+ return instructions?.split(":")[1]?.trim()
+ }
+ return instructions
+ })
- onMount(async () => {
- const result = await globalSDK.client.provider.oauth.callback({
- providerID: props.provider,
- method: methodIndex(),
- })
- if (result.error) {
- // TODO: show error
- dialog.clear()
- return
- }
- await complete()
+ onMount(async () => {
+ const result = await globalSDK.client.provider.oauth.callback({
+ providerID: props.provider,
+ method: store.methodIndex,
})
+ if (result.error) {
+ // TODO: show error
+ dialog.clear()
+ return
+ }
+ await complete()
+ })
- return (
- <div class="flex flex-col gap-6">
- <div class="text-14-regular text-text-base">
- Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
- connect your account and use {provider().name} models in OpenCode.
- </div>
- <TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
- <div class="text-14-regular text-text-base flex items-center gap-4">
- <Spinner />
- <span>Waiting for authorization...</span>
- </div>
+ return (
+ <div class="flex flex-col gap-6">
+ <div class="text-14-regular text-text-base">
+ Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
+ connect your account and use {provider().name} models in OpenCode.
</div>
- )
- })}
- </Match>
- </Switch>
- </Match>
- </Switch>
- </div>
+ <TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
+ <div class="text-14-regular text-text-base flex items-center gap-4">
+ <Spinner />
+ <span>Waiting for authorization...</span>
+ </div>
+ </div>
+ )
+ })}
+ </Match>
+ </Switch>
+ </Match>
+ </Switch>
</div>
- </Dialog.Body>
+ </div>
</Dialog>
)
}
diff --git a/packages/desktop/src/components/dialog-file-select.tsx b/packages/desktop/src/components/dialog-file-select.tsx
deleted file mode 100644
index 3afe06062..000000000
--- a/packages/desktop/src/components/dialog-file-select.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Component } from "solid-js"
-import { useLocal } from "@/context/local"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
-import { FileIcon } from "@opencode-ai/ui/file-icon"
-import { getDirectory, getFilename } from "@opencode-ai/util/path"
-
-export const DialogFileSelect: Component<{
- onOpenChange?: (open: boolean) => void
- onSelect?: (path: string) => void
-}> = (props) => {
- const local = useLocal()
- let closeButton!: HTMLButtonElement
-
- return (
- <Dialog modal defaultOpen onOpenChange={props.onOpenChange}>
- <Dialog.Header>
- <Dialog.Title>Select file</Dialog.Title>
- <Dialog.CloseButton ref={closeButton} tabIndex={-1} />
- </Dialog.Header>
- <Dialog.Body>
- <List
- class="px-2.5"
- search={{ placeholder: "Search files", autofocus: true }}
- emptyMessage="No files found"
- items={local.file.searchFiles}
- key={(x) => x}
- onSelect={(x) => {
- if (x) {
- props.onSelect?.(x)
- }
- closeButton.click()
- }}
- >
- {(i) => (
- <div class="w-full flex items-center justify-between rounded-md">
- <div class="flex items-center gap-x-2 grow min-w-0">
- <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
- <div class="flex items-center text-14-regular">
- <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
- {getDirectory(i)}
- </span>
- <span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
- </div>
- </div>
- </div>
- )}
- </List>
- </Dialog.Body>
- </Dialog>
- )
-}
diff --git a/packages/desktop/src/components/dialog-manage-models.tsx b/packages/desktop/src/components/dialog-manage-models.tsx
index 2904f9a5b..de1c3cb15 100644
--- a/packages/desktop/src/components/dialog-manage-models.tsx
+++ b/packages/desktop/src/components/dialog-manage-models.tsx
@@ -1,6 +1,5 @@
import { Component } from "solid-js"
import { useLocal } from "@/context/local"
-import { useDialog } from "@/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
@@ -8,58 +7,41 @@ import { Switch } from "@opencode-ai/ui/switch"
export const DialogManageModels: Component = () => {
const local = useLocal()
- const dialog = useDialog()
-
return (
- <Dialog
- modal
- defaultOpen
- onOpenChange={(open) => {
- if (!open) {
- dialog.clear()
- }
- }}
- >
- <Dialog.Header>
- <Dialog.Title>Manage models</Dialog.Title>
- <Dialog.CloseButton tabIndex={-1} />
- </Dialog.Header>
- <Dialog.Description>Customize which models appear in the model selector.</Dialog.Description>
- <Dialog.Body>
- <List
- class="px-2.5"
- search={{ placeholder: "Search models", autofocus: true }}
- emptyMessage="No model results"
- key={(x) => `${x?.provider?.id}:${x?.id}`}
- items={local.model.list()}
- filterKeys={["provider.name", "name", "id"]}
- sortBy={(a, b) => a.name.localeCompare(b.name)}
- groupBy={(x) => x.provider.name}
- sortGroupsBy={(a, b) => {
- const aProvider = a.items[0].provider.id
- const bProvider = b.items[0].provider.id
- if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
- if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
- return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
- }}
- onSelect={(x) => {
- if (!x) return
- local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !x.visible)
- }}
- >
- {(i) => (
- <div class="w-full flex items-center justify-between gap-x-2.5">
- <span>{i.name}</span>
- <Switch
- checked={!!i.visible}
- onChange={(checked) => {
- local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
- }}
- />
- </div>
- )}
- </List>
- </Dialog.Body>
+ <Dialog title="Manage models" description="Customize which models appear in the model selector.">
+ <List
+ class="px-2.5"
+ search={{ placeholder: "Search models", autofocus: true }}
+ emptyMessage="No model results"
+ key={(x) => `${x?.provider?.id}:${x?.id}`}
+ items={local.model.list()}
+ filterKeys={["provider.name", "name", "id"]}
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
+ groupBy={(x) => x.provider.name}
+ sortGroupsBy={(a, b) => {
+ const aProvider = a.items[0].provider.id
+ const bProvider = b.items[0].provider.id
+ if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
+ if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
+ return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
+ }}
+ onSelect={(x) => {
+ if (!x) return
+ local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !x.visible)
+ }}
+ >
+ {(i) => (
+ <div class="w-full flex items-center justify-between gap-x-2.5">
+ <span>{i.name}</span>
+ <Switch
+ checked={!!i.visible}
+ onChange={(checked) => {
+ local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
+ }}
+ />
+ </div>
+ )}
+ </List>
</Dialog>
)
}
diff --git a/packages/desktop/src/components/dialog-model-unpaid.tsx b/packages/desktop/src/components/dialog-model-unpaid.tsx
deleted file mode 100644
index d218770d9..000000000
--- a/packages/desktop/src/components/dialog-model-unpaid.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { Component, onCleanup, onMount, Show } from "solid-js"
-import { useLocal } from "@/context/local"
-import { useDialog } from "@/context/dialog"
-import { popularProviders, useProviders } from "@/hooks/use-providers"
-import { Button } from "@opencode-ai/ui/button"
-import { Tag } from "@opencode-ai/ui/tag"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List, ListRef } from "@opencode-ai/ui/list"
-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 DialogModelUnpaid: Component = () => {
- const local = useLocal()
- const dialog = useDialog()
- const providers = useProviders()
-
- let listRef: ListRef | undefined
- const handleKey = (e: KeyboardEvent) => {
- if (e.key === "Escape") return
- listRef?.onKeyDown(e)
- }
-
- onMount(() => {
- document.addEventListener("keydown", handleKey)
- onCleanup(() => {
- document.removeEventListener("keydown", handleKey)
- })
- })
-
- return (
- <Dialog
- modal
- defaultOpen
- onOpenChange={(open) => {
- if (!open) {
- dialog.clear()
- }
- }}
- >
- <Dialog.Header>
- <Dialog.Title>Select model</Dialog.Title>
- <Dialog.CloseButton tabIndex={-1} />
- </Dialog.Header>
- <Dialog.Body>
- <div class="flex flex-col gap-3 px-2.5">
- <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
- <List
- ref={(ref) => (listRef = ref)}
- items={local.model.list}
- current={local.model.current()}
- key={(x) => `${x.provider.id}:${x.id}`}
- onSelect={(x) => {
- local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
- recent: true,
- })
- dialog.clear()
- }}
- >
- {(i) => (
- <div class="w-full flex items-center gap-x-2.5">
- <span>{i.name}</span>
- <Tag>Free</Tag>
- <Show when={i.latest}>
- <Tag>Latest</Tag>
- </Show>
- </div>
- )}
- </List>
- <div />
- <div />
- </div>
- <div class="px-1.5 pb-1.5">
- <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
- <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
- <div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
- <div class="w-full">
- <List
- class="w-full"
- key={(x) => x?.id}
- items={providers.popular}
- activeIcon="plus-small"
- sortBy={(a, b) => {
- if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
- return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
- return a.name.localeCompare(b.name)
- }}
- onSelect={(x) => {
- if (!x) return
- dialog.replace(() => <DialogConnect provider={x.id} />)
- }}
- >
- {(i) => (
- <div class="w-full flex items-center gap-x-4">
- <ProviderIcon
- data-slot="list-item-extra-icon"
- id={i.id as IconName}
- // TODO: clean this up after we update icon in models.dev
- classList={{
- "text-icon-weak-base": true,
- "size-4 mx-0.5": i.id === "opencode",
- "size-5": i.id !== "opencode",
- }}
- />
- <span>{i.name}</span>
- <Show when={i.id === "opencode"}>
- <Tag>Recommended</Tag>
- </Show>
- <Show when={i.id === "anthropic"}>
- <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
- </Show>
- </div>
- )}
- </List>
- <Button
- variant="ghost"
- class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
- icon="dot-grid"
- onClick={() => {
- dialog.replace(() => <DialogSelectProvider />)
- }}
- >
- View all providers
- </Button>
- </div>
- </div>
- </div>
- </div>
- </Dialog.Body>
- </Dialog>
- )
-}
diff --git a/packages/desktop/src/components/dialog-model.tsx b/packages/desktop/src/components/dialog-model.tsx
deleted file mode 100644
index e8f9df055..000000000
--- a/packages/desktop/src/components/dialog-model.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { Component, createMemo, Show } from "solid-js"
-import { useLocal } from "@/context/local"
-import { useDialog } from "@/context/dialog"
-import { popularProviders } from "@/hooks/use-providers"
-import { Button } from "@opencode-ai/ui/button"
-import { Tag } from "@opencode-ai/ui/tag"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
-import { DialogSelectProvider } from "./dialog-select-provider"
-import { DialogManageModels } from "./dialog-manage-models"
-
-export const DialogModel: Component<{ provider?: string }> = (props) => {
- const local = useLocal()
- const dialog = useDialog()
-
- let closeButton!: HTMLButtonElement
- const models = createMemo(() =>
- local.model
- .list()
- .filter((m) => m.visible)
- .filter((m) => (props.provider ? m.provider.id === props.provider : true)),
- )
-
- return (
- <Dialog
- modal
- defaultOpen
- onOpenChange={(open) => {
- if (!open) {
- dialog.clear()
- }
- }}
- >
- <Dialog.Header>
- <Dialog.Title>Select model</Dialog.Title>
- <Button
- class="h-7 -my-1 text-14-medium"
- icon="plus-small"
- tabIndex={-1}
- onClick={() => dialog.replace(() => <DialogSelectProvider />)}
- >
- Connect provider
- </Button>
- <Dialog.CloseButton ref={closeButton} tabIndex={-1} style={{ display: "none" }} />
- </Dialog.Header>
- <Dialog.Body>
- <List
- class="px-2.5"
- search={{ placeholder: "Search models", autofocus: true }}
- emptyMessage="No model results"
- key={(x) => `${x.provider.id}:${x.id}`}
- items={models}
- current={local.model.current()}
- filterKeys={["provider.name", "name", "id"]}
- sortBy={(a, b) => a.name.localeCompare(b.name)}
- groupBy={(x) => x.provider.name}
- sortGroupsBy={(a, b) => {
- if (a.category === "Recent" && b.category !== "Recent") return -1
- if (b.category === "Recent" && a.category !== "Recent") return 1
- const aProvider = a.items[0].provider.id
- const bProvider = b.items[0].provider.id
- if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
- if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
- return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
- }}
- onSelect={(x) => {
- local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
- recent: true,
- })
- closeButton.click()
- }}
- >
- {(i) => (
- <div class="w-full flex items-center gap-x-2.5">
- <span>{i.name}</span>
- <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
- <Tag>Free</Tag>
- </Show>
- <Show when={i.latest}>
- <Tag>Latest</Tag>
- </Show>
- </div>
- )}
- </List>
- <Button
- variant="ghost"
- class="ml-2.5 mt-5 mb-6 text-text-base self-start"
- onClick={() => dialog.replace(() => <DialogManageModels />)}
- >
- Manage models
- </Button>
- </Dialog.Body>
- </Dialog>
- )
-}
diff --git a/packages/desktop/src/components/dialog-select-file.tsx b/packages/desktop/src/components/dialog-select-file.tsx
new file mode 100644
index 000000000..0250963b0
--- /dev/null
+++ b/packages/desktop/src/components/dialog-select-file.tsx
@@ -0,0 +1,44 @@
+import { useLocal } from "@/context/local"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+import { FileIcon } from "@opencode-ai/ui/file-icon"
+import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { useSession } from "@/context/session"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+
+export function DialogSelectFile() {
+ const session = useSession()
+ const local = useLocal()
+ const dialog = useDialog()
+ return (
+ <Dialog title="Select file">
+ <List
+ class="px-2.5"
+ search={{ placeholder: "Search files", autofocus: true }}
+ emptyMessage="No files found"
+ items={local.file.searchFiles}
+ key={(x) => x}
+ onSelect={(path) => {
+ if (path) {
+ session.layout.openTab("file://" + path)
+ }
+ dialog.clear()
+ }}
+ >
+ {(i) => (
+ <div class="w-full flex items-center justify-between rounded-md">
+ <div class="flex items-center gap-x-2 grow min-w-0">
+ <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
+ <div class="flex items-center text-14-regular">
+ <span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
+ {getDirectory(i)}
+ </span>
+ <span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
+ </div>
+ </div>
+ </div>
+ )}
+ </List>
+ </Dialog>
+ )
+}
diff --git a/packages/desktop/src/components/dialog-select-model-unpaid.tsx b/packages/desktop/src/components/dialog-select-model-unpaid.tsx
new file mode 100644
index 000000000..1c9e9cc75
--- /dev/null
+++ b/packages/desktop/src/components/dialog-select-model-unpaid.tsx
@@ -0,0 +1,119 @@
+import { Component, onCleanup, onMount, Show } from "solid-js"
+import { useLocal } from "@/context/local"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { Button } from "@opencode-ai/ui/button"
+import { Tag } from "@opencode-ai/ui/tag"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List, ListRef } from "@opencode-ai/ui/list"
+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 DialogSelectModelUnpaid: Component = () => {
+ const local = useLocal()
+ const dialog = useDialog()
+ const providers = useProviders()
+
+ let listRef: ListRef | undefined
+ const handleKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") return
+ listRef?.onKeyDown(e)
+ }
+
+ onMount(() => {
+ document.addEventListener("keydown", handleKey)
+ onCleanup(() => {
+ document.removeEventListener("keydown", handleKey)
+ })
+ })
+
+ return (
+ <Dialog title="Select model">
+ <div class="flex flex-col gap-3 px-2.5">
+ <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
+ <List
+ ref={(ref) => (listRef = ref)}
+ items={local.model.list}
+ current={local.model.current()}
+ key={(x) => `${x.provider.id}:${x.id}`}
+ onSelect={(x) => {
+ local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
+ recent: true,
+ })
+ dialog.clear()
+ }}
+ >
+ {(i) => (
+ <div class="w-full flex items-center gap-x-2.5">
+ <span>{i.name}</span>
+ <Tag>Free</Tag>
+ <Show when={i.latest}>
+ <Tag>Latest</Tag>
+ </Show>
+ </div>
+ )}
+ </List>
+ <div />
+ <div />
+ </div>
+ <div class="px-1.5 pb-1.5">
+ <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
+ <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
+ <div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
+ <div class="w-full">
+ <List
+ class="w-full"
+ key={(x) => x?.id}
+ items={providers.popular}
+ activeIcon="plus-small"
+ sortBy={(a, b) => {
+ if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
+ return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
+ return a.name.localeCompare(b.name)
+ }}
+ onSelect={(x) => {
+ if (!x) return
+ dialog.replace(() => <DialogConnect provider={x.id} />)
+ }}
+ >
+ {(i) => (
+ <div class="w-full flex items-center gap-x-4">
+ <ProviderIcon
+ data-slot="list-item-extra-icon"
+ id={i.id as IconName}
+ // TODO: clean this up after we update icon in models.dev
+ classList={{
+ "text-icon-weak-base": true,
+ "size-4 mx-0.5": i.id === "opencode",
+ "size-5": i.id !== "opencode",
+ }}
+ />
+ <span>{i.name}</span>
+ <Show when={i.id === "opencode"}>
+ <Tag>Recommended</Tag>
+ </Show>
+ <Show when={i.id === "anthropic"}>
+ <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
+ </Show>
+ </div>
+ )}
+ </List>
+ <Button
+ variant="ghost"
+ class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
+ icon="dot-grid"
+ onClick={() => {
+ dialog.replace(() => <DialogSelectProvider />)
+ }}
+ >
+ View all providers
+ </Button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </Dialog>
+ )
+}
diff --git a/packages/desktop/src/components/dialog-select-model.tsx b/packages/desktop/src/components/dialog-select-model.tsx
new file mode 100644
index 000000000..805db47fe
--- /dev/null
+++ b/packages/desktop/src/components/dialog-select-model.tsx
@@ -0,0 +1,85 @@
+import { Component, createMemo, Show } from "solid-js"
+import { useLocal } from "@/context/local"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { popularProviders } from "@/hooks/use-providers"
+import { Button } from "@opencode-ai/ui/button"
+import { Tag } from "@opencode-ai/ui/tag"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+import { DialogSelectProvider } from "./dialog-select-provider"
+import { DialogManageModels } from "./dialog-manage-models"
+
+export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
+ const local = useLocal()
+ const dialog = useDialog()
+
+ let closeButton!: HTMLButtonElement
+ const models = createMemo(() =>
+ local.model
+ .list()
+ .filter((m) => m.visible)
+ .filter((m) => (props.provider ? m.provider.id === props.provider : true)),
+ )
+
+ return (
+ <Dialog
+ title="Select model"
+ action={
+ <Button
+ class="h-7 -my-1 text-14-medium"
+ icon="plus-small"
+ tabIndex={-1}
+ onClick={() => dialog.replace(() => <DialogSelectProvider />)}
+ >
+ Connect provider
+ </Button>
+ }
+ >
+ <List
+ class="px-2.5"
+ search={{ placeholder: "Search models", autofocus: true }}
+ emptyMessage="No model results"
+ key={(x) => `${x.provider.id}:${x.id}`}
+ items={models}
+ current={local.model.current()}
+ filterKeys={["provider.name", "name", "id"]}
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
+ groupBy={(x) => x.provider.name}
+ sortGroupsBy={(a, b) => {
+ if (a.category === "Recent" && b.category !== "Recent") return -1
+ if (b.category === "Recent" && a.category !== "Recent") return 1
+ const aProvider = a.items[0].provider.id
+ const bProvider = b.items[0].provider.id
+ if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
+ if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
+ return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
+ }}
+ onSelect={(x) => {
+ local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
+ recent: true,
+ })
+ closeButton.click()
+ }}
+ >
+ {(i) => (
+ <div class="w-full flex items-center gap-x-2.5">
+ <span>{i.name}</span>
+ <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
+ <Tag>Free</Tag>
+ </Show>
+ <Show when={i.latest}>
+ <Tag>Latest</Tag>
+ </Show>
+ </div>
+ )}
+ </List>
+ <Button
+ variant="ghost"
+ class="ml-3 mt-5 mb-6 text-text-base self-start"
+ onClick={() => dialog.replace(() => <DialogManageModels />)}
+ >
+ Manage models
+ </Button>
+ </Dialog>
+ )
+}
diff --git a/packages/desktop/src/components/dialog-select-provider.tsx b/packages/desktop/src/components/dialog-select-provider.tsx
index 1c54184bd..292d5fccb 100644
--- a/packages/desktop/src/components/dialog-select-provider.tsx
+++ b/packages/desktop/src/components/dialog-select-provider.tsx
@@ -1,5 +1,5 @@
import { Component, Show } from "solid-js"
-import { useDialog } from "@/context/dialog"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
@@ -13,66 +13,52 @@ export const DialogSelectProvider: Component = () => {
const providers = useProviders()
return (
- <Dialog
- modal
- defaultOpen
- onOpenChange={(open) => {
- if (!open) {
- dialog.clear()
- }
- }}
- >
- <Dialog.Header>
- <Dialog.Title>Connect provider</Dialog.Title>
- <Dialog.CloseButton tabIndex={-1} />
- </Dialog.Header>
- <Dialog.Body>
- <List
- class="px-2.5"
- search={{ placeholder: "Search providers", autofocus: true }}
- activeIcon="plus-small"
- key={(x) => x?.id}
- items={providers.all}
- filterKeys={["id", "name"]}
- groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
- sortBy={(a, b) => {
- if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
- return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
- return a.name.localeCompare(b.name)
- }}
- sortGroupsBy={(a, b) => {
- if (a.category === "Popular" && b.category !== "Popular") return -1
- if (b.category === "Popular" && a.category !== "Popular") return 1
- return 0
- }}
- onSelect={(x) => {
- if (!x) return
- dialog.replace(() => <DialogConnect provider={x.id} />)
- }}
- >
- {(i) => (
- <div class="px-1.25 w-full flex items-center gap-x-4">
- <ProviderIcon
- data-slot="list-item-extra-icon"
- id={i.id as IconName}
- // TODO: clean this up after we update icon in models.dev
- classList={{
- "text-icon-weak-base": true,
- "size-4 mx-0.5": i.id === "opencode",
- "size-5": i.id !== "opencode",
- }}
- />
- <span>{i.name}</span>
- <Show when={i.id === "opencode"}>
- <Tag>Recommended</Tag>
- </Show>
- <Show when={i.id === "anthropic"}>
- <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
- </Show>
- </div>
- )}
- </List>
- </Dialog.Body>
+ <Dialog title="Connect provider">
+ <List
+ class="px-2.5"
+ search={{ placeholder: "Search providers", autofocus: true }}
+ activeIcon="plus-small"
+ key={(x) => x?.id}
+ items={providers.all}
+ filterKeys={["id", "name"]}
+ groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
+ sortBy={(a, b) => {
+ if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
+ return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
+ return a.name.localeCompare(b.name)
+ }}
+ sortGroupsBy={(a, b) => {
+ if (a.category === "Popular" && b.category !== "Popular") return -1
+ if (b.category === "Popular" && a.category !== "Popular") return 1
+ return 0
+ }}
+ onSelect={(x) => {
+ if (!x) return
+ dialog.replace(() => <DialogConnect provider={x.id} />)
+ }}
+ >
+ {(i) => (
+ <div class="px-1.25 w-full flex items-center gap-x-4">
+ <ProviderIcon
+ data-slot="list-item-extra-icon"
+ id={i.id as IconName}
+ // TODO: clean this up after we update icon in models.dev
+ classList={{
+ "text-icon-weak-base": true,
+ "size-4 mx-0.5": i.id === "opencode",
+ "size-5": i.id !== "opencode",
+ }}
+ />
+ <span>{i.name}</span>
+ <Show when={i.id === "opencode"}>
+ <Tag>Recommended</Tag>
+ </Show>
+ <Show when={i.id === "anthropic"}>
+ <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
+ </Show>
+ </div>
+ )}
+ </List>
</Dialog>
)
}
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index faecd9520..296fe8b2f 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -15,9 +15,9 @@ 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 { useDialog } from "@/context/dialog"
-import { DialogModel } from "@/components/dialog-model"
-import { DialogModelUnpaid } from "@/components/dialog-model-unpaid"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { DialogSelectModel } from "@/components/dialog-select-model"
+import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
interface PromptInputProps {
@@ -616,7 +616,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Button
as="div"
variant="ghost"
- onClick={() => dialog.push(() => (providers.paid().length > 0 ? <DialogModel /> : <DialogModelUnpaid />))}
+ onClick={() =>
+ dialog.push(() => (providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />))
+ }
>
{local.model.current()?.name ?? "Select model"}
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
diff --git a/packages/desktop/src/context/dialog.tsx b/packages/desktop/src/context/dialog.tsx
deleted file mode 100644
index cc49764fe..000000000
--- a/packages/desktop/src/context/dialog.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-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/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx
index 7b8d2ab9e..0dbb3f6d6 100644
--- a/packages/desktop/src/pages/directory-layout.tsx
+++ b/packages/desktop/src/pages/directory-layout.tsx
@@ -6,7 +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 { DialogRoot } from "@/context/dialog"
+import { DialogRoot } from "@opencode-ai/ui/context/dialog"
export default function Layout(props: ParentProps) {
const params = useParams()
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index c36cc234e..7af562d57 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -33,7 +33,7 @@ import { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
-import { useDialog } from "@/context/dialog"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
export default function Layout(props: ParentProps) {
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index c4adea000..a21135f76 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -15,7 +15,6 @@ import { Code } from "@opencode-ai/ui/code"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { SessionReview } from "@opencode-ai/ui/session-review"
-import { DialogFileSelect } from "@/components/dialog-file-select"
import {
DragDropProvider,
DragDropSensors,
@@ -33,15 +32,17 @@ import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Terminal } from "@/components/terminal"
import { checksum } from "@opencode-ai/util/encode"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { DialogSelectFile } from "@/components/dialog-select-file"
export default function Page() {
const layout = useLayout()
const local = useLocal()
const sync = useSync()
const session = useSession()
+ const dialog = useDialog()
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
- fileSelectOpen: false,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
})
@@ -72,7 +73,7 @@ export default function Page() {
}
if (event.getModifierState(MOD) && event.key.toLowerCase() === "p") {
event.preventDefault()
- setStore("fileSelectOpen", true)
+ dialog.replace(() => <DialogSelectFile />)
return
}
if (event.ctrlKey && event.key.toLowerCase() === "t") {
@@ -388,7 +389,7 @@ export default function Page() {
icon="plus-small"
variant="ghost"
iconSize="large"
- onClick={() => setStore("fileSelectOpen", true)}
+ onClick={() => dialog.replace(() => <DialogSelectFile />)}
/>
</Tooltip>
</div>
@@ -610,12 +611,6 @@ export default function Page() {
</ul>
</Show>
</div>
- <Show when={store.fileSelectOpen}>
- <DialogFileSelect
- onOpenChange={(open) => setStore("fileSelectOpen", open)}
- onSelect={(path) => session.layout.openTab("file://" + path)}
- />
- </Show>
</div>
<Show when={layout.terminal.opened()}>
<div