diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 37d4d6891..15a920584 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -225,6 +225,65 @@ function createGlobalSync() { const sessionLoads = new Map>() const sessionMeta = new Map() + const sessionRecentWindow = 4 * 60 * 60 * 1000 + const sessionRecentLimit = 50 + + function sessionUpdatedAt(session: Session) { + return session.time.updated ?? session.time.created + } + + function compareSessionRecent(a: Session, b: Session) { + const aUpdated = sessionUpdatedAt(a) + const bUpdated = sessionUpdatedAt(b) + if (aUpdated !== bUpdated) return bUpdated - aUpdated + return a.id.localeCompare(b.id) + } + + function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) { + if (limit <= 0) return [] as Session[] + const selected: Session[] = [] + const seen = new Set() + for (const session of sessions) { + if (!session?.id) continue + 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() + } + return selected + } + + function trimSessions(input: Session[], options: { limit: number; permission: Record }) { + const limit = Math.max(0, options.limit) + const cutoff = Date.now() - sessionRecentWindow + const all = input + .filter((s) => !!s?.id) + .filter((s) => !s.time?.archived) + .sort((a, b) => a.id.localeCompare(b.id)) + + const roots = all.filter((s) => !s.parentID) + const children = all.filter((s) => !!s.parentID) + + const base = roots.slice(0, limit) + const recent = takeRecentSessions(roots.slice(limit), sessionRecentLimit, cutoff) + const keepRoots = [...base, ...recent] + + const keepRootIds = new Set(keepRoots.map((s) => s.id)) + const keepChildren = children.filter((s) => { + if (s.parentID && keepRootIds.has(s.parentID)) return true + const perms = options.permission[s.id] ?? [] + if (perms.length > 0) return true + return sessionUpdatedAt(s) > cutoff + }) + + return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id)) + } + function ensureChild(directory: string) { if (!directory) console.error("No directory provided") if (!children[directory]) { @@ -323,7 +382,13 @@ function createGlobalSync() { const [store, setStore] = child(directory, { bootstrap: false }) const meta = sessionMeta.get(directory) - if (meta && meta.limit >= store.limit) return + if (meta && meta.limit >= store.limit) { + const next = trimSessions(store.session, { limit: store.limit, permission: store.permission }) + if (next.length !== store.session.length) { + setStore("session", reconcile(next, { key: "id" })) + } + return + } const promise = globalSDK.client.session .list({ directory, roots: true }) @@ -337,21 +402,9 @@ function createGlobalSync() { // a request is in-flight still get the expanded result. const limit = store.limit - const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory)) - if (sandboxWorkspace) { - setStore("sessionTotal", nonArchived.length) - setStore("session", reconcile(nonArchived, { key: "id" })) - sessionMeta.set(directory, { limit }) - return - } + const children = store.session.filter((s) => !!s.parentID) + const sessions = trimSessions([...nonArchived, ...children], { limit, permission: store.permission }) - const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000 - // Include up to the limit, plus any updated in the last 4 hours - const sessions = nonArchived.filter((s, i) => { - 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" })) @@ -536,25 +589,25 @@ function createGlobalSync() { break } case "session.created": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) + const info = event.properties.info + const result = Binary.search(store.session, info.id, (s) => s.id) if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) + setStore("session", result.index, reconcile(info)) break } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - if (!event.properties.info.parentID) { - setStore("sessionTotal", store.sessionTotal + 1) + const next = store.session.slice() + next.splice(result.index, 0, info) + const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission }) + setStore("session", reconcile(trimmed, { key: "id" })) + if (!info.parentID) { + setStore("sessionTotal", (value) => value + 1) } break } case "session.updated": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (event.properties.info.time.archived) { + const info = event.properties.info + const result = Binary.search(store.session, info.id, (s) => s.id) + if (info.time.archived) { if (result.found) { setStore( "session", @@ -563,20 +616,18 @@ function createGlobalSync() { }), ) } - if (event.properties.info.parentID) break + if (info.parentID) break setStore("sessionTotal", (value) => Math.max(0, value - 1)) break } if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) + setStore("session", result.index, reconcile(info)) break } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) + const next = store.session.slice() + next.splice(result.index, 0, info) + const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission }) + setStore("session", reconcile(trimmed, { key: "id" })) break } case "session.deleted": { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index fd6f6e527..b13cb1ac3 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1592,6 +1592,7 @@ export default function Layout(props: ParentProps) { mobile?: boolean dense?: boolean popover?: boolean + children?: Map }): JSX.Element => { const notification = useNotification() const notifications = createMemo(() => notification.session.unseen(props.session.id)) @@ -1600,6 +1601,16 @@ export default function Layout(props: ParentProps) { const hasPermissions = createMemo(() => { const permissions = sessionStore.permission?.[props.session.id] ?? [] if (permissions.length > 0) return true + + const childIDs = props.children?.get(props.session.id) + if (childIDs) { + for (const id of childIDs) { + const childPermissions = sessionStore.permission?.[id] ?? [] + if (childPermissions.length > 0) return true + } + return false + } + const childSessions = sessionStore.session.filter((s) => s.parentID === props.session.id) for (const child of childSessions) { const childPermissions = sessionStore.permission?.[child.id] ?? [] @@ -1898,6 +1909,19 @@ export default function Layout(props: ParentProps) { .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions(Date.now())), ) + const children = createMemo(() => { + const map = new Map() + for (const session of workspaceStore.session) { + if (!session.parentID) continue + const existing = map.get(session.parentID) + if (existing) { + existing.push(session.id) + continue + } + map.set(session.parentID, [session.id]) + } + return map + }) const local = createMemo(() => props.directory === props.project.worktree) const active = createMemo(() => { const current = params.dir ? base64Decode(params.dir) : "" @@ -1911,10 +1935,9 @@ export default function Layout(props: ParentProps) { const open = createMemo(() => store.workspaceExpanded[props.directory] ?? local()) const boot = createMemo(() => open() || active()) const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0) - const hasMore = createMemo(() => local() && workspaceStore.sessionTotal > workspaceStore.session.length) + const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const busy = createMemo(() => isBusy(props.directory)) const loadMore = async () => { - if (!local()) return setWorkspaceStore("limit", (limit) => limit + 5) await globalSync.project.loadSessions(props.directory) } @@ -2073,7 +2096,9 @@ export default function Layout(props: ParentProps) { - {(session) => } + {(session) => ( + + )}
@@ -2288,8 +2313,21 @@ export default function Layout(props: ParentProps) { .filter((session) => !session.parentID && !session.time?.archived) .toSorted(sortSessions(Date.now())), ) + const children = createMemo(() => { + const map = new Map() + for (const session of workspaceStore.session) { + if (!session.parentID) continue + const existing = map.get(session.parentID) + if (existing) { + existing.push(session.id) + continue + } + map.set(session.parentID, [session.id]) + } + return map + }) const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0) - const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length) + const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length) const loadMore = async () => { setWorkspaceStore("limit", (limit) => limit + 5) await globalSync.project.loadSessions(props.project.worktree) @@ -2308,7 +2346,7 @@ export default function Layout(props: ParentProps) { - {(session) => } + {(session) => }