Adam 5 месяцев назад
Родитель
Сommit
f589fc2327

+ 40 - 30
packages/app/src/components/select-dialog.tsx

@@ -1,22 +1,18 @@
-import { createEffect, Show, For, createMemo, type JSX } from "solid-js"
+import { createEffect, Show, For, createMemo, type JSX, createResource } from "solid-js"
 import { Dialog } from "@kobalte/core/dialog"
 import { Icon, IconButton } from "@/ui"
 import { createStore } from "solid-js/store"
-import { entries, flatMap, groupBy, map, mapValues, pipe } from "remeda"
+import { entries, flatMap, groupBy, map, pipe } from "remeda"
 import { createList } from "solid-list"
 import fuzzysort from "fuzzysort"
 
 interface SelectDialogProps<T> {
-  items: T[]
+  items: T[] | ((filter: string) => Promise<T[]>)
   key: (item: T) => string
   render: (item: T) => JSX.Element
+  filter?: string[]
   current?: T
   placeholder?: string
-  filter?:
-    | false
-    | {
-        keys: string[]
-      }
   groupBy?: (x: T) => string
   onSelect?: (value: T | undefined) => void
   onClose?: () => void
@@ -29,24 +25,31 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
     mouseActive: false,
   })
 
-  const grouped = createMemo(() => {
-    const needle = store.filter.toLowerCase()
-    const result = pipe(
-      props.items,
-      (x) =>
-        !needle || !props.filter
-          ? x
-          : fuzzysort.go(needle, x, { keys: props.filter && props.filter.keys }).map((x) => x.obj),
-      groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
-      mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))),
-      entries(),
-      map(([k, v]) => ({ category: k, items: v })),
-    )
-    return result
-  })
+  const [grouped] = createResource(
+    () => store.filter,
+    async (filter) => {
+      const needle = filter.toLowerCase()
+      const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
+      const result = pipe(
+        all,
+        (x) => {
+          if (!needle) return x
+          if (!props.filter && Array.isArray(x) && x.every((e) => typeof e === "string")) {
+            return fuzzysort.go(needle, x).map((x) => x.target) as T[]
+          }
+          return fuzzysort.go(needle, x, { keys: props.filter! }).map((x) => x.obj)
+        },
+        groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
+        // mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))),
+        entries(),
+        map(([k, v]) => ({ category: k, items: v })),
+      )
+      return result
+    },
+  )
   const flat = createMemo(() => {
     return pipe(
-      grouped(),
+      grouped() || [],
       flatMap((x) => x.items),
     )
   })
@@ -55,7 +58,11 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
     initialActive: props.current ? props.key(props.current) : undefined,
     loop: true,
   })
