Skip to content
Open
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
13 changes: 7 additions & 6 deletions packages/app/src/components/prompt-input/submit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode } from "@opencode-ai/util/encode"
import { Binary } from "@opencode-ai/util/binary"
import { useNavigate, useParams } from "@solidjs/router"
import type { Accessor } from "solid-js"
import type { FileSelection } from "@/context/file"
Expand All @@ -13,6 +12,7 @@ import { usePermission } from "@/context/permission"
import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
import { compareSessionRecent } from "@/context/global-sync/session-trim"
import { Identifier } from "@/utils/id"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { buildRequestParts } from "./build-request-parts"
Expand Down Expand Up @@ -270,13 +270,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const seed = (dir: string, info: Session) => {
const [, setStore] = globalSync.child(dir)
setStore("session", (list: Session[]) => {
const result = Binary.search(list, info.id, (item) => item.id)
const next = [...list]
if (result.found) {
next[result.index] = info
const next = list.filter((item) => item.id !== info.id)
if (info.parentID) {
next.push(info)
return next
}
next.splice(result.index, 0, info)

const index = next.findIndex((item) => !!item.parentID || compareSessionRecent(info, item) < 0)
next.splice(index === -1 ? next.length : index, 0, info)
return next
})
}
Expand Down
5 changes: 1 addition & 4 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,7 @@ function createGlobalSync() {
list: (query) => globalSDK.client.session.list(query),
})
.then((x) => {
const nonArchived = (x.data ?? [])
.filter((s) => !!s?.id)
.filter((s) => !s.time?.archived)
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
const nonArchived = (x.data ?? []).filter((s) => !!s?.id).filter((s) => !s.time?.archived)
const limit = store.limit
const childSessions = store.session.filter((s) => !!s.parentID)
const sessions = trimSessions([...nonArchived, ...childSessions], {
Expand Down
52 changes: 31 additions & 21 deletions packages/app/src/context/global-sync/event-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { compareSessionRecent, trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"

export function applyGlobalEvent(input: {
Expand Down Expand Up @@ -93,6 +93,26 @@ export function applyDirectoryEvent(input: {
vcsCache?: VcsCache
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
}) {
const sessionIndex = (sessionID: string) => input.store.session.findIndex((s) => s.id === sessionID)
const upsertSession = (info: Session) => {
const next = input.store.session.filter((session) => session.id !== info.id)
if (!info.parentID) {
const index = next.findIndex((session) => !!session.parentID || compareSessionRecent(info, session) < 0)
next.splice(index === -1 ? next.length : index, 0, info)
return next
}

const start = next.findIndex((session) => !!session.parentID)
if (start === -1) {
next.push(info)
return next
}

const offset = next.slice(start).findIndex((session) => compareSessionRecent(info, session) < 0)
next.splice(offset === -1 ? next.length : start + offset, 0, info)
return next
}

const event = input.event
switch (event.type) {
case "server.instance.disposed": {
Expand All @@ -101,28 +121,23 @@ export function applyDirectoryEvent(input: {
}
case "session.created": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (result.found) {
input.setStore("session", result.index, reconcile(info))
break
}
const next = input.store.session.slice()
next.splice(result.index, 0, info)
const found = sessionIndex(info.id) !== -1
const next = upsertSession(info)
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
if (!found && !info.parentID) input.setStore("sessionTotal", (value) => value + 1)
break
}
case "session.updated": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
const index = sessionIndex(info.id)
if (info.time.archived) {
if (result.found) {
if (index !== -1) {
input.setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
draft.splice(index, 1)
}),
)
}
Expand All @@ -131,25 +146,20 @@ export function applyDirectoryEvent(input: {
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
if (result.found) {
input.setStore("session", result.index, reconcile(info))
break
}
const next = input.store.session.slice()
next.splice(result.index, 0, info)
const next = upsertSession(info)
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" }))
cleanupDroppedSessionCaches(input.store, input.setStore, trimmed, input.setSessionTodo)
break
}
case "session.deleted": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (result.found) {
const index = sessionIndex(info.id)
if (index !== -1) {
input.setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
draft.splice(index, 1)
}),
)
}
Expand Down
18 changes: 15 additions & 3 deletions packages/app/src/context/global-sync/session-trim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,23 @@ describe("trimSessions", () => {
})

expect(result.map((x) => x.id)).toEqual([
"child-kept-by-permission",
"child-kept-by-recency",
"child-kept-by-root",
"root-1",
"root-2",
"child-kept-by-root",
"child-kept-by-permission",
"child-kept-by-recency",
])
})

test("preserves fetched root order instead of reshaping by id", () => {
const now = 20_000_000
const list = [
session({ id: "z", created: now - 20_000_000, updated: now - 20_000_000 }),
session({ id: "a", created: now - 19_000_000, updated: now - 19_000_000 }),
session({ id: "m", created: now - 18_000_000, updated: now - 18_000_000 }),
]

const result = trimSessions(list, { limit: 2, permission: {}, now })
expect(result.map((item) => item.id)).toEqual(["z", "a"])
})
})
15 changes: 9 additions & 6 deletions packages/app/src/context/global-sync/session-trim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ export function takeRecentSessions(sessions: Session[], limit: number, cutoff: n
if (seen.has(session.id)) continue
seen.add(session.id)
if (sessionUpdatedAt(session) <= cutoff) continue
const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0)
if (index === -1) selected.push(session)
if (index !== -1) selected.splice(index, 0, session)
if (selected.length > limit) selected.pop()
selected.push(session)
if (selected.length >= limit) break
}
return selected
}
Expand All @@ -36,10 +34,15 @@ export function trimSessions(
) {
const limit = Math.max(0, options.limit)
const cutoff = (options.now ?? Date.now()) - SESSION_RECENT_WINDOW
const seen = new Set<string>()
const all = input
.filter((s) => !!s?.id)
.filter((s) => !s.time?.archived)
.sort((a, b) => cmp(a.id, b.id))
.filter((s) => {
if (seen.has(s.id)) return false
seen.add(s.id)
return true
})
const roots = all.filter((s) => !s.parentID)
const children = all.filter((s) => !!s.parentID)
const base = roots.slice(0, limit)
Expand All @@ -52,5 +55,5 @@ export function trimSessions(
if (perms.length > 0) return true
return sessionUpdatedAt(s) > cutoff
})
return [...keepRoots, ...keepChildren].sort((a, b) => cmp(a.id, b.id))
return [...keepRoots, ...keepChildren]
}
5 changes: 2 additions & 3 deletions packages/app/src/context/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useGlobalSync } from "./global-sync"
import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { Binary } from "@opencode-ai/util/binary"
import { base64Encode } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64"
import { EventSessionError } from "@opencode-ai/sdk/v2"
Expand Down Expand Up @@ -208,8 +207,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const lookup = async (directory: string, sessionID?: string) => {
if (!sessionID) return undefined
const [syncStore] = globalSync.child(directory, { bootstrap: false })
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
if (match.found) return syncStore.session[match.index]
const session = syncStore.session.find((item) => item.id === sessionID)
if (session) return session
return globalSDK.client.session
.get({ directory, sessionID })
.then((x) => x.data)
Expand Down
31 changes: 17 additions & 14 deletions packages/app/src/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getSessionPrefetchPromise,
setSessionPrefetch,
} from "./global-sync/session-prefetch"
import { compareSessionRecent } from "./global-sync/session-trim"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
Expand Down Expand Up @@ -194,9 +195,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({

const getSession = (sessionID: string) => {
const store = current()[0]
const match = Binary.search(store.session, sessionID, (s) => s.id)
if (match.found) return store.session[match.index]
return undefined
return store.session.find((session) => session.id === sessionID)
}

const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
Expand Down Expand Up @@ -456,7 +455,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}
}

