fix(agents): use correct SWRInfinite cache key and add optimistic update for session editing

Fixes SWRInfinite cache key usage and adds optimistic updates for session editing.
This commit is contained in:
Phantom
2026-03-15 11:32:13 +08:00
committed by GitHub
parent c6ce1deecd
commit 8b98b8f9c6
5 changed files with 46 additions and 26 deletions

View File

@@ -55,6 +55,8 @@ const processError = (error: unknown, fallbackMessage: string) => {
return new Error(fallbackMessage, { cause: error })
}
export const DEFAULT_SESSION_PAGE_SIZE = 20
export class AgentApiClient {
private axios: Axios
private apiVersion: ApiVersion = 'v1'

View File

@@ -1,3 +1,4 @@
import { DEFAULT_SESSION_PAGE_SIZE } from '@renderer/api/agent'
import type {
AgentSessionEntity,
CreateAgentSessionResponse,
@@ -12,9 +13,7 @@ import useSWRInfinite from 'swr/infinite'
import { useAgentClient } from './useAgentClient'
const DEFAULT_PAGE_SIZE = 20
export const useSessions = (agentId: string | null, pageSize = DEFAULT_PAGE_SIZE) => {
export const useSessions = (agentId: string | null, pageSize = DEFAULT_SESSION_PAGE_SIZE) => {
const { t } = useTranslation()
const client = useAgentClient()

View File

@@ -1,12 +1,34 @@
import { DEFAULT_SESSION_PAGE_SIZE } from '@renderer/api/agent'
import type { AgentSessionEntity, ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
import type { UpdateAgentBaseOptions, UpdateAgentSessionFunction } from '@renderer/types/agent'
import { getErrorMessage } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import { unstable_serialize } from 'swr/infinite'
import { useAgentClient } from './useAgentClient'
type InfiniteData = ListAgentSessionsResponse[]
const mutateInfiniteList = (
infKey: string,
sessionId: string,
updater: (session: AgentSessionEntity) => AgentSessionEntity
) => {
mutate<InfiniteData>(
infKey,
(prev) => {
if (!prev) return prev
return prev.map((page) => ({
...page,
data: page.data.map((session) => (session.id === sessionId ? updater(session) : session))
}))
},
{ revalidate: false }
)
}
export const useUpdateSession = (agentId: string | null) => {
const { t } = useTranslation()
const client = useAgentClient()
@@ -17,20 +39,26 @@ export const useUpdateSession = (agentId: string | null) => {
const paths = client.getSessionPaths(agentId)
const listKey = paths.base
const sessionId = form.id
const itemKey = paths.withId(sessionId)
const infKey = unstable_serialize(() => [listKey, 0, DEFAULT_SESSION_PAGE_SIZE])
// Optimistic update
mutateInfiniteList(infKey, sessionId, (session) => ({ ...session, ...form }))
mutate<AgentSessionEntity>(itemKey, (prev) => (prev ? { ...prev, ...form } : prev), { revalidate: false })
try {
const itemKey = paths.withId(sessionId)
// may change to optimistic update
const result = await client.updateSession(agentId, form)
mutate<ListAgentSessionsResponse['data']>(
listKey,
(prev) => prev?.map((session) => (session.id === result.id ? result : session)) ?? []
)
mutate(itemKey, result)
// Update with server response
mutateInfiniteList(infKey, sessionId, () => result)
mutate(itemKey, result, { revalidate: false })
if (options?.showSuccessToast ?? true) {
window.toast.success(t('common.update_success'))
}
return result
} catch (error) {
// Rollback: revalidate to get fresh data
mutate(infKey)
mutate(itemKey)
window.toast.error({ title: t('agent.session.update.error.failed'), description: getErrorMessage(error) })
return undefined
}

View File

@@ -188,9 +188,9 @@ const SessionItem = ({ session, agentId, onDelete, onPress }: SessionItemProps)
<SessionEditInput {...inputProps} style={{ opacity: isSaving ? 0.5 : 1 }} />
) : (
<>
<SessionName>
<div className="truncate text-[13px]">
<SessionLabel session={session} />
</SessionName>
</div>
<DeleteButton />
</>
)}
@@ -256,15 +256,6 @@ const SessionNameContainer = styled.div`
justify-content: space-between;
`
const SessionName = styled.div`
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 13px;
position: relative;
`
const SessionEditInput = styled.input`
background: var(--color-background);
border: none;

View File

@@ -90,13 +90,13 @@ export type AgentLabelProps = {
hideIcon?: boolean
}
export const AgentLabel: React.FC<AgentLabelProps> = ({ agent, classNames, hideIcon }) => {
export const AgentLabel = ({ agent, classNames, hideIcon }: AgentLabelProps) => {
const emoji = agent?.configuration?.avatar
return (
<div className={cn('flex w-full items-center gap-2 truncate', classNames?.container)}>
{!hideIcon && <EmojiIcon emoji={emoji || '⭐️'} className={classNames?.avatar} size={24} />}
<span className={cn('truncate', 'text-[var(--color-text)]', classNames?.name)}>
<span className={cn('truncate', 'text-(--color-text)', classNames?.name)}>
{agent?.name ?? (agent?.type ? getAgentTypeLabel(agent.type) : '')}
</span>
</div>
@@ -108,11 +108,11 @@ export type SessionLabelProps = {
className?: string
}
export const SessionLabel: React.FC<SessionLabelProps> = ({ session, className }) => {
export const SessionLabel = ({ session, className }: SessionLabelProps) => {
const displayName = session?.name ?? session?.id
return (
<>
<span className={cn('truncate text-[var(--color-text)] text-sm', className)}>{displayName}</span>
<span className={cn('truncate text-(--color-text) text-sm', className)}>{displayName}</span>
</>
)
}
@@ -149,7 +149,7 @@ export const SettingsContainer: React.FC<React.ComponentPropsWithRef<'div'> & Sc
...props
}) => {
return (
<Scrollbar className={cn('p-[16px]', className)} {...props}>
<Scrollbar className={cn('p-4', className)} {...props}>
{children}
</Scrollbar>
)