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 0000000000..f2dc8f143a --- /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 9ea04b2dfc..aad9c7ab32 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 0000000000..c3c1f9dc5b --- /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 0000000000..1769c384f0 --- /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 0000000000..75f7e51808 --- /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 0000000000..f15664969c --- /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 79f68c03bf..129072cda4 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 4597c5f77d..257926cf9d 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 a96c7cff6a..7d33a756d5 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 0e483688a6..46baaa3693 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 77f61e1442..29b25e9a55 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