Browse Source

wip(desktop): progress

Adam 2 months ago
parent
commit
190fa4c87a

+ 63 - 48
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 { Select } from "@opencode-ai/ui/select"
 import { Tag } from "@opencode-ai/ui/tag"
 import { Tag } from "@opencode-ai/ui/tag"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { useLayout } from "@/context/layout"
 
 
 interface PromptInputProps {
 interface PromptInputProps {
   class?: string
   class?: string
@@ -56,6 +57,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const sync = useSync()
   const sync = useSync()
   const local = useLocal()
   const local = useLocal()
   const session = useSession()
   const session = useSession()
+  const layout = useLayout()
   let editorRef!: HTMLDivElement
   let editorRef!: HTMLDivElement
 
 
   const [store, setStore] = createStore<{
   const [store, setStore] = createStore<{
@@ -453,54 +455,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
               class="capitalize"
               class="capitalize"
               variant="ghost"
               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>
           </div>
           <Tooltip
           <Tooltip
             placement="top"
             placement="top"

+ 13 - 13
packages/desktop/src/context/global-sync.tsx

@@ -1,7 +1,6 @@
 import type {
 import type {
   Message,
   Message,
   Agent,
   Agent,
-  Provider,
   Session,
   Session,
   Part,
   Part,
   Config,
   Config,
@@ -12,6 +11,7 @@ import type {
   FileDiff,
   FileDiff,
   Todo,
   Todo,
   SessionStatus,
   SessionStatus,
+  ProviderListResponse,
 } from "@opencode-ai/sdk/v2"
 } from "@opencode-ai/sdk/v2"
 import { createStore, produce, reconcile } from "solid-js/store"
 import { createStore, produce, reconcile } from "solid-js/store"
 import { Binary } from "@opencode-ai/util/binary"
 import { Binary } from "@opencode-ai/util/binary"
@@ -20,9 +20,9 @@ import { useGlobalSDK } from "./global-sdk"
 
 
 type State = {
 type State = {
   ready: boolean
   ready: boolean
-  // provider: Provider[]
   agent: Agent[]
   agent: Agent[]
   project: string
   project: string
+  provider: ProviderListResponse
   config: Config
   config: Config
   path: Path
   path: Path
   session: Session[]
   session: Session[]
@@ -49,15 +49,16 @@ type State = {
 export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
 export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
   name: "GlobalSync",
   name: "GlobalSync",
   init: () => {
   init: () => {
+    const sdk = useGlobalSDK()
     const [globalStore, setGlobalStore] = createStore<{
     const [globalStore, setGlobalStore] = createStore<{
       ready: boolean
       ready: boolean
-      projects: Project[]
-      providers: Provider[]
+      project: Project[]
+      provider: ProviderListResponse
       children: Record<string, State>
       children: Record<string, State>
     }>({
     }>({
       ready: false,
       ready: false,
-      projects: [],
-      providers: [],
+      project: [],
+      provider: { all: [], connected: [], default: {} },
       children: {},
       children: {},
     })
     })
 
 
@@ -66,11 +67,11 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       if (!children[directory]) {
       if (!children[directory]) {
         setGlobalStore("children", directory, {
         setGlobalStore("children", directory, {
           project: "",
           project: "",
+          provider: { all: [], connected: [], default: {} },
           config: {},
           config: {},
           path: { state: "", config: "", worktree: "", directory: "", home: "" },
           path: { state: "", config: "", worktree: "", directory: "", home: "" },
           ready: false,
           ready: false,
           agent: [],
           agent: [],
-          // provider: [],
           session: [],
           session: [],
           session_status: {},
           session_status: {},
           session_diff: {},
           session_diff: {},
@@ -86,7 +87,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       return children[directory]
       return children[directory]
     }
     }
 
 
-    const sdk = useGlobalSDK()
     sdk.event.listen((e) => {
     sdk.event.listen((e) => {
       const directory = e.name
       const directory = e.name
       const event = e.details
       const event = e.details
@@ -94,13 +94,13 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       if (directory === "global") {
       if (directory === "global") {
         switch (event.type) {
         switch (event.type) {
           case "project.updated": {
           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) {
             if (result.found) {
-              setGlobalStore("projects", result.index, reconcile(event.properties))
+              setGlobalStore("project", result.index, reconcile(event.properties))
               return
               return
             }
             }
             setGlobalStore(
             setGlobalStore(
-              "projects",
+              "project",
               produce((draft) => {
               produce((draft) => {
                 draft.splice(result.index, 0, event.properties)
                 draft.splice(result.index, 0, event.properties)
               }),
               }),
@@ -184,14 +184,14 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
     Promise.all([
     Promise.all([
       sdk.client.project.list().then(async (x) => {
       sdk.client.project.list().then(async (x) => {
         setGlobalStore(
         setGlobalStore(
-          "projects",
+          "project",
           x
           x
             .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
             .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
             .sort((a, b) => a.id.localeCompare(b.id)),
             .sort((a, b) => a.id.localeCompare(b.id)),
         )
         )
       }),
       }),
       sdk.client.provider.list().then((x) => {
       sdk.client.provider.list().then((x) => {
-        setGlobalStore("providers", x.data ?? [])
+        setGlobalStore("provider", x.data ?? {})
       }),
       }),
     ]).then(() => setGlobalStore("ready", true))
     ]).then(() => setGlobalStore("ready", true))
 
 

+ 18 - 2
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() {
     function pickAvailableColor() {
       const available = PASTEL_COLORS.filter((c) => !colors().has(c))
       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 }) {
     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 []
       if (!metadata) return []
       return [
       return [
         {
         {
@@ -168,6 +173,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           setStore("review", "state", "tab")
           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)
+          }
+        },
+      },
     }
     }
   },
   },
 })
 })

+ 21 - 17
packages/desktop/src/context/local.tsx

@@ -39,8 +39,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
     const sync = useSync()
     const sync = useSync()
 
 
     function isModelValid(model: ModelKey) {
     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)[]) {
     function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -115,17 +115,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       })
       })
 
 
       const list = createMemo(() =>
       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)
       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
             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(() => {
       const currentModel = createMemo(() => {

+ 1 - 1
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,
       () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
     )
     )
     const model = createMemo(() =>
     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] ?? []) : []))
     const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
 
 

+ 3 - 3
packages/desktop/src/context/sync.tsx

@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
 
 
     const load = {
     const load = {
       project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
       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!)),
       path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
       agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
       agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
       session: () =>
       session: () =>
@@ -42,8 +42,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         return store.ready
         return store.ready
       },
       },
       get project() {
       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
         return undefined
       },
       },
       session: {
       session: {

+ 2 - 2
packages/desktop/src/pages/home.tsx

@@ -38,7 +38,7 @@ export default function Home() {
     <div class="mx-auto mt-55">
     <div class="mx-auto mt-55">
       <Logo class="w-xl opacity-12" />
       <Logo class="w-xl opacity-12" />
       <Switch>
       <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="mt-20 w-full flex flex-col gap-4">
             <div class="flex gap-2 items-center justify-between pl-3">
             <div class="flex gap-2 items-center justify-between pl-3">
               <div class="text-14-medium text-text-strong">Recent projects</div>
               <div class="text-14-medium text-text-strong">Recent projects</div>
@@ -50,7 +50,7 @@ export default function Home() {
             </div>
             </div>
             <ul class="flex flex-col gap-2">
             <ul class="flex flex-col gap-2">
               <For
               <For
-                each={sync.data.projects
+                each={sync.data.project
                   .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
                   .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
                   .slice(0, 5)}
                   .slice(0, 5)}
               >
               >

+ 60 - 30
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 { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Button } from "@opencode-ai/ui/button"
 import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Icon } from "@opencode-ai/ui/icon"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
@@ -31,6 +32,9 @@ import {
 import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
 import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
 import { SelectDialog } from "@opencode-ai/ui/select-dialog"
 import { SelectDialog } from "@opencode-ai/ui/select-dialog"
 import { Tag } from "@opencode-ai/ui/tag"
 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) {
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
@@ -46,15 +50,18 @@ export default function Layout(props: ParentProps) {
   const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
   const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
   const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
   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) {
   function navigateToProject(directory: string | undefined) {
     if (!directory) return
     if (!directory) return
@@ -93,7 +100,9 @@ export default function Layout(props: ParentProps) {
     }
     }
   }
   }
 
 
-  async function connectProvider() {}
+  async function connectProvider() {
+    layout.dialog.open("provider")
+  }
 
 
   createEffect(() => {
   createEffect(() => {
     if (!params.dir || !params.id) return
     if (!params.dir || !params.id) return
@@ -484,7 +493,7 @@ export default function Layout(props: ParentProps) {
           </div>
           </div>
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
             <Switch>
             <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="rounded-md bg-background-stronger shadow-xs-border-base">
                   <div class="p-3 flex flex-col gap-2">
                   <div class="p-3 flex flex-col gap-2">
                     <div class="text-12-medium text-text-strong">Getting started</div>
                     <div class="text-12-medium text-text-strong">Getting started</div>
@@ -493,7 +502,7 @@ export default function Layout(props: ParentProps) {
                   </div>
                   </div>
                   <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
                   <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
                     <Button
                     <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"
                       size="large"
                       icon="plus-small"
                       icon="plus-small"
                       onClick={connectProvider}
                       onClick={connectProvider}
@@ -506,7 +515,7 @@ export default function Layout(props: ParentProps) {
               <Match when={true}>
               <Match when={true}>
                 <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
                 <Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
                   <Button
                   <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"
                     variant="ghost"
                     size="large"
                     size="large"
                     icon="plus-small"
                     icon="plus-small"
@@ -520,7 +529,7 @@ export default function Layout(props: ParentProps) {
             <Show when={platform.openDirectoryPickerDialog}>
             <Show when={platform.openDirectoryPickerDialog}>
               <Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
               <Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
                 <Button
                 <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"
                   variant="ghost"
                   size="large"
                   size="large"
                   icon="folder-add-left"
                   icon="folder-add-left"
@@ -533,7 +542,7 @@ export default function Layout(props: ParentProps) {
             <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
             <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
               <Button
               <Button
                 disabled
                 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"
                 variant="ghost"
                 size="large"
                 size="large"
                 icon="settings-gear"
                 icon="settings-gear"
@@ -546,7 +555,7 @@ export default function Layout(props: ParentProps) {
                 as={"a"}
                 as={"a"}
                 href="https://opencode.ai/desktop-feedback"
                 href="https://opencode.ai/desktop-feedback"
                 target="_blank"
                 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"
                 variant="ghost"
                 size="large"
                 size="large"
                 icon="bubble-5"
                 icon="bubble-5"
@@ -557,32 +566,53 @@ export default function Layout(props: ParentProps) {
           </div>
           </div>
         </div>
         </div>
         <main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
         <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
           <SelectDialog
             defaultOpen
             defaultOpen
             title="Connect provider"
             title="Connect provider"
             placeholder="Search providers"
             placeholder="Search providers"
+            activeIcon="plus-small"
             key={(x) => x?.id}
             key={(x) => x?.id}
-            items={providers()}
+            items={providers().all}
             // current={local.model.current()}
             // 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) => (
             {(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>
                 <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>
-                <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>
                 </Show>
               </div>
               </div>
             )}
             )}

+ 1 - 1
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 class="text-12-mono text-text-base">v{info().version}</div>
                         </div>
                         </div>
                         <div class="flex gap-2 items-center">
                         <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 class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
                         </div>
                         </div>
                         <div class="text-12-regular text-text-weaker">
                         <div class="text-12-regular text-text-weaker">

+ 3 - 3
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"
 import type { IconName } from "./provider-icons/types"
 
 
 export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
 export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
-  name: IconName
+  id: IconName
 }
 }
 
 
 export const ProviderIcon: Component<ProviderIconProps> = (props) => {
 export const ProviderIcon: Component<ProviderIconProps> = (props) => {
-  const [local, rest] = splitProps(props, ["name", "class", "classList"])
+  const [local, rest] = splitProps(props, ["id", "class", "classList"])
   return (
   return (
     <svg
     <svg
       data-component="provider-icon"
       data-component="provider-icon"
@@ -18,7 +18,7 @@ export const ProviderIcon: Component<ProviderIconProps> = (props) => {
         [local.class ?? ""]: !!local.class,
         [local.class ?? ""]: !!local.class,
       }}
       }}
     >
     >
-      <use href={`${sprite}#${local.name}`} />
+      <use href={`${sprite}#${local.id}`} />
     </svg>
     </svg>
   )
   )
 }
 }

+ 8 - 4
packages/ui/src/components/select-dialog.css

@@ -11,7 +11,7 @@
   display: flex;
   display: flex;
   height: 40px;
   height: 40px;
   flex-shrink: 0;
   flex-shrink: 0;
-  padding: 4px 10px 4px 6px;
+  padding: 4px 10px 4px 16px;
   align-items: center;
   align-items: center;
   gap: 12px;
   gap: 12px;
   align-self: stretch;
   align-self: stretch;
@@ -121,6 +121,9 @@
         letter-spacing: var(--letter-spacing-normal);
         letter-spacing: var(--letter-spacing-normal);
 
 
         [data-slot="select-dialog-item-selected-icon"] {
         [data-slot="select-dialog-item-selected-icon"] {
+          color: var(--icon-strong-base);
+        }
+        [data-slot="select-dialog-item-active-icon"] {
           display: none;
           display: none;
           color: var(--icon-strong-base);
           color: var(--icon-strong-base);
         }
         }
@@ -128,12 +131,13 @@
         &[data-active="true"] {
         &[data-active="true"] {
           border-radius: var(--radius-md);
           border-radius: var(--radius-md);
           background: var(--surface-raised-base-hover);
           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;
             display: block;
           }
           }
         }
         }
+        &:active {
+          background: var(--surface-raised-base-active);
+        }
       }
       }
     }
     }
   }
   }

+ 8 - 2
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 { createStore } from "solid-js/store"
 import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
 import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
 import { Dialog, DialogProps } from "./dialog"
 import { Dialog, DialogProps } from "./dialog"
-import { Icon } from "./icon"
+import { Icon, IconProps } from "./icon"
 import { Input } from "./input"
 import { Input } from "./input"
 import { IconButton } from "./icon-button"
 import { IconButton } from "./icon-button"
 
 
@@ -16,6 +16,7 @@ interface SelectDialogProps<T>
   onSelect?: (value: T | undefined) => void
   onSelect?: (value: T | undefined) => void
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
   actions?: JSX.Element
   actions?: JSX.Element
+  activeIcon?: IconProps["name"]
 }
 }
 
 
 export function SelectDialog<T>(props: SelectDialogProps<T>) {
 export function SelectDialog<T>(props: SelectDialogProps<T>) {
@@ -165,7 +166,12 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
                           }}
                           }}
                         >
                         >
                           {others.children(item)}
                           {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>
                         </button>
                       )}
                       )}
                     </For>
                     </For>