feat(agents): add agent management with delete confirmation

- Add agent deletion confirmation dialog with translations
- Implement agent list display and drag-and-drop functionality
- Include avatar support for agent types
- Add success message for agent deletion
This commit is contained in:
icarus 2025-09-14 06:43:24 +08:00
parent 941a6666e6
commit 9a71a01b66
6 changed files with 183 additions and 20 deletions

View File

@ -20,7 +20,7 @@ import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { useAgents } from '@renderer/hooks/useAgents'
import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { AgentEntity, isAgentType } from '@renderer/types'
import { AgentEntity, AgentType, isAgentType } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Plus } from 'lucide-react'
import { ChangeEvent, FormEvent, useCallback, useMemo, useRef, useState } from 'react'
@ -204,7 +204,8 @@ export const AddAgentModal: React.FC = () => {
instructions: form.instructions,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
model: form.model
model: form.model,
avatar: getAvatar(form.type)
} satisfies AgentEntity
logger.debug('Agent', agent)
addAgent(agent)
@ -282,3 +283,11 @@ export const AddAgentModal: React.FC = () => {
</ErrorBoundary>
)
}
const getAvatar = (type: AgentType) => {
switch (type) {
case 'claude-code':
return ClaudeIcon
}
return undefined
}

View File

@ -1,9 +1,10 @@
import { useAppDispatch } from '@renderer/store'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { addAgent, removeAgent, setAgents, updateAgent } from '@renderer/store/agents'
import { AgentEntity } from '@renderer/types'
import { useCallback } from 'react'
export const useAgents = () => {
const agents = useAppSelector((state) => state.agents.agentsNew)
const dispatch = useAppDispatch()
/**
* Adds a new agent to the store
@ -50,6 +51,7 @@ export const useAgents = () => {
)
return {
agents,
addAgent: addAgent_,
removeAgent: removeAgent_,
updateAgent: updateAgent_,

View File

@ -9,6 +9,13 @@
"label": "Agent Type",
"placeholder": "Select an agent type"
}
},
"delete": {
"content": "Deleting the agent will forcibly stop and delete all sessions associated with the agent. Are you sure?",
"title": "Delete Agent"
},
"edit": {
"title": "Edit Agent"
}
},
"agents": {
@ -763,6 +770,7 @@
"default": "Default",
"delete": "Delete",
"delete_confirm": "Are you sure you want to delete?",
"delete_success": "Deleted successfully",
"description": "Description",
"detail": "Detail",
"disabled": "Disabled",

View File

@ -9,6 +9,10 @@
"label": "Agent 类型",
"placeholder": "选择 Agent 类型"
}
},
"delete": {
"content": "删除该 Agent 将强制终止并删除该 Agent 下的所有会话。您确定吗?",
"title": "删除 Agent"
}
},
"agents": {
@ -763,6 +767,7 @@
"default": "默认",
"delete": "删除",
"delete_confirm": "确定要删除吗?",
"delete_success": "删除成功",
"description": "描述",
"detail": "详情",
"disabled": "已禁用",

View File

@ -1,19 +1,21 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons'
import { Button } from '@heroui/react'
import { Button, Divider } from '@heroui/react'
import { DraggableList } from '@renderer/components/DraggableList'
import { AddAgentModal } from '@renderer/components/Popups/AddAgentModal'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { AgentEntity, Assistant, AssistantsSortType } from '@renderer/types'
import { Tooltip } from 'antd'
import { Plus } from 'lucide-react'
import { FC, useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AgentItem from './components/AgentItem'
import AssistantItem from './components/AssistantItem'
interface AssistantsTabProps {
@ -35,6 +37,7 @@ const Assistants: FC<AssistantsTabProps> = ({
const { getGroupedAssistants, collapsedTags, toggleTagCollapse } = useTags()
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
const containerRef = useRef<HTMLDivElement>(null)
const { agents, setAgents, removeAgent } = useAgents()
const onDelete = useCallback(
(assistant: Assistant) => {
@ -48,6 +51,14 @@ const Assistants: FC<AssistantsTabProps> = ({
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
)
const onDeleteAgent = useCallback(
(agent: AgentEntity) => {
removeAgent(agent.id)
window.toast.success(t('common.delete_success'))
},
[removeAgent, t]
)
const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType)
@ -81,19 +92,6 @@ const Assistants: FC<AssistantsTabProps> = ({
)
}, [onCreateAssistant, t])
// const AddAgentButton = useCallback(() => {
// return (
// <AssistantAddItem onClick={onCreateAgent}>
// <AddItemWrapper>
// <Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
// <Typography.Text style={{ color: 'inherit' }} ellipsis={{ tooltip: t('agent.add.title') }}>
// {t('agent.add.title')}
// </Typography.Text>
// </AddItemWrapper>
// </AssistantAddItem>
// )
// }, [onCreateAgent, t])
if (assistantsTabSortType === 'tags') {
return (
<Container className="assistants-tab" ref={containerRef}>
@ -150,6 +148,15 @@ const Assistants: FC<AssistantsTabProps> = ({
return (
<Container className="assistants-tab" ref={containerRef}>
<DraggableList
list={agents}
onUpdate={setAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent) => <AgentItem agent={agent} isActive={false} onDelete={onDeleteAgent} />}
</DraggableList>
{!dragging && <AddAgentModal />}
<Divider className="my-2" />
<DraggableList
list={assistants}
onUpdate={updateAssistants}
@ -171,8 +178,6 @@ const Assistants: FC<AssistantsTabProps> = ({
)}
</DraggableList>
{!dragging && renderAddAssistantButton}
{/* {!dragging && <AddAgentButton />} */}
{!dragging && <AddAgentModal />}
<div style={{ minHeight: 10 }}></div>
</Container>
)

