feat(sessions): progressive loading, server-side search, and workspace scoping (#511)

# Session Pagination & Search Enhancement

## Changes

### Progressive Session Loading
- `fetchSessions()` now accepts `{ limit, search }` options
- Added pagination state: `sessionFetchLimit`, `sessionHasMore`
(50-session pages)
- Scroll-to-bottom sentinel triggers `loadMoreSessions()` via
IntersectionObserver
- Hydration loads initial 50 sessions; subsequent scrolls fetch 100,
150, etc.

### Server-Side Search
- Added `searchSessions()` using `session.list({ search, limit: 50,
directory })`
- Merges results into store (preserves active session, no replacement)
- Added `"sessionList.loading.more"` i18n key (7 locales)

### Hybrid Search Performance
- Client-side filtering runs first — instant results for loaded sessions
- Server search only fires when no client matches exist
- Debounce reduced from 300ms → 150ms for server fallback

### Workspace Scoping
- All `session.list()` calls pass `directory: instance.folder`
- Prevents cross-workspace session pollution

## Files
- `packages/ui/src/stores/session-state.ts`
- `packages/ui/src/stores/session-api.ts`
- `packages/ui/src/stores/instances.ts`
- `packages/ui/src/stores/sessions.ts`
- `packages/ui/src/components/session-list.tsx`
- `packages/ui/src/lib/i18n/messages/*/session.ts`

---------

Co-authored-by: Shantur Rathore <i@shantur.com>
This commit is contained in:
VooDisss 2026-06-02 12:32:34 +03:00 committed by GitHub
parent 873235ee1e
commit b3594d29e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 494 additions and 24 deletions

View file

@ -47,6 +47,7 @@ import {
clearActiveParentSession,
createSession,
fetchSessions,
getSessionFetchLimit,
updateSessionAgent,
updateSessionModel,
} from "./stores/sessions"
@ -405,7 +406,7 @@ const App: Component = () => {
clearActiveParentSession(instanceId)
try {
await fetchSessions(instanceId)
await fetchSessions(instanceId, { start: 0, limit: getSessionFetchLimit(instanceId) })
} catch (error) {
log.error("Failed to refresh sessions after closing", error)
}

View file

@ -20,6 +20,13 @@ import {
sessions as sessionStateSessions,
setActiveSessionFromList,
toggleSessionParentExpanded,
loadMoreSessions,
searchSessions,
getSessionHasMore,
clearSessionSearch,
getSessionSearchQuery,
getSessionSearchThreads,
isSessionSearchLoading,
} from "../stores/sessions"
import { getGitRepoStatus, getWorktreeSlugForParentSession } from "../stores/worktrees"
import { getLogger } from "../lib/logger"
@ -65,6 +72,71 @@ const SessionList: Component<SessionListProps> = (props) => {
onCleanup(() => window.clearInterval(timer))
})
const [sentinelEl, setSentinelEl] = createSignal<HTMLDivElement | null>(null)
const hasMore = createMemo(() => {
if (normalizedQuery()) return false
return getSessionHasMore(props.instanceId)
})
const isFetchingSessions = createMemo(() => {
return loading().fetchingSessions.get(props.instanceId) ?? false
})
createEffect(() => {
const el = sentinelEl()
if (!el || !hasMore() || isFetchingSessions()) return
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0]
if (entry?.isIntersecting && hasMore() && !isFetchingSessions()) {
void loadMoreSessions(props.instanceId).catch((error) => {
log.error("Failed to load more sessions:", error)
})
}
},
{ root: el.parentElement ?? null, rootMargin: "0px 0px 200px 0px" }
)
observer.observe(el)
onCleanup(() => observer.disconnect())
})
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
createEffect(() => {
const query = normalizedQuery()
if (!props.enableFilterBar) {
clearSessionSearch(props.instanceId)
return
}
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer)
}
if (!query) {
clearSessionSearch(props.instanceId)
return
}
// Always run server search in background for workspace-complete results.
// Client-side filtering (filteredThreads) shows instant results from loaded sessions.
const queryAtDispatch = query
searchDebounceTimer = setTimeout(() => {
void searchSessions(props.instanceId, queryAtDispatch)
.catch((error) => {
log.error("Failed to search sessions:", error)
})
}, 150)
onCleanup(() => {
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer)
}
})
})
const normalizeSessionLabel = (sessionId: string) => {
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
const title = (session?.title ?? "").trim()
@ -82,6 +154,12 @@ const SessionList: Component<SessionListProps> = (props) => {
const query = normalizedQuery()
if (!query) return props.threads
const searchQuery = getSessionSearchQuery(props.instanceId)
const searchLoading = isSessionSearchLoading(props.instanceId)
if (searchQuery === query && !searchLoading) {
return getSessionSearchThreads(props.instanceId)
}
const next: SessionThread[] = []
for (const thread of props.threads) {
const parentMatches = sessionMatchesQuery(thread.parent.id, query)
@ -795,10 +873,22 @@ const SessionList: Component<SessionListProps> = (props) => {
</>
)
}}
</For>
</div>
</Show>
</div>
</For>
<Show when={hasMore() || isFetchingSessions()}>
<div
ref={(el) => setSentinelEl(el)}
class="session-list-sentinel flex items-center justify-center py-3 text-text-weak text-xs"
data-session-sentinel
>
<Show when={isFetchingSessions()}>
<span class="animate-pulse">{t("sessionList.loading.more")}</span>
</Show>
</div>
</Show>
</div>
</Show>
</div>
<Show when={props.showFooter !== false}>
<div class="session-list-footer p-3 border-t border-base">

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Search sessions…",
"sessionList.filter.ariaLabel": "Search sessions",
"sessionList.loading.more": "Loading more sessions…",
"sessionList.selection.selectAllLabel": "Select all",
"sessionList.selection.selectAllAriaLabel": "Select all sessions",
"sessionList.selection.clearLabel": "Clear",

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Buscar sesiones…",
"sessionList.filter.ariaLabel": "Buscar sesiones",
"sessionList.loading.more": "Cargando más sesiones…",
"sessionList.selection.selectAllLabel": "Seleccionar todo",
"sessionList.selection.selectAllAriaLabel": "Seleccionar todas las sesiones",
"sessionList.selection.clearLabel": "Limpiar",

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Rechercher des sessions…",
"sessionList.filter.ariaLabel": "Rechercher des sessions",
"sessionList.loading.more": "Chargement de plus de sessions…",
"sessionList.selection.selectAllLabel": "Tout sélectionner",
"sessionList.selection.selectAllAriaLabel": "Sélectionner toutes les sessions",
"sessionList.selection.clearLabel": "Effacer",

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "חפש סשנים…",
"sessionList.filter.ariaLabel": "חפש סשנים",
"sessionList.loading.more": "טוען עוד סשנים…",
"sessionList.selection.selectAllLabel": "בחר הכל",
"sessionList.selection.selectAllAriaLabel": "בחר את כל הסשנים",
"sessionList.selection.clearLabel": "נקה",

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "セッションを検索…",
"sessionList.filter.ariaLabel": "セッションを検索",
"sessionList.loading.more": "セッションをさらに読み込んでいます…",
"sessionList.selection.selectAllLabel": "すべて選択",
"sessionList.selection.selectAllAriaLabel": "すべてのセッションを選択",
"sessionList.selection.clearLabel": "クリア",

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "Поиск сессий…",
"sessionList.filter.ariaLabel": "Поиск сессий",
"sessionList.loading.more": "Загрузка дополнительных сессий…",
"sessionList.selection.selectAllLabel": "Выбрать все",
"sessionList.selection.selectAllAriaLabel": "Выбрать все сессии",
"sessionList.selection.clearLabel": "Очистить",

View file

@ -47,6 +47,7 @@ export const sessionMessages = {
"sessionList.filter.placeholder": "搜索会话…",
"sessionList.filter.ariaLabel": "搜索会话",
"sessionList.loading.more": "正在加载更多会话…",
"sessionList.selection.selectAllLabel": "全选",
"sessionList.selection.selectAllAriaLabel": "选择所有会话",
"sessionList.selection.clearLabel": "清除",

View file

@ -17,6 +17,7 @@ import {
fetchAgents,
fetchProviders,
clearInstanceDraftPrompts,
resetSessionPagination,
} from "./sessions"
import {
ensureWorktreesLoaded,
@ -280,6 +281,7 @@ async function hydrateInstanceData(instanceId: string, options?: { force?: boole
await ensureWorktreesLoaded(instanceId)
await ensureWorktreeMapLoaded(instanceId)
}
resetSessionPagination(instanceId)
await fetchSessions(instanceId)
await fetchAgents(instanceId)
await fetchProviders(instanceId)

View file

@ -33,6 +33,15 @@ import {
cleanupBlankSessions,
syncInstanceSessionIndicator,
updateThreadTotalsForParent,
SESSION_PAGE_SIZE,
getSessionNextStart,
setSessionPage,
prependSessionListId,
removeSessionListId,
beginSessionSearch,
clearSessionSearch,
isLatestSessionSearch,
setSessionSearchResults,
} from "./session-state"
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
import { normalizeMessagePart } from "./message-v2/normalizers"
@ -120,7 +129,7 @@ interface SessionForkResponse {
}
}
async function fetchSessions(instanceId: string): Promise<void> {
async function fetchSessions(instanceId: string, options?: { start?: number; limit?: number }): Promise<void> {
const instance = instances().get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
@ -137,19 +146,28 @@ async function fetchSessions(instanceId: string): Promise<void> {
try {
const projectResponse = await rootClient.project.current()
const projectId = projectResponse.data?.id
const sessionListOptions = instance.folder ? { directory: instance.folder } : undefined
const start = options?.start ?? 0
const limit = options?.limit ?? SESSION_PAGE_SIZE
log.info("session.list", { instanceId, projectId, directory: sessionListOptions?.directory })
const response = sessionListOptions
? await rootClient.session.list(sessionListOptions)
: await rootClient.session.list()
const sessionListOptions: { directory?: string; roots?: boolean; start?: number; limit?: number } = {
roots: true,
start,
limit,
}
if (instance.folder) sessionListOptions.directory = instance.folder
log.info("session.list", { instanceId, projectId, start, limit, directory: sessionListOptions.directory })
const response = await rootClient.session.list(sessionListOptions)
const sessionMap = new Map<string, Session>()
if (!response.data || !Array.isArray(response.data)) {
setSessionPage(instanceId, [], start, false)
return
}
const hasMore = response.data.length >= limit
let statusById: Record<string, any> = {}
try {
const statusResponse = await rootClient.session.status()
@ -208,11 +226,17 @@ async function fetchSessions(instanceId: string): Promise<void> {
setSessions((prev) => {
const next = new Map(prev)
next.set(instanceId, sessionMap)
const instanceSessions = new Map(next.get(instanceId) ?? new Map())
for (const session of sessionMap.values()) {
instanceSessions.set(session.id, session)
}
next.set(instanceId, instanceSessions)
return next
})
syncInstanceSessionIndicator(instanceId, sessionMap)
setSessionPage(instanceId, Array.from(validSessionIds), start, hasMore)
syncInstanceSessionIndicator(instanceId)
setMessagesLoaded((prev) => {
const next = new Map(prev)
@ -220,7 +244,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
if (loadedSet) {
const filtered = new Set<string>()
for (const id of loadedSet) {
if (validSessionIds.has(id)) {
if (sessions().get(instanceId)?.has(id)) {
filtered.add(id)
}
}
@ -230,7 +254,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
})
pruneDraftPrompts(instanceId, new Set(sessionMap.keys()))
pruneDraftPrompts(instanceId, new Set(sessions().get(instanceId)?.keys() ?? []))
const parentIds = Array.from(sessionMap.values())
.filter((session) => session.parentId === null)
@ -255,6 +279,106 @@ async function fetchSessions(instanceId: string): Promise<void> {
}
}
async function loadMoreSessions(instanceId: string): Promise<void> {
await fetchSessions(instanceId, { start: getSessionNextStart(instanceId), limit: SESSION_PAGE_SIZE })
}
async function searchSessions(instanceId: string, query: string): Promise<void> {
const trimmedQuery = query.trim()
if (!trimmedQuery) return
const instance = instances().get(instanceId)
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
const rootClient = getRootClient(instanceId)
const requestId = beginSessionSearch(instanceId, trimmedQuery)
try {
log.info("session.search", { instanceId, query: trimmedQuery, directory: instance.folder })
const response = await rootClient.session.list({
search: trimmedQuery,
limit: SESSION_PAGE_SIZE,
directory: instance.folder,
})
if (!isLatestSessionSearch(instanceId, trimmedQuery, requestId)) return
const searchResults = response.data ?? []
if (searchResults.length === 0) {
setSessionSearchResults(instanceId, trimmedQuery, [], requestId)
return
}
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(next.get(instanceId) ?? new Map())
for (const apiSession of searchResults) {
const existingSession = instanceSessions.get(apiSession.id)
instanceSessions.set(apiSession.id, toClientSession(instanceId, apiSession, existingSession))
}
next.set(instanceId, instanceSessions)
return next
})
// Fetch any missing parents so child results are rendered correctly
const currentSessions = sessions().get(instanceId)
const missingParentIds = new Set<string>()
for (const apiSession of searchResults) {
const parentId = apiSession.parentID
if (parentId && !currentSessions?.has(parentId)) {
missingParentIds.add(parentId)
}
}
if (missingParentIds.size > 0) {
const parentFetches = Array.from(missingParentIds).map(async (parentId) => {
try {
const parentResponse = await rootClient.session.get({ sessionID: parentId })
if (parentResponse.data) {
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(next.get(instanceId) ?? new Map())
const existingSession = instanceSessions.get(parentId)
instanceSessions.set(parentId, toClientSession(instanceId, parentResponse.data, existingSession))
next.set(instanceId, instanceSessions)
return next
})
}
} catch (error) {
log.warn("Failed to fetch missing parent session:", { parentId, error })
}
})
await Promise.all(parentFetches)
}
if (!isLatestSessionSearch(instanceId, trimmedQuery, requestId)) return
const hydratedSessions = sessions().get(instanceId)
const hasUnrenderableChildResult = searchResults.some((session) => {
const parentId = session.parentID
return Boolean(parentId && !hydratedSessions?.has(parentId))
})
if (hasUnrenderableChildResult) {
clearSessionSearch(instanceId)
return
}
syncInstanceSessionIndicator(instanceId)
setSessionSearchResults(instanceId, trimmedQuery, searchResults.map((session) => session.id), requestId)
} catch (error) {
log.error("Failed to search sessions:", error)
if (isLatestSessionSearch(instanceId, trimmedQuery, requestId)) {
clearSessionSearch(instanceId)
}
throw error
}
}
function toClientSession(instanceId: string, apiSession: any, existingSession?: Session): Session {
return {
id: apiSession.id,
@ -311,10 +435,24 @@ async function fetchSessionChildren(instanceId: string, parentSessionId: string)
const currentSessions = sessions().get(instanceId)
const children = apiChildren.map((apiSession) => toClientSession(instanceId, apiSession, currentSessions?.get(apiSession.id)))
const returnedChildIds = new Set(children.map((child) => child.id))
let staleChildIds: string[] = []
setSessions((prev) => {
const next = new Map(prev)
const instanceSessions = new Map(next.get(instanceId))
staleChildIds = []
for (const session of instanceSessions.values()) {
if (session.parentId === parentSessionId && !returnedChildIds.has(session.id)) {
staleChildIds.push(session.id)
}
}
for (const staleChildId of staleChildIds) {
instanceSessions.delete(staleChildId)
}
for (const child of children) {
instanceSessions.set(child.id, child)
}
@ -322,6 +460,43 @@ async function fetchSessionChildren(instanceId: string, parentSessionId: string)
return next
})
if (staleChildIds.length > 0) {
const staleChildIdSet = new Set(staleChildIds)
setMessagesLoaded((prev) => {
const loadedSet = prev.get(instanceId)
if (!loadedSet) return prev
const updated = new Set(loadedSet)
let changed = false
for (const staleChildId of staleChildIdSet) {
changed = updated.delete(staleChildId) || changed
}
if (!changed) return prev
const next = new Map(prev)
next.set(instanceId, updated)
return next
})
setSessionInfoByInstance((prev) => {
const instanceInfo = prev.get(instanceId)
if (!instanceInfo) return prev
const updated = new Map(instanceInfo)
let changed = false
for (const staleChildId of staleChildIdSet) {
changed = updated.delete(staleChildId) || changed
}
if (!changed) return prev
const next = new Map(prev)
next.set(instanceId, updated)
return next
})
for (const staleChildId of staleChildIds) {
messageStoreBus.getOrCreate(instanceId).clearSession(staleChildId)
clearCacheForSession(instanceId, staleChildId)
}
}
syncInstanceSessionIndicator(instanceId)
updateThreadTotalsForParent(instanceId, parentSessionId)
@ -415,6 +590,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
})
syncInstanceSessionIndicator(instanceId)
prependSessionListId(instanceId, session.id)
const instanceProviders = providers().get(instanceId) || []
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
@ -593,6 +769,7 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
})
syncInstanceSessionIndicator(instanceId)
removeSessionListId(instanceId, sessionId)
clearSessionDraftPrompt(instanceId, sessionId)
@ -924,6 +1101,8 @@ export {
fetchProviders,
fetchSessions,
loadMoreSessions,
searchSessions,
fetchSessionChildren,
forkSession,
loadMessages,

View file

@ -62,6 +62,134 @@ type InstanceIndicatorCounts = {
const [instanceIndicatorCounts, setInstanceIndicatorCounts] = createSignal<Map<string, InstanceIndicatorCounts>>(new Map())
const SESSION_PAGE_SIZE = 100
type SessionPaginationState = {
ids: string[]
nextStart: number
hasMore: boolean
}
type SessionSearchState = {
query: string
ids: string[]
loading: boolean
requestId: number
}
const [sessionPagination, setSessionPagination] = createSignal<Map<string, SessionPaginationState>>(new Map())
const [sessionSearch, setSessionSearch] = createSignal<Map<string, SessionSearchState>>(new Map())
function getSessionPaginationState(instanceId: string): SessionPaginationState {
return sessionPagination().get(instanceId) ?? { ids: [], nextStart: 0, hasMore: true }
}
function getSessionListIds(instanceId: string): string[] {
return getSessionPaginationState(instanceId).ids
}
function getSessionFetchLimit(instanceId: string): number {
return Math.max(getSessionPaginationState(instanceId).ids.length, SESSION_PAGE_SIZE)
}
function getSessionNextStart(instanceId: string): number {
return getSessionPaginationState(instanceId).nextStart
}
function setSessionPage(instanceId: string, ids: string[], start: number, hasMore: boolean): void {
setSessionPagination((prev) => {
const next = new Map(prev)
const current = prev.get(instanceId) ?? { ids: [], nextStart: 0, hasMore: true }
const nextIds = start <= 0 ? ids : Array.from(new Set([...current.ids, ...ids]))
next.set(instanceId, {
ids: nextIds,
nextStart: start + ids.length,
hasMore,
})
return next
})
}
function getSessionHasMore(instanceId: string): boolean {
return getSessionPaginationState(instanceId).hasMore
}
function resetSessionPagination(instanceId: string): void {
setSessionPagination((prev) => {
const next = new Map(prev)
next.set(instanceId, { ids: [], nextStart: 0, hasMore: true })
return next
})
}
function prependSessionListId(instanceId: string, sessionId: string): void {
setSessionPagination((prev) => {
const next = new Map(prev)
const current = prev.get(instanceId) ?? { ids: [], nextStart: 0, hasMore: true }
const ids = [sessionId, ...current.ids.filter((id) => id !== sessionId)]
next.set(instanceId, { ...current, ids, nextStart: current.nextStart + (current.ids.includes(sessionId) ? 0 : 1) })
return next
})
}
function removeSessionListId(instanceId: string, sessionId: string): void {
setSessionPagination((prev) => {
const next = new Map(prev)
const current = prev.get(instanceId) ?? { ids: [], nextStart: 0, hasMore: true }
const ids = current.ids.filter((id) => id !== sessionId)
next.set(instanceId, { ...current, ids, nextStart: Math.max(0, current.nextStart - (ids.length === current.ids.length ? 0 : 1)) })
return next
})
}
function beginSessionSearch(instanceId: string, query: string): number {
const current = sessionSearch().get(instanceId)
const requestId = (current?.requestId ?? 0) + 1
setSessionSearch((prev) => {
const next = new Map(prev)
next.set(instanceId, { query, ids: current?.ids ?? [], loading: true, requestId })
return next
})
return requestId
}
function isLatestSessionSearch(instanceId: string, query: string, requestId: number): boolean {
const current = sessionSearch().get(instanceId)
return Boolean(current && current.query === query && current.requestId === requestId)
}
function setSessionSearchResults(instanceId: string, query: string, ids: string[], requestId: number): boolean {
if (!isLatestSessionSearch(instanceId, query, requestId)) return false
setSessionSearch((prev) => {
const next = new Map(prev)
next.set(instanceId, { query, ids, loading: false, requestId })
return next
})
return true
}
function clearSessionSearch(instanceId: string): void {
setSessionSearch((prev) => {
const current = prev.get(instanceId)
const requestId = (current?.requestId ?? 0) + 1
const next = new Map(prev)
next.set(instanceId, { query: "", ids: [], loading: false, requestId })
return next
})
}
function getSessionSearchResultIds(instanceId: string): string[] {
return sessionSearch().get(instanceId)?.ids ?? []
}
function getSessionSearchQuery(instanceId: string): string {
return sessionSearch().get(instanceId)?.query ?? ""
}
function isSessionSearchLoading(instanceId: string): boolean {
return sessionSearch().get(instanceId)?.loading ?? false
}
function getIndicatorBucket(session: Pick<Session, "status" | "pendingPermission" | "pendingQuestion">): InstanceSessionIndicatorStatus | "idle" {
if (session.pendingPermission || session.pendingQuestion) {
return "permission"
@ -477,9 +605,9 @@ function getOrCreateSessionThreadCache(instanceId: string): SessionThreadCache {
return cache
}
function getSessionThreads(instanceId: string): SessionThread[] {
function buildSessionThreads(instanceId: string, rootIds: string[], childIds?: Set<string>): SessionThread[] {
const instanceSessions = sessions().get(instanceId)
if (!instanceSessions || instanceSessions.size === 0) {
if (!instanceSessions || instanceSessions.size === 0 || rootIds.length === 0) {
sessionThreadCache.delete(instanceId)
return []
}
@ -487,17 +615,12 @@ function getSessionThreads(instanceId: string): SessionThread[] {
const cache = getOrCreateSessionThreadCache(instanceId)
const seenParents = new Set<string>()
const parents: Session[] = []
const childrenByParent = new Map<string, Session[]>()
for (const session of instanceSessions.values()) {
if (session.parentId === null) {
parents.push(session)
continue
}
const parentId = session.parentId
if (!parentId) continue
if (childIds && !childIds.has(session.id)) continue
const children = childrenByParent.get(parentId)
if (children) {
children.push(session)
@ -508,7 +631,10 @@ function getSessionThreads(instanceId: string): SessionThread[] {
const threads: SessionThread[] = []
for (const parent of parents) {
for (const parentId of rootIds) {
const parent = instanceSessions.get(parentId)
if (!parent || parent.parentId !== null) continue
seenParents.add(parent.id)
const children = childrenByParent.get(parent.id) ?? []
@ -550,6 +676,34 @@ function getSessionThreads(instanceId: string): SessionThread[] {
return threads
}
function getSessionThreads(instanceId: string): SessionThread[] {
return buildSessionThreads(instanceId, getSessionListIds(instanceId))
}
function getSessionSearchThreads(instanceId: string): SessionThread[] {
const resultIds = getSessionSearchResultIds(instanceId)
if (resultIds.length === 0) return []
const instanceSessions = sessions().get(instanceId)
if (!instanceSessions) return []
const rootIds: string[] = []
const childIds = new Set<string>()
for (const sessionId of resultIds) {
const session = instanceSessions.get(sessionId)
if (!session) continue
if (session.parentId === null) {
rootIds.push(session.id)
} else {
childIds.add(session.id)
if (!rootIds.includes(session.parentId)) rootIds.push(session.parentId)
}
}
return buildSessionThreads(instanceId, rootIds, childIds)
}
function isSessionParentExpanded(instanceId: string, parentSessionId: string): boolean {
return Boolean(expandedSessionParents().get(instanceId)?.has(parentSessionId))
}
@ -826,6 +980,7 @@ export {
getChildSessions,
getSessionFamily,
getSessionThreads,
getSessionSearchThreads,
getVisibleSessionIds,
isSessionParentExpanded,
setSessionParentExpanded,
@ -837,4 +992,22 @@ export {
getSessionInfo,
isBlankSession,
cleanupBlankSessions,
SESSION_PAGE_SIZE,
sessionPagination,
sessionSearch,
getSessionListIds,
getSessionFetchLimit,
getSessionNextStart,
setSessionPage,
getSessionHasMore,
resetSessionPagination,
prependSessionListId,
removeSessionListId,
beginSessionSearch,
isLatestSessionSearch,
setSessionSearchResults,
clearSessionSearch,
getSessionSearchResultIds,
getSessionSearchQuery,
isSessionSearchLoading,
}

View file

@ -17,6 +17,8 @@ import {
getSessionDraftPrompt,
getSessionFamily,
getSessionInfo,
getSessionSearchQuery,
getSessionSearchThreads,
getSessionThreads,
getThreadTotals,
getSessions,
@ -37,6 +39,11 @@ import {
setSessionParentExpanded,
setSessionStatus,
toggleSessionParentExpanded,
clearSessionSearch,
getSessionFetchLimit,
getSessionHasMore,
isSessionSearchLoading,
resetSessionPagination,
} from "./session-state"
import { getDefaultModel } from "./session-models"
@ -46,6 +53,8 @@ import {
fetchAgents,
fetchProviders,
fetchSessions,
loadMoreSessions,
searchSessions,
fetchSessionChildren,
forkSession,
loadMessages,
@ -111,6 +120,8 @@ export {
fetchAgents,
fetchProviders,
fetchSessions,
loadMoreSessions,
searchSessions,
fetchSessionChildren,
forkSession,
getActiveParentSession,
@ -121,6 +132,8 @@ export {
getSessionDraftPrompt,
getSessionFamily,
getSessionInfo,
getSessionSearchQuery,
getSessionSearchThreads,
getSessionThreads,
getThreadTotals,
getSessions,
@ -145,5 +158,10 @@ export {
toggleSessionParentExpanded,
updateSessionAgent,
updateSessionModel,
clearSessionSearch,
getSessionFetchLimit,
getSessionHasMore,
isSessionSearchLoading,
resetSessionPagination,
}
export type { SessionInfo }