Merge branch 'feat/agents-new' of https://github.com/CherryHQ/cherry-studio into feat/agents-new

This commit is contained in:
suyao 2025-09-22 19:24:54 +08:00
commit c3adcf663f
No known key found for this signature in database
15 changed files with 118 additions and 37 deletions

View File

@ -20,6 +20,7 @@ import { getModelLogo } from '@renderer/config/models'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useApiModels } from '@renderer/hooks/agents/useModels'
import { useSessions } from '@renderer/hooks/agents/useSessions'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types'
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -86,7 +87,8 @@ export const SessionModal: React.FC<Props> = ({
const { t } = useTranslation()
const loadingRef = useRef(false)
// const { setTimeoutTimer } = useTimer()
const { createSession, updateSession } = useSessions(agentId)
const { createSession } = useSessions(agentId)
const updateSession = useUpdateSession(agentId)
// Only support claude code for now
const { models } = useApiModels({ providerType: 'anthropic' })
const { agent } = useAgent(agentId)
@ -164,7 +166,7 @@ export const SessionModal: React.FC<Props> = ({
}
if (form.accessible_paths.length === 0) {
window.toast.error(t('agent.session.accessible_paths.required'))
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
loadingRef.current = false
return
}

View File

@ -1,4 +1,4 @@
import { CreateSessionForm, UpdateSessionForm } from '@renderer/types'
import { CreateSessionForm } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@ -46,25 +46,14 @@ export const useSessions = (agentId: string) => {
const deleteSession = useCallback(
async (id: string) => {
if (!agentId) return
if (!agentId) return false
try {
await client.deleteSession(agentId, id)
mutate((prev) => prev?.filter((session) => session.id !== id))
return true
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
}
},
[agentId, client, mutate, t]
)
const updateSession = useCallback(
async (form: UpdateSessionForm) => {
if (!agentId) return
try {
const result = await client.updateSession(agentId, form)
mutate((prev) => prev?.map((session) => (session.id === form.id ? result : session)))
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.update.error.failed')))
return false
}
},
[agentId, client, mutate, t]
@ -76,7 +65,6 @@ export const useSessions = (agentId: string) => {
isLoading,
createSession,
getSession,
deleteSession,
updateSession
deleteSession
}
}

View File

@ -0,0 +1,36 @@
import { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import { useAgentClient } from './useAgentClient'
export const useUpdateSession = (agentId: string) => {
const { t } = useTranslation()
const client = useAgentClient()
const paths = client.getSessionPaths(agentId)
const listKey = paths.base
const updateSession = useCallback(
async (form: UpdateSessionForm) => {
const sessionId = form.id
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)
window.toast.success(t('common.update_success'))
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed')))
}
},
[agentId, client, listKey, paths, t]
)
return updateSession
}

View File

@ -52,7 +52,8 @@
"delete": {
"content": "Are you sure to delete this session?",
"error": {
"failed": "Failed to delete the session"
"failed": "Failed to delete the session",
"last": "At least one session must be kept"
},
"title": "Delete session"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "确定要删除此会话吗?",
"error": {
"failed": "删除会话失败"
"failed": "删除会话失败",
"last": "至少需要保留一个会话"
},
"title": "删除会话"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "您確定要刪除此工作階段嗎?",
"error": {
"failed": "無法刪除工作階段"
"failed": "無法刪除工作階段",
"last": "至少必須保留一個工作階段"
},
"title": "刪除工作階段"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;",
"error": {
"failed": "Αποτυχία διαγραφής της συνεδρίας"
"failed": "Αποτυχία διαγραφής της συνεδρίας",
"last": "Τουλάχιστον μία συνεδρία πρέπει να διατηρηθεί"
},
"title": "Διαγραφή συνεδρίας"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "¿Estás seguro de eliminar esta sesión?",
"error": {
"failed": "Error al eliminar la sesión"
"failed": "Error al eliminar la sesión",
"last": "Debe mantenerse al menos una sesión"
},
"title": "Eliminar sesión"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "Êtes-vous sûr de vouloir supprimer cette session ?",
"error": {
"failed": "Échec de la suppression de la session"
"failed": "Échec de la suppression de la session",
"last": "Au moins une session doit être conservée"
},
"title": "Supprimer la session"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "このセッションを削除してもよろしいですか?",
"error": {
"failed": "セッションの削除に失敗しました"
"failed": "セッションの削除に失敗しました",
"last": "少なくとも1つのセッションを維持する必要があります"
},
"title": "セッションを削除"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "Tem certeza de que deseja excluir esta sessão?",
"error": {
"failed": "Falha ao excluir a sessão"
"failed": "Falha ao excluir a sessão",
"last": "Pelo menos uma sessão deve ser mantida"
},
"title": "Excluir sessão"
},

View File

@ -52,7 +52,8 @@
"delete": {
"content": "Вы уверены, что хотите удалить этот сеанс?",
"error": {
"failed": "Не удалось удалить сеанс"
"failed": "Не удалось удалить сеанс",
"last": "Должен быть сохранён хотя бы один сеанс"
},
"title": "Удалить сеанс"
},

View File

@ -13,11 +13,13 @@ interface SessionItemProps {
session: AgentSessionEntity
// use external agentId as SSOT, instead of session.agent_id
agentId: string
isDisabled?: boolean
isLoading?: boolean
onDelete: () => void
onPress: () => void
}
const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress }) => {
const SessionItem: FC<SessionItemProps> = ({ session, agentId, isDisabled, isLoading, onDelete, onPress }) => {
const { t } = useTranslation()
const { isOpen, onOpen, onClose } = useDisclosure()
const { chat } = useRuntime()
@ -38,7 +40,11 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
<>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
<ButtonContainer
isDisabled={isDisabled}
isLoading={isLoading}
onPress={onPress}
className={isActive ? 'active' : ''}>
<SessionLabelContainer className="name" title={session.name ?? session.id}>
<SessionLabel />
</SessionLabelContainer>

View File

@ -1,9 +1,14 @@
import { Alert, Button, Spinner } from '@heroui/react'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useSessions } from '@renderer/hooks/agents/useSessions'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useAppDispatch } from '@renderer/store'
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
import {
setActiveSessionIdAction,
setActiveTopicOrSessionAction,
setSessionWaitingAction
} from '@renderer/store/runtime'
import { CreateSessionForm } from '@renderer/types'
import { AnimatePresence, motion } from 'framer-motion'
import { Plus } from 'lucide-react'
@ -22,8 +27,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
const { t } = useTranslation()
const { agent } = useAgent(agentId)
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
const updateSession = useUpdateSession(agentId)
const { chat } = useRuntime()
const { activeSessionId } = chat
const { activeSessionId, sessionWaiting } = chat
const dispatch = useAppDispatch()
const setActiveSessionId = useCallback(
@ -34,14 +40,38 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
[dispatch]
)
const handleCreateSession = useCallback(() => {
const handleCreateSession = useCallback(async () => {
if (!agent) return
const session = {
...agent,
id: undefined
} satisfies CreateSessionForm
createSession(session)
}, [agent, createSession])
const created = await createSession(session)
if (created) {
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
}
}, [agent, agentId, createSession, dispatch])
const handleDeleteSession = useCallback(
async (id: string) => {
if (sessions.length === 1) {
window.toast.error(t('agent.session.delete.error.last'))
return
}
dispatch(setSessionWaitingAction({ id, value: true }))
const success = await deleteSession(id)
if (success) {
const newSessionId = sessions.find((s) => s.id !== id)?.id
if (newSessionId) {
dispatch(setActiveSessionIdAction({ agentId, sessionId: newSessionId }))
} else {
// may clear messages instead of forbidden deletion
}
}
dispatch(setSessionWaitingAction({ id, value: false }))
},
[agentId, deleteSession, dispatch, sessions]
)
const currentActiveSessionId = activeSessionId[agentId]
@ -93,7 +123,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
<SessionItem
session={session}
agentId={agentId}
onDelete={() => deleteSession(session.id)}
isDisabled={sessionWaiting[session.id]}
isLoading={sessionWaiting[session.id]}
onDelete={() => handleDeleteSession(session.id)}
onPress={() => setActiveSessionId(agentId, session.id)}
/>
</motion.div>

View File

@ -18,6 +18,8 @@ export interface ChatState {
renamingTopics: string[]
/** topic ids that are newly renamed */
newlyRenamedTopics: string[]
/** is a session waiting for updating/deleting. undefined and false share same semantics. */
sessionWaiting: Record<string, boolean>
}
export interface WebSearchState {
@ -90,7 +92,8 @@ const initialState: RuntimeState = {
activeTopicOrSession: 'topic',
activeSessionId: {},
renamingTopics: [],
newlyRenamedTopics: []
newlyRenamedTopics: [],
sessionWaiting: {}
},
websearch: {
activeSearches: {}
@ -184,6 +187,10 @@ const runtimeSlice = createSlice({
},
addIknowAction: (state, action: PayloadAction<string>) => {
state.iknow[action.payload] = true
},
setSessionWaitingAction: (state, action: PayloadAction<{ id: string; value: boolean }>) => {
const { id, value } = action.payload
state.chat.sessionWaiting[id] = value
}
}
})
@ -212,6 +219,7 @@ export const {
setActiveTopicOrSessionAction,
setRenamingTopics,
setNewlyRenamedTopics,
setSessionWaitingAction,
// WebSearch related actions
setActiveSearches,
setWebSearchStatus