Adam 2 месяцев назад
Родитель
Сommit
0ca758e135

+ 81 - 56
packages/desktop/src/components/prompt-input.tsx

@@ -1,5 +1,17 @@
 import { useFilteredList } from "@opencode-ai/ui/hooks"
-import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js"
+import {
+  createEffect,
+  on,
+  Component,
+  Show,
+  For,
+  onMount,
+  onCleanup,
+  Switch,
+  Match,
+  createSignal,
+  createMemo,
+} from "solid-js"
 import { createStore } from "solid-js/store"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
@@ -470,60 +482,73 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             </Button>
             <Show when={layout.dialog.opened() === "model"}>
               <Switch>
-                <Match when={providers().connected().length > 0}>
-                  <SelectDialog
-                    defaultOpen
-                    onOpenChange={(open) => {
-                      if (open) {
-                        layout.dialog.open("model")
-                      } else {
-                        layout.dialog.close("model")
-                      }
-                    }}
-                    title="Select model"
-                    placeholder="Search models"
-                    emptyMessage="No model results"
-                    key={(x) => `${x.provider.id}:${x.id}`}
-                    items={local.model.list()}
-                    current={local.model.current()}
-                    filterKeys={["provider.name", "name", "id"]}
-                    // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
-                    groupBy={(x) => x.provider.name}
-                    sortGroupsBy={(a, b) => {
-                      if (a.category === "Recent" && b.category !== "Recent") return -1
-                      if (b.category === "Recent" && a.category !== "Recent") return 1
-                      const aProvider = a.items[0].provider.id
-                      const bProvider = b.items[0].provider.id
-                      if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
-                      if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
-                      return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
-                    }}
-                    onSelect={(x) =>
-                      local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
-                    }
-                    actions={
-                      <Button
-                        class="h-7 -my-1 text-14-medium"
-                        icon="plus-small"
-                        tabIndex={-1}
-                        onClick={() => layout.dialog.open("provider")}
+                <Match when={providers.paid().length > 0}>
+                  {iife(() => {
+                    const models = createMemo(() =>
+                      local.model
+                        .list()
+                        .filter((m) =>
+                          layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true,
+                        ),
+                    )
+                    return (
+                      <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={models}
+                        current={local.model.current()}
+                        filterKeys={["provider.name", "name", "id"]}
+                        // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
+                        groupBy={(x) => x.provider.name}
+                        sortGroupsBy={(a, b) => {
+                          if (a.category === "Recent" && b.category !== "Recent") return -1
+                          if (b.category === "Recent" && a.category !== "Recent") return 1
+                          const aProvider = a.items[0].provider.id
+                          const bProvider = b.items[0].provider.id
+                          if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
+                          if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
+                          return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
+                        }}
+                        onSelect={(x) =>
+                          local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
+                            recent: true,
+                          })
+                        }
+                        actions={
+                          <Button
+                            class="h-7 -my-1 text-14-medium"
+                            icon="plus-small"
+                            tabIndex={-1}
+                            onClick={() => layout.dialog.open("provider")}
+                          >
+                            Connect provider
+                          </Button>
+                        }
                       >
-                        Connect provider
-                      </Button>
-                    }
-                  >
-                    {(i) => (
-                      <div class="w-full flex items-center gap-x-2.5">
-                        <span>{i.name}</span>
-                        <Show when={!i.cost || i.cost?.input === 0}>
-                          <Tag>Free</Tag>
-                        </Show>
-                        <Show when={i.latest}>
-                          <Tag>Latest</Tag>
-                        </Show>
-                      </div>
-                    )}
-                  </SelectDialog>
+                        {(i) => (
+                          <div class="w-full flex items-center gap-x-2.5">
+                            <span>{i.name}</span>
+                            <Show when={!i.cost || i.cost?.input === 0}>
+                              <Tag>Free</Tag>
+                            </Show>
+                            <Show when={i.latest}>
+                              <Tag>Latest</Tag>
+                            </Show>
+                          </div>
+                        )}
+                      </SelectDialog>
+                    )
+                  })}
                 </Match>
                 <Match when={true}>
                   {iife(() => {
@@ -554,7 +579,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                             <div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
                             <List
                               ref={(ref) => (listRef = ref)}
-                              items={local.model.list()}
+                              items={local.model.list}
                               current={local.model.current()}
                               key={(x) => `${x.provider.id}:${x.id}`}
                               onSelect={(x) => {
@@ -587,7 +612,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                                   <List
                                     class="w-full"
                                     key={(x) => x?.id}
-                                    items={providers().popular()}
+                                    items={providers.popular}
                                     activeIcon="plus-small"
                                     sortBy={(a, b) => {
                                       if (popularProviders.includes(a.id) && popularProviders.includes(b.id))

+ 29 - 18
packages/desktop/src/context/layout.tsx

@@ -45,15 +45,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         name: "default-layout.v7",
       },
     )
-    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,
-      },
+        provider?: string
+        state?: "pending" | "complete" | "error"
+        error?: string
+      }
       dialog: {
-        open: undefined as undefined | Dialog,
-      },
+        open?: Dialog
+      }
+    }>({
+      connect: {},
+      dialog: {},
     })
     const usedColors = new Set<string>()
 
@@ -177,22 +180,30 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       dialog: {
         opened: createMemo(() => ephemeral.dialog?.open),
         open(dialog: Dialog) {
-          setEphemeral("dialog", "open", dialog)
-          if (dialog !== "connect") {
-            setEphemeral("connect", {})
-          }
+          batch(() => {
+            // if (dialog !== "connect") {
+            //   setEphemeral("connect", {})
+            // }
+            setEphemeral("dialog", "open", dialog)
+          })
         },
         close(dialog: Dialog) {
-          if (ephemeral.dialog?.open === dialog) {
-            setEphemeral("dialog", "open", undefined)
-            setEphemeral("connect", {})
+          if (ephemeral.dialog.open === dialog) {
+            setEphemeral(
+              produce((state) => {
+                state.dialog.open = undefined
+                state.connect = {}
+              }),
+            )
           }
         },
         connect(provider: string) {
-          batch(() => {
-            setEphemeral("dialog", "open", "connect")
-            setEphemeral("connect", { provider, state: "pending" })
-          })
+          setEphemeral(
+            produce((state) => {
+              state.dialog.open = "connect"
+              state.connect = { provider, state: "pending" }
+            }),
+          )
         },
       },
       connect: {

+ 13 - 15
packages/desktop/src/context/local.tsx

@@ -41,10 +41,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
     const providers = useProviders()
 
     function isModelValid(model: ModelKey) {
-      const provider = providers().all.find((x) => x.id === model.providerID)
+      const provider = providers.all().find((x) => x.id === model.providerID)
       return (
         !!provider?.models[model.modelID] &&
-        providers()
+        providers
           .connected()
           .map((p) => p.id)
           .includes(model.providerID)
@@ -123,16 +123,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       })
 
       const list = createMemo(() =>
-        providers()
-          .connected()
-          .flatMap((p) =>
-            Object.values(p.models).map((m) => ({
-              ...m,
-              name: m.name.replace("(latest)", "").trim(),
-              provider: p,
-              latest: m.name.includes("(latest)"),
-            })),
-          ),
+        providers.connected().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)
 
@@ -153,11 +151,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           }
         }
 
-        for (const p of providers().connected()) {
-          if (p.id in providers().default) {
+        for (const p of providers.connected()) {
+          if (p.id in providers.default()) {
             return {
               providerID: p.id,
-              modelID: providers().default[p.id],
+              modelID: providers.default()[p.id],
             }
           }
         }

+ 4 - 4
packages/desktop/src/hooks/use-providers.ts

@@ -19,11 +19,11 @@ export function useProviders() {
   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)))
-  return createMemo(() => ({
-    all: providers().all,
-    default: providers().default,
+  return {
+    all: createMemo(() => providers().all),
+    default: createMemo(() => providers().default),
     popular,
     connected,
     paid,
-  }))
+  }
 }

