mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +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 { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
|
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||||
import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types'
|
import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types'
|
||||||
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -86,7 +87,8 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const loadingRef = useRef(false)
|
const loadingRef = useRef(false)
|
||||||
// const { setTimeoutTimer } = useTimer()
|
// const { setTimeoutTimer } = useTimer()
|
||||||
const { createSession, updateSession } = useSessions(agentId)
|
const { createSession } = useSessions(agentId)
|
||||||
|
const updateSession = useUpdateSession(agentId)
|
||||||
// Only support claude code for now
|
// Only support claude code for now
|
||||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||||
const { agent } = useAgent(agentId)
|
const { agent } = useAgent(agentId)
|
||||||
@ -164,7 +166,7 @@ export const SessionModal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (form.accessible_paths.length === 0) {
|
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
|
loadingRef.current = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { CreateSessionForm, UpdateSessionForm } from '@renderer/types'
|
import { CreateSessionForm } from '@renderer/types'
|
||||||
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -46,25 +46,14 @@ export const useSessions = (agentId: string) => {
|
|||||||
|
|
||||||
const deleteSession = useCallback(
|
const deleteSession = useCallback(
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
if (!agentId) return
|
if (!agentId) return false
|
||||||
try {
|
try {
|
||||||
await client.deleteSession(agentId, id)
|
await client.deleteSession(agentId, id)
|
||||||
mutate((prev) => prev?.filter((session) => session.id !== id))
|
mutate((prev) => prev?.filter((session) => session.id !== id))
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
|
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.session.delete.error.failed')))
|
||||||
}
|
return false
|
||||||
},
|
|
||||||
[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')))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[agentId, client, mutate, t]
|
[agentId, client, mutate, t]
|
||||||
@ -76,7 +65,6 @@ export const useSessions = (agentId: string) => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
createSession,
|
createSession,
|
||||||
getSession,
|
getSession,
|
||||||
deleteSession,
|
deleteSession
|
||||||
updateSession
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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": {
|
"delete": {
|
||||||
"content": "Are you sure to delete this session?",
|
"content": "Are you sure to delete this session?",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Failed to delete the session"
|
"failed": "Failed to delete the session",
|
||||||
|
"last": "At least one session must be kept"
|
||||||
},
|
},
|
||||||
"title": "Delete session"
|
"title": "Delete session"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "确定要删除此会话吗?",
|
"content": "确定要删除此会话吗?",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "删除会话失败"
|
"failed": "删除会话失败",
|
||||||
|
"last": "至少需要保留一个会话"
|
||||||
},
|
},
|
||||||
"title": "删除会话"
|
"title": "删除会话"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "您確定要刪除此工作階段嗎?",
|
"content": "您確定要刪除此工作階段嗎?",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "無法刪除工作階段"
|
"failed": "無法刪除工作階段",
|
||||||
|
"last": "至少必須保留一個工作階段"
|
||||||
},
|
},
|
||||||
"title": "刪除工作階段"
|
"title": "刪除工作階段"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;",
|
"content": "Είσαι σίγουρος ότι θέλεις να διαγράψεις αυτήν τη συνεδρία;",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Αποτυχία διαγραφής της συνεδρίας"
|
"failed": "Αποτυχία διαγραφής της συνεδρίας",
|
||||||
|
"last": "Τουλάχιστον μία συνεδρία πρέπει να διατηρηθεί"
|
||||||
},
|
},
|
||||||
"title": "Διαγραφή συνεδρίας"
|
"title": "Διαγραφή συνεδρίας"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "¿Estás seguro de eliminar esta sesión?",
|
"content": "¿Estás seguro de eliminar esta sesión?",
|
||||||
"error": {
|
"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"
|
"title": "Eliminar sesión"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
"content": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||||
"error": {
|
"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"
|
"title": "Supprimer la session"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "このセッションを削除してもよろしいですか?",
|
"content": "このセッションを削除してもよろしいですか?",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "セッションの削除に失敗しました"
|
"failed": "セッションの削除に失敗しました",
|
||||||
|
"last": "少なくとも1つのセッションを維持する必要があります"
|
||||||
},
|
},
|
||||||
"title": "セッションを削除"
|
"title": "セッションを削除"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "Tem certeza de que deseja excluir esta sessão?",
|
"content": "Tem certeza de que deseja excluir esta sessão?",
|
||||||
"error": {
|
"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"
|
"title": "Excluir sessão"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"content": "Вы уверены, что хотите удалить этот сеанс?",
|
"content": "Вы уверены, что хотите удалить этот сеанс?",
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Не удалось удалить сеанс"
|
"failed": "Не удалось удалить сеанс",
|
||||||
|
"last": "Должен быть сохранён хотя бы один сеанс"
|
||||||
},
|
},
|
||||||
"title": "Удалить сеанс"
|
"title": "Удалить сеанс"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,11 +13,13 @@ interface SessionItemProps {
|
|||||||
session: AgentSessionEntity
|
session: AgentSessionEntity
|
||||||
// use external agentId as SSOT, instead of session.agent_id
|
// use external agentId as SSOT, instead of session.agent_id
|
||||||
agentId: string
|
agentId: string
|
||||||
|
isDisabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
onPress: () => 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 { t } = useTranslation()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
@ -38,7 +40,11 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
|||||||
<>
|
<>
|
||||||
<ContextMenu modal={false}>
|
<ContextMenu modal={false}>
|
||||||
<ContextMenuTrigger>
|
<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}>
|
<SessionLabelContainer className="name" title={session.name ?? session.id}>
|
||||||
<SessionLabel />
|
<SessionLabel />
|
||||||
</SessionLabelContainer>
|
</SessionLabelContainer>
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { Alert, Button, Spinner } from '@heroui/react'
|
import { Alert, Button, Spinner } from '@heroui/react'
|
||||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||||
|
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
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 { CreateSessionForm } from '@renderer/types'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
@ -22,8 +27,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { agent } = useAgent(agentId)
|
const { agent } = useAgent(agentId)
|
||||||
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
||||||
|
const updateSession = useUpdateSession(agentId)
|
||||||
const { chat } = useRuntime()
|
const { chat } = useRuntime()
|
||||||
const { activeSessionId } = chat
|
const { activeSessionId, sessionWaiting } = chat
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const setActiveSessionId = useCallback(
|
const setActiveSessionId = useCallback(
|
||||||
@ -34,14 +40,38 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCreateSession = useCallback(() => {
|
const handleCreateSession = useCallback(async () => {
|
||||||
if (!agent) return
|
if (!agent) return
|
||||||
const session = {
|
const session = {
|
||||||
...agent,
|
...agent,
|
||||||
id: undefined
|
id: undefined
|
||||||
} satisfies CreateSessionForm
|
} satisfies CreateSessionForm
|
||||||
createSession(session)
|
const created = await createSession(session)
|
||||||
}, [agent, createSession])
|
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]
|
const currentActiveSessionId = activeSessionId[agentId]
|
||||||
|
|
||||||
@ -93,7 +123,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
|||||||
<SessionItem
|
<SessionItem
|
||||||
session={session}
|
session={session}
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
onDelete={() => deleteSession(session.id)}
|
isDisabled={sessionWaiting[session.id]}
|
||||||
|
isLoading={sessionWaiting[session.id]}
|
||||||
|
onDelete={() => handleDeleteSession(session.id)}
|
||||||
onPress={() => setActiveSessionId(agentId, session.id)}
|
onPress={() => setActiveSessionId(agentId, session.id)}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export interface ChatState {
|
|||||||
renamingTopics: string[]
|
renamingTopics: string[]
|
||||||
/** topic ids that are newly renamed */
|
/** topic ids that are newly renamed */
|
||||||
newlyRenamedTopics: string[]
|
newlyRenamedTopics: string[]
|
||||||
|
/** is a session waiting for updating/deleting. undefined and false share same semantics. */
|
||||||
|
sessionWaiting: Record<string, boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebSearchState {
|
export interface WebSearchState {
|
||||||
@ -90,7 +92,8 @@ const initialState: RuntimeState = {
|
|||||||
activeTopicOrSession: 'topic',
|
activeTopicOrSession: 'topic',
|
||||||
activeSessionId: {},
|
activeSessionId: {},
|
||||||
renamingTopics: [],
|
renamingTopics: [],
|
||||||
newlyRenamedTopics: []
|
newlyRenamedTopics: [],
|
||||||
|
sessionWaiting: {}
|
||||||
},
|
},
|
||||||
websearch: {
|
websearch: {
|
||||||
activeSearches: {}
|
activeSearches: {}
|
||||||
@ -184,6 +187,10 @@ const runtimeSlice = createSlice({
|
|||||||
},
|
},
|
||||||
addIknowAction: (state, action: PayloadAction<string>) => {
|
addIknowAction: (state, action: PayloadAction<string>) => {
|
||||||
state.iknow[action.payload] = true
|
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,
|
setActiveTopicOrSessionAction,
|
||||||
setRenamingTopics,
|
setRenamingTopics,
|
||||||
setNewlyRenamedTopics,
|
setNewlyRenamedTopics,
|
||||||
|
setSessionWaitingAction,
|
||||||
// WebSearch related actions
|
// WebSearch related actions
|
||||||
setActiveSearches,
|
setActiveSearches,
|
||||||
setWebSearchStatus
|
setWebSearchStatus
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user