diff --git a/src/main/services/agents/services/claudecode/transform.ts b/src/main/services/agents/services/claudecode/transform.ts index 4af3716c1d..f421e91f22 100644 --- a/src/main/services/agents/services/claudecode/transform.ts +++ b/src/main/services/agents/services/claudecode/transform.ts @@ -73,6 +73,15 @@ const emptyUsage: LanguageModelUsage = { */ const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}` +/** + * Filters out command-* tags from text content to prevent internal command + * messages from appearing in the user-facing UI. + * Removes tags like ... and ... + */ +const filterCommandTags = (text: string): string => { + return text.replace(/]+>.*?<\/command-[^>]+>/gs, '').trim() +} + /** * Extracts provider metadata from the raw Claude message so we can surface it * on every emitted stream part for observability and debugging purposes. @@ -270,12 +279,17 @@ function handleUserMessage( const chunks: AgentStreamPart[] = [] const providerMetadata = sdkMessageToProviderMetadata(message) const content = message.message.content - + const isSynthetic = message.isSynthetic ?? false if (typeof content === 'string') { if (!content) { return chunks } + const filteredContent = filterCommandTags(content) + if (!filteredContent) { + return chunks + } + const id = message.uuid?.toString() || generateMessageId() chunks.push({ type: 'text-start', @@ -285,7 +299,7 @@ function handleUserMessage( chunks.push({ type: 'text-delta', id, - text: content, + text: filteredContent, providerMetadata }) chunks.push({ @@ -323,24 +337,30 @@ function handleUserMessage( providerExecuted: true }) } - } else if (block.type === 'text') { - const id = message.uuid?.toString() || generateMessageId() - chunks.push({ - type: 'text-start', - id, - providerMetadata - }) - chunks.push({ - type: 'text-delta', - id, - text: (block as { text: string }).text, - providerMetadata - }) - chunks.push({ - type: 'text-end', - id, - providerMetadata - }) + } else if (block.type === 'text' && !isSynthetic) { + const rawText = (block as { text: string }).text + const filteredText = filterCommandTags(rawText) + + // Only push text chunks if there's content after filtering + if (filteredText) { + const id = message.uuid?.toString() || generateMessageId() + chunks.push({ + type: 'text-start', + id, + providerMetadata + }) + chunks.push({ + type: 'text-delta', + id, + text: filteredText, + providerMetadata + }) + chunks.push({ + type: 'text-end', + id, + providerMetadata + }) + } } else { logger.warn('Unhandled user content block', { type: (block as any).type }) } diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/SkillTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/SkillTool.tsx new file mode 100644 index 0000000000..2c68beb184 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/SkillTool.tsx @@ -0,0 +1,16 @@ +import { AccordionItem } from '@heroui/react' +import { PencilRuler } from 'lucide-react' + +import { ToolTitle } from './GenericTools' +import type { SkillToolInput, SkillToolOutput } from './types' + +export function SkillTool({ input, output }: { input: SkillToolInput; output?: SkillToolOutput }) { + return ( + } label="Skill" params={input.command} />}> + {output} + + ) +} 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 d8535f7fcc..8b10508bf1 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx @@ -17,6 +17,7 @@ import { MultiEditTool } from './MultiEditTool' import { NotebookEditTool } from './NotebookEditTool' import { ReadTool } from './ReadTool' import { SearchTool } from './SearchTool' +import { SkillTool } from './SkillTool' import { TaskTool } from './TaskTool' import { TodoWriteTool } from './TodoWriteTool' import { AgentToolsType, ToolInput, ToolOutput } from './types' @@ -24,6 +25,7 @@ import { UnknownToolRenderer } from './UnknownToolRenderer' import { WebFetchTool } from './WebFetchTool' import { WebSearchTool } from './WebSearchTool' import { WriteTool } from './WriteTool' + const logger = loggerService.withContext('MessageAgentTools') // 创建工具渲染器映射,这样就实现了完全的类型安全 @@ -42,7 +44,8 @@ export const toolRenderers = { [AgentToolsType.MultiEdit]: MultiEditTool, [AgentToolsType.BashOutput]: BashOutputTool, [AgentToolsType.NotebookEdit]: NotebookEditTool, - [AgentToolsType.ExitPlanMode]: ExitPlanModeTool + [AgentToolsType.ExitPlanMode]: ExitPlanModeTool, + [AgentToolsType.Skill]: SkillTool } as const // 类型守卫函数 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 1abd474860..f4271b3a2e 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/types.ts +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/types.ts @@ -1,4 +1,5 @@ export enum AgentToolsType { + Skill = 'Skill', Read = 'Read', Task = 'Task', Bash = 'Bash', @@ -22,6 +23,15 @@ export type TextOutput = { } // Read 工具的类型定义 +export interface SkillToolInput { + /** + * The skill to use + */ + command: string +} + +export type SkillToolOutput = string + export interface ReadToolInput { /** * The absolute path to the file to read diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx index 88cf2dab56..20f7f01651 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageTool.tsx @@ -2,6 +2,7 @@ import { NormalToolResponse } from '@renderer/types' import type { ToolMessageBlock } from '@renderer/types/newMessage' import { MessageAgentTools } from './MessageAgentTools' +import { AgentToolsType } from './MessageAgentTools/types' import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch' import { MessageMemorySearchToolTitle } from './MessageMemorySearch' import { MessageWebSearchToolTitle } from './MessageWebSearch' @@ -9,27 +10,12 @@ import { MessageWebSearchToolTitle } from './MessageWebSearch' interface Props { block: ToolMessageBlock } -const prefix = 'builtin_' -const agentPrefix = 'mcp__' -const agentTools = [ - 'Read', - 'Task', - 'Bash', - 'Search', - 'Glob', - 'TodoWrite', - 'WebSearch', - 'Grep', - 'Write', - 'WebFetch', - 'Edit', - 'MultiEdit', - 'BashOutput', - 'NotebookEdit', - 'ExitPlanMode' -] -const isAgentTool = (toolName: string) => { - if (agentTools.includes(toolName) || toolName.startsWith(agentPrefix)) { +const builtinToolsPrefix = 'builtin_' +const agentMcpToolsPrefix = 'mcp__' +const agentTools = Object.values(AgentToolsType) + +const isAgentTool = (toolName: AgentToolsType) => { + if (agentTools.includes(toolName) || toolName.startsWith(agentMcpToolsPrefix)) { return true } return false @@ -38,8 +24,8 @@ const isAgentTool = (toolName: string) => { const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => { let toolName = toolResponse.tool.name const toolType = toolResponse.tool.type - if (toolName.startsWith(prefix)) { - toolName = toolName.slice(prefix.length) + if (toolName.startsWith(builtinToolsPrefix)) { + toolName = toolName.slice(builtinToolsPrefix.length) switch (toolName) { case 'web_search': case 'web_search_preview': @@ -51,7 +37,7 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => default: return null } - } else if (isAgentTool(toolName)) { + } else if (isAgentTool(toolName as AgentToolsType)) { return } return null