diff options
| author | Adam <[email protected]> | 2025-12-10 17:17:34 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-10 17:17:37 -0600 |
| commit | 85cfa226c34e41660ddfdcb04543af2e494ae168 (patch) | |
| tree | f98a6631bf169470c37eeacc79129b826da59dd2 /packages/desktop/src | |
| parent | cbb591eb7dfe8e27298945f10e5d6cfff4405630 (diff) | |
| download | opencode-85cfa226c34e41660ddfdcb04543af2e494ae168.tar.gz opencode-85cfa226c34e41660ddfdcb04543af2e494ae168.zip | |
wip(desktop): progress
Diffstat (limited to 'packages/desktop/src')
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 221 | ||||
| -rw-r--r-- | packages/desktop/src/hooks/use-providers.ts | 31 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 19 |
3 files changed, 203 insertions, 68 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 985dbae8e..0672dfc85 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -17,6 +17,13 @@ import { Select } from "@opencode-ai/ui/select" import { Tag } from "@opencode-ai/ui/tag" import { getDirectory, getFilename } from "@opencode-ai/util/path" 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 +65,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const local = useLocal() const session = useSession() const layout = useLayout() + const providers = useProviders() let editorRef!: HTMLDivElement const [store, setStore] = createStore<{ @@ -461,60 +469,167 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <Icon name="chevron-down" size="small" /> </Button> <Show when={layout.dialog.opened() === "model"}> - <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) => { - 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 }) - } - actions={ - <Button - class="h-7 -my-1 text-14-medium" - icon="plus-small" - tabIndex={-1} - onClick={() => layout.dialog.open("provider")} + <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> + } > - 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> + {(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> + </div> + </div> + </div> + </Dialog.Body> + </Dialog> + ) + })} + </Match> + </Switch> </Show> </div> <Tooltip diff --git a/packages/desktop/src/hooks/use-providers.ts b/packages/desktop/src/hooks/use-providers.ts new file mode 100644 index 000000000..c3fcc7898 --- /dev/null +++ b/packages/desktop/src/hooks/use-providers.ts @@ -0,0 +1,31 @@ +import { useGlobalSync } from "@/context/global-sync" +import { base64Decode } from "@opencode-ai/util/encode" +import { useParams } from "@solidjs/router" +import { createMemo } from "solid-js" + +export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] + +export function useProviders() { + const params = useParams() + const globalSync = useGlobalSync() + const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) + const providers = createMemo(() => { + if (currentDirectory()) { + const [projectStore] = globalSync.child(currentDirectory()) + return projectStore.provider + } + return globalSync.data.provider + }) + const connected = createMemo(() => + providers().all.filter( + (p) => providers().connected.includes(p.id) && Object.values(p.models).find((m) => m.cost?.input), + ), + ) + const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id))) + return createMemo(() => ({ + all: providers().all, + default: providers().default, + popular, + connected, + })) +} diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 2ea6c4ba0..10d4cbfda 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -33,8 +33,7 @@ import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" import { SelectDialog } from "@opencode-ai/ui/select-dialog" import { Tag } from "@opencode-ai/ui/tag" import { IconName } from "@opencode-ai/ui/icons/provider" - -const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] +import { popularProviders, useProviders } from "@/hooks/use-providers" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -50,18 +49,7 @@ export default function Layout(props: ParentProps) { const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? []) const currentSession = createMemo(() => sessions().find((s) => s.id === params.id)) - const providers = createMemo(() => { - if (currentDirectory()) { - const [projectStore] = globalSync.child(currentDirectory()) - return projectStore.provider - } - return globalSync.data.provider - }) - const connectedProviders = createMemo(() => - providers().all.filter( - (p) => providers().connected.includes(p.id) && Object.values(p.models).find((m) => m.cost?.input), - ), - ) + const providers = useProviders() function navigateToProject(directory: string | undefined) { if (!directory) return @@ -493,7 +481,7 @@ export default function Layout(props: ParentProps) { </div> <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3"> <Switch> - <Match when={!connectedProviders().length && layout.sidebar.opened()}> + <Match when={!providers().connected().length && layout.sidebar.opened()}> <div class="rounded-md bg-background-stronger shadow-xs-border-base"> <div class="p-3 flex flex-col gap-2"> <div class="text-12-medium text-text-strong">Getting started</div> @@ -599,6 +587,7 @@ export default function Layout(props: ParentProps) { {(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={{ |
