summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-14 19:33:40 -0600
committerAdam <[email protected]>2025-12-14 21:38:58 -0600
commit4246cdb069502c96ab11e260eb36a07a0370b710 (patch)
treea6340608c5d4954b860806ca807e95682385be96 /packages/desktop/src/components
parent7ade6d386daeea120415b69f9df522001350db7b (diff)
downloadopencode-4246cdb069502c96ab11e260eb36a07a0370b710.tar.gz
opencode-4246cdb069502c96ab11e260eb36a07a0370b710.zip
wip(desktop): progress
Diffstat (limited to 'packages/desktop/src/components')
-rw-r--r--packages/desktop/src/components/dialog-connect.tsx2
-rw-r--r--packages/desktop/src/components/dialog-file-select.tsx52
-rw-r--r--packages/desktop/src/components/dialog-manage-models.tsx65
-rw-r--r--packages/desktop/src/components/dialog-model-unpaid.tsx133
-rw-r--r--packages/desktop/src/components/dialog-model.tsx275
-rw-r--r--packages/desktop/src/components/dialog-select-provider.tsx101
-rw-r--r--packages/desktop/src/components/prompt-input.tsx9
7 files changed, 396 insertions, 241 deletions
diff --git a/packages/desktop/src/components/dialog-connect.tsx b/packages/desktop/src/components/dialog-connect.tsx
index d482b3f50..3a1e05f27 100644
--- a/packages/desktop/src/components/dialog-connect.tsx
+++ b/packages/desktop/src/components/dialog-connect.tsx
@@ -117,7 +117,7 @@ export const DialogConnect: Component<{ provider: string }> = (props) => {
title: `${provider().name} connected`,
description: `${provider().name} models are now available to use.`,
})
- dialog.replace(() => <DialogModel connectedProvider={props.provider} />)
+ dialog.replace(() => <DialogModel provider={props.provider} />)
}, 500)
}
diff --git a/packages/desktop/src/components/dialog-file-select.tsx b/packages/desktop/src/components/dialog-file-select.tsx
new file mode 100644
index 000000000..3afe06062
--- /dev/null
+++ b/packages/desktop/src/components/dialog-file-select.tsx
@@ -0,0 +1,52 @@
+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
new file mode 100644
index 000000000..2904f9a5b
--- /dev/null
+++ b/packages/desktop/src/components/dialog-manage-models.tsx
@@ -0,0 +1,65 @@
+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"
+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>
+ )
+}
diff --git a/packages/desktop/src/components/dialog-model-unpaid.tsx b/packages/desktop/src/components/dialog-model-unpaid.tsx
new file mode 100644
index 000000000..d218770d9
--- /dev/null
+++ b/packages/desktop/src/components/dialog-model-unpaid.tsx
@@ -0,0 +1,133 @@
+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
index 7f90e1a78..e8f9df055 100644
--- a/packages/desktop/src/components/dialog-model.tsx
+++ b/packages/desktop/src/components/dialog-model.tsx
@@ -1,208 +1,95 @@
-import { Component, createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
+import { Component, createMemo, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { useDialog } from "@/context/dialog"
-import { popularProviders, useProviders } from "@/hooks/use-providers"
-import { SelectDialog } from "@opencode-ai/ui/select-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, 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 { List } from "@opencode-ai/ui/list"
import { DialogSelectProvider } from "./dialog-select-provider"
-import { DialogConnect } from "./dialog-connect"
+import { DialogManageModels } from "./dialog-manage-models"
-export const DialogModel: Component<{ connectedProvider?: string }> = (props) => {
+export const DialogModel: Component<{ provider?: string }> = (props) => {
const local = useLocal()
const dialog = useDialog()
- const providers = useProviders()
- return (
- <Switch>
- <Match when={providers.paid().length > 0}>
- {iife(() => {
- const models = createMemo(() =>
- local.model
- .list()
- .filter((m) => m.visible)
- .filter((m) => (props.connectedProvider ? m.provider.id === props.connectedProvider : true)),
- )
- return (
- <SelectDialog
- defaultOpen
- onOpenChange={(open) => {
- if (!open) {
- dialog.clear()
- }
- }}
- title="Select model"
- placeholder="Search models"
- 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,
- })
- }
- actions={
- <Button
- class="h-7 -my-1 text-14-medium"
- icon="plus-small"
- tabIndex={-1}
- onClick={() => dialog.replace(() => <DialogSelectProvider />)}
- >
- Connect provider
- </Button>
- }
- >
- {(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>
- )}
- </SelectDialog>
- )
- })}
- </Match>
- <Match when={true}>
- {iife(() => {
- let listRef: ListRef | undefined
- const handleKey = (e: KeyboardEvent) => {
- if (e.key === "Escape") return
- listRef?.onKeyDown(e)
- }
+ let closeButton!: HTMLButtonElement
+ const models = createMemo(() =>
+ local.model
+ .list()
+ .filter((m) => m.visible)
+ .filter((m) => (props.provider ? m.provider.id === props.provider : true)),
+ )
- 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>
+ <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,
})
- })
-
- 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>
- )
- })}
- </Match>
- </Switch>
+ 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-provider.tsx b/packages/desktop/src/components/dialog-select-provider.tsx
index 6dabdb8b4..1c54184bd 100644
--- a/packages/desktop/src/components/dialog-select-provider.tsx
+++ b/packages/desktop/src/components/dialog-select-provider.tsx
@@ -1,7 +1,8 @@
import { Component, Show } from "solid-js"
import { useDialog } from "@/context/dialog"
import { popularProviders, useProviders } from "@/hooks/use-providers"
-import { SelectDialog } from "@opencode-ai/ui/select-dialog"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
@@ -12,56 +13,66 @@ export const DialogSelectProvider: Component = () => {
const providers = useProviders()
return (
- <SelectDialog
+ <Dialog
+ modal
defaultOpen
- title="Connect provider"
- placeholder="Search providers"
- 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} />)
- }}
onOpenChange={(open) => {
if (!open) {
dialog.clear()
}
}}
>
- {(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>
- )}
- </SelectDialog>
+ <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>
)
}
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index ca0ccf96a..faecd9520 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -17,6 +17,8 @@ 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 { useProviders } from "@/hooks/use-providers"
interface PromptInputProps {
class?: string
@@ -58,6 +60,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const local = useLocal()
const session = useSession()
const dialog = useDialog()
+ const providers = useProviders()
let editorRef!: HTMLDivElement
const [store, setStore] = createStore<{
@@ -610,7 +613,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
class="capitalize"
variant="ghost"
/>
- <Button as="div" variant="ghost" onClick={() => dialog.push(() => <DialogModel />)}>
+ <Button
+ as="div"
+ variant="ghost"
+ onClick={() => dialog.push(() => (providers.paid().length > 0 ? <DialogModel /> : <DialogModelUnpaid />))}
+ >
{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" />