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