From 85cfa226c34e41660ddfdcb04543af2e494ae168 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:17:34 -0600 Subject: [PATCH] wip(desktop): progress --- .../desktop/src/components/prompt-input.tsx | 221 +++++++++++++----- packages/desktop/src/hooks/use-providers.ts | 31 +++ packages/desktop/src/pages/layout.tsx | 19 +- packages/ui/src/components/input.tsx | 10 +- packages/ui/src/components/list.css | 8 + packages/ui/src/components/list.tsx | 3 +- 6 files changed, 222 insertions(+), 70 deletions(-) create mode 100644 packages/desktop/src/hooks/use-providers.ts 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 = (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 = (props) => { - { - 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={ - + } > - Connect provider - - } - > - {(i) => ( -
- {i.name} - - Free - - - Latest - -
- )} -
+ {(i) => ( +
+ {i.name} + + Free + + + Latest + +
+ )} + + + + {iife(() => { + let listRef: ListRef | undefined + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") return + listRef?.onKeyDown(e) + } + return ( + { + if (open) { + layout.dialog.open("model") + } else { + layout.dialog.close("model") + } + }} + > + + Select model + + + + +
+
Free models provided by OpenCode
+ (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) => ( +
+ {i.name} + Free + + Latest + +
+ )} +
+
+
+
+
+
+
+
+ Add more models from popular providers +
+ 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) => ( +
+ + {i.name} + + Recommended + + +
+ Connect with Claude Pro/Max or API key +
+
+
+ )} +
+
+
+
+ +
+ ) + })} +
+
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) {
- +
Getting started
@@ -599,6 +587,7 @@ export default function Layout(props: ParentProps) { {(i) => (
, "value" | "onChange" | "onKeyDown">> { label?: string hideLabel?: boolean + hidden?: boolean description?: string } @@ -14,6 +15,7 @@ export function Input(props: InputProps) { const [local, others] = splitProps(props, [ "class", "label", + "hidden", "hideLabel", "description", "value", @@ -21,7 +23,13 @@ export function Input(props: InputProps) { "onKeyDown", ]) return ( - + {local.label} diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index 63d9a2fe1..38dcb773b 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -97,10 +97,18 @@ [data-slot="list-item-active-icon"] { display: block; } + [data-slot="list-item-extra-icon"] { + color: var(--icon-strong-base) !important; + } } &:active { background: var(--surface-raised-base-active); } + &:hover { + [data-slot="list-item-extra-icon"] { + color: var(--icon-strong-base) !important; + } + } } } } diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 3fbeb35f6..a7f2db9ef 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -4,6 +4,7 @@ import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" import { Icon, IconProps } from "./icon" export interface ListProps extends FilteredListProps { + class?: string children: (item: T) => JSX.Element emptyMessage?: string onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void @@ -90,7 +91,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) }) return ( -
+
0} fallback={