Browse Source

wip(desktop): progress

Adam 2 months ago
parent
commit
91d743ef9a

+ 5 - 0
packages/desktop/src/components/prompt-input.tsx

@@ -483,6 +483,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   <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">

+ 21 - 52
packages/desktop/src/context/global-sync.tsx

@@ -18,41 +18,9 @@ import { Binary } from "@opencode-ai/util/binary"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSDK } from "./global-sdk"
 
-const PASTEL_COLORS = [
-  "#FCEAFD", // pastel pink
-  "#FFDFBA", // pastel peach
-  "#FFFFBA", // pastel yellow
-  "#BAFFC9", // pastel green
-  "#EAF6FD", // pastel blue
-  "#EFEAFD", // pastel lavender
-  "#FEC8D8", // pastel rose
-  "#D4F0F0", // pastel cyan
-  "#FDF0EA", // pastel coral
-  "#C1E1C1", // pastel mint
-]
-
-function pickAvailableColor(usedColors: Set<string>) {
-  const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
-  if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
-  return available[Math.floor(Math.random() * available.length)]
-}
-
-async function ensureProjectColor(
-  project: Project,
-  sdk: ReturnType<typeof useGlobalSDK>,
-  usedColors: Set<string>,
-): Promise<Project> {
-  if (project.icon?.color) return project
-  const color = pickAvailableColor(usedColors)
-  usedColors.add(color)
-  const updated = { ...project, icon: { ...project.icon, color } }
-  sdk.client.project.update({ projectID: project.id, icon: { color } })
-  return updated
-}
-
 type State = {
   ready: boolean
-  provider: Provider[]
+  // provider: Provider[]
   agent: Agent[]
   project: string
   config: Config
@@ -84,10 +52,12 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
     const [globalStore, setGlobalStore] = createStore<{
       ready: boolean
       projects: Project[]
+      providers: Provider[]
       children: Record<string, State>
     }>({
       ready: false,
       projects: [],
+      providers: [],
       children: {},
     })
 
@@ -100,7 +70,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
           path: { state: "", config: "", worktree: "", directory: "", home: "" },
           ready: false,
           agent: [],
-          provider: [],
+          // provider: [],
           session: [],
           session_status: {},
           session_diff: {},
@@ -124,20 +94,17 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       if (directory === "global") {
         switch (event.type) {
           case "project.updated": {
-            const usedColors = new Set(globalStore.projects.map((p) => p.icon?.color).filter(Boolean) as string[])
-            ensureProjectColor(event.properties, sdk, usedColors).then((project) => {
-              const result = Binary.search(globalStore.projects, project.id, (s) => s.id)
-              if (result.found) {
-                setGlobalStore("projects", result.index, reconcile(project))
-                return
-              }
-              setGlobalStore(
-                "projects",
-                produce((draft) => {
-                  draft.splice(result.index, 0, project)
-                }),
-              )
-            })
+            const result = Binary.search(globalStore.projects, event.properties.id, (s) => s.id)
+            if (result.found) {
+              setGlobalStore("projects", result.index, reconcile(event.properties))
+              return
+            }
+            setGlobalStore(
+              "projects",
+              produce((draft) => {
+                draft.splice(result.index, 0, event.properties)
+              }),
+            )
             break
           }
         }
@@ -216,14 +183,16 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
 
     Promise.all([
       sdk.client.project.list().then(async (x) => {
-        const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
-        const usedColors = new Set(filtered.map((p) => p.icon?.color).filter(Boolean) as string[])
-        const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk, usedColors)))
         setGlobalStore(
           "projects",
-          projects.sort((a, b) => a.id.localeCompare(b.id)),
+          x
+            .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
+            .sort((a, b) => a.id.localeCompare(b.id)),
         )
       }),
+      sdk.client.provider.list().then((x) => {
+        setGlobalStore("providers", x.data ?? [])
+      }),
     ]).then(() => setGlobalStore("ready", true))
 
     return {

+ 53 - 14
packages/desktop/src/context/layout.tsx

@@ -4,6 +4,20 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
 import { makePersisted } from "@solid-primitives/storage"
 import { useGlobalSync } from "./global-sync"
 import { useGlobalSDK } from "./global-sdk"
+import { Project } from "@opencode-ai/sdk/v2"
+
+const PASTEL_COLORS = [
+  "#FCEAFD", // pastel pink
+  "#FFDFBA", // pastel peach
+  "#FFFFBA", // pastel yellow
+  "#BAFFC9", // pastel green
+  "#EAF6FD", // pastel blue
+  "#EFEAFD", // pastel lavender
+  "#FEC8D8", // pastel rose
+  "#D4F0F0", // pastel cyan
+  "#FDF0EA", // pastel coral
+  "#C1E1C1", // pastel mint
+]
 
 export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
   name: "Layout",
@@ -30,6 +44,42 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
       },
     )
 
