mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-07 00:51:34 +00:00
feat(core): filter sessions by path and add setting to disable (#24849)
Some checks failed
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
nix-eval / nix-eval (push) Waiting to run
publish / build-cli (push) Blocked by required conditions
publish / version (push) Waiting to run
publish / sign-cli-windows (push) Blocked by required conditions
publish / build-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-tauri (map[host:blacksmith-8vcpu-ubuntu-2404-arm target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-tauri (map[host:windows-2025 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish (push) Blocked by required conditions
storybook / storybook build (push) Waiting to run
test / unit (linux) (push) Waiting to run
test / unit (windows) (push) Waiting to run
test / e2e (linux) (push) Waiting to run
test / e2e (windows) (push) Waiting to run
typecheck / typecheck (push) Waiting to run
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404, x86_64-linux) (push) Has been cancelled
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404-arm, aarch64-linux) (push) Has been cancelled
nix-hashes / compute-hash (macos-15-intel, x86_64-darwin) (push) Has been cancelled
nix-hashes / compute-hash (macos-latest, aarch64-darwin) (push) Has been cancelled
nix-hashes / update-hashes (push) Has been cancelled
Some checks failed
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
nix-eval / nix-eval (push) Waiting to run
publish / build-cli (push) Blocked by required conditions
publish / version (push) Waiting to run
publish / sign-cli-windows (push) Blocked by required conditions
publish / build-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-tauri (map[host:blacksmith-8vcpu-ubuntu-2404-arm target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-tauri (map[host:windows-2025 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish (push) Blocked by required conditions
storybook / storybook build (push) Waiting to run
test / unit (linux) (push) Waiting to run
test / unit (windows) (push) Waiting to run
test / e2e (linux) (push) Waiting to run
test / e2e (windows) (push) Waiting to run
typecheck / typecheck (push) Waiting to run
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404, x86_64-linux) (push) Has been cancelled
nix-hashes / compute-hash (blacksmith-4vcpu-ubuntu-2404-arm, aarch64-linux) (push) Has been cancelled
nix-hashes / compute-hash (macos-15-intel, x86_64-darwin) (push) Has been cancelled
nix-hashes / compute-hash (macos-latest, aarch64-darwin) (push) Has been cancelled
nix-hashes / update-hashes (push) Has been cancelled
This commit is contained in:
parent
379e7f3f20
commit
9209c04370
10 changed files with 360 additions and 27 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
149
packages/opencode/test/cli/cmd/tui/sync.test.tsx
Normal file
149
packages/opencode/test/cli/cmd/tui/sync.test.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { onMount } from "solid-js"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { ArgsProvider } from "../../../../src/cli/cmd/tui/context/args"
|
||||
import { ExitProvider } from "../../../../src/cli/cmd/tui/context/exit"
|
||||
import { KVProvider, useKV } from "../../../../src/cli/cmd/tui/context/kv"
|
||||
import { ProjectProvider } from "../../../../src/cli/cmd/tui/context/project"
|
||||
import { SDKProvider, type EventSource } from "../../../../src/cli/cmd/tui/context/sdk"
|
||||
import { SyncProvider, useSync } from "../../../../src/cli/cmd/tui/context/sync"
|
||||
import { tmpdir } from "../../../fixture/fixture"
|
||||
|
||||
const worktree = "/tmp/opencode"
|
||||
const directory = `${worktree}/packages/opencode`
|
||||
|
||||
async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
while (!fn()) {
|
||||
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
}
|
||||
|
||||
function json(data: unknown) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
function eventSource(): EventSource {
|
||||
return {
|
||||
subscribe: async () => () => {},
|
||||
}
|
||||
}
|
||||
|
||||
function createFetch() {
|
||||
const session = [] as URL[]
|
||||
const fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = new URL(input instanceof Request ? input.url : String(input))
|
||||
if (url.pathname === "/session") session.push(url)
|
||||
|
||||
switch (url.pathname) {
|
||||
case "/agent":
|
||||
case "/command":
|
||||
case "/experimental/workspace":
|
||||
case "/experimental/workspace/status":
|
||||
case "/formatter":
|
||||
case "/lsp":
|
||||
return json([])
|
||||
case "/config":
|
||||
case "/experimental/resource":
|
||||
case "/mcp":
|
||||
case "/provider/auth":
|
||||
case "/session/status":
|
||||
return json({})
|
||||
case "/config/providers":
|
||||
return json({ providers: {}, default: {} })
|
||||
case "/experimental/console":
|
||||
return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
|
||||
case "/path":
|
||||
return json({ home: "", state: "", config: "", worktree, directory })
|
||||
case "/project/current":
|
||||
return json({ id: "proj_test" })
|
||||
case "/provider":
|
||||
return json({ all: [], default: {}, connected: [] })
|
||||
case "/session":
|
||||
return json([])
|
||||
case "/vcs":
|
||||
return json({ branch: "main" })
|
||||
}
|
||||
|
||||
throw new Error(`unexpected request: ${url.pathname}`)
|
||||
}) as typeof globalThis.fetch
|
||||
|
||||
return { fetch, session }
|
||||
}
|
||||
|
||||
async function mount() {
|
||||
const calls = createFetch()
|
||||
let sync!: ReturnType<typeof useSync>
|
||||
let kv!: ReturnType<typeof useKV>
|
||||
let done!: () => void
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
done = resolve
|
||||
})
|
||||
|
||||
const app = await testRender(() => (
|
||||
<ArgsProvider>
|
||||
<ExitProvider>
|
||||
<KVProvider>
|
||||
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={eventSource()}>
|
||||
<ProjectProvider>
|
||||
<SyncProvider>
|
||||
<Probe
|
||||
onReady={(ctx) => {
|
||||
sync = ctx.sync
|
||||
kv = ctx.kv
|
||||
done()
|
||||
}}
|
||||
/>
|
||||
</SyncProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ArgsProvider>
|
||||
))
|
||||
|
||||
await ready
|
||||
await wait(() => sync.status === "complete")
|
||||
return { app, kv, sync, session: calls.session }
|
||||
}
|
||||
|
||||
function Probe(props: { onReady: (ctx: { kv: ReturnType<typeof useKV>; sync: ReturnType<typeof useSync> }) => void }) {
|
||||
const kv = useKV()
|
||||
const sync = useSync()
|
||||
|
||||
onMount(() => {
|
||||
props.onReady({ kv, sync })
|
||||
})
|
||||
|
||||
return <box />
|
||||
}
|
||||
|
||||
describe("tui sync", () => {
|
||||
test("refresh scopes sessions by default and lists project sessions when disabled", async () => {
|
||||
const previous = Global.Path.state
|
||||
await using tmp = await tmpdir()
|
||||
Global.Path.state = tmp.path
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
const { app, kv, sync, session } = await mount()
|
||||
|
||||
try {
|
||||
expect(kv.get("session_directory_filter_enabled", true)).toBe(true)
|
||||
expect(session.at(-1)?.searchParams.get("scope")).toBeNull()
|
||||
expect(session.at(-1)?.searchParams.get("path")).toBe("packages/opencode")
|
||||
|
||||
kv.set("session_directory_filter_enabled", false)
|
||||
await sync.session.refresh()
|
||||
|
||||
expect(session.at(-1)?.searchParams.get("scope")).toBe("project")
|
||||
expect(session.at(-1)?.searchParams.get("path")).toBeNull()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
Global.Path.state = previous
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -4,8 +4,15 @@ import { Instance } from "../../src/project/instance"
|
|||
import { Session as SessionNs } from "@/session/session"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { mkdir } from "fs/promises"
|
||||
import path from "path"
|
||||
import { Database } from "@/storage/db"
|
||||
import { SessionTable } from "@/session/session.sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
void Log.init({ print: false })
|
||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
|
||||
function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
|
||||
return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
|
||||
|
|
@ -19,28 +26,140 @@ const svc = {
|
|||
}
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
describe("session.list", () => {
|
||||
test("filters by directory", async () => {
|
||||
test("does not filter by directory when directory is omitted", async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = false
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "opencode"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const first = await svc.create({})
|
||||
const root = await svc.create({ title: "root" })
|
||||
|
||||
await using other = await tmpdir({ git: true })
|
||||
const second = await Instance.provide({
|
||||
directory: other.path,
|
||||
fn: async () => svc.create({}),
|
||||
const parent = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages"),
|
||||
fn: async () => svc.create({ title: "parent" }),
|
||||
})
|
||||
const current = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode"),
|
||||
fn: async () => svc.create({ title: "current" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "sibling" }),
|
||||
})
|
||||
|
||||
const sessions = [...svc.list({ directory: tmp.path })]
|
||||
const ids = sessions.map((s) => s.id)
|
||||
const ids = [...svc.list()].map((s) => s.id)
|
||||
expect(ids).toContain(root.id)
|
||||
expect(ids).toContain(parent.id)
|
||||
expect(ids).toContain(current.id)
|
||||
expect(ids).toContain(sibling.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(ids).toContain(first.id)
|
||||
expect(ids).not.toContain(second.id)
|
||||
test("filters by directory when directory is provided", async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = false
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "opencode"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const root = await svc.create({ title: "root" })
|
||||
|
||||
const parent = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages"),
|
||||
fn: async () => svc.create({ title: "parent" }),
|
||||
})
|
||||
const current = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode"),
|
||||
fn: async () => svc.create({ title: "current" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "sibling" }),
|
||||
})
|
||||
|
||||
const ids = [...svc.list({ directory: path.join(tmp.path, "packages", "opencode") })].map((s) => s.id)
|
||||
expect(ids).not.toContain(root.id)
|
||||
expect(ids).not.toContain(parent.id)
|
||||
expect(ids).toContain(current.id)
|
||||
expect(ids).not.toContain(sibling.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("filters by path and ignores directory when path is provided", async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = false
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "opencode", "src", "deep"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const parent = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode"),
|
||||
fn: async () => svc.create({ title: "parent" }),
|
||||
})
|
||||
const current = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode", "src"),
|
||||
fn: async () => svc.create({ title: "current" }),
|
||||
})
|
||||
const deeper = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode", "src", "deep"),
|
||||
fn: async () => svc.create({ title: "deeper" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "sibling" }),
|
||||
})
|
||||
|
||||
const pathIDs = [
|
||||
...svc.list({ directory: path.join(tmp.path, "packages", "app"), path: "packages/opencode/src" }),
|
||||
].map((s) => s.id)
|
||||
expect(pathIDs).not.toContain(parent.id)
|
||||
expect(pathIDs).toContain(current.id)
|
||||
expect(pathIDs).toContain(deeper.id)
|
||||
expect(pathIDs).not.toContain(sibling.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("falls back to directory when filtering legacy sessions without path", async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = false
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "opencode", "src"), { recursive: true })
|
||||
await mkdir(path.join(tmp.path, "packages", "app"), { recursive: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const current = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "opencode", "src"),
|
||||
fn: async () => svc.create({ title: "legacy-current" }),
|
||||
})
|
||||
const sibling = await Instance.provide({
|
||||
directory: path.join(tmp.path, "packages", "app"),
|
||||
fn: async () => svc.create({ title: "legacy-sibling" }),
|
||||
})
|
||||
|
||||
Database.use((db) => db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, current.id)).run())
|
||||
Database.use((db) => db.update(SessionTable).set({ path: null }).where(eq(SessionTable.id, sibling.id)).run())
|
||||
|
||||
const pathIDs = [
|
||||
...svc.list({ directory: path.join(tmp.path, "packages", "opencode", "src"), path: "packages/opencode/src" }),
|
||||
].map((s) => s.id)
|
||||
expect(pathIDs).toContain(current.id)
|
||||
expect(pathIDs).not.toContain(sibling.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1647,6 +1647,8 @@ export class Session2 extends HeyApiClient {
|
|||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
scope?: "project"
|
||||
path?: string
|
||||
roots?: boolean | "true" | "false"
|
||||
start?: number
|
||||
search?: string
|
||||
|
|
@ -1661,6 +1663,8 @@ export class Session2 extends HeyApiClient {
|
|||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "query", key: "scope" },
|
||||
{ in: "query", key: "path" },
|
||||
{ in: "query", key: "roots" },
|
||||
{ in: "query", key: "start" },
|
||||
{ in: "query", key: "search" },
|
||||
|
|
|
|||
|
|
@ -3289,6 +3289,14 @@ export type SessionListData = {
|
|||
*/
|
||||
directory?: string
|
||||
workspace?: string
|
||||
/**
|
||||
* List all sessions for the current project
|
||||
*/
|
||||
scope?: "project"
|
||||
/**
|
||||
* Filter sessions by project-relative path
|
||||
*/
|
||||
path?: string
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue