mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 03:40:33 +08:00
Merge branch 'feat/agents-new' of https://github.com/CherryHQ/cherry-studio into feat/agents-new
This commit is contained in:
commit
c3adcf663f
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
36
src/renderer/src/hooks/agents/useUpdateSession.ts
Normal file
36
src/renderer/src/hooks/agents/useUpdateSession.ts
Normal 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
|
||||
}
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "确定要删除此会话吗?",
|
||||
"error": {
|
||||
"failed": "删除会话失败"
|
||||
"failed": "删除会话失败",
|
||||
"last": "至少需要保留一个会话"
|
||||
},
|
||||
"title": "删除会话"
|
||||
},
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "您確定要刪除此工作階段嗎?",
|
||||
"error": {
|
||||
"failed": "無法刪除工作階段"
|
||||
"failed": "無法刪除工作階段",
|
||||
"last": "至少必須保留一個工作階段"
|
||||
},
|
||||
"title": "刪除工作階段"
|
||||
},
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;",
|
||||
"error": {
|
||||
"failed": "Αποτυχία διαγραφής της συνεδρίας"
|
||||
"failed": "Αποτυχία διαγραφής της συνεδρίας",
|
||||
"last": "Τουλάχιστον μία συνεδρία πρέπει να διατηρηθεί"
|
||||
},
|
||||
"title": "Διαγραφή συνεδρίας"
|
||||
},
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "このセッションを削除してもよろしいですか?",
|
||||
"error": {
|
||||
"failed": "セッションの削除に失敗しました"
|
||||
"failed": "セッションの削除に失敗しました",
|
||||
"last": "少なくとも1つのセッションを維持する必要があります"
|
||||
},
|
||||
"title": "セッションを削除"
|
||||
},
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -52,7 +52,8 @@
|
||||
"delete": {
|
||||
"content": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"error": {
|
||||
"failed": "Не удалось удалить сеанс"
|
||||
"failed": "Не удалось удалить сеанс",
|
||||
"last": "Должен быть сохранён хотя бы один сеанс"
|
||||
},
|
||||
"title": "Удалить сеанс"
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user