diff --git a/src/main/apiServer/routes/models.ts b/src/main/apiServer/routes/models.ts index 8481e1ea59..d776d5ea91 100644 --- a/src/main/apiServer/routes/models.ts +++ b/src/main/apiServer/routes/models.ts @@ -104,12 +104,6 @@ const router = express logger.warn('No models available from providers', { filter }) } - logger.info('Models response ready', { - filter, - total: response.total, - modelIds: response.data.map((m) => m.id) - }) - return res.json(response satisfies ApiModelsResponse) } catch (error: any) { logger.error('Error fetching models', { error }) diff --git a/src/main/apiServer/services/models.ts b/src/main/apiServer/services/models.ts index a32d6d37dc..52f0db857f 100644 --- a/src/main/apiServer/services/models.ts +++ b/src/main/apiServer/services/models.ts @@ -32,7 +32,7 @@ export class ModelsService { for (const model of models) { const provider = providers.find((p) => p.id === model.provider) - logger.debug(`Processing model ${model.id}`) + // logger.debug(`Processing model ${model.id}`) if (!provider) { logger.debug(`Skipping model ${model.id} . Reason: Provider not found.`) continue diff --git a/src/main/services/agents/services/claudecode/__tests__/transform.test.ts b/src/main/services/agents/services/claudecode/__tests__/transform.test.ts index e38f897f2a..2565f5e605 100644 --- a/src/main/services/agents/services/claudecode/__tests__/transform.test.ts +++ b/src/main/services/agents/services/claudecode/__tests__/transform.test.ts @@ -21,6 +21,11 @@ describe('stripLocalCommandTags', () => { 'line1\nkeep\nError' expect(stripLocalCommandTags(input)).toBe('line1\nkeep\nError') }) + + it('if no tags present, returns original string', () => { + const input = 'just some normal text' + expect(stripLocalCommandTags(input)).toBe(input) + }) }) describe('Claude → AiSDK transform', () => { @@ -188,6 +193,111 @@ describe('Claude → AiSDK transform', () => { expect(toolResult.output).toBe('ok') }) + it('handles tool calls without streaming events (no content_block_start/stop)', () => { + const state = new ClaudeStreamState({ agentSessionId: '12344' }) + const parts: ReturnType[number][] = [] + + const messages: SDKMessage[] = [ + { + ...baseStreamMetadata, + type: 'assistant', + uuid: uuid(20), + message: { + id: 'msg-tool-no-stream', + type: 'message', + role: 'assistant', + model: 'claude-test', + content: [ + { + type: 'tool_use', + id: 'tool-read', + name: 'Read', + input: { file_path: '/test.txt' } + }, + { + type: 'tool_use', + id: 'tool-bash', + name: 'Bash', + input: { command: 'ls -la' } + } + ], + stop_reason: 'tool_use', + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20 + } + } + } as unknown as SDKMessage, + { + ...baseStreamMetadata, + type: 'user', + uuid: uuid(21), + message: { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-read', + content: 'file contents', + is_error: false + } + ] + } + } as SDKMessage, + { + ...baseStreamMetadata, + type: 'user', + uuid: uuid(22), + message: { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-bash', + content: 'total 42\n...', + is_error: false + } + ] + } + } as SDKMessage + ] + + for (const message of messages) { + const transformed = transformSDKMessageToStreamParts(message, state) + parts.push(...transformed) + } + + const types = parts.map((part) => part.type) + expect(types).toEqual(['tool-call', 'tool-call', 'tool-result', 'tool-result']) + + const toolCalls = parts.filter((part) => part.type === 'tool-call') as Extract< + (typeof parts)[number], + { type: 'tool-call' } + >[] + expect(toolCalls).toHaveLength(2) + expect(toolCalls[0].toolName).toBe('Read') + expect(toolCalls[0].toolCallId).toBe('12344:tool-read') + expect(toolCalls[1].toolName).toBe('Bash') + expect(toolCalls[1].toolCallId).toBe('12344:tool-bash') + + const toolResults = parts.filter((part) => part.type === 'tool-result') as Extract< + (typeof parts)[number], + { type: 'tool-result' } + >[] + expect(toolResults).toHaveLength(2) + // This is the key assertion - toolName should NOT be 'unknown' + expect(toolResults[0].toolName).toBe('Read') + expect(toolResults[0].toolCallId).toBe('12344:tool-read') + expect(toolResults[0].input).toEqual({ file_path: '/test.txt' }) + expect(toolResults[0].output).toBe('file contents') + + expect(toolResults[1].toolName).toBe('Bash') + expect(toolResults[1].toolCallId).toBe('12344:tool-bash') + expect(toolResults[1].input).toEqual({ command: 'ls -la' }) + expect(toolResults[1].output).toBe('total 42\n...') + }) + it('handles streaming text completion', () => { const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id }) const parts: ReturnType[number][] = [] @@ -300,4 +410,87 @@ describe('Claude → AiSDK transform', () => { expect(finishStep.finishReason).toBe('stop') expect(finishStep.usage).toEqual({ inputTokens: 2, outputTokens: 4, totalTokens: 6 }) }) + + it('emits fallback text when Claude sends a snapshot instead of deltas', () => { + const state = new ClaudeStreamState({ agentSessionId: '12344' }) + const parts: ReturnType[number][] = [] + + const messages: SDKMessage[] = [ + { + ...baseStreamMetadata, + type: 'stream_event', + uuid: uuid(30), + event: { + type: 'message_start', + message: { + id: 'msg-fallback', + type: 'message', + role: 'assistant', + model: 'claude-test', + content: [], + stop_reason: null, + stop_sequence: null, + usage: {} + } + } + } as unknown as SDKMessage, + { + ...baseStreamMetadata, + type: 'stream_event', + uuid: uuid(31), + event: { + type: 'content_block_start', + index: 0, + content_block: { + type: 'text', + text: '' + } + } + } as unknown as SDKMessage, + { + ...baseStreamMetadata, + type: 'assistant', + uuid: uuid(32), + message: { + id: 'msg-fallback-content', + type: 'message', + role: 'assistant', + model: 'claude-test', + content: [ + { + type: 'text', + text: 'Final answer without streaming deltas.' + } + ], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 3, + output_tokens: 7 + } + } + } as unknown as SDKMessage + ] + + for (const message of messages) { + const transformed = transformSDKMessageToStreamParts(message, state) + parts.push(...transformed) + } + + const types = parts.map((part) => part.type) + expect(types).toEqual(['start-step', 'text-start', 'text-delta', 'text-end', 'finish-step']) + + const delta = parts.find((part) => part.type === 'text-delta') as Extract< + (typeof parts)[number], + { type: 'text-delta' } + > + expect(delta.text).toBe('Final answer without streaming deltas.') + + const finish = parts.find((part) => part.type === 'finish-step') as Extract< + (typeof parts)[number], + { type: 'finish-step' } + > + expect(finish.usage).toEqual({ inputTokens: 3, outputTokens: 7, totalTokens: 10 }) + expect(finish.finishReason).toBe('stop') + }) }) diff --git a/src/main/services/agents/services/claudecode/claude-stream-state.ts b/src/main/services/agents/services/claudecode/claude-stream-state.ts index 19a664a8d1..30b5790c82 100644 --- a/src/main/services/agents/services/claudecode/claude-stream-state.ts +++ b/src/main/services/agents/services/claudecode/claude-stream-state.ts @@ -153,6 +153,20 @@ export class ClaudeStreamState { return this.blocksByIndex.get(index) } + getFirstOpenTextBlock(): TextBlockState | undefined { + const candidates: TextBlockState[] = [] + for (const block of this.blocksByIndex.values()) { + if (block.kind === 'text') { + candidates.push(block) + } + } + if (candidates.length === 0) { + return undefined + } + candidates.sort((a, b) => a.index - b.index) + return candidates[0] + } + getToolBlockById(toolCallId: string): ToolBlockState | undefined { const index = this.toolIndexByNamespacedId.get(toolCallId) if (index === undefined) return undefined @@ -217,10 +231,10 @@ export class ClaudeStreamState { * Persists the final input payload for a tool block once the provider signals * completion so that downstream tool results can reference the original call. */ - completeToolBlock(toolCallId: string, input: unknown, providerMetadata?: ProviderMetadata): void { + completeToolBlock(toolCallId: string, toolName: string, input: unknown, providerMetadata?: ProviderMetadata): void { const block = this.getToolBlockByRawId(toolCallId) this.registerToolCall(toolCallId, { - toolName: block?.toolName ?? 'unknown', + toolName, input, providerMetadata }) diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 327031d2f3..83d3e49311 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -414,23 +414,6 @@ class ClaudeCodeService implements AgentServiceInterface { } } - if (message.type === 'assistant' || message.type === 'user') { - logger.silly('claude response', { - message, - content: JSON.stringify(message.message.content) - }) - } else if (message.type === 'stream_event') { - // logger.silly('Claude stream event', { - // message, - // event: JSON.stringify(message.event) - // }) - } else { - logger.silly('Claude response', { - message, - event: JSON.stringify(message) - }) - } - const chunks = transformSDKMessageToStreamParts(message, streamState) for (const chunk of chunks) { stream.emit('data', { diff --git a/src/main/services/agents/services/claudecode/transform.ts b/src/main/services/agents/services/claudecode/transform.ts index cbd2f735d6..00be683ba8 100644 --- a/src/main/services/agents/services/claudecode/transform.ts +++ b/src/main/services/agents/services/claudecode/transform.ts @@ -110,7 +110,7 @@ const sdkMessageToProviderMetadata = (message: SDKMessage): ProviderMetadata => * blocks across calls so that incremental deltas can be correlated correctly. */ export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage, state: ClaudeStreamState): AgentStreamPart[] { - logger.silly('Transforming SDKMessage', { message: sdkMessage }) + logger.silly('Transforming SDKMessage', { message: JSON.stringify(sdkMessage) }) switch (sdkMessage.type) { case 'assistant': return handleAssistantMessage(sdkMessage, state) @@ -186,14 +186,13 @@ function handleAssistantMessage( for (const block of content) { switch (block.type) { - case 'text': - if (!isStreamingActive) { - const sanitizedText = stripLocalCommandTags(block.text) - if (sanitizedText) { - textBlocks.push(sanitizedText) - } + case 'text': { + const sanitizedText = stripLocalCommandTags(block.text) + if (sanitizedText) { + textBlocks.push(sanitizedText) } break + } case 'tool_use': handleAssistantToolUse(block as ToolUseContent, providerMetadata, state, chunks) break @@ -203,7 +202,16 @@ function handleAssistantMessage( } } - if (!isStreamingActive && textBlocks.length > 0) { + if (textBlocks.length === 0) { + return chunks + } + + const combinedText = textBlocks.join('') + if (!combinedText) { + return chunks + } + + if (!isStreamingActive) { const id = message.uuid?.toString() || generateMessageId() state.beginStep() chunks.push({ @@ -219,7 +227,7 @@ function handleAssistantMessage( chunks.push({ type: 'text-delta', id, - text: textBlocks.join(''), + text: combinedText, providerMetadata }) chunks.push({ @@ -230,7 +238,27 @@ function handleAssistantMessage( return finalizeNonStreamingStep(message, state, chunks) } - return chunks + const existingTextBlock = state.getFirstOpenTextBlock() + const fallbackId = existingTextBlock?.id || message.uuid?.toString() || generateMessageId() + if (!existingTextBlock) { + chunks.push({ + type: 'text-start', + id: fallbackId, + providerMetadata + }) + } + chunks.push({ + type: 'text-delta', + id: fallbackId, + text: combinedText, + providerMetadata + }) + chunks.push({ + type: 'text-end', + id: fallbackId, + providerMetadata + }) + return finalizeNonStreamingStep(message, state, chunks) } /** @@ -252,7 +280,7 @@ function handleAssistantToolUse( providerExecuted: true, providerMetadata }) - state.completeToolBlock(block.id, block.input, providerMetadata) + state.completeToolBlock(block.id, block.name, block.input, providerMetadata) } /** @@ -459,6 +487,9 @@ function handleStreamEvent( } case 'message_stop': { + if (!state.hasActiveStep()) { + break + } const pending = state.getPendingUsage() chunks.push({ type: 'finish-step', diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx index 71fa6307d2..b47bb3f64a 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashOutputTool.tsx @@ -1,7 +1,6 @@ import type { CollapseProps } from 'antd' import { Tag } from 'antd' import { CheckCircle, Terminal, XCircle } from 'lucide-react' -import { useMemo } from 'react' import { ToolTitle } from './GenericTools' import type { BashOutputToolInput, BashOutputToolOutput } from './types' @@ -16,6 +15,63 @@ interface ParsedBashOutput { tool_use_error?: string } +const parseBashOutput = (output?: BashOutputToolOutput): ParsedBashOutput | null => { + if (!output) return null + + try { + const parser = new DOMParser() + const hasToolError = output.includes('') + 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() + } + + return { + 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') + } + } catch { + return null + } +} + +const getStatusConfig = (parsedOutput: ParsedBashOutput | null) => { + if (!parsedOutput) return null + + 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 +} + export function BashOutputTool({ input, output @@ -23,73 +79,8 @@ export function BashOutputTool({ input: BashOutputToolInput output?: BashOutputToolOutput }): NonNullable[number] { - // 解析 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]) + const parsedOutput = parseBashOutput(output) + const statusConfig = getStatusConfig(parsedOutput) const children = parsedOutput ? ( 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 0b665a9fd4..043d8a94c4 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/ReadTool.tsx @@ -1,12 +1,47 @@ import type { CollapseProps } from 'antd' 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, TextOutput } from './types' import { AgentToolsType } from './types' +const removeSystemReminderTags = (text: string): string => { + return text.replace(/[\s\S]*?<\/system-reminder>/gi, '') +} + +const normalizeOutputString = (output?: ReadToolOutputType): string | null => { + if (!output) return null + + const toText = (item: TextOutput) => removeSystemReminderTags(item.text) + + if (Array.isArray(output)) { + return output + .filter((item): item is TextOutput => item.type === 'text') + .map(toText) + .join('') + } + + return removeSystemReminderTags(output) +} + +const getOutputStats = (outputString: string | null) => { + if (!outputString) return null + + const bytes = new Blob([outputString]).size + const formatSize = (size: number) => { + if (size < 1024) return `${size} B` + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB` + return `${(size / (1024 * 1024)).toFixed(1)} MB` + } + + return { + lineCount: outputString.split('\n').length, + fileSize: bytes, + formatSize + } +} + export function ReadTool({ input, output @@ -14,50 +49,8 @@ export function ReadTool({ input: ReadToolInputType output?: ReadToolOutputType }): NonNullable[number] { - // 移除 system-reminder 标签及其内容的辅助函数 - const removeSystemReminderTags = (text: string): string => { - // 使用正则表达式匹配 标签及其内容,包括换行符 - return text.replace(/[\s\S]*?<\/system-reminder>/gi, '') - } - - // 将 output 统一转换为字符串 - const outputString = useMemo(() => { - if (!output) return null - - let processedOutput: string - - // 如果是 TextOutput[] 类型,提取所有 text 内容 - if (Array.isArray(output)) { - processedOutput = output - .filter((item): item is TextOutput => item.type === 'text') - .map((item) => removeSystemReminderTags(item.text)) - .join('') - } else { - // 如果是字符串,直接使用 - processedOutput = output - } - - // 移除 system-reminder 标签及其内容 - return removeSystemReminderTags(processedOutput) - }, [output]) - - // 如果有输出,计算统计信息 - 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]) + const outputString = normalizeOutputString(output) + const stats = getOutputStats(outputString) return { key: AgentToolsType.Read, diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx index 969eda9507..8a6965b6f6 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/UnknownToolRenderer.tsx @@ -11,11 +11,24 @@ interface UnknownToolProps { output?: unknown } -export function UnknownToolRenderer({ - toolName = '', - input, - output -}: UnknownToolProps): NonNullable[number] { +const getToolDisplayName = (name: string) => { + if (name.startsWith('mcp__')) { + const parts = name.substring(5).split('__') + if (parts.length >= 2) { + return `${parts[0]}:${parts.slice(1).join(':')}` + } + } + return name +} + +const getToolDescription = (toolName: string) => { + if (toolName.startsWith('mcp__')) { + return 'MCP Server Tool' + } + return 'Tool' +} + +const UnknownToolContent = ({ input, output }: { input?: unknown; output?: unknown }) => { const { highlightCode } = useCodeStyle() const [inputHtml, setInputHtml] = useState('') const [outputHtml, setOutputHtml] = useState('') @@ -34,58 +47,49 @@ export function UnknownToolRenderer({ } }, [output, highlightCode]) - const getToolDisplayName = (name: string) => { - if (name.startsWith('mcp__')) { - const parts = name.substring(5).split('__') - if (parts.length >= 2) { - return `${parts[0]}:${parts.slice(1).join(':')}` - } - } - return name + if (input === undefined && output === undefined) { + return No data available for this tool } - const getToolDescription = () => { - if (toolName.startsWith('mcp__')) { - return 'MCP Server Tool' - } - return 'Tool' - } + return ( + + {input !== undefined && ( + + Input: + + + )} + {output !== undefined && ( + + Output: + + + )} + + ) +} + +export function UnknownToolRenderer({ + toolName = '', + input, + output +}: UnknownToolProps): NonNullable[number] { return { key: 'unknown-tool', label: ( } label={getToolDisplayName(toolName)} - params={getToolDescription()} + params={getToolDescription(toolName)} /> ), - children: ( - - {input !== undefined && ( - - Input: - - - )} - - {output !== undefined && ( - - Output: - - - )} - - {input === undefined && output === undefined && ( - No data available for this tool - )} - - ) + children: } } 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 27abf7426c..42a1cf403b 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/index.tsx @@ -6,8 +6,6 @@ import { Collapse } from 'antd' // 导出所有类型 export * from './types' -import { useMemo } from 'react' - // 导入所有渲染器 import ToolPermissionRequestCard from '../ToolPermissionRequestCard' import { BashOutputTool } from './BashOutputTool' @@ -57,22 +55,19 @@ export function isValidAgentToolsType(toolName: unknown): toolName is AgentTools return typeof toolName === 'string' && Object.values(AgentToolsType).includes(toolName as AgentToolsType) } -// 统一的渲染函数 -function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) { +// 统一的渲染组件 +function ToolContent({ toolName, input, output }: { toolName: AgentToolsType; input: ToolInput; output?: ToolOutput }) { const Renderer = toolRenderers[toolName] + const renderedItem = Renderer + ? Renderer({ input: input as any, output: output as any }) + : UnknownToolRenderer({ input: input as any, output: output as any, toolName }) - // eslint-disable-next-line react-hooks/rules-of-hooks - const toolContentItem = useMemo(() => { - const rendered = Renderer - ? Renderer({ input: input as any, output: output as any }) - : UnknownToolRenderer({ input: input as any, output: output as any, toolName }) - return { - ...rendered, - classNames: { - body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll' - } as NonNullable[number]['classNames'] - } as NonNullable[number] - }, [Renderer, input, output, toolName]) + const toolContentItem: NonNullable[number] = { + ...renderedItem, + classNames: { + body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll' + } + } return ( } - return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput) + return ( + + ) } diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index c9b33eea8e..a70fdf572d 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -585,9 +585,11 @@ const fetchAndProcessAgentResponseImpl = async ( return } + // Only mark as cleared if there was a previous session ID (not initial assignment) + sessionWasCleared = !!latestAgentSessionId + latestAgentSessionId = sessionId agentSession.agentSessionId = sessionId - sessionWasCleared = true logger.debug(`Agent session ID updated`, { topicId,