diff options
| author | David Hill <[email protected]> | 2025-12-11 00:46:07 +0000 |
|---|---|---|
| committer | David Hill <[email protected]> | 2025-12-11 00:46:07 +0000 |
| commit | a468044c9fb4ece1f1ae4685d46eb875751b66d8 (patch) | |
| tree | a1879744be543886fa1f50190aff37db8b218c52 /packages/desktop/src/components | |
| parent | f0274fd29f7301b8ca79b1573b26efd8d08f31d6 (diff) | |
| parent | fadeed1fa4caacf34054b231784513a841745766 (diff) | |
| download | opencode-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.tsx | 237 |
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" |