View File

@ -0,0 +1,134 @@
import { Avatar, cn } from '@heroui/react'
import { loggerService } from '@logger'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { AgentEntity } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('AgentItem')
interface AgentItemProps {
agent: AgentEntity
isActive: boolean
onDelete: (agent: AgentEntity) => void
onTagClick?: (tag: string) => void
}
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
const { t } = useTranslation()
// const { agents } = useAgents()
const menuItems = useMemo(
() =>
getMenuItems({
agent,
t,
onDelete
}),
[agent, t, onDelete]
)
const AgentLabel = useCallback(() => {
return (
<>
{agent.avatar && <Avatar className="h-6 w-6" src={agent.avatar} />}
<span className="text-sm">{agent.name}</span>
</>
)
}, [agent.avatar, agent.name])
const handleClick = () => logger.debug('not implemented')
return (
<Dropdown
menu={{ items: menuItems }}
trigger={['contextMenu']}
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
<Container onClick={handleClick} className={isActive ? 'active' : ''}>
<AssistantNameRow className="name" title={agent.name}>
<AgentLabel />
</AssistantNameRow>
</Container>
</Dropdown>
)
}
// 提取创建菜单配置的函数
function getMenuItems({ agent, t, onDelete }): MenuProps['items'] {
return [
{
label: t('common.edit'),
key: 'edit',
icon: <EditIcon size={14} />,
onClick: () => window.toast.info('not implemented')
},
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteIcon size={14} className="lucide-custom" />,
danger: true,
onClick: () => {
window.modal.confirm({
title: t('agent.delete.title'),
content: t('agent.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => onDelete(agent)
})
}
}
]
}
const Container: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
{...props}
className={cn(
'relative flex h-[37px] flex-row justify-between p-2',
'rounded-[var(--list-item-border-radius)]',
'border-[0.5px] border-transparent',
'w-[calc(var(--assistants-width)_-_20px)]',
'hover:bg-[var(--color-list-item-hover)]',
className?.includes('active') && 'bg-[var(--color-list-item)] shadow-sm',
className
)}
/>
)
const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
{...props}
className={cn('text-[13px] text-[var(--color-text)]', 'flex flex-row items-center gap-2', className)}
/>
)
// const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
// <div
// {...props}
// className={cn(
// 'flex flex-row items-center justify-center',
// 'h-[22px] min-h-[22px] min-w-[22px]',
// 'absolute rounded-[11px]',
// 'bg-[var(--color-background)]',
// 'top-[6px] right-[9px]',
// 'px-[5px]',
// 'border-[0.5px] border-[var(--color-border)]',
// className
// )}
// />
// )
// const TopicCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
// <div
// {...props}
// className={cn(
// 'text-[10px] text-[var(--color-text)]',
// 'rounded-[10px]',
// 'flex flex-row items-center justify-center',
// className
// )}
// />
// )
export default memo(AgentItem)