Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type State = {
config: Config
path: Path
session: Session[]
sessionTotal: number
session_status: {
[sessionID: string]: SessionStatus
}
Expand Down Expand Up @@ -98,6 +99,7 @@ function createGlobalSync() {
agent: [],
command: [],
session: [],
sessionTotal: 0,
session_status: {},
session_diff: {},
todo: {},
Expand All @@ -117,8 +119,10 @@ function createGlobalSync() {

async function loadSessions(directory: string) {
const [store, setStore] = child(directory)
globalSDK.client.session
.list({ directory })
const limit = store.limit

return globalSDK.client.session
.list({ directory, roots: true })
.then((x) => {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
const nonArchived = (x.data ?? [])
Expand All @@ -128,10 +132,12 @@ function createGlobalSync() {
.sort((a, b) => a.id.localeCompare(b.id))
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
if (i < store.limit) return true
if (i < limit) return true
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
return updated > fourHoursAgo
})
// Store total session count (used for "load more" pagination)
setStore("sessionTotal", nonArchived.length)
setStore("session", reconcile(sessions, { key: "id" }))
})
.catch((err) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,7 @@ export default function Layout(props: ParentProps) {
.toSorted(sortSessions),
)
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
const hasMoreSessions = createMemo(() => store.sessionTotal > store.session.length)
const loadMoreSessions = async () => {
setProjectStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.project.worktree)
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,8 @@ export namespace Server {
validator(
"query",
z.object({
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
start: z.coerce
.number()
.optional()
Expand All @@ -737,6 +739,8 @@ export namespace Server {
const term = query.search?.toLowerCase()
const sessions: Session.Info[] = []
for await (const session of Session.list()) {
if (query.directory !== undefined && session.directory !== query.directory) continue
if (query.roots && session.parentID) continue
if (query.start !== undefined && session.time.updated < query.start) continue
if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
sessions.push(session)
Expand Down
39 changes: 39 additions & 0 deletions packages/opencode/test/server/session-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
import { Session } from "../../src/session"
import { Log } from "../../src/util/log"

const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false })

describe("session.list", () => {
test("filters by directory", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const app = Server.App()

const first = await Session.create({})

const otherDir = path.join(projectRoot, "..", "__session_list_other")
const second = await Instance.provide({
directory: otherDir,
fn: async () => Session.create({}),
})

const response = await app.request(`/session?directory=${encodeURIComponent(projectRoot)}`)
expect(response.status).toBe(200)

const body = (await response.json()) as unknown[]
const ids = body
.map((s) => (typeof s === "object" && s && "id" in s ? (s as { id: string }).id : undefined))
.filter((x): x is string => typeof x === "string")

expect(ids).toContain(first.id)
expect(ids).not.toContain(second.id)
},
})
})
})
2 changes: 2 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,7 @@ export class Session extends HeyApiClient {
public list<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
roots?: boolean
start?: number
search?: string
limit?: number
Expand All @@ -793,6 +794,7 @@ export class Session extends HeyApiClient {
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "roots" },
{ in: "query", key: "start" },
{ in: "query", key: "search" },
{ in: "query", key: "limit" },
Expand Down
7 changes: 7 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2589,7 +2589,14 @@ export type SessionListData = {
body?: never
path?: never
query?: {
/**
* Filter sessions by project directory
*/
directory?: string
/**
* Only return root sessions (no parentID)
*/
roots?: boolean
/**
* Filter sessions updated on or after this timestamp (milliseconds since epoch)
*/
Expand Down