Răsfoiți Sursa

Merge branch 'dev' of https://github.com/sst/opencode into dev

David Hill 2 luni în urmă
părinte
comite
eb6596cb97

+ 2 - 0
packages/console/app/src/app.tsx

@@ -3,6 +3,7 @@ import { Router } from "@solidjs/router"
 import { FileRoutes } from "@solidjs/start/router"
 import { FileRoutes } from "@solidjs/start/router"
 import { Suspense } from "solid-js"
 import { Suspense } from "solid-js"
 import { Favicon } from "@opencode-ai/ui/favicon"
 import { Favicon } from "@opencode-ai/ui/favicon"
+import { Font } from "@opencode-ai/ui/font"
 import "@ibm/plex/css/ibm-plex.css"
 import "@ibm/plex/css/ibm-plex.css"
 import "./app.css"
 import "./app.css"
 
 
@@ -15,6 +16,7 @@ export default function App() {
           <Title>opencode</Title>
           <Title>opencode</Title>
           <Meta name="description" content="OpenCode - The open source coding agent." />
           <Meta name="description" content="OpenCode - The open source coding agent." />
           <Favicon />
           <Favicon />
+          <Font />
           <Suspense>{props.children}</Suspense>
           <Suspense>{props.children}</Suspense>
         </MetaProvider>
         </MetaProvider>
       )}
       )}

+ 0 - 8
packages/console/app/src/style/token/font.css

@@ -1,11 +1,3 @@
-@font-face {
-  font-family: "Berkeley Mono";
-  src: url("/fonts/berkley-mono.woff2") format("woff2");
-  font-weight: 400;
-  font-style: normal;
-  font-display: swap;
-}
-
 body {
 body {
   --font-size-2xs: 0.6875rem;
   --font-size-2xs: 0.6875rem;
   --font-size-xs: 0.75rem;
   --font-size-xs: 0.75rem;

+ 52 - 45
packages/desktop/src/components/prompt-input.tsx

@@ -579,54 +579,61 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                           </div>
                           </div>
                           <div class="px-1.5 pb-1.5">
                           <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 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="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
                                 <div class="px-2 text-14-medium text-text-base">
                                 <div class="px-2 text-14-medium text-text-base">
                                   Add more models from popular providers
                                   Add more models from popular providers
                                 </div>
                                 </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 class="w-full">
+                                  <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) => {
+                                      if (!x) return
+                                      layout.dialog.connect(x.id)
+                                    }}
+                                  >
+                                    {(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 px-[11px] py-3.5 gap-4.5 text-14-medium"
+                                    icon="dot-grid"
+                                    onClick={() => {
+                                      layout.dialog.open("provider")
+                                    }}
+                                  >
+                                    View all providers
+                                  </Button>
+                                </div>
                               </div>
                               </div>
                             </div>
                             </div>
                           </div>
                           </div>

+ 81 - 30
packages/desktop/src/context/global-sync.tsx

@@ -1,22 +1,25 @@
-import type {
-  Message,
-  Agent,
-  Session,
-  Part,
-  Config,
-  Path,
-  File,
-  FileNode,
-  Project,
-  FileDiff,
-  Todo,
-  SessionStatus,
-  ProviderListResponse,
-} from "@opencode-ai/sdk/v2"
+import {
+  type Message,
+  type Agent,
+  type Session,
+  type Part,
+  type Config,
+  type Path,
+  type File,
+  type FileNode,
+  type Project,
+  type FileDiff,
+  type Todo,
+  type SessionStatus,
+  type ProviderListResponse,
+  type ProviderAuthResponse,
+  createOpencodeClient,
+} from "@opencode-ai/sdk/v2/client"
 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"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSDK } from "./global-sdk"
 import { useGlobalSDK } from "./global-sdk"
+import { onMount } from "solid-js"
 
 
 type State = {
 type State = {
   ready: boolean
   ready: boolean
@@ -49,19 +52,48 @@ 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 globalSDK = useGlobalSDK()
     const [globalStore, setGlobalStore] = createStore<{
     const [globalStore, setGlobalStore] = createStore<{
       ready: boolean
       ready: boolean
       project: Project[]
       project: Project[]
       provider: ProviderListResponse
       provider: ProviderListResponse
+      provider_auth: ProviderAuthResponse
       children: Record<string, State>
       children: Record<string, State>
     }>({
     }>({
       ready: false,
       ready: false,
       project: [],
       project: [],
       provider: { all: [], connected: [], default: {} },
       provider: { all: [], connected: [], default: {} },
+      provider_auth: {},
       children: {},
       children: {},
     })
     })
 
 
+    async function bootstrapInstance(directory: string) {
+      const [store, setStore] = child(directory)
+      const sdk = createOpencodeClient({
+        baseUrl: globalSDK.url,
+        directory,
+      })
+      const load = {
+        project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
+        provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
+        path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
+        agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
+        session: () =>
+          sdk.session.list().then((x) => {
+            const sessions = (x.data ?? [])
+              .slice()
+              .sort((a, b) => a.id.localeCompare(b.id))
+              .slice(0, store.limit)
+            setStore("session", sessions)
+          }),
+        status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
+        config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
+        changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
+        node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
+      }
+      await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
+    }
+
     const children: Record<string, ReturnType<typeof createStore<State>>> = {}
     const children: Record<string, ReturnType<typeof createStore<State>>> = {}
     function child(directory: string) {
     function child(directory: string) {
       if (!children[directory]) {
       if (!children[directory]) {
@@ -83,16 +115,21 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
           changes: [],
           changes: [],
         })
         })
         children[directory] = createStore(globalStore.children[directory])
         children[directory] = createStore(globalStore.children[directory])
+        bootstrapInstance(directory)
       }
       }
       return children[directory]
       return children[directory]
     }
     }
 
 
