summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-10 14:48:08 -0600
committerAdam <[email protected]>2025-12-10 15:17:03 -0600
commit190fa4c87aa2b3f954a419f716add1fc29e4011e (patch)
tree835e91a7457c4d92fd8d3118d225f12843b80a5b
parent91d743ef9a5c346fe17bb857db68dca92a6e9ba1 (diff)
downloadopencode-190fa4c87aa2b3f954a419f716add1fc29e4011e.tar.gz
opencode-190fa4c87aa2b3f954a419f716add1fc29e4011e.zip
wip(desktop): progress
-rw-r--r--packages/desktop/src/components/prompt-input.tsx111
-rw-r--r--packages/desktop/src/context/global-sync.tsx26
-rw-r--r--packages/desktop/src/context/layout.tsx20
-rw-r--r--packages/desktop/src/context/local.tsx38
-rw-r--r--packages/desktop/src/context/session.tsx2
-rw-r--r--packages/desktop/src/context/sync.tsx6
-rw-r--r--packages/desktop/src/pages/home.tsx4
-rw-r--r--packages/desktop/src/pages/layout.tsx90
-rw-r--r--packages/enterprise/src/routes/share/[shareID].tsx2
-rw-r--r--packages/ui/src/components/provider-icon.tsx6
-rw-r--r--packages/ui/src/components/select-dialog.css12
-rw-r--r--packages/ui/src/components/select-dialog.tsx10
12 files changed, 201 insertions, 126 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index 97d27ee1e..985dbae8e 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -16,6 +16,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
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"
interface PromptInputProps {
class?: string
@@ -56,6 +57,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const sync = useSync()
const local = useLocal()
const session = useSession()
+ const layout = useLayout()
let editorRef!: HTMLDivElement
const [store, setStore] = createStore<{
@@ -453,54 +455,67 @@ 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)}
- 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 })
- }
- 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>
- }
- actions={
- <Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1}>
- 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>
+ <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"}>
+ <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")}
+ >
+ 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>
+ </Show>
</div>
<Tooltip
placement="top"
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 3e2b6bf7d..3a6062fb8 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -1,7 +1,6 @@
import type {
Message,
Agent,
- Provider,
Session,
Part,
Config,
@@ -12,6 +11,7 @@ import type {
FileDiff,
Todo,
SessionStatus,
+ ProviderListResponse,
} from "@opencode-ai/sdk/v2"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
@@ -20,9 +20,9 @@ import { useGlobalSDK } from "./global-sdk"
type State = {
ready: boolean
- // provider: Provider[]
agent: Agent[]
project: string
+ provider: ProviderListResponse
config: Config
path: Path
session: Session[]
@@ -49,15 +49,16 @@ type State = {
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
name: "GlobalSync",
init: () => {
+ const sdk = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
- projects: Project[]
- providers: Provider[]
+ project: Project[]
+ provider: ProviderListResponse
children: Record<string, State>
}>({
ready: false,
- projects: [],
- providers: [],
+ project: [],
+ provider: { all: [], connected: [], default: {} },
children: {},
})
@@ -66,11 +67,11 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
if (!children[directory]) {
setGlobalStore("children", directory, {
project: "",
+ provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
- // provider: [],
session: [],
session_status: {},
session_diff: {},
@@ -86,7 +87,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
return children[directory]
}
- const sdk = useGlobalSDK()
sdk.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -94,13 +94,13 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
if (directory === "global") {
switch (event.type) {
case "project.updated": {
- const result = Binary.search(globalStore.projects, event.properties.id, (s) => s.id)
+ const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
if (result.found) {
- setGlobalStore("projects", result.index, reconcile(event.properties))
+ setGlobalStore("project", result.index, reconcile(event.properties))
return
}
setGlobalStore(
- "projects",
+ "project",
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
@@ -184,14 +184,14 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
Promise.all([
sdk.client.project.list().then(async (x) => {
setGlobalStore(
- "projects",
+ "project",
x
.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
.sort((a, b) => a.id.localeCompare(b.id)),
)
}),
sdk.client.provider.list().then((x) => {
- setGlobalStore("providers", x.data ?? [])
+ setGlobalStore("provider", x.data ?? {})
}),
]).then(() => setGlobalStore("ready", true))
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 13c4679d6..1de8550cb 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -40,9 +40,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
- name: "default-layout.v6",
+ name: "default-layout.v7",
},
)
+ const [ephemeral, setEphemeral] = createStore({
+ dialog: {
+ open: undefined as undefined | "provider" | "model",
+ },
+ })
function pickAvailableColor() {
const available = PASTEL_COLORS.filter((c) => !colors().has(c))
@@ -51,7 +56,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
function enrich(project: { worktree: string; expanded: boolean }) {
- const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
+ const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
if (!metadata) return []
return [
{
@@ -168,6 +173,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("review", "state", "tab")
},
},
+ dialog: {
+ opened: createMemo(() => ephemeral.dialog?.open),
+ open(dialog: "provider" | "model") {
+ setEphemeral("dialog", "open", dialog)
+ },
+ close(dialog: "provider" | "model") {
+ if (ephemeral.dialog?.open === dialog) {
+ setEphemeral("dialog", "open", undefined)
+ }
+ },
+ },
}
},
})
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index 58a65b0de..74d3ac364 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -39,8 +39,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const sync = useSync()
function isModelValid(model: ModelKey) {
- const provider = sync.data.provider.find((x) => x.id === model.providerID)
- return !!provider?.models[model.modelID]
+ const provider = sync.data.provider?.all.find((x) => x.id === model.providerID)
+ return !!provider?.models[model.modelID] && sync.data.provider?.connected.includes(model.providerID)
}
function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -115,17 +115,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
const list = createMemo(() =>
- sync.data.provider.flatMap((p) =>
- Object.values(p.models).map(
- (m) =>
- ({
- ...m,
- name: m.name.replace("(latest)", "").trim(),
- provider: p,
- latest: m.name.includes("(latest)"),
- }) as LocalModel,
+ sync.data.provider.all
+ .filter((p) => sync.data.provider.connected.includes(p.id))
+ .flatMap((p) =>
+ Object.values(p.models).map((m) => ({
+ ...m,
+ name: m.name.replace("(latest)", "").trim(),
+ provider: p,
+ latest: m.name.includes("(latest)"),
+ })),
),
- ),
)
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
@@ -145,12 +144,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return item
}
}
- const provider = sync.data.provider[0]
- const model = Object.values(provider.models)[0]
- return {
- providerID: provider.id,
- modelID: model.id,
+
+ for (const p of sync.data.provider.connected) {
+ if (p in sync.data.provider.default) {
+ return {
+ providerID: p,
+ modelID: sync.data.provider.default[p],
+ }
+ }
}
+
+ throw new Error("No default model found")
})
const currentModel = createMemo(() => {
diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx
index 31004811b..db2b3af7c 100644
--- a/packages/desktop/src/context/session.tsx
+++ b/packages/desktop/src/context/session.tsx
@@ -94,7 +94,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
() => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
)
const model = createMemo(() =>
- last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
+ last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
)
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 85986c327..1a11cd599 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const load = {
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
- provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
+ provider: () => sdk.client.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
session: () =>
@@ -42,8 +42,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return store.ready
},
get project() {
- const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
- if (match.found) return globalSync.data.projects[match.index]
+ const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
+ if (match.found) return globalSync.data.project[match.index]
return undefined
},
session: {
diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx
index 4aac241e1..205ffd815 100644
--- a/packages/desktop/src/pages/home.tsx
+++ b/packages/desktop/src/pages/home.tsx
@@ -38,7 +38,7 @@ export default function Home() {
<div class="mx-auto mt-55">
<Logo class="w-xl opacity-12" />
<Switch>
- <Match when={sync.data.projects.length > 0}>
+ <Match when={sync.data.project.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">Recent projects</div>
@@ -50,7 +50,7 @@ export default function Home() {
</div>
<ul class="flex flex-col gap-2">
<For
- each={sync.data.projects
+ each={sync.data.project
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
.slice(0, 5)}
>
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 3e0094756..2ea6c4ba0 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -9,6 +9,7 @@ import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
@@ -31,6 +32,9 @@ import {
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"]
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -46,15 +50,18 @@ 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(() => globalSync.data.providers)
- const hasProviders = createMemo(() => {
- const [projectStore] = globalSync.child(currentDirectory())
- return projectStore.provider.filter((p) => p.id !== "opencode").length > 0
- })
-
- createEffect(() => {
- console.log(providers())
+ 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),
+ ),
+ )
function navigateToProject(directory: string | undefined) {
if (!directory) return
@@ -93,7 +100,9 @@ export default function Layout(props: ParentProps) {
}
}
- async function connectProvider() {}
+ async function connectProvider() {
+ layout.dialog.open("provider")
+ }
createEffect(() => {
if (!params.dir || !params.id) return
@@ -484,7 +493,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={!hasProviders() && layout.sidebar.opened()}>
+ <Match when={!connectedProviders().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>
@@ -493,7 +502,7 @@ export default function Layout(props: ParentProps) {
</div>
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
<Button
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-[7px]"
+ class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
size="large"
icon="plus-small"
onClick={connectProvider}
@@ -506,7 +515,7 @@ export default function Layout(props: ParentProps) {
<Match when={true}>
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
<Button
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="plus-small"
@@ -520,7 +529,7 @@ export default function Layout(props: ParentProps) {
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="folder-add-left"
@@ -533,7 +542,7 @@ export default function Layout(props: ParentProps) {
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="settings-gear"
@@ -546,7 +555,7 @@ export default function Layout(props: ParentProps) {
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="bubble-5"
@@ -557,32 +566,53 @@ export default function Layout(props: ParentProps) {
</div>
</div>
<main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
- <Show when={true}>
+ <Show when={layout.dialog.opened() === "provider"}>
<SelectDialog
defaultOpen
title="Connect provider"
placeholder="Search providers"
+ activeIcon="plus-small"
key={(x) => x?.id}
- items={providers()}
+ items={providers().all}
// 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}
- onSelect={(x) =>
- // local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
- {
- return
+ 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) => }
+ onOpenChange={(open) => {
+ if (open) {
+ layout.dialog.open("provider")
+ } else {
+ layout.dialog.close("provider")
}
- }
+ }}
>
{(i) => (
- <div class="w-full flex items-center gap-x-2.5">
+ <div class="px-1.25 w-full flex items-center gap-x-4">
+ <ProviderIcon
+ 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.cost || i.cost?.input === 0}>
- <Tag>Free</Tag>
+ <Show when={i.id === "opencode"}>
+ <Tag>Recommended</Tag>
</Show>
- <Show when={i.latest}>
- <Tag>Latest</Tag>
+ <Show when={i.id === "anthropic"}>
+ <div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
</Show>
</div>
)}
diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 15a36b2ff..1c593ca87 100644
--- a/packages/enterprise/src/routes/share/[shareID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -212,7 +212,7 @@ export default function () {
<div class="text-12-mono text-text-base">v{info().version}</div>
</div>
<div class="flex gap-2 items-center">
- <ProviderIcon name={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
+ <ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
</div>
<div class="text-12-regular text-text-weaker">
diff --git a/packages/ui/src/components/provider-icon.tsx b/packages/ui/src/components/provider-icon.tsx
index 924dcd25c..d653765a5 100644
--- a/packages/ui/src/components/provider-icon.tsx
+++ b/packages/ui/src/components/provider-icon.tsx
@@ -4,11 +4,11 @@ import sprite from "./provider-icons/sprite.svg"
import type { IconName } from "./provider-icons/types"
export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
- name: IconName
+ id: IconName
}
export const ProviderIcon: Component<ProviderIconProps> = (props) => {
- const [local, rest] = splitProps(props, ["name", "class", "classList"])
+ const [local, rest] = splitProps(props, ["id", "class", "classList"])
return (
<svg
data-component="provider-icon"
@@ -18,7 +18,7 @@ export const ProviderIcon: Component<ProviderIconProps> = (props) => {
[local.class ?? ""]: !!local.class,
}}
>
- <use href={`${sprite}#${local.name}`} />
+ <use href={`${sprite}#${local.id}`} />
</svg>
)
}
diff --git a/packages/ui/src/components/select-dialog.css b/packages/ui/src/components/select-dialog.css
index cc834f795..f5687ad8e 100644
--- a/packages/ui/src/components/select-dialog.css
+++ b/packages/ui/src/components/select-dialog.css
@@ -11,7 +11,7 @@
display: flex;
height: 40px;
flex-shrink: 0;
- padding: 4px 10px 4px 6px;
+ padding: 4px 10px 4px 16px;
align-items: center;
gap: 12px;
align-self: stretch;
@@ -121,6 +121,9 @@
letter-spacing: var(--letter-spacing-normal);
[data-slot="select-dialog-item-selected-icon"] {
+ color: var(--icon-strong-base);
+ }
+ [data-slot="select-dialog-item-active-icon"] {
display: none;
color: var(--icon-strong-base);
}
@@ -128,12 +131,13 @@
&[data-active="true"] {
border-radius: var(--radius-md);
background: var(--surface-raised-base-hover);
- }
- &[data-selected="true"] {
- [data-slot="select-dialog-item-selected-icon"] {
+ [data-slot="select-dialog-item-active-icon"] {
display: block;
}
}
+ &:active {
+ background: var(--surface-raised-base-active);
+ }
}
}
}
diff --git a/packages/ui/src/components/select-dialog.tsx b/packages/ui/src/components/select-dialog.tsx
index b93993ad4..86f723225 100644
--- a/packages/ui/src/components/select-dialog.tsx
+++ b/packages/ui/src/components/select-dialog.tsx
@@ -2,7 +2,7 @@ import { createEffect, Show, For, type JSX, splitProps, createSignal } from "sol
import { createStore } from "solid-js/store"
import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
import { Dialog, DialogProps } from "./dialog"
-import { Icon } from "./icon"
+import { Icon, IconProps } from "./icon"
import { Input } from "./input"
import { IconButton } from "./icon-button"
@@ -16,6 +16,7 @@ interface SelectDialogProps<T>
onSelect?: (value: T | undefined) => void
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
actions?: JSX.Element
+ activeIcon?: IconProps["name"]
}
export function SelectDialog<T>(props: SelectDialogProps<T>) {
@@ -165,7 +166,12 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
}}
>
{others.children(item)}
- <Icon data-slot="select-dialog-item-selected-icon" name="check-small" />
+ <Show when={item === others.current}>
+ <Icon data-slot="select-dialog-item-selected-icon" name="check-small" />
+ </Show>
+ <Show when={others.activeIcon}>
+ {(icon) => <Icon data-slot="select-dialog-item-active-icon" name={icon()} />}
+ </Show>
</button>
)}
</For>