const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
const hasSession = store.session.some((session) => session.id === sessionID)
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
if (cached && hasSession && !opts?.force) return

Expand All @@ -471,12 +470,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore(
"session",
produce((draft) => {
const match = Binary.search(draft, sessionID, (s) => s.id)
if (match.found) {
draft[match.index] = data
const index = draft.findIndex((session) => session.id === sessionID)
if (index !== -1) {
draft[index] = data
return
}
draft.splice(match.index, 0, data)
if (data.parentID) {
draft.push(data)
return
}
const next = draft.findIndex(
(session) => !!session.parentID || compareSessionRecent(data, session) < 0,
)
draft.splice(next === -1 ? draft.length : next, 0, data)
}),
)
})
Expand Down Expand Up @@ -585,10 +591,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const [store, setStore] = globalSync.child(directory)
setStore("limit", (x) => x + count)
await client.session.list().then((x) => {
const sessions = (x.data ?? [])
.filter((s) => !!s?.id)
.sort((a, b) => cmp(a.id, b.id))
.slice(0, store.limit)
const sessions = (x.data ?? []).filter((s) => !!s?.id).slice(0, store.limit)
setStore("session", reconcile(sessions, { key: "id" }))
})
},
Expand All @@ -600,8 +603,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
await client.session.update({ sessionID, time: { archived: Date.now() } })
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
const index = draft.session.findIndex((s) => s.id === sessionID)
if (index !== -1) draft.session.splice(index, 1)
}),
)
},
Expand Down
Loading
Loading