This commit is contained in:
Pleasure1234 2025-12-18 12:21:25 +00:00 committed by GitHub
commit b9405cb6c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 138 additions and 26 deletions

View File

@ -172,10 +172,19 @@ export class AgentApiClient {
}
}
public async listSessions(agentId: string): Promise<ListAgentSessionsResponse> {
public async listSessions(agentId: string, options?: ListOptions): Promise<ListAgentSessionsResponse> {
const url = this.getSessionPaths(agentId).base
try {
const response = await this.axios.get(url)
const params = new URLSearchParams()
if (options?.limit !== undefined) params.append('limit', String(options.limit))
if (options?.offset !== undefined) params.append('offset', String(options.offset))
if (options?.sortBy) params.append('sortBy', options.sortBy)
if (options?.orderBy) params.append('orderBy', options.orderBy)
const queryString = params.toString()
const fullUrl = queryString ? `${url}?${queryString}` : url
const response = await this.axios.get(fullUrl)
const result = ListAgentSessionsResponseSchema.safeParse(response.data)
if (!result.success) {
throw new Error('Not a valid Sessions array.')

View File

@ -1,29 +1,72 @@
import type { CreateAgentSessionResponse, CreateSessionForm, GetAgentSessionResponse } from '@renderer/types'
import type {
CreateAgentSessionResponse,
CreateSessionForm,
GetAgentSessionResponse,
ListAgentSessionsResponse
} from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import useSWRInfinite from 'swr/infinite'
import { useAgentClient } from './useAgentClient'
const PAGE_SIZE = 20
export const useSessions = (agentId: string | null) => {
const { t } = useTranslation()
const client = useAgentClient()
const key = agentId ? client.getSessionPaths(agentId).base : null
const fetcher = async () => {
if (!agentId) throw new Error('No active agent.')
const data = await client.listSessions(agentId)
return data.data
const getKey = (pageIndex: number, previousPageData: ListAgentSessionsResponse | null) => {
if (!agentId) return null
if (previousPageData && previousPageData.data.length === 0) return null
return [client.getSessionPaths(agentId).base, pageIndex]
}
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
const fetcher = async ([, pageIndex]: [string, number]) => {
if (!agentId) throw new Error('No active agent.')
return await client.listSessions(agentId, {
limit: PAGE_SIZE,
offset: pageIndex * PAGE_SIZE
})
}
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(getKey, fetcher)
const sessions = useMemo(() => {
if (!data) return []
return data.flatMap((page) => page.data)
}, [data])
const total = data?.[0]?.total ?? 0
const hasMore = sessions.length < total
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined')
const loadMore = useCallback(() => {
if (!isLoadingMore && hasMore) {
setSize(size + 1)
}
}, [isLoadingMore, hasMore, setSize, size])
const createSession = useCallback(
async (form: CreateSessionForm): Promise<CreateAgentSessionResponse | null> => {
if (!agentId) return null
try {
const result = await client.createSession(agentId, form)
await mutate((prev) => [result, ...(prev ?? [])], { revalidate: false })
await mutate(
(prev) => {
if (!prev || prev.length === 0) {
return [{ data: [result], total: 1, limit: PAGE_SIZE, offset: 0 }]
}
const newTotal = prev[0].total + 1
return prev.map((page, index) => ({
...page,
data: index === 0 ? [result, ...page.data] : page.data,
total: newTotal
}))
},
{ revalidate: false }
)
return result
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.create.error.failed')))
@ -38,7 +81,14 @@ export const useSessions = (agentId: string | null) => {
if (!agentId) return null
try {
const result = await client.getSession(agentId, id)
mutate((prev) => prev?.map((session) => (session.id === result.id ? result : session)))
mutate(
(prev) =>
prev?.map((page) => ({
...page,
data: page.data.map((session) => (session.id === result.id ? result : session))
})),
{ revalidate: false }
)
return result
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.get.error.failed')))
@ -53,7 +103,18 @@ export const useSessions = (agentId: string | null) => {
if (!agentId) return false
try {
await client.deleteSession(agentId, id)
mutate((prev) => prev?.filter((session) => session.id !== id))
mutate(
(prev) => {
if (!prev || prev.length === 0) return prev
const newTotal = prev[0].total - 1
return prev.map((page) => ({
...page,
data: page.data.filter((session) => session.id !== id),
total: newTotal
}))
},
{ revalidate: false }
)
return true
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
@ -64,9 +125,14 @@ export const useSessions = (agentId: string | null) => {
)
return {
sessions: data ?? [],
sessions,
total,
hasMore,
error,
isLoading,
isLoadingMore,
isValidating,
loadMore,
createSession,
getSession,
deleteSession

View File

@ -1,4 +1,4 @@
import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList'
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
import { useSessions } from '@renderer/hooks/agents/useSessions'
import { useRuntime } from '@renderer/hooks/useRuntime'
@ -12,7 +12,8 @@ import {
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { Alert, Spin } from 'antd'
import { motion } from 'framer-motion'
import { memo, useCallback, useEffect } from 'react'
import { throttle } from 'lodash'
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -25,11 +26,39 @@ interface SessionsProps {
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
const { t } = useTranslation()
const { sessions, isLoading, error, deleteSession } = useSessions(agentId)
const { sessions, isLoading, error, deleteSession, hasMore, loadMore, isLoadingMore } = useSessions(agentId)
const { chat } = useRuntime()
const { activeSessionIdMap } = chat
const dispatch = useAppDispatch()
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
const listRef = useRef<DynamicVirtualListRef>(null)
// Throttled scroll handler to avoid excessive calls
const handleScroll = useMemo(
() =>
throttle(() => {
const scrollElement = listRef.current?.scrollElement()
if (!scrollElement) return
const { scrollTop, scrollHeight, clientHeight } = scrollElement
if (scrollHeight - scrollTop - clientHeight < 100 && hasMore && !isLoadingMore) {
loadMore()
}
}, 150),
[hasMore, isLoadingMore, loadMore]
)
// Handle scroll to load more
useEffect(() => {
const scrollElement = listRef.current?.scrollElement()
if (!scrollElement) return
scrollElement.addEventListener('scroll', handleScroll)
return () => {
handleScroll.cancel()
scrollElement.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
const setActiveSessionId = useCallback(
(agentId: string, sessionId: string | null) => {
@ -97,6 +126,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
return (
<StyledVirtualList
ref={listRef}
className="sessions-tab"
list={sessions}
estimateSize={() => 9 * 4}
@ -108,14 +138,21 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
{t('agent.session.add.title')}
</AddButton>
}>
{(session) => (
<SessionItem
key={session.id}
session={session}
agentId={agentId}
onDelete={() => handleDeleteSession(session.id)}
onPress={() => setActiveSessionId(agentId, session.id)}
/>
{(session, index) => (
<>
<SessionItem
key={session.id}
session={session}
agentId={agentId}
onDelete={() => handleDeleteSession(session.id)}
onPress={() => setActiveSessionId(agentId, session.id)}
/>
{index === sessions.length - 1 && isLoadingMore && (
<div className="flex justify-center py-2">
<Spin size="small" />
</div>
)}
</>
)}
</StyledVirtualList>
)