summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/components
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-12-11 00:46:07 +0000
committerDavid Hill <[email protected]>2025-12-11 00:46:07 +0000
commita468044c9fb4ece1f1ae4685d46eb875751b66d8 (patch)
treea1879744be543886fa1f50190aff37db8b218c52 /packages/desktop/src/components
parentf0274fd29f7301b8ca79b1573b26efd8d08f31d6 (diff)
parentfadeed1fa4caacf34054b231784513a841745766 (diff)
downloadopencode-a468044c9fb4ece1f1ae4685d46eb875751b66d8.tar.gz
opencode-a468044c9fb4ece1f1ae4685d46eb875751b66d8.zip
Merge branch 'dev' of https://github.com/sst/opencode into dev
Diffstat (limited to 'packages/desktop/src/components')
-rw-r--r--packages/desktop/src/components/prompt-input.tsx237
1 files changed, 185 insertions, 52 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index 6590b6d18..22f2c1642 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -3,7 +3,6 @@ import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Mat
import { createStore } from "solid-js/store"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
-import { DateTime } from "luxon"
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
import { useSDK } from "@/context/sdk"
import { useNavigate } from "@solidjs/router"
@@ -14,10 +13,17 @@ import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button"
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Select } from "@opencode-ai/ui/select"
+import { Tag } from "@opencode-ai/ui/tag"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { type IconName } from "@opencode-ai/ui/icons/provider"
+import { useLayout } from "@/context/layout"
+import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List, ListRef } from "@opencode-ai/ui/list"
+import { iife } from "@opencode-ai/util/iife"
+import { Input } from "@opencode-ai/ui/input"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { IconName } from "@opencode-ai/ui/icons/provider"
interface PromptInputProps {
class?: string
@@ -58,6 +64,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const sync = useSync()
const local = useLocal()
const session = useSession()
+ const layout = useLayout()
+ const providers = useProviders()
let editorRef!: HTMLDivElement
const [store, setStore] = createStore<{
@@ -455,55 +463,180 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
class="capitalize"
variant="ghost"
/>
- <SelectDialog
- title="Select model"
- placeholder="Search models"
- emptyMessage="No model results"
- key={(x) => `${x.provider.id}:${x.id}`}
- items={local.model.list()}
- current={local.model.current()}
- filterKeys={["provider.name", "name", "id"]}
- groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
- sortGroupsBy={(a, b) => {
- const order = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
- 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 (order.includes(aProvider) && !order.includes(bProvider)) return -1
- if (!order.includes(aProvider) && order.includes(bProvider)) return 1
- return order.indexOf(aProvider) - order.indexOf(bProvider)
- }}
- onSelect={(x) =>
- local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
- }
- trigger={
- <Button as="div" variant="ghost">
- {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>
- }
- >
- {(i) => (
- <div class="w-full flex items-center justify-between gap-x-3">
- <div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
- <ProviderIcon name={i.provider.id as IconName} class="size-6 p-0.5 shrink-0" />
- <div class="flex gap-x-3 items-baseline flex-[1_0_0]">
- <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
- <Show when={false}>
- <span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
- {DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
- </span>
- </Show>
- </div>
- </div>
- <Show when={!i.cost || i.cost?.input === 0}>
- <div class="overflow-hidden text-12-medium text-text-strong">Free</div>
- </Show>
- </div>
- )}
- </SelectDialog>
+ <Button as="div" variant="ghost" onClick={() => layout.dialog.open("model")}>
+ {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"}>
+ <Switch>
+ <Match when={providers().connected().length > 0}>
+ <SelectDialog
+ defaultOpen
+ onOpenChange={(open) => {
+ if (open) {
+ layout.dialog.open("model")
+ } else {
+ layout.dialog.close("model")
+ }
+ }}
+ title="Select model"
+ placeholder="Search models"
+ emptyMessage="No model results"
+ key={(x) => `${x.provider.id}:${x.id}`}
+ items={local.model.list()}
+ current={local.model.current()}
+ filterKeys={["provider.name", "name", "id"]}
+ // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.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={() => layout.dialog.open("provider")}
+ >
+ Connect provider
+ </Button>
+ }
+ >
+ {(i) => (
+ <div class="w-full flex items-center gap-x-2.5">
+ <span>{i.name}</span>
+ <Show when={!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)
+ }
+ return (
+ <Dialog
+ modal
+ defaultOpen
+ onOpenChange={(open) => {
+ if (open) {
+ layout.dialog.open("model")
+ } else {
+ layout.dialog.close("model")
+ }
+ }}
+ >
+ <Dialog.Header>
+ <Dialog.Title>Select model</Dialog.Title>
+ <Dialog.CloseButton tabIndex={-1} />
+ </Dialog.Header>
+ <Dialog.Body>
+ <Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
+ <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,
+ })
+ layout.dialog.close("model")
+ }}
+ >
+ {(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-6">
+ <div class="px-2 text-14-medium text-text-base">
+ Add more models from popular providers
+ </div>
+ <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) => {
+ layout.dialog.close("model")
+ }}
+ >
+ {(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">
+ <div class="flex items-center gap-2">
+ <Icon name="plus-small" />
+ <div class="text-text-strong">View all providers</div>
+ </div>
+ </Button>
+ </div>
+ </div>
+ </div>
+ </Dialog.Body>
+ </Dialog>
+ )
+ })}
+ </Match>
+ </Switch>
+ </Show>
</div>
<Tooltip
placement="top"