diff --git a/src/main/apiServer/routes/agents/handlers/messages.ts b/src/main/apiServer/routes/agents/handlers/messages.ts index 8f05bf8c0..5f5bfc146 100644 --- a/src/main/apiServer/routes/agents/handlers/messages.ts +++ b/src/main/apiServer/routes/agents/handlers/messages.ts @@ -228,3 +228,53 @@ export const createMessage = async (req: Request, res: Response): Promise res.end() } } + +export const deleteMessage = async (req: Request, res: Response): Promise => { + try { + const { agentId, sessionId, messageId: messageIdParam } = req.params + const messageId = Number(messageIdParam) + + await verifyAgentAndSession(agentId, sessionId) + + const deleted = await sessionMessageService.deleteSessionMessage(sessionId, messageId) + + if (!deleted) { + logger.warn(`Message ${messageId} not found for session ${sessionId}`) + return res.status(404).json({ + error: { + message: 'Message not found for this session', + type: 'not_found', + code: 'session_message_not_found' + } + }) + } + + logger.info(`Message ${messageId} deleted successfully for session ${sessionId}`) + return res.status(204).send() + } catch (error: any) { + if (error?.status === 404) { + logger.warn('Delete message failed - missing resource', { + agentId: req.params.agentId, + sessionId: req.params.sessionId, + messageId: req.params.messageId, + error + }) + return res.status(404).json({ + error: { + message: error.message, + type: 'not_found', + code: error.code ?? 'session_message_not_found' + } + }) + } + + logger.error('Error deleting session message:', error) + return res.status(500).json({ + error: { + message: 'Failed to delete session message', + type: 'internal_error', + code: 'session_message_delete_failed' + } + }) + } +} diff --git a/src/main/apiServer/routes/agents/index.ts b/src/main/apiServer/routes/agents/index.ts index 5d3393dc1..7a54f8acc 100644 --- a/src/main/apiServer/routes/agents/index.ts +++ b/src/main/apiServer/routes/agents/index.ts @@ -11,6 +11,7 @@ import { validateSession, validateSessionId, validateSessionMessage, + validateSessionMessageId, validateSessionReplace, validateSessionUpdate } from './validators' @@ -362,7 +363,7 @@ const agentsRouter = express.Router() /** * @swagger - * /api/agents: + * /agents: * post: * summary: Create a new agent * tags: [Agents] @@ -391,7 +392,7 @@ agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.crea /** * @swagger - * /api/agents: + * /agents: * get: * summary: List all agents with pagination * tags: [Agents] @@ -429,7 +430,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers. /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * get: * summary: Get agent by ID * tags: [Agents] @@ -457,7 +458,7 @@ agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers. agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent) /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * put: * summary: Replace agent (full update) * tags: [Agents] @@ -497,7 +498,7 @@ agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHand agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent) /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * patch: * summary: Update agent (partial update) * tags: [Agents] @@ -537,7 +538,7 @@ agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValid agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent) /** * @swagger - * /api/agents/{agentId}: + * /agents/{agentId}: * delete: * summary: Delete agent * tags: [Agents] @@ -567,7 +568,7 @@ const createSessionsRouter = (): express.Router => { // Session CRUD routes (nested under agent) /** * @swagger - * /api/agents/{agentId}/sessions: + * /agents/{agentId}/sessions: * post: * summary: Create a new session for an agent * tags: [Sessions] @@ -608,7 +609,7 @@ const createSessionsRouter = (): express.Router => { /** * @swagger - * /api/agents/{agentId}/sessions: + * /agents/{agentId}/sessions: * get: * summary: List sessions for an agent * tags: [Sessions] @@ -657,7 +658,7 @@ const createSessionsRouter = (): express.Router => { sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * get: * summary: Get session by ID * tags: [Sessions] @@ -691,7 +692,7 @@ const createSessionsRouter = (): express.Router => { sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * put: * summary: Replace session (full update) * tags: [Sessions] @@ -743,7 +744,7 @@ const createSessionsRouter = (): express.Router => { ) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * patch: * summary: Update session (partial update) * tags: [Sessions] @@ -795,7 +796,7 @@ const createSessionsRouter = (): express.Router => { ) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}: + * /agents/{agentId}/sessions/{sessionId}: * delete: * summary: Delete session * tags: [Sessions] @@ -834,7 +835,7 @@ const createMessagesRouter = (): express.Router => { // Message CRUD routes (nested under agent/session) /** * @swagger - * /api/agents/{agentId}/sessions/{sessionId}/messages: + * /agents/{agentId}/sessions/{sessionId}/messages: * post: * summary: Create a new message in a session * tags: [Messages] @@ -902,8 +903,45 @@ const createMessagesRouter = (): express.Router => { * application/json: * schema: * $ref: '#/components/schemas/ErrorResponse' - */ + */ messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage) + + /** + * @swagger + * /agents/{agentId}/sessions/{sessionId}/messages/{messageId}: + * delete: + * summary: Delete a message from a session + * tags: [Messages] + * parameters: + * - in: path + * name: agentId + * required: true + * schema: + * type: string + * description: Agent ID + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * description: Session ID + * - in: path + * name: messageId + * required: true + * schema: + * type: integer + * description: Message ID + * responses: + * 204: + * description: Message deleted successfully + * 404: + * description: Agent, session, or message not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + */ + messagesRouter.delete('/:messageId', validateSessionMessageId, handleValidationErrors, messageHandlers.deleteMessage) return messagesRouter } diff --git a/src/main/apiServer/routes/agents/validators/messages.ts b/src/main/apiServer/routes/agents/validators/messages.ts index 9a4f7dbbf..8d7cddfa7 100644 --- a/src/main/apiServer/routes/agents/validators/messages.ts +++ b/src/main/apiServer/routes/agents/validators/messages.ts @@ -1,7 +1,11 @@ -import { CreateSessionMessageRequestSchema } from '@types' +import { CreateSessionMessageRequestSchema, SessionMessageIdParamSchema } from '@types' import { createZodValidator } from './zodValidator' export const validateSessionMessage = createZodValidator({ body: CreateSessionMessageRequestSchema }) + +export const validateSessionMessageId = createZodValidator({ + params: SessionMessageIdParamSchema +}) diff --git a/src/main/apiServer/services/mcp.ts b/src/main/apiServer/services/mcp.ts index 4cb19477b..e5cff2e15 100644 --- a/src/main/apiServer/services/mcp.ts +++ b/src/main/apiServer/services/mcp.ts @@ -102,19 +102,14 @@ class MCPApiService extends EventEmitter { async getServerInfo(id: string): Promise { try { - logger.silly(`getServerInfo called with id: ${id}`) const server = await this.getServerById(id) if (!server) { logger.warn(`Server with id ${id} not found`) return null } - logger.silly(`Returning server info for id ${id}`) const client = await mcpService.initClient(server) const tools = await client.listTools() - - logger.silly(`Server with id ${id} info:`, { tools: JSON.stringify(tools.tools) }) - return { id: server.id, name: server.name, diff --git a/src/main/ipc.ts b/src/main/ipc.ts index c375d8683..28097ad88 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -11,7 +11,16 @@ import { handleZoomFactor } from '@main/utils/zoom' import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' -import { FileMetadata, Notification, OcrProvider, Provider, Shortcut, SupportedOcrFile, ThemeMode } from '@types' +import { + AgentPersistedMessage, + FileMetadata, + Notification, + OcrProvider, + Provider, + Shortcut, + SupportedOcrFile, + ThemeMode, +} from "@types"; import checkDiskSpace from 'check-disk-space' import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' import fontList from 'font-list' @@ -209,14 +218,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } }) - ipcMain.handle(IpcChannel.AgentMessage_GetHistory, async (_event, { sessionId }: { sessionId: string }) => { - try { - return await agentMessageRepository.getSessionHistory(sessionId) - } catch (error) { - logger.error('Failed to get agent session history', error as Error) - throw error + ipcMain.handle( + IpcChannel.AgentMessage_GetHistory, + async ( + _event, + { sessionId }: { sessionId: string } + ): Promise => { + try { + return await agentMessageRepository.getSessionHistory(sessionId); + } catch (error) { + logger.error("Failed to get agent session history", error as Error); + throw error; + } } - }) + ); //only for mac if (isMac) { diff --git a/src/main/services/agents/services/SessionMessageService.ts b/src/main/services/agents/services/SessionMessageService.ts index 64897ca2b..0aa17425f 100644 --- a/src/main/services/agents/services/SessionMessageService.ts +++ b/src/main/services/agents/services/SessionMessageService.ts @@ -6,7 +6,7 @@ import type { ListOptions } from '@types' import { TextStreamPart } from 'ai' -import { desc, eq } from 'drizzle-orm' +import { and, desc, eq } from 'drizzle-orm' import { BaseService } from '../BaseService' import { sessionMessagesTable } from '../database/schema' @@ -145,6 +145,16 @@ export class SessionMessageService extends BaseService { return { messages } } + async deleteSessionMessage(sessionId: string, messageId: number): Promise { + this.ensureInitialized() + + const result = await this.database + .delete(sessionMessagesTable) + .where(and(eq(sessionMessagesTable.id, messageId), eq(sessionMessagesTable.session_id, sessionId))) + + return result.rowsAffected > 0 + } + async createSessionMessage( session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest, @@ -270,6 +280,7 @@ export class SessionMessageService extends BaseService { .orderBy(desc(sessionMessagesTable.created_at)) .limit(1) + logger.silly('Last agent session ID result:', { agentSessionId: result[0]?.agent_session_id, sessionId }) return result[0]?.agent_session_id || '' } catch (error) { logger.error('Failed to get last agent session ID', { diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 99000d9b3..3ddb34cf5 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -74,6 +74,8 @@ class ClaudeCodeService implements AgentServiceInterface { ELECTRON_RUN_AS_NODE: '1' } + const errorChunks: string[] = [] + // Build SDK options from parameters const options: Options = { abortController, @@ -82,7 +84,8 @@ class ClaudeCodeService implements AgentServiceInterface { model: modelInfo.modelId, pathToClaudeCodeExecutable: this.claudeExecutablePath, stderr: (chunk: string) => { - logger.info('claude stderr', { chunk }) + logger.warn('claude stderr', { chunk }) + errorChunks.push(chunk) }, appendSystemPrompt: session.instructions, permissionMode: session.configuration?.permission_mode, @@ -116,11 +119,16 @@ class ClaudeCodeService implements AgentServiceInterface { logger.silly('Starting Claude Code SDK query', { prompt, - options + cwd: options.cwd, + model: options.model, + permissionMode: options.permissionMode, + maxTurns: options.maxTurns, + allowedTools: options.allowedTools, + resume: options.resume }) // Start async processing - this.processSDKQuery(prompt, options, aiStream) + this.processSDKQuery(prompt, options, aiStream, errorChunks) return aiStream } @@ -142,7 +150,12 @@ class ClaudeCodeService implements AgentServiceInterface { /** * Process SDK query and emit stream events */ - private async processSDKQuery(prompt: string, options: Options, stream: ClaudeCodeStream): Promise { + private async processSDKQuery( + prompt: string, + options: Options, + stream: ClaudeCodeStream, + errorChunks: string[] + ): Promise { const jsonOutput: SDKMessage[] = [] let hasCompleted = false const startTime = Date.now() @@ -209,17 +222,12 @@ class ClaudeCodeService implements AgentServiceInterface { return } - // Original error handling for non-abort errors - logger.error('SDK query error:', { - error: errorObj instanceof Error ? errorObj.message : String(errorObj), - duration, - messageCount: jsonOutput.length - }) - + errorChunks.push(errorObj instanceof Error ? errorObj.message : String(errorObj)) + const errorMessage = errorChunks.join('\n\n') // Emit error event stream.emit('data', { type: 'error', - error: errorObj instanceof Error ? errorObj : new Error(String(errorObj)) + error: new Error(errorMessage) }) } } diff --git a/src/renderer/src/api/agent.ts b/src/renderer/src/api/agent.ts index b453f116c..4d22b5a72 100644 --- a/src/renderer/src/api/agent.ts +++ b/src/renderer/src/api/agent.ts @@ -206,6 +206,16 @@ export class AgentApiClient { } } + public async deleteSessionMessage(agentId: string, sessionId: string, messageId: number): Promise { + const base = this.getSessionMessagesPath(agentId, sessionId) + const url = `${base}/${messageId}` + try { + await this.axios.delete(url) + } catch (error) { + throw processError(error, 'Failed to delete session message.') + } + } + public async updateSession(agentId: string, session: UpdateSessionForm): Promise { const url = this.getSessionPaths(agentId).withId(session.id) try { diff --git a/src/renderer/src/config/registry/messageMenubar.ts b/src/renderer/src/config/registry/messageMenubar.ts index 03fe0d973..5f9f06802 100644 --- a/src/renderer/src/config/registry/messageMenubar.ts +++ b/src/renderer/src/config/registry/messageMenubar.ts @@ -36,7 +36,13 @@ export const DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [ 'more-menu' ] -export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = ['copy', 'translate', 'notes', 'more-menu'] +export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [ + 'copy', + 'translate', + 'notes', + 'delete', + 'more-menu' +] const messageMenubarRegistry = new Map([ [DEFAULT_MESSAGE_MENUBAR_SCOPE, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }], diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 7c7cacfca..e6a64e0dc 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -133,7 +133,7 @@ "plan": { "behavior": "Solo herramientas de solo lectura. La ejecución está deshabilitada.", "description": "Claude solo puede usar herramientas de solo lectura y presenta un plan antes de ejecutarlo.", - "title": "[to be translated]:Planning mode (coming soon)" + "title": "Modo de planificación (próximamente)" } }, "preapproved": { diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx new file mode 100644 index 000000000..f2dc8f143 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx @@ -0,0 +1,190 @@ +import { AccordionItem, Card, CardBody, Chip, Code } from '@heroui/react' +import { CheckCircle, Terminal, XCircle } from 'lucide-react' +import { useMemo } from 'react' + +import { ToolTitle } from './GenericTools' +import type { BashOutputToolInput, BashOutputToolOutput } from './types' +import { AgentToolsType } from './types' + +interface ParsedBashOutput { + status?: string + exit_code?: number + stdout?: string + stderr?: string + timestamp?: string + tool_use_error?: string +} + +export function BashOutputTool({ input, output }: { input: BashOutputToolInput; output?: BashOutputToolOutput }) { + // 解析 XML 输出 + const parsedOutput = useMemo(() => { + if (!output) return null + + try { + const parser = new DOMParser() + // 检查是否包含 tool_use_error 标签 + const hasToolError = output.includes('') + // 包装成有效的 XML(如果还没有根元素) + const xmlStr = output.includes('') || hasToolError ? `${output}` : output + const xmlDoc = parser.parseFromString(xmlStr, 'application/xml') + + // 检查是否有解析错误 + const parserError = xmlDoc.querySelector('parsererror') + if (parserError) { + return null + } + + const getElementText = (tagName: string): string | undefined => { + const element = xmlDoc.getElementsByTagName(tagName)[0] + return element?.textContent?.trim() + } + + const result: ParsedBashOutput = { + status: getElementText('status'), + exit_code: getElementText('exit_code') ? parseInt(getElementText('exit_code')!) : undefined, + stdout: getElementText('stdout'), + stderr: getElementText('stderr'), + timestamp: getElementText('timestamp'), + tool_use_error: getElementText('tool_use_error') + } + + return result + } catch { + return null + } + }, [output]) + + // 获取状态配置 + const statusConfig = useMemo(() => { + if (!parsedOutput) return null + + // 如果有 tool_use_error,直接显示错误状态 + if (parsedOutput.tool_use_error) { + return { + color: 'danger', + icon: , + text: 'Error' + } as const + } + + const isCompleted = parsedOutput.status === 'completed' + const isSuccess = parsedOutput.exit_code === 0 + + return { + color: isCompleted && isSuccess ? 'success' : isCompleted && !isSuccess ? 'danger' : 'warning', + icon: + isCompleted && isSuccess ? ( + + ) : isCompleted && !isSuccess ? ( + + ) : ( + + ), + text: isCompleted ? (isSuccess ? 'Success' : 'Failed') : 'Running' + } as const + }, [parsedOutput]) + + return ( + } + label="Bash Output" + params={ +
+ + {input.bash_id} + + {statusConfig && ( + + {statusConfig.text} + + )} +
+ } + /> + } + classNames={{ + content: 'space-y-3 px-1' + }}> + {parsedOutput ? ( + <> + {/* Status Info */} +
+ {parsedOutput.exit_code !== undefined && ( + + Exit Code: {parsedOutput.exit_code} + + )} + {parsedOutput.timestamp && ( + + {new Date(parsedOutput.timestamp).toLocaleString()} + + )} +
+ + {/* Standard Output */} + {parsedOutput.stdout && ( + + +
stdout:
+
+                  {parsedOutput.stdout}
+                
+
+
+ )} + + {/* Standard Error */} + {parsedOutput.stderr && ( + + +
stderr:
+
+                  {parsedOutput.stderr}
+                
+
+
+ )} + + {/* Tool Use Error */} + {parsedOutput.tool_use_error && ( + + +
+ + Error: +
+
+                  {parsedOutput.tool_use_error}
+                
+
+
+ )} + + ) : ( + // 原始输出(如果解析失败或非 XML 格式) + output && ( + + +
+                {output}
+              
+
+
+ ) + )} +
+ ) +} diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx index 9ea04b2df..aad9c7ab3 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx @@ -21,7 +21,7 @@ export function BashTool({ input, output }: { input: BashToolInputType; output?: /> } subtitle={ - + {input.command} }> diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/EditTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/EditTool.tsx new file mode 100644 index 000000000..c3c1f9dc5 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/EditTool.tsx @@ -0,0 +1,46 @@ +import { AccordionItem } from '@heroui/react' +import { FileEdit } from 'lucide-react' + +import { ToolTitle } from './GenericTools' +import type { EditToolInput, EditToolOutput } from './types' +import { AgentToolsType } from './types' + +// 处理多行文本显示 +export const renderCodeBlock = (content: string, variant: 'old' | 'new') => { + const lines = content.split('\n') + const textColorClass = + variant === 'old' ? 'text-danger-600 dark:text-danger-400' : 'text-success-600 dark:text-success-400' + + return ( + // 删除线 +
+      {lines.map((line, idx) => (
+        
+ + {variant === 'old' && '-'} + {variant === 'new' && '+'} + {idx + 1} + + {line || ' '} +
+ ))} +
+ ) +} + +export function EditTool({ input, output }: { input: EditToolInput; output?: EditToolOutput }) { + return ( + } label="Edit" params={input.file_path} />}> + {/* Diff View */} + {/* Old Content */} + {renderCodeBlock(input.old_string, 'old')} + {/* New Content */} + {renderCodeBlock(input.new_string, 'new')} + {/* Output */} + {output} + + ) +} diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ExitPlanModeTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ExitPlanModeTool.tsx new file mode 100644 index 000000000..1769c384f --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ExitPlanModeTool.tsx @@ -0,0 +1,24 @@ +import { AccordionItem } from '@heroui/react' +import { DoorOpen } from 'lucide-react' +import ReactMarkdown from 'react-markdown' + +import { ToolTitle } from './GenericTools' +import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types' +import { AgentToolsType } from './types' + +export function ExitPlanModeTool({ input, output }: { input: ExitPlanModeToolInput; output?: ExitPlanModeToolOutput }) { + return ( + } + label="ExitPlanMode" + stats={`${input.plan.split('\n\n').length} plans`} + /> + }> + {{input.plan + '\n\n' + (output ?? '')}} + + ) +} diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/MultiEditTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/MultiEditTool.tsx new file mode 100644 index 000000000..75f7e5180 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/MultiEditTool.tsx @@ -0,0 +1,23 @@ +import { AccordionItem } from '@heroui/react' +import { FileText } from 'lucide-react' + +import { renderCodeBlock } from './EditTool' +import { ToolTitle } from './GenericTools' +import type { MultiEditToolInput, MultiEditToolOutput } from './types' +import { AgentToolsType } from './types' + +export function MultiEditTool({ input, output }: { input: MultiEditToolInput; output?: MultiEditToolOutput }) { + return ( + } label="MultiEdit" params={input.file_path} />}> + {input.edits.map((edit, index) => ( +
+ {renderCodeBlock(edit.old_string, 'old')} + {renderCodeBlock(edit.new_string, 'new')} +
+ ))} +
+ ) +} diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/NotebookEditTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/NotebookEditTool.tsx new file mode 100644 index 000000000..f15664969 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/NotebookEditTool.tsx @@ -0,0 +1,19 @@ +import { AccordionItem } from '@heroui/react' +import { FileText } from 'lucide-react' +import ReactMarkdown from 'react-markdown' + +import { ToolTitle } from './GenericTools' +import type { NotebookEditToolInput, NotebookEditToolOutput } from './types' +import { AgentToolsType } from './types' + +export function NotebookEditTool({ input, output }: { input: NotebookEditToolInput; output?: NotebookEditToolOutput }) { + return ( + } label="NotebookEdit" />} + subtitle={input.notebook_path}> + {output} + + ) +} diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx index 79f68c03b..129072cda 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx @@ -1,24 +1,46 @@ import { AccordionItem } from '@heroui/react' import { FileText } from 'lucide-react' +import { useMemo } from 'react' import ReactMarkdown from 'react-markdown' import { ToolTitle } from './GenericTools' -import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType } from './types' +import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType, TextOutput } from './types' import { AgentToolsType } from './types' export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) { + // 将 output 统一转换为字符串 + const outputString = useMemo(() => { + if (!output) return null + + // 如果是 TextOutput[] 类型,提取所有 text 内容 + if (Array.isArray(output)) { + return output + .filter((item): item is TextOutput => item.type === 'text') + .map((item) => item.text) + .join('') + } + + // 如果是字符串,直接返回 + return output + }, [output]) + // 如果有输出,计算统计信息 - const stats = output - ? { - lineCount: output.split('\n').length, - fileSize: new Blob([output]).size, - formatSize: (bytes: number) => { - if (bytes < 1024) return `${bytes} B` - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` - return `${(bytes / (1024 * 1024)).toFixed(1)} MB` - } - } - : null + const stats = useMemo(() => { + if (!outputString) return null + + const bytes = new Blob([outputString]).size + const formatSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + } + + return { + lineCount: outputString.split('\n').length, + fileSize: bytes, + formatSize + } + }, [outputString]) return ( } label="Read File" params={input.file_path.split('/').pop()} - stats={output && stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined} + stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined} /> }> - {output ? ( - //
- {output} - //
- ) : null} + {outputString ? {outputString} : null}
) } diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/TodoWriteTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/TodoWriteTool.tsx index 4597c5f77..257926cf9 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/TodoWriteTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/TodoWriteTool.tsx @@ -14,31 +14,28 @@ const getStatusConfig = (status: TodoItem['status']) => { case 'completed': return { color: 'success' as const, - icon: , - label: '已完成' + icon: } case 'in_progress': return { color: 'primary' as const, - icon: , - label: '进行中' + icon: } case 'pending': return { color: 'default' as const, - icon: , - label: '待处理' + icon: } default: return { color: 'default' as const, - icon: , - label: '待处理' + icon: } } } export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) { + const doneCount = input.todos.filter((todo) => todo.status === 'completed').length return ( } - label="Todo Update" + label="Todo Write" + params={`${doneCount} Done`} stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`} /> }> @@ -55,15 +53,10 @@ export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType const statusConfig = getStatusConfig(todo.status) return ( - +
- - {statusConfig.label} + + {statusConfig.icon}
diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx index a96c7cff6..7d33a756d 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx @@ -6,9 +6,14 @@ import { NormalToolResponse } from '@renderer/types' export * from './types' // 导入所有渲染器 +import { BashOutputTool } from './BashOutputTool' import { BashTool } from './BashTool' +import { EditTool } from './EditTool' +import { ExitPlanModeTool } from './ExitPlanModeTool' import { GlobTool } from './GlobTool' import { GrepTool } from './GrepTool' +import { MultiEditTool } from './MultiEditTool' +import { NotebookEditTool } from './NotebookEditTool' import { ReadTool } from './ReadTool' import { SearchTool } from './SearchTool' import { TaskTool } from './TaskTool' @@ -31,7 +36,12 @@ export const toolRenderers = { [AgentToolsType.WebSearch]: WebSearchTool, [AgentToolsType.Grep]: GrepTool, [AgentToolsType.Write]: WriteTool, - [AgentToolsType.WebFetch]: WebFetchTool + [AgentToolsType.WebFetch]: WebFetchTool, + [AgentToolsType.Edit]: EditTool, + [AgentToolsType.MultiEdit]: MultiEditTool, + [AgentToolsType.BashOutput]: BashOutputTool, + [AgentToolsType.NotebookEdit]: NotebookEditTool, + [AgentToolsType.ExitPlanMode]: ExitPlanModeTool } as const // 类型守卫函数 @@ -51,7 +61,8 @@ function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: 'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full', indicator: 'flex-shrink-0', subtitle: 'text-xs', - content: 'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 h-fit max-h-96 scroll-auto' + content: + 'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll' }} defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}> {/* */} diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/types.ts b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/types.ts index 0e483688a..46baaa369 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/types.ts +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/types.ts @@ -8,7 +8,17 @@ export enum AgentToolsType { WebSearch = 'WebSearch', Grep = 'Grep', Write = 'Write', - WebFetch = 'WebFetch' + WebFetch = 'WebFetch', + Edit = 'Edit', + MultiEdit = 'MultiEdit', + BashOutput = 'BashOutput', + NotebookEdit = 'NotebookEdit', + ExitPlanMode = 'ExitPlanMode' +} + +export type TextOutput = { + type: 'text' + text: string } // Read 工具的类型定义 @@ -16,7 +26,7 @@ export interface ReadToolInput { file_path: string } -export type ReadToolOutput = string +export type ReadToolOutput = string | TextOutput[] // Task 工具的类型定义 export type TaskToolInput = { @@ -25,10 +35,7 @@ export type TaskToolInput = { subagent_type: string } -export type TaskToolOutput = { - type: 'text' - text: string -}[] +export type TaskToolOutput = TextOutput[] // Bash 工具的类型定义 export type BashToolInput = { @@ -84,6 +91,7 @@ export interface GrepToolInput { export type GrepToolOutput = string +// Write 工具的类型定义 export type WriteToolInput = { content: string file_path: string @@ -91,6 +99,44 @@ export type WriteToolInput = { export type WriteToolOutput = string +// Edit 工具的类型定义 +export type EditToolInput = { + file_path: string + old_string: string + new_string: string +} +export type EditToolOutput = string + +// MultiEdit 工具的类型定义 +export type MultiEditToolInput = { + file_path: string + edits: { + old_string: string + new_string: string + }[] +} +export type MultiEditToolOutput = string + +// BashOutput 工具的类型定义 +export type BashOutputToolInput = { + bash_id: string +} +export type BashOutputToolOutput = string + +// NotebookEdit 工具的类型定义 +export type NotebookEditToolInput = { + notebook_path: string + edit_mode: string + cell_type: string + new_source: string +} +export type NotebookEditToolOutput = string + +export type ExitPlanModeToolInput = { + plan: string +} +export type ExitPlanModeToolOutput = string + // 联合类型 export type ToolInput = | ReadToolInput @@ -103,6 +149,11 @@ export type ToolInput = | WebFetchToolInput | GrepToolInput | WriteToolInput + | EditToolInput + | MultiEditToolInput + | BashOutputToolInput + | NotebookEditToolInput + | ExitPlanModeToolInput export type ToolOutput = | ReadToolOutput | TaskToolOutput @@ -114,6 +165,11 @@ export type ToolOutput = | GrepToolOutput | WebFetchToolOutput | WriteToolOutput + | EditToolOutput + | MultiEditToolOutput + | BashOutputToolOutput + | NotebookEditToolOutput + | ExitPlanModeToolOutput // 工具渲染器接口 export interface ToolRenderer { render: (props: { input: ToolInput; output?: ToolOutput }) => React.ReactElement diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx index 77f61e144..29b25e9a5 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx @@ -35,6 +35,11 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => case 'Grep': case 'Write': case 'WebFetch': + case 'Edit': + case 'MultiEdit': + case 'BashOutput': + case 'NotebookEdit': + case 'ExitPlanMode': return default: return null diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentToolingSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentToolingSettings.tsx index 332eeee1b..1270ebf5a 100644 --- a/src/renderer/src/pages/settings/AgentSettings/AgentToolingSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/AgentToolingSettings.tsx @@ -108,6 +108,7 @@ export const AgentToolingSettings: FC = ({ agent, upd const { t } = useTranslation() const client = useAgentClient() const { mcpServers: allServers } = useMCPServers() + const [modal, contextHolder] = Modal.useModal() const [configuration, setConfiguration] = useState(defaultConfiguration) const [selectedMode, setSelectedMode] = useState(defaultConfiguration.permission_mode) @@ -192,8 +193,12 @@ export const AgentToolingSettings: FC = ({ agent, upd } if (removedDefaults.length > 0) { - Modal.confirm({ - title: t('agent.settings.tooling.permissionMode.confirmChange.title', 'Change permission mode?'), + modal.confirm({ + title: ( + + {t('agent.settings.tooling.permissionMode.confirmChange.title', 'Change permission mode?')} + + ), content: (

@@ -207,7 +212,11 @@ export const AgentToolingSettings: FC = ({ agent, upd

    {removedDefaults.map((id) => { const tool = availableTools.find((item) => item.id === id) - return
  • {tool?.name ?? id}
  • + return ( +
  • + {tool?.name ?? id} +
  • + ) })}
@@ -215,13 +224,27 @@ export const AgentToolingSettings: FC = ({ agent, upd ), okText: t('common.confirm'), cancelText: t('common.cancel'), - onOk: applyChange + onOk: applyChange, + classNames: { + content: 'bg-background! border! border-solid! rounded border-grey border-default-200!' + } }) } else { void applyChange() } }, - [agent, selectedMode, isUpdatingMode, availableTools, userAddedIds, autoToolIds, configuration, updateAgent, t] + [ + agent, + selectedMode, + isUpdatingMode, + availableTools, + userAddedIds, + autoToolIds, + configuration, + updateAgent, + modal, + t + ] ) const handleToggleTool = useCallback( @@ -300,6 +323,7 @@ export const AgentToolingSettings: FC = ({ agent, upd return ( + {contextHolder} {t('agent.settings.tooling.steps.permissionMode.title', 'Step 1 · Permission mode')} @@ -309,6 +333,7 @@ export const AgentToolingSettings: FC = ({ agent, upd const isSelected = card.mode === selectedMode const disabled = card.unsupported const showCaution = card.caution + return ( = ({ agent, upd {t(card.behaviorKey, card.behaviorFallback)} {showCaution ? ( - - - {t( - 'agent.settings.tooling.permissionMode.bypassPermissions.warning', - 'Use with caution — all tools will run without asking for approval.' - )} - +
+ + + {t( + 'agent.settings.tooling.permissionMode.bypassPermissions.warning', + 'Use with caution — all tools will run without asking for approval.' + )} + +
) : null}
@@ -375,7 +402,7 @@ export const AgentToolingSettings: FC = ({ agent, upd onValueChange={setSearchTerm} placeholder={t('agent.settings.tooling.preapproved.search', 'Search tools')} aria-label={t('agent.settings.tooling.preapproved.search', 'Search tools')} - className="max-w-md" + className="w-full" />
{filteredTools.length === 0 ? ( diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index af6350d48..09a237a37 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -267,6 +267,10 @@ export const SessionIdParamSchema = z.object({ sessionId: z.string().min(1, 'Session ID is required') }) +export const SessionMessageIdParamSchema = z.object({ + messageId: z.coerce.number().int().positive('Message ID must be a positive integer') +}) + // Query validation schemas export const PaginationQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(100).optional().default(20),