+    function pickAvailableColor() {
+      const available = PASTEL_COLORS.filter((c) => !colors().has(c))
+      if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
+      return available[Math.floor(Math.random() * available.length)]
+    }
+
+    function enrich(project: { worktree: string; expanded: boolean }) {
+      const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
+      if (!metadata) return []
+      return [
+        {
+          ...project,
+          ...metadata,
+        },
+      ]
+    }
+
+    function colorize(project: Project & { expanded: boolean }) {
+      if (project.icon?.color) return project
+      const color = pickAvailableColor()
+      project.icon = { ...project.icon, color }
+      globalSdk.client.project.update({ projectID: project.id, icon: { color } })
+      return project
+    }
+
+    const enriched = createMemo(() => store.projects.flatMap(enrich))
+    const list = createMemo(() => enriched().flatMap(colorize))
+    const colors = createMemo(
+      () =>
+        new Set(
+          list()
+            .map((p) => p.icon?.color)
+            .filter(Boolean),
+        ),
+    )
+
     async function loadProjectSessions(directory: string) {
       const [, setStore] = globalSync.child(directory)
       globalSdk.client.session.list({ directory }).then((x) => {
@@ -43,26 +93,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
 
     onMount(() => {
       Promise.all(
-        store.projects.map(({ worktree }) => {
-          return loadProjectSessions(worktree)
+        store.projects.map((project) => {
+          return loadProjectSessions(project.worktree)
         }),
       )
     })
 
-    function enrich(project: { worktree: string; expanded: boolean }) {
-      const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
-      if (!metadata) return []
-      return [
-        {
-          ...project,
-          ...metadata,
-        },
-      ]
-    }
-
     return {
       projects: {
-        list: createMemo(() => store.projects.flatMap(enrich)),
+        list,
         open(directory: string) {
           if (store.projects.find((x) => x.worktree === directory)) return
           loadProjectSessions(directory)

+ 38 - 0
packages/desktop/src/pages/layout.tsx

@@ -29,6 +29,8 @@ import {
   useDragDropContext,
 } from "@thisbeyond/solid-dnd"
 import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
+import { SelectDialog } from "@opencode-ai/ui/select-dialog"
+import { Tag } from "@opencode-ai/ui/tag"
 
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
@@ -44,11 +46,16 @@ export default function Layout(props: ParentProps) {
   const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
   const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+  const providers = createMemo(() => globalSync.data.providers)
   const hasProviders = createMemo(() => {
     const [projectStore] = globalSync.child(currentDirectory())
     return projectStore.provider.filter((p) => p.id !== "opencode").length > 0
   })
 
+  createEffect(() => {
+    console.log(providers())
+  })
+
   function navigateToProject(directory: string | undefined) {
     if (!directory) return
     const lastSession = store.lastSession[directory]
@@ -550,6 +557,37 @@ export default function Layout(props: ParentProps) {
           </div>
         </div>
         <main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
+        <Show when={true}>
+          <SelectDialog
+            defaultOpen
+            title="Connect provider"
+            placeholder="Search providers"
+            key={(x) => x?.id}
+            items={providers()}
+            // 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
+              }
+            }
+          >
+            {(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>
   )

+ 17 - 17
packages/tauri/src-tauri/Cargo.lock

@@ -2,6 +2,23 @@
 # It is not intended for manual editing.
 version = 4
 
+[[package]]
+name = "OpenCode"
+version = "0.0.0"
+dependencies = [
+ "listeners",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-dialog",
+ "tauri-plugin-opener",
+ "tauri-plugin-process",
+ "tauri-plugin-shell",
+ "tauri-plugin-updater",
+ "tokio",
+]
+
 [[package]]
 name = "adler2"
 version = "2.0.1"
@@ -2500,23 +2517,6 @@ dependencies = [
  "pathdiff",
 ]
 
-[[package]]
-name = "opencode-desktop"
-version = "0.0.0"
-dependencies = [
- "listeners",
- "serde",
- "serde_json",
- "tauri",
- "tauri-build",
- "tauri-plugin-dialog",
- "tauri-plugin-opener",
- "tauri-plugin-process",
- "tauri-plugin-shell",
- "tauri-plugin-updater",
- "tokio",
-]
-
 [[package]]
 name = "option-ext"
 version = "0.2.0"

+ 1 - 1
packages/tauri/src-tauri/Cargo.toml

@@ -1,5 +1,5 @@
 [package]
-name = "opencode-desktop"
+name = "OpenCode"
 version = "0.0.0"
 description = "A Tauri App"
 authors = ["you"]

+ 4 - 3
packages/ui/src/components/avatar.tsx

@@ -9,22 +9,23 @@ export interface AvatarProps extends ComponentProps<"div"> {
 
 export function Avatar(props: AvatarProps) {
   const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"])
+  const src = split.src // did this so i can zero it out to test fallback
   return (
     <div
       {...rest}
       data-component="avatar"
       data-size={split.size || "normal"}
-      data-has-image={split.src ? "" : undefined}
+      data-has-image={src ? "" : undefined}
       classList={{
         ...(split.classList ?? {}),
         [split.class ?? ""]: !!split.class,
       }}
       style={{
         ...(typeof split.style === "object" ? split.style : {}),
-        ...(!split.src && split.background ? { "--avatar-bg": split.background } : {}),
+        ...(!src && split.background ? { "--avatar-bg": split.background } : {}),
       }}
     >
-      <Show when={split.src} fallback={split.fallback?.[0]}>
+      <Show when={src} fallback={split.fallback?.[0]}>
         {(src) => <img src={src()} draggable={false} class="size-full object-cover" />}
       </Show>
     </div>

+ 13 - 5
packages/ui/src/components/button.css

@@ -102,12 +102,20 @@
     height: 24px;
     padding: 0 6px;
     &[data-icon] {
-      padding: 0 8px 0 6px;
+      padding: 0 12px 0 4px;
     }
 
     font-size: var(--font-size-small);
     line-height: var(--line-height-large);
     gap: 6px;
+
+    /* 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: var(--line-height-large); /* 166.667% */
+    letter-spacing: var(--letter-spacing-normal);
   }
 
   &[data-size="large"] {
@@ -115,17 +123,17 @@
     padding: 0 8px;
 
     &[data-icon] {
-      padding: 0 8px 0 6px;
+      padding: 0 12px 0 8px;
     }
 
     gap: 8px;
 
-    /* text-12-medium */
+    /* text-14-medium */
     font-family: var(--font-family-sans);
-    font-size: var(--font-size-small);
+    font-size: 14px;
     font-style: normal;
     font-weight: var(--font-weight-medium);
-    line-height: var(--line-height-large); /* 166.667% */
+    line-height: var(--line-height-large); /* 142.857% */
     letter-spacing: var(--letter-spacing-normal);
   }
 

+ 0 - 1
packages/ui/src/components/select-dialog.css

@@ -75,7 +75,6 @@
     position: relative;
     display: flex;
     flex-direction: column;
-    gap: 4px;
 
     [data-slot="select-dialog-header"] {
       display: flex;

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

@@ -15,6 +15,7 @@ interface SelectDialogProps<T>
   children: (item: T) => JSX.Element
   onSelect?: (value: T | undefined) => void
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
+  actions?: JSX.Element
 }
 
 export function SelectDialog<T>(props: SelectDialogProps<T>) {
@@ -98,7 +99,8 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
     <Dialog modal {...dialog} onOpenChange={handleOpenChange}>
       <Dialog.Header>
         <Dialog.Title>{others.title}</Dialog.Title>
-        <Dialog.CloseButton ref={closeButton} tabIndex={-1} />
+        <Show when={others.actions}>{others.actions}</Show>
+        <Dialog.CloseButton ref={closeButton} tabIndex={-1} style={{ display: others.actions ? "none" : undefined }} />
       </Dialog.Header>
       <div data-slot="select-dialog-content">
         <div data-component="select-dialog-input">
@@ -136,7 +138,7 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
             fallback={
               <div data-slot="select-dialog-empty-state">
                 <div data-slot="select-dialog-message">
-                  {props.emptyMessage ?? "No search results"} for{" "}
+                  {props.emptyMessage ?? "No results"} for{" "}
                   <span data-slot="select-dialog-filter">&quot;{filter()}&quot;</span>
                 </div>
               </div>