+ 150 - 141
packages/desktop/src/pages/layout.tsx

@@ -487,7 +487,7 @@ export default function Layout(props: ParentProps) {
           </div>
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
             <Switch>
-              <Match when={!providers().paid().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="p-3 flex flex-col gap-2">
                     <div class="text-12-medium text-text-strong">Getting started</div>
@@ -567,7 +567,7 @@ export default function Layout(props: ParentProps) {
             placeholder="Search providers"
             activeIcon="plus-small"
             key={(x) => x?.id}
-            items={providers().all}
+            items={providers.all}
             filterKeys={["id", "name"]}
             groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
             sortBy={(a, b) => {
@@ -620,16 +620,19 @@ export default function Layout(props: ParentProps) {
             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])
+            const providerID = createMemo(() => layout.connect.provider()!)
+            const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!)
+            const methods = createMemo(
+              () =>
+                globalSync.data.provider_auth[providerID()] ?? [
+                  {
+                    type: "api",
+                    label: "API key",
+                  },
+                ],
+            )
+            if (methods().length === 1) {
+              setStore("method", methods()[0])
             }
 
             let listRef: ListRef | undefined
@@ -670,145 +673,151 @@ export default function Layout(props: ParentProps) {
                 <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>
+                      <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)
+                    <Switch>
+                      <Match 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,
-                        })
+                              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>
+                      </Match>
+                      <Match when={store.method?.type === "api"}>
+                        {iife(() => {
+                          const [formStore, setFormStore] = createStore({
+                            value: "",
+                            error: undefined as string | undefined,
+                          })
 
-                        async function handleSubmit(e: SubmitEvent) {
-                          e.preventDefault()
+                          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
+                            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
-                          }
+                            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()
-                        }
+                            setFormStore("error", undefined)
+                            await globalSDK.client.auth.set({
+                              providerID: providerID(),
+                              auth: {
+                                type: "api",
+                                key: apiKey,
+                              },
+                            })
+                            await globalSDK.client.global.dispose()
+                            setTimeout(() => {
+                              layout.connect.complete()
+                            }, 500)
+                          }
 
-                        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.
+                          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">
-                                    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.
+                                    Enter your {provider.name} API key to connect your account and use {provider.name}{" "}
+                                    models in OpenCode.
                                   </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>
+                                </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>
+                          )
+                        })}
+                      </Match>
+                    </Switch>
                   </div>
                 </Dialog.Body>
               </Dialog>

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

@@ -5,7 +5,7 @@ import { createStore } from "solid-js/store"
 import { createList } from "solid-list"
 
 export interface FilteredListProps<T> {
-  items: T[] | ((filter: string) => Promise<T[]>)
+  items: (filter: string) => T[] | Promise<T[]>
   key: (item: T) => string
   filterKeys?: string[]
   current?: T
@@ -22,7 +22,7 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
     () => store.filter,
     async (filter) => {
       const needle = filter?.toLowerCase()
-      const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
+      const all = (await props.items(needle)) || []
       const result = pipe(
         all,
         (x) => {