summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-01-04 13:38:30 -0500
committerDax Raad <[email protected]>2026-01-04 13:38:30 -0500
commit7304ba616e813861869f5102d7262f60724b76c8 (patch)
tree154040747f9d1dc1958e1ff25e7bd5e4faa71189
parentcdd6ea514b94d8ce71770bb02f33ab979e9ee0f6 (diff)
downloadopencode-7304ba616e813861869f5102d7262f60724b76c8.tar.gz
opencode-7304ba616e813861869f5102d7262f60724b76c8.zip
tui: add session search functionality with debounced input and server-side filtering
-rw-r--r--bun.lock3
-rw-r--r--packages/opencode/package.json1
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx21
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/sync.tsx3
-rw-r--r--packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx6
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/signal.ts7
-rw-r--r--packages/opencode/src/server/server.ts21
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts17
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts12
9 files changed, 79 insertions, 12 deletions
diff --git a/bun.lock b/bun.lock
index 0d8ccc526..8c57d8630 100644
--- a/bun.lock
+++ b/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=="],
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 62c786c7f..fb110f934 100644
--- a/packages/opencode/package.json
+++ b/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:",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
index cb7b5d282..07de4d472 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
+++ b/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)
}}
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index 8daa70b76..8a14d8b2e 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/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
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index 2a39bb01e..a89223d49 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/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
})
diff --git a/packages/opencode/src/cli/cmd/tui/util/signal.ts b/packages/opencode/src/cli/cmd/tui/util/signal.ts
new file mode 100644
index 000000000..15b57886d
--- /dev/null
+++ b/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)]
+}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 3789c3239..04ec4673e 100644
--- a/packages/opencode/src/server/server.ts
+++ b/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)
},
)
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 81d50b28e..ac5ea1211 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/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,
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 5ca8fa8f6..431135db3 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/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"
}