-    sdk.event.listen((e) => {
+    globalSDK.event.listen((e) => {
       const directory = e.name
       const directory = e.name
       const event = e.details
       const event = e.details
 
 
       if (directory === "global") {
       if (directory === "global") {
         switch (event.type) {
         switch (event.type) {
+          case "global.disposed": {
+            bootstrap()
+            break
+          }
           case "project.updated": {
           case "project.updated": {
             const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
             const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
             if (result.found) {
             if (result.found) {
@@ -113,6 +150,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
 
 
       const [store, setStore] = child(directory)
       const [store, setStore] = child(directory)
       switch (event.type) {
       switch (event.type) {
+        case "server.instance.disposed": {
+          bootstrapInstance(directory)
+          break
+        }
         case "session.updated": {
         case "session.updated": {
           const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
           const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
           if (result.found) {
           if (result.found) {
@@ -181,19 +222,28 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       }
       }
     })
     })
 
 
-    Promise.all([
-      sdk.client.project.list().then(async (x) => {
-        setGlobalStore(
-          "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("provider", x.data ?? {})
-      }),
-    ]).then(() => setGlobalStore("ready", true))
+    async function bootstrap() {
+      return Promise.all([
+        globalSDK.client.project.list().then(async (x) => {
+          setGlobalStore(
+            "project",
+            x
+              .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
+              .sort((a, b) => a.id.localeCompare(b.id)),
+          )
+        }),
+        globalSDK.client.provider.list().then((x) => {
+          setGlobalStore("provider", x.data ?? {})
+        }),
+        globalSDK.client.provider.auth().then((x) => {
+          setGlobalStore("provider_auth", x.data ?? {})
+        }),
+      ]).then(() => setGlobalStore("ready", true))
+    }
+
+    onMount(() => {
+      bootstrap()
+    })
 
 
     return {
     return {
       data: globalStore,
       data: globalStore,
@@ -201,6 +251,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
         return globalStore.ready
         return globalStore.ready
       },
       },
       child,
       child,
+      bootstrap,
     }
     }
   },
   },
 })
 })

+ 45 - 5
packages/desktop/src/context/layout.tsx

@@ -1,5 +1,5 @@
-import { createStore } from "solid-js/store"
-import { createMemo, onMount } from "solid-js"
+import { createStore, produce } from "solid-js/store"
+import { batch, createMemo, onMount } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { makePersisted } from "@solid-primitives/storage"
 import { makePersisted } from "@solid-primitives/storage"
 import { useGlobalSync } from "./global-sync"
 import { useGlobalSync } from "./global-sync"
@@ -19,6 +19,8 @@ const PASTEL_COLORS = [
   "#C1E1C1", // pastel mint
   "#C1E1C1", // pastel mint
 ]
 ]
 
 
+type Dialog = "provider" | "model" | "connect"
+
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
   name: "Layout",
   name: "Layout",
   init: () => {
   init: () => {
@@ -44,8 +46,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       },
       },
     )
     )
     const [ephemeral, setEphemeral] = createStore({
     const [ephemeral, setEphemeral] = createStore({
+      connect: {
+        provider: undefined as undefined | string,
+        state: undefined as undefined | "pending" | "complete" | "error",
+        error: undefined as undefined | string,
+      },
       dialog: {
       dialog: {
-        open: undefined as undefined | "provider" | "model",
+        open: undefined as undefined | Dialog,
       },
       },
     })
     })
     const usedColors = new Set<string>()
     const usedColors = new Set<string>()
@@ -169,14 +176,47 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       },
       },
       dialog: {
       dialog: {
         opened: createMemo(() => ephemeral.dialog?.open),
         opened: createMemo(() => ephemeral.dialog?.open),
-        open(dialog: "provider" | "model") {
+        open(dialog: Dialog) {
           setEphemeral("dialog", "open", dialog)
           setEphemeral("dialog", "open", dialog)
+          if (dialog !== "connect") {
+            setEphemeral("connect", {})
+          }
         },
         },
-        close(dialog: "provider" | "model") {
+        close(dialog: Dialog) {
           if (ephemeral.dialog?.open === dialog) {
           if (ephemeral.dialog?.open === dialog) {
             setEphemeral("dialog", "open", undefined)
             setEphemeral("dialog", "open", undefined)
+            setEphemeral("connect", {})
           }
           }
         },
         },
+        connect(provider: string) {
+          batch(() => {
+            setEphemeral("dialog", "open", "connect")
+            setEphemeral("connect", { provider, state: "pending" })
+          })
+        },
+      },
+      connect: {
+        provider: createMemo(() => ephemeral.connect.provider),
+        state: createMemo(() => ephemeral.connect.state),
+        complete() {
+          setEphemeral(
+            produce((state) => {
+              state.dialog.open = "model"
+              state.connect.state = "complete"
+            }),
+          )
+        },
+        error(message: string) {
+          setEphemeral(
+            produce((state) => {
+              state.connect.state = "error"
+              state.connect.error = message
+            }),
+          )
+        },
+        clear() {
+          setEphemeral("connect", {})
+        },
       },
       },
     }
     }
   },
   },

