feat(core): filter sessions by path and add setting to disable (#24849)

This commit is contained in:
James Long
2026-04-28 16:49:13 -04:00
committed by GitHub
parent 379e7f3f20
commit 9209c04370
10 changed files with 360 additions and 27 deletions

View File

@@ -736,6 +736,18 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
dialog.clear()
},
},
{
title: kv.get("session_directory_filter_enabled", true)
? "Disable session directory filtering"
: "Enable session directory filtering",
value: "app.toggle.session_directory_filter",
category: "System",
onSelect: async (dialog) => {
kv.set("session_directory_filter_enabled", !kv.get("session_directory_filter_enabled", true))
await sync.session.refresh()
dialog.clear()
},
},
{
title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
value: "app.toggle.diffwrap",

View File

@@ -32,11 +32,14 @@ export function DialogSessionList() {
const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
const [searchResults, { refetch }] = createResource(search, async (query) => {
if (!query) return undefined
const result = await sdk.client.session.list({ search: query, limit: 30 })
return result.data ?? []
})
const [searchResults, { refetch }] = createResource(
() => ({ query: search(), filter: sync.session.query() }),
async (input) => {
if (!input.query) return undefined
const result = await sdk.client.session.list({ search: input.query, limit: 30, ...input.filter })
return result.data ?? []
},
)
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const sessions = createMemo(() => searchResults() ?? sync.data.session)

View File

@@ -30,6 +30,8 @@ import { useArgs } from "./args"
import { batch, onMount } from "solid-js"
import * as Log from "@opencode-ai/core/util/log"
import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
import path from "path"
import { useKV } from "./kv"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
@@ -107,10 +109,27 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const event = useEvent()
const project = useProject()
const sdk = useSDK()
const kv = useKV()
const fullSyncedSessions = new Set<string>()
let syncedWorkspace = project.workspace.current()
function sessionListQuery(): { scope?: "project"; path?: string } {
if (!kv.get("session_directory_filter_enabled", true)) return { scope: "project" }
if (!project.data.instance.path.worktree || !project.data.instance.path.directory) return { scope: "project" }
return {
path: path
.relative(path.resolve(project.data.instance.path.worktree), project.data.instance.path.directory)
.replaceAll("\\", "/"),
}
}
function listSessions() {
return sdk.client.session
.list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() })
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
}
event.subscribe((event) => {
switch (event.type) {
case "server.instance.disposed":
@@ -360,10 +379,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
fullSyncedSessions.clear()
syncedWorkspace = workspace
}
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
const sessionListPromise = sdk.client.session
.list({ start: start })
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
const projectPromise = project.sync()
const sessionListPromise = projectPromise.then(() => listSessions())
// blocking - include session.list when continuing a session
const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
@@ -374,7 +391,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.catch(() => emptyConsoleState)
const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
const projectPromise = project.sync()
const blockingRequests: Promise<unknown>[] = [
providersPromise,
providerListPromise,
@@ -479,11 +495,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (match.found) return store.session[match.index]
return undefined
},
query() {
return sessionListQuery()
},
async refresh() {
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
const list = await sdk.client.session
.list({ start })
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
const list = await listSessions()
setStore("session", reconcile(list))
},
status(sessionID: string) {

View File

@@ -45,6 +45,8 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
)
const ListQuery = Schema.Struct({
directory: Schema.optional(Schema.String),
scope: Schema.optional(Schema.Literals(["project"])),
path: Schema.optional(Schema.String),
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
search: Schema.optional(Schema.String),
@@ -444,6 +446,8 @@ export const sessionHandlers = HttpApiBuilder.group(SessionApi, "session", (hand
Array.from(
Session.list({
directory: ctx.query.directory,
scope: ctx.query.scope,
path: ctx.query.path,
roots: ctx.query.roots,
start: ctx.query.start,
search: ctx.query.search,

View File

@@ -62,7 +62,11 @@ export const SessionRoutes = lazy(() =>
validator(
"query",
z.object({
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
directory: z.string().optional().meta({ description: "Filter sessions by directory" }),
// TODO: in 2.0 remove `scope` and `directory` and default
// to list all sessions for a project
scope: z.enum(["project"]).optional().meta({ description: "List all sessions for the current project" }),
path: z.string().optional().meta({ description: "Filter sessions by project-relative path" }),
roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
start: z.coerce
.number()
@@ -76,7 +80,8 @@ export const SessionRoutes = lazy(() =>
const query = c.req.valid("query")
const sessions: Session.Info[] = []
for await (const session of Session.list({
directory: query.directory,
directory: query.scope === "project" ? undefined : query.directory,
path: query.path,
roots: queryBoolean(query.roots),
start: query.start,
search: query.search,

View File

@@ -18,6 +18,7 @@ import { desc } from "drizzle-orm"
import { like } from "drizzle-orm"
import { inArray } from "drizzle-orm"
import { lt } from "drizzle-orm"
import { or } from "drizzle-orm"
import { SyncEvent } from "../sync"
import type { SQL } from "drizzle-orm"
import { PartTable, SessionTable } from "./session.sql"
@@ -759,6 +760,8 @@ export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(S
export function* list(input?: {
directory?: string
scope?: "project"
path?: string
workspaceID?: WorkspaceID
roots?: boolean
start?: number
@@ -771,7 +774,17 @@ export function* list(input?: {
if (input?.workspaceID) {
conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
}
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (input?.path !== undefined) {
if (input.path) {
const conds = [eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`)]
conditions.push(
input.directory
? or(...conds, and(isNull(SessionTable.path), eq(SessionTable.directory, input.directory))!)!
: or(...conds)!,
)
}
} else if (input?.scope !== "project" && !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (input?.directory) {
conditions.push(eq(SessionTable.directory, input.directory))
}