Explorar o código

tui: add session search functionality with debounced input and server-side filtering

Dax Raad hai 1 mes
pai
achega
7304ba616e

+ 3 - 0
bun.lock

@@ -290,6 +290,7 @@
         "@parcel/watcher": "2.5.1",
         "@pierre/diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
+        "@solid-primitives/scheduled": "1.5.2",
         "@standard-schema/spec": "1.0.0",
         "@zip.js/zip.js": "2.7.62",
         "ai": "catalog:",
@@ -1616,6 +1617,8 @@
 
     "@solid-primitives/rootless": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="],
 
+    "@solid-primitives/scheduled": ["@solid-primitives/[email protected]", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
+
     "@solid-primitives/scroll": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="],
 
     "@solid-primitives/static-store": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="],

+ 1 - 0
packages/opencode/package.json

@@ -86,6 +86,7 @@
     "@parcel/watcher": "2.5.1",
     "@pierre/diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",
+    "@solid-primitives/scheduled": "1.5.2",
     "@standard-schema/spec": "1.0.0",
     "@zip.js/zip.js": "2.7.62",
     "ai": "catalog:",

+ 14 - 7
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -2,13 +2,14 @@ import { useDialog } from "@tui/ui/dialog"
 import { DialogSelect } from "@tui/ui/dialog-select"
 import { useRoute } from "@tui/context/route"
 import { useSync } from "@tui/context/sync"
-import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
+import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
 import { Locale } from "@/util/locale"
 import { Keybind } from "@/util/keybind"
 import { useTheme } from "../context/theme"
 import { useSDK } from "../context/sdk"
 import { DialogSessionRename } from "./dialog-session-rename"
 import { useKV } from "../context/kv"
+import { createDebouncedSignal } from "../util/signal"
 import "opentui-spinner/solid"
 
 export function DialogSessionList() {
@@ -20,6 +21,13 @@ export function DialogSessionList() {
   const kv = useKV()
 
   const [toDelete, setToDelete] = createSignal<string>()
+  const [search, setSearch] = createDebouncedSignal("", 150)
+
+  const [searchResults] = createResource(search, async (query) => {
+    if (!query) return undefined
+    const result = await sdk.client.session.list({ search: query, limit: 30 })
+    return result.data ?? []
+  })
 
   const deleteKeybind = "ctrl+d"
 
@@ -27,9 +35,11 @@ export function DialogSessionList() {
 
   const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
 
+  const sessions = createMemo(() => searchResults() ?? sync.data.session)
+
   const options = createMemo(() => {
     const today = new Date().toDateString()
-    return sync.data.session
+    return sessions()
       .filter((x) => x.parentID === undefined)
       .toSorted((a, b) => b.time.updated - a.time.updated)
       .map((x) => {
@@ -54,11 +64,6 @@ export function DialogSessionList() {
           ) : undefined,
         }
       })
-      .slice(0, 150)
-  })
-
-  createEffect(() => {
-    console.log("session count", sync.data.session.length)
   })
 
   onMount(() => {
@@ -69,7 +74,9 @@ export function DialogSessionList() {
     <DialogSelect
       title="Sessions"
       options={options()}
+      skipFilter={true}
       current={currentSessionID()}
+      onFilter={setSearch}
       onMove={() => {
         setToDelete(undefined)
       }}

+ 2 - 1
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -269,8 +269,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
 
     async function bootstrap() {
       console.log("bootstrapping")
+      const start = Date.now() - 30 * 24 * 60 * 60 * 1000
       const sessionListPromise = sdk.client.session
-        .list()
+        .list({ start: start })
         .then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))))
 
       // blocking - include session.list when continuing a session

+ 4 - 2
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

@@ -71,12 +71,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
   let input: InputRenderable
 
   const filtered = createMemo(() => {
+    if (props.skipFilter) {
+      return props.options.filter((x) => x.disabled !== true)
+    }
     const needle = store.filter.toLowerCase()
     const result = pipe(
       props.options,
       filter((x) => x.disabled !== true),
-      (x) =>
-        !needle || props.skipFilter ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
+      (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
     )
     return result
   })

+ 7 - 0
packages/opencode/src/cli/cmd/tui/util/signal.ts

@@ -0,0 +1,7 @@
+import { createSignal, type Accessor } from "solid-js"
+import { debounce, type Scheduled } from "@solid-primitives/scheduled"
+
+export function createDebouncedSignal<T>(value: T, ms: number): [Accessor<T>, Scheduled<[value: T]>] {
+  const [get, set] = createSignal(value)
+  return [get, debounce((v: T) => set(() => v), ms)]
+}

+ 20 - 1
packages/opencode/src/server/server.ts

@@ -701,8 +701,27 @@ export namespace Server {
             },
           },
         }),
+        validator(
+          "query",
+          z.object({
+            start: z.coerce
+              .number()
+              .optional()
+              .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
+            search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
+            limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
+          }),
+        ),
         async (c) => {
-          const sessions = await Array.fromAsync(Session.list())
+          const query = c.req.valid("query")
+          const term = query.search?.toLowerCase()
+          const sessions: Session.Info[] = []
+          for await (const session of Session.list()) {
+            if (query.start !== undefined && session.time.updated < query.start) continue
+            if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
+            sessions.push(session)
+            if (query.limit !== undefined && sessions.length >= query.limit) break
+          }
           return c.json(sessions)
         },
       )

+ 16 - 1
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -774,10 +774,25 @@ export class Session extends HeyApiClient {
   public list<ThrowOnError extends boolean = false>(
     parameters?: {
       directory?: string
+      start?: number
+      search?: string
+      limit?: number
     },
     options?: Options<never, ThrowOnError>,
   ) {
-    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "query", key: "directory" },
+            { in: "query", key: "start" },
+            { in: "query", key: "search" },
+            { in: "query", key: "limit" },
+          ],
+        },
+      ],
+    )
     return (options?.client ?? this.client).get<SessionListResponses, unknown, ThrowOnError>({
       url: "/session",
       ...options,

+ 12 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -2508,6 +2508,18 @@ export type SessionListData = {
   path?: never
   query?: {
     directory?: string
+    /**
+     * Filter sessions updated on or after this timestamp (milliseconds since epoch)
+     */
+    start?: number
+    /**
+     * Filter sessions by title (case-insensitive)
+     */
+    search?: string
+    /**
+     * Maximum number of sessions to return
+     */
+    limit?: number
   }
   url: "/session"
 }