-  const resetSelection = () => list.setActive(props.key(flat()[0]))
+  const resetSelection = () => {
+    const all = flat()
+    if (all.length === 0) return
+    list.setActive(props.key(all[0]))
+  }
 
   createEffect(() => {
     store.filter
@@ -64,8 +71,9 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
   })
 
   createEffect(() => {
-    if (store.mouseActive) return
-    if (list.active() === props.key(flat()[0])) {
+    const all = flat()
+    if (store.mouseActive || all.length === 0) return
+    if (list.active() === props.key(all[0])) {
       scrollRef?.scrollTo(0, 0)
       return
     }
@@ -156,9 +164,11 @@ export function SelectDialog<T>(props: SelectDialogProps<T>) {
               <For each={grouped()}>
                 {(group) => (
                   <>
-                    <div class="top-0 sticky z-10 bg-background-panel p-2 text-xs text-text-muted/60 tracking-wider uppercase">
-                      {group.category}
-                    </div>
+                    <Show when={group.category}>
+                      <div class="top-0 sticky z-10 bg-background-panel p-2 text-xs text-text-muted/60 tracking-wider uppercase">
+                        {group.category}
+                      </div>
+                    </Show>
                     <div class="p-2">
                       <For each={group.items}>
                         {(item) => (

+ 5 - 1
packages/app/src/context/local.tsx

@@ -163,7 +163,7 @@ function init() {
       })
     }
 
-    const open = async (path: string) => {
+    const open = async (path: string, options?: { pin?: boolean }) => {
       const relative = path.replace(sync.data.path.directory + "/", "")
       if (!store.node[relative]) {
         const parent = relative.split("/").slice(0, -1).join("/")
@@ -181,6 +181,7 @@ function init() {
         ]
       })
       setStore("active", relative)
+      if (options?.pin) setStore("node", path, "pinned", true)
       if (store.node[relative].loaded) return
       return load(relative)
     }
@@ -199,6 +200,8 @@ function init() {
       })
     }
 
+    const search = (query: string) => sdk.find.files({ query: { query } }).then((x) => x.data!)
+
     const bus = useEvent()
     bus.listen((event) => {
       switch (event.type) {
@@ -303,6 +306,7 @@ function init() {
             !x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
         )
       },
+      search,
     }
   })()
 

+ 25 - 5
packages/app/src/pages/index.tsx

@@ -20,6 +20,7 @@ import type { LocalFile } from "@/context/local"
 import SessionList from "@/components/session-list"
 import SessionTimeline from "@/components/session-timeline"
 import { createStore } from "solid-js/store"
+import { getDirectory, getFilename } from "@/utils"
 
 export default function Page() {
   const sdk = useSDK()
@@ -30,6 +31,7 @@ export default function Page() {
     prompt: "",
     dragging: undefined as "left" | "right" | undefined,
     modelSelectOpen: false,
+    fileSelectOpen: false,
   })
 
   let inputRef: HTMLInputElement | undefined = undefined
@@ -47,12 +49,12 @@ export default function Page() {
   const handleKeyDown = (e: KeyboardEvent) => {
     if (e.getModifierState(MOD) && e.shiftKey && e.key.toLowerCase() === "p") {
       e.preventDefault()
-      setStore("modelSelectOpen", true)
+      // TODO: command palette
       return
     }
     if (e.getModifierState(MOD) && e.key.toLowerCase() === "p") {
       e.preventDefault()
-      setStore("modelSelectOpen", true)
+      setStore("fileSelectOpen", true)
       return
     }
 
@@ -554,14 +556,32 @@ export default function Page() {
               </div>
             </div>
           )}
-          filter={{
-            keys: ["provider.name", "name", "id"],
-          }}
+          filter={["provider.name", "name", "id"]}
           groupBy={(x) => x.provider.name}
           onClose={() => setStore("modelSelectOpen", false)}
           onSelect={(x) => local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined)}
         />
       </Show>
+      <Show when={store.fileSelectOpen}>
+        <SelectDialog
+          items={local.file.search}
+          key={(x) => x}
+          render={(i) => (
+            <div class="w-full flex items-center justify-between">
+              <div class="flex items-center gap-x-2 text-text-muted grow min-w-0">
+                <FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
+                <span class="text-xs text-text whitespace-nowrap">{getFilename(i)}</span>
+                <span class="text-xs text-text-muted/80 whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
+                  {getDirectory(i)}
+                </span>
+              </div>
+              <div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
+            </div>
+          )}
+          onClose={() => setStore("fileSelectOpen", false)}
+          onSelect={(x) => (x ? local.file.open(x, { pin: true }) : undefined)}
+        />
+      </Show>
     </div>
   )
 }

+ 5 - 0
packages/app/src/utils/path.ts

@@ -3,6 +3,11 @@ export function getFilename(path: string) {
   return parts[parts.length - 1]
 }
 
+export function getDirectory(path: string) {
+  const parts = path.split("/")
+  return parts.slice(0, parts.length - 1).join("/")
+}
+
 export function getFileExtension(path: string) {
   const parts = path.split(".")
   return parts[parts.length - 1]