+ 16 - 8
packages/desktop/src/context/local.tsx

@@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useSDK } from "./sdk"
 import { useSDK } from "./sdk"
 import { useSync } from "./sync"
 import { useSync } from "./sync"
 import { base64Encode } from "@opencode-ai/util/encode"
 import { base64Encode } from "@opencode-ai/util/encode"
+import { useProviders } from "@/hooks/use-providers"
 
 
 export type LocalFile = FileNode &
 export type LocalFile = FileNode &
   Partial<{
   Partial<{
@@ -37,10 +38,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
   init: () => {
   init: () => {
     const sdk = useSDK()
     const sdk = useSDK()
     const sync = useSync()
     const sync = useSync()
+    const providers = useProviders()
 
 
     function isModelValid(model: ModelKey) {
     function isModelValid(model: ModelKey) {
-      const provider = sync.data.provider?.all.find((x) => x.id === model.providerID)
-      return !!provider?.models[model.modelID] && sync.data.provider?.connected.includes(model.providerID)
+      const provider = providers().all.find((x) => x.id === model.providerID)
+      return (
+        !!provider?.models[model.modelID] &&
+        providers()
+          .connected()
+          .map((p) => p.id)
+          .includes(model.providerID)
+      )
     }
     }
 
 
     function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
     function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -115,8 +123,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       })
       })
 
 
       const list = createMemo(() =>
       const list = createMemo(() =>
-        sync.data.provider.all
-          .filter((p) => sync.data.provider.connected.includes(p.id))
+        providers()
+          .connected()
           .flatMap((p) =>
           .flatMap((p) =>
             Object.values(p.models).map((m) => ({
             Object.values(p.models).map((m) => ({
               ...m,
               ...m,
@@ -145,11 +153,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           }
           }
         }
         }
 
 
-        for (const p of sync.data.provider.connected) {
-          if (p in sync.data.provider.default) {
+        for (const p of providers().connected()) {
+          if (p.id in providers().default) {
             return {
             return {
-              providerID: p,
-              modelID: sync.data.provider.default[p],
+              providerID: p.id,
+              modelID: providers().default[p.id],
             }
             }
           }
           }
         }
         }

+ 7 - 24
packages/desktop/src/context/sync.tsx

@@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     const globalSync = useGlobalSync()
     const globalSync = useGlobalSync()
     const sdk = useSDK()
     const sdk = useSDK()
     const [store, setStore] = globalSync.child(sdk.directory)
     const [store, setStore] = globalSync.child(sdk.directory)
-
-    const load = {
-      project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
-      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: () =>
-        sdk.client.session.list().then((x) => {
-          const sessions = (x.data ?? [])
-            .slice()
-            .sort((a, b) => a.id.localeCompare(b.id))
-            .slice(0, store.limit)
-          setStore("session", sessions)
-        }),
-      status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
-      config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
-      changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
-      node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
-    }
-
-    Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
-
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
 
 
     return {
     return {
@@ -78,11 +56,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         },
         },
         fetch: async (count = 10) => {
         fetch: async (count = 10) => {
           setStore("limit", (x) => x + count)
           setStore("limit", (x) => x + count)
-          await load.session()
+          await sdk.client.session.list().then((x) => {
+            const sessions = (x.data ?? [])
+              .slice()
+              .sort((a, b) => a.id.localeCompare(b.id))
+              .slice(0, store.limit)
+            setStore("session", sessions)
+          })
         },
         },
         more: createMemo(() => store.session.length >= store.limit),
         more: createMemo(() => store.session.length >= store.limit),
       },
       },
-      load,
       absolute,
       absolute,
       get directory() {
       get directory() {
         return store.path.directory
         return store.path.directory

+ 3 - 5
packages/desktop/src/hooks/use-providers.ts

@@ -16,16 +16,14 @@ export function useProviders() {
     }
     }
     return globalSync.data.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 connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
+  const paid = createMemo(() => connected().filter((p) => Object.values(p.models).find((m) => m.cost?.input)))
   const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
   const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
   return createMemo(() => ({
   return createMemo(() => ({
     all: providers().all,
     all: providers().all,
     default: providers().default,
     default: providers().default,
     popular,
     popular,
     connected,
     connected,
+    paid,
   }))
   }))
 }
 }

+ 213 - 5
packages/desktop/src/pages/layout.tsx

@@ -17,7 +17,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { getFilename } from "@opencode-ai/util/path"
 import { getFilename } from "@opencode-ai/util/path"
 import { Select } from "@opencode-ai/ui/select"
 import { Select } from "@opencode-ai/ui/select"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { Session, Project } from "@opencode-ai/sdk/v2/client"
+import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
 import { usePlatform } from "@/context/platform"
 import { usePlatform } from "@/context/platform"
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
 import {
 import {
@@ -34,6 +34,11 @@ 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"
 import { IconName } from "@opencode-ai/ui/icons/provider"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { iife } from "@opencode-ai/util/iife"
+import { List, ListRef } from "@opencode-ai/ui/list"
+import { Input } from "@opencode-ai/ui/input"
+import { useGlobalSDK } from "@/context/global-sdk"
 
 
 export default function Layout(props: ParentProps) {
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
@@ -42,6 +47,7 @@ export default function Layout(props: ParentProps) {
   })
   })
 
 
   const params = useParams()
   const params = useParams()
+  const globalSDK = useGlobalSDK()
   const globalSync = useGlobalSync()
   const globalSync = useGlobalSync()
   const layout = useLayout()
   const layout = useLayout()
   const platform = usePlatform()
   const platform = usePlatform()
@@ -434,7 +440,7 @@ export default function Layout(props: ParentProps) {
               <Button
               <Button
                 variant="ghost"
                 variant="ghost"
                 size="large"
                 size="large"
-                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
+                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
                 onClick={layout.sidebar.toggle}
                 onClick={layout.sidebar.toggle}
               >
               >
                 <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
                 <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -481,7 +487,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={!providers().connected().length && layout.sidebar.opened()}>
+              <Match when={!providers().paid().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>
@@ -562,7 +568,6 @@ export default function Layout(props: ParentProps) {
             activeIcon="plus-small"
             activeIcon="plus-small"
             key={(x) => x?.id}
             key={(x) => x?.id}
             items={providers().all}
             items={providers().all}
-            // current={local.model.current()}
             filterKeys={["id", "name"]}
             filterKeys={["id", "name"]}
             groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
             groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
             sortBy={(a, b) => {
             sortBy={(a, b) => {
@@ -575,7 +580,10 @@ export default function Layout(props: ParentProps) {
               if (b.category === "Popular" && a.category !== "Popular") return 1
               if (b.category === "Popular" && a.category !== "Popular") return 1
               return 0
               return 0
             }}
             }}
-            // onSelect={(x) => }
+            onSelect={(x) => {
+              if (!x) return
+              layout.dialog.connect(x.id)
+            }}
             onOpenChange={(open) => {
             onOpenChange={(open) => {
               if (open) {
               if (open) {
                 layout.dialog.open("provider")
                 layout.dialog.open("provider")
@@ -607,6 +615,206 @@ export default function Layout(props: ParentProps) {
             )}
             )}
           </SelectDialog>
           </SelectDialog>
         </Show>
         </Show>
+        <Show when={layout.dialog.opened() === "connect"}>
+          {iife(() => {
+            const [store, setStore] = createStore({
+              method: undefined as undefined | ProviderAuthMethod,
+            })
+            const providerID = layout.connect.provider()!
+            const provider = globalSync.data.provider.all.find((x) => x.id === providerID)!
+            const methods = globalSync.data.provider_auth[providerID] ?? [
+              {
+                type: "api",
+                label: "API key",
+              },
+            ]
+            if (methods.length === 1) {
+              setStore("method", methods[0])
+            }
+
+            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("connect")
+                  } else {
+                    layout.dialog.close("connect")
+                  }
+                }}
+              >
+                <Dialog.Header class="px-4.5">
+                  <Dialog.Title class="flex items-center">
+                    <IconButton
+                      tabIndex={-1}
+                      icon="arrow-left"
+                      variant="ghost"
+                      onClick={() => {
+                        if (store.method && methods.length > 1) {
+                          setStore("method", undefined)
+                          return
+                        }
+                        layout.dialog.open("provider")
+                      }}
+                    />
+                  </Dialog.Title>
+                  <Dialog.CloseButton tabIndex={-1} />
+                </Dialog.Header>
+                <Dialog.Body>
+                  <div class="flex flex-col gap-6 px-2.5 pb-3">
+                    <div class="px-2.5 flex gap-4 items-center">
+                      <ProviderIcon id={providerID as IconName} class="size-5 shrink-0 icon-strong-base" />
+                      <div class="text-16-medium text-text-strong">Connect {provider.name}</div>
+                    </div>
+                    <Show when={store.method === undefined}>
+                      <div class="px-2.5 text-14-regular text-text-base">Select login method for {provider.name}.</div>
+                      <div class="">
+                        <Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
+                        <List
+                          ref={(ref) => (listRef = ref)}
+                          items={methods}
+                          key={(m) => m?.label}
+                          onSelect={(method) => {
+                            if (!method) return
+                            setStore("method", method)
+
+                            if (method.type === "oauth") {
+                              // const result = await sdk.client.provider.oauth.authorize({
+                              //   providerID: provider.id,
+                              //   method: index,
+                              // })
+                              // if (result.data?.method === "code") {
+                              //   dialog.replace(() => (
+                              //     <CodeMethod
+                              //       providerID={provider.id}
+                              //       title={method.label}
+                              //       index={index}
+                              //       authorization={result.data!}
+                              //     />
+                              //   ))
+                              // }
+                              // if (result.data?.method === "auto") {
+                              //   dialog.replace(() => (
+                              //     <AutoMethod
+                              //       providerID={provider.id}
+                              //       title={method.label}
+                              //       index={index}
+                              //       authorization={result.data!}
+                              //     />
+                              //   ))
+                              // }
+                            }
+                            if (method.type === "api") {
+                              // return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
+                            }
+                          }}
+                        >
+                          {(i) => (
+                            <div class="w-full flex items-center gap-x-2.5">
+                              {/* TODO: add checkmark thing */}
+                              <span>{i.label}</span>
+                            </div>
+                          )}
+                        </List>
+                      </div>
+                    </Show>
+                    <Show when={store.method?.type === "api"}>
+                      {iife(() => {
+                        const [formStore, setFormStore] = createStore({
+                          value: "",
+                          error: undefined as string | undefined,
+                        })
+
+                        async function handleSubmit(e: SubmitEvent) {
+                          e.preventDefault()
+
+                          const form = e.currentTarget as HTMLFormElement
+                          const formData = new FormData(form)
+                          const apiKey = formData.get("apiKey") as string
+
+                          if (!apiKey?.trim()) {
+                            setFormStore("error", "API key is required")
+                            return
+                          }
+
+                          setFormStore("error", undefined)
+                          await globalSDK.client.auth.set({
+                            providerID,
+                            auth: {
+                              type: "api",
+                              key: apiKey,
+                            },
+                          })
+                          await globalSDK.client.global.dispose()
+                          layout.connect.complete()
+                        }
+
+                        return (
+                          <div class="px-2.5 pb-10 flex flex-col gap-6">
+                            <Switch>
+                              <Match when={provider.id === "opencode"}>
+                                <div class="flex flex-col gap-4">
+                                  <div class="text-14-regular text-text-base">
+                                    OpenCode Zen gives you access to a curated set of reliable optimized models for
+                                    coding agents.
+                                  </div>
+                                  <div class="text-14-regular text-text-base">
+                                    With a single API key you’ll get access to models such as Claude, GPT, Gemini, GLM
+                                    and more.
+                                  </div>
+                                  <div class="text-14-regular text-text-base">
+                                    Visit{" "}
+                                    <button
+                                      tabIndex={-1}
+                                      class="text-text-strong underline"
+                                      onClick={() => platform.openLink("https://opencode.ai/zen")}
+                                    >
+                                      opencode.ai/zen
+                                    </button>{" "}
+                                    to collect your API key.
+                                  </div>
+                                </div>
+                              </Match>
+                              <Match when={true}>
+                                <div class="text-14-regular text-text-base">
+                                  Enter your {provider.name} API key to connect your account and use {provider.name}{" "}
+                                  models in OpenCode.
+                                </div>
+                              </Match>
+                            </Switch>
+                            <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+                              <Input
+                                autofocus
+                                type="text"
+                                label={`${provider.name} API key`}
+                                placeholder="API key"
+                                name="apiKey"
+                                value={formStore.value}
+                                onChange={setFormStore.bind(null, "value")}
+                                validationState={formStore.error ? "invalid" : undefined}
+                                error={formStore.error}
+                              />
+                              <Button class="w-auto" type="submit" size="large" variant="primary">
+                                Submit
+                              </Button>
+                            </form>
+                          </div>
+                        )
+                      })}
+                    </Show>
+                  </div>
+                </Dialog.Body>
+              </Dialog>
+            )
+          })}
+        </Show>
       </div>
       </div>
     </div>
     </div>
   )
   )

+ 0 - 0
packages/console/app/public/fonts/berkley-mono.woff2 → packages/ui/src/assets/fonts/tx-02.woff2


+ 17 - 15
packages/ui/src/components/button.css

@@ -11,27 +11,29 @@
   outline: none;
   outline: none;
 
 
   &[data-variant="primary"] {
   &[data-variant="primary"] {
-    border-color: var(--border-base);
-    background-color: var(--surface-brand-base);
-    color: var(--text-on-brand-strong);
+    background-color: var(--icon-strong-base);
+    border-color: var(--border-weak-base);
+    color: var(--icon-invert-base);
+
+    [data-slot="icon-svg"] {
+      color: var(--icon-invert-base);
+    }
 
 
     &:hover:not(:disabled) {
     &:hover:not(:disabled) {
-      border-color: var(--border-hover);
-      background-color: var(--surface-brand-hover);
+      background-color: var(--icon-strong-hover);
     }
     }
     &:focus:not(:disabled) {
     &:focus:not(:disabled) {
-      border-color: var(--border-focus);
-      background-color: var(--surface-brand-focus);
+      background-color: var(--icon-strong-focus);
     }
     }
     &:active:not(:disabled) {
     &:active:not(:disabled) {
-      border-color: var(--border-active);
-      background-color: var(--surface-brand-active);
+      background-color: var(--icon-strong-active);
     }
     }
     &:disabled {
     &:disabled {
-      border-color: var(--border-disabled);
-      background-color: var(--surface-disabled);
-      color: var(--text-weak);
-      cursor: not-allowed;
+      background-color: var(--icon-strong-disabled);
+
+      [data-slot="icon-svg"] {
+        color: var(--icon-invert-base);
+      }
     }
     }
   }
   }
 
 
@@ -120,13 +122,13 @@
 
 
   &[data-size="large"] {
   &[data-size="large"] {
     height: 32px;
     height: 32px;
-    padding: 0 8px;
+    padding: 6px 12px;
 
 
     &[data-icon] {
     &[data-icon] {
       padding: 0 12px 0 8px;
       padding: 0 12px 0 8px;
     }
     }
 
 
-    gap: 8px;
+    gap: 4px;
 
 
     /* text-14-medium */
     /* text-14-medium */
     font-family: var(--font-family-sans);
     font-family: var(--font-family-sans);

+ 6 - 1
packages/ui/src/components/dialog.tsx

@@ -5,7 +5,7 @@ import {
   DialogCloseButtonProps,
   DialogCloseButtonProps,
   DialogDescriptionProps,
   DialogDescriptionProps,
 } from "@kobalte/core/dialog"
 } from "@kobalte/core/dialog"
-import { ComponentProps, type JSX, onCleanup, Show, splitProps } from "solid-js"
+import { ComponentProps, type JSX, onCleanup, onMount, Show, splitProps } from "solid-js"
 import { IconButton } from "./icon-button"
 import { IconButton } from "./icon-button"
 
 
 export interface DialogProps extends DialogRootProps {
 export interface DialogProps extends DialogRootProps {
@@ -35,6 +35,11 @@ export function DialogRoot(props: DialogProps) {
     })
     })
   }
   }
 
 
+  onMount(() => {
+    // @ts-ignore
+    document?.activeElement?.blur?.()
+  })
+
   return (
   return (
     <Kobalte {...others}>
     <Kobalte {...others}>
       <Show when={props.trigger}>
       <Show when={props.trigger}>

+ 1 - 0
packages/ui/src/components/icon.tsx

@@ -3,6 +3,7 @@ import { splitProps, type ComponentProps } from "solid-js"
 const icons = {
 const icons = {
   "align-right": `<path d="M12.292 6.04167L16.2503 9.99998L12.292 13.9583M2.91699 9.99998H15.6253M17.0837 3.75V16.25" stroke="currentColor" stroke-linecap="square"/>`,
   "align-right": `<path d="M12.292 6.04167L16.2503 9.99998L12.292 13.9583M2.91699 9.99998H15.6253M17.0837 3.75V16.25" stroke="currentColor" stroke-linecap="square"/>`,
   "arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
   "arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
+  "arrow-left": `<path d="M8.33464 4.58398L2.91797 10.0007L8.33464 15.4173M3.33464 10.0007H17.0846" stroke="currentColor" stroke-linecap="square"/>`,
   "bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
   "bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
   "bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
   "bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
   "check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
   "check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,

+ 75 - 1
packages/ui/src/components/input.css

@@ -1,6 +1,5 @@
 [data-component="input"] {
 [data-component="input"] {
   width: 100%;
   width: 100%;
-  /* [data-slot="input-label"] {} */
 
 
   [data-slot="input-input"] {
   [data-slot="input-input"] {
     width: 100%;
     width: 100%;
@@ -22,4 +21,79 @@
       color: var(--text-weak);
       color: var(--text-weak);
     }
     }
   }
   }
+
+  &[data-variant="normal"] {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+
+    [data-slot="input-label"] {
+      color: var(--text-weak);
+
+      /* text-12-medium */
+      font-family: var(--font-family-sans);
+      font-size: var(--font-size-small);
+      font-style: normal;
+      font-weight: var(--font-weight-medium);
+      line-height: 18px; /* 150% */
+      letter-spacing: var(--letter-spacing-normal);
+    }
+
+    [data-slot="input-input"] {
+      color: var(--text-strong);
+
+      display: flex;
+      height: 32px;
+      padding: 2px 12px;
+      align-items: center;
+      gap: 8px;
+      align-self: stretch;
+
+      border-radius: var(--radius-md);
+      border: 1px solid var(--border-weak-base);
+      background: var(--input-base);
+
+      /* text-14-regular */
+      font-family: var(--font-family-sans);
+      font-size: 14px;
+      font-style: normal;
+      font-weight: var(--font-weight-regular);
+      line-height: var(--line-height-large); /* 142.857% */
+      letter-spacing: var(--letter-spacing-normal);
+
+      &:focus {
+        outline: none;
+
+        /* border/shadow-xs/select */
+        box-shadow:
+          0 0 0 3px var(--border-weak-selected),
+          0 0 0 1px var(--border-selected),
+          0 1px 2px -1px rgba(19, 16, 16, 0.25),
+          0 1px 2px 0 rgba(19, 16, 16, 0.08),
+          0 1px 3px 0 rgba(19, 16, 16, 0.12);
+      }
+
+      &[data-invalid] {
+        background: var(--surface-critical-weak);
+        border: 1px solid var(--border-critical-selected);
+      }
+
+      &::placeholder {
+        color: var(--text-weak);
+      }
+    }
+
+    [data-slot="input-error"] {
+      color: var(--text-on-critical-base);
+
+      /* text-12-medium */
+      font-family: var(--font-family-sans);
+      font-size: var(--font-size-small);
+      font-style: normal;
+      font-weight: var(--font-weight-medium);
+      line-height: 18px; /* 150% */
+      letter-spacing: var(--letter-spacing-normal);
+    }
+  }
 }
 }

+ 36 - 6
packages/ui/src/components/input.tsx

@@ -4,31 +4,61 @@ import type { ComponentProps } from "solid-js"
 
 
 export interface InputProps
 export interface InputProps
   extends ComponentProps<typeof Kobalte.Input>,
   extends ComponentProps<typeof Kobalte.Input>,
-    Partial<Pick<ComponentProps<typeof Kobalte>, "value" | "onChange" | "onKeyDown">> {
+    Partial<
+      Pick<
+        ComponentProps<typeof Kobalte>,
+        | "name"
+        | "defaultValue"
+        | "value"
+        | "onChange"
+        | "onKeyDown"
+        | "validationState"
+        | "required"
+        | "disabled"
+        | "readOnly"
+      >
+    > {
   label?: string
   label?: string
   hideLabel?: boolean
   hideLabel?: boolean
   hidden?: boolean
   hidden?: boolean
   description?: string
   description?: string
+  error?: string
+  variant?: "normal" | "ghost"
 }
 }
 
 
 export function Input(props: InputProps) {
 export function Input(props: InputProps) {
   const [local, others] = splitProps(props, [
   const [local, others] = splitProps(props, [
+    "name",
+    "defaultValue",
+    "value",
+    "onChange",
+    "onKeyDown",
+    "validationState",
+    "required",
+    "disabled",
+    "readOnly",
     "class",
     "class",
     "label",
     "label",
     "hidden",
     "hidden",
     "hideLabel",
     "hideLabel",
     "description",
     "description",
-    "value",
-    "onChange",
-    "onKeyDown",
+    "error",
+    "variant",
   ])
   ])
   return (
   return (
     <Kobalte
     <Kobalte
       data-component="input"
       data-component="input"
-      style={{ height: local.hidden ? 0 : undefined }}
+      data-variant={local.variant || "normal"}
+      name={local.name}
+      defaultValue={local.defaultValue}
       value={local.value}
       value={local.value}
       onChange={local.onChange}
       onChange={local.onChange}
       onKeyDown={local.onKeyDown}
       onKeyDown={local.onKeyDown}
+      required={local.required}
+      disabled={local.disabled}
+      readOnly={local.readOnly}
+      style={{ height: local.hidden ? 0 : undefined }}
+      validationState={local.validationState}
     >
     >
       <Show when={local.label}>
       <Show when={local.label}>
         <Kobalte.Label data-slot="input-label" classList={{ "sr-only": local.hideLabel }}>
         <Kobalte.Label data-slot="input-label" classList={{ "sr-only": local.hideLabel }}>
@@ -39,7 +69,7 @@ export function Input(props: InputProps) {
       <Show when={local.description}>
       <Show when={local.description}>
         <Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
         <Kobalte.Description data-slot="input-description">{local.description}</Kobalte.Description>
       </Show>
       </Show>
-      <Kobalte.ErrorMessage data-slot="input-error" />
+      <Kobalte.ErrorMessage data-slot="input-error">{local.error}</Kobalte.ErrorMessage>
     </Kobalte>
     </Kobalte>
   )
   )
 }
 }

+ 6 - 5
packages/ui/src/components/list.tsx

@@ -65,8 +65,8 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
     element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
     element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
   })
   })
 
 
-  const handleSelect = (item: T | undefined) => {
-    props.onSelect?.(item)
+  const handleSelect = (item: T | undefined, index: number) => {
+    props.onSelect?.(item, index)
   }
   }
 
 
   const handleKey = (e: KeyboardEvent) => {
   const handleKey = (e: KeyboardEvent) => {
@@ -75,11 +75,12 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
 
 
     const all = flat()
     const all = flat()
     const selected = all.find((x) => props.key(x) === active())
     const selected = all.find((x) => props.key(x) === active())
+    const index = selected ? all.indexOf(selected) : -1
     props.onKeyEvent?.(e, selected)
     props.onKeyEvent?.(e, selected)
 
 
     if (e.key === "Enter") {
     if (e.key === "Enter") {
       e.preventDefault()
       e.preventDefault()
-      if (selected) handleSelect(selected)
+      if (selected) handleSelect(selected, index)
     } else {
     } else {
       onKeyDown(e)
       onKeyDown(e)
     }
     }
@@ -110,13 +111,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
               </Show>
               </Show>
               <div data-slot="list-items">
               <div data-slot="list-items">
                 <For each={group.items}>
                 <For each={group.items}>
-                  {(item) => (
+                  {(item, i) => (
                     <button
                     <button
                       data-slot="list-item"
                       data-slot="list-item"
                       data-key={props.key(item)}
                       data-key={props.key(item)}
                       data-active={props.key(item) === active()}
                       data-active={props.key(item) === active()}
                       data-selected={item === props.current}
                       data-selected={item === props.current}
-                      onClick={() => handleSelect(item)}
+                      onClick={() => handleSelect(item, i())}
                       onMouseMove={() => {
                       onMouseMove={() => {
                         setStore("mouseActive", true)
                         setStore("mouseActive", true)
                         setActive(props.key(item))
                         setActive(props.key(item))

+ 4 - 3
packages/ui/src/components/select-dialog.tsx

@@ -1,9 +1,9 @@
 import { createEffect, Show, type JSX, splitProps, createSignal } from "solid-js"
 import { createEffect, Show, type JSX, splitProps, createSignal } from "solid-js"
 import { Dialog, DialogProps } from "./dialog"
 import { Dialog, DialogProps } from "./dialog"
 import { Icon } from "./icon"
 import { Icon } from "./icon"
-import { Input } from "./input"
 import { IconButton } from "./icon-button"
 import { IconButton } from "./icon-button"
 import { List, ListRef, ListProps } from "./list"
 import { List, ListRef, ListProps } from "./list"
+import { Input } from "./input"
 
 
 interface SelectDialogProps<T>
 interface SelectDialogProps<T>
   extends Omit<ListProps<T>, "filter">,
   extends Omit<ListProps<T>, "filter">,
@@ -29,8 +29,8 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
     })
     })
   })
   })
 
 
-  const handleSelect = (item: T | undefined) => {
-    others.onSelect?.(item)
+  const handleSelect = (item: T | undefined, index: number) => {
+    others.onSelect?.(item, index)
     closeButton.click()
     closeButton.click()
   }
   }
 
 
@@ -58,6 +58,7 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
             <Input
             <Input
               ref={inputRef}
               ref={inputRef}
               autofocus
               autofocus
+              variant="ghost"
               data-slot="select-dialog-input"
               data-slot="select-dialog-input"
               type="text"
               type="text"
               value={filter()}
               value={filter()}

+ 4 - 3
packages/ui/src/hooks/use-filtered-list.tsx

@@ -12,7 +12,7 @@ export interface FilteredListProps<T> {
   groupBy?: (x: T) => string
   groupBy?: (x: T) => string
   sortBy?: (a: T, b: T) => number
   sortBy?: (a: T, b: T) => number
   sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
   sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
-  onSelect?: (value: T | undefined) => void
+  onSelect?: (value: T | undefined, index: number) => void
 }
 }
 
 
 export function useFilteredList<T>(props: FilteredListProps<T>) {
 export function useFilteredList<T>(props: FilteredListProps<T>) {
@@ -63,8 +63,9 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
   const onKeyDown = (event: KeyboardEvent) => {
   const onKeyDown = (event: KeyboardEvent) => {
     if (event.key === "Enter") {
     if (event.key === "Enter") {
       event.preventDefault()
       event.preventDefault()
-      const selected = flat().find((x) => props.key(x) === list.active())
-      if (selected) props.onSelect?.(selected)
+      const selectedIndex = flat().findIndex((x) => props.key(x) === list.active())
+      const selected = flat()[selectedIndex]
+      if (selected) props.onSelect?.(selected, selectedIndex)
     } else {
     } else {
       list.onKeyDown(event)
       list.onKeyDown(event)
     }
     }