Compare commits

...

3 Commits

15 changed files with 147 additions and 18 deletions

View File

@ -400,6 +400,7 @@ pub struct OutboundMessage {
pub tool_call_id: Option<String>, pub tool_call_id: Option<String>,
pub tool_name: Option<String>, pub tool_name: Option<String>,
pub tool_arguments: Option<serde_json::Value>, pub tool_arguments: Option<serde_json::Value>,
pub reasoning_content: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -439,6 +440,7 @@ impl OutboundMessage {
tool_call_id: None, tool_call_id: None,
tool_name: None, tool_name: None,
tool_arguments: None, tool_arguments: None,
reasoning_content: None,
} }
} }
@ -493,6 +495,7 @@ impl OutboundMessage {
tool_call_id: Some(message_id.into()), tool_call_id: Some(message_id.into()),
tool_name: Some(tool_name), tool_name: Some(tool_name),
tool_arguments: Some(tool_arguments), tool_arguments: Some(tool_arguments),
reasoning_content: None,
} }
} }
@ -522,6 +525,7 @@ impl OutboundMessage {
tool_call_id: Some(tool_call_id.into()), tool_call_id: Some(tool_call_id.into()),
tool_name: Some(tool_name), tool_name: Some(tool_name),
tool_arguments: None, tool_arguments: None,
reasoning_content: None,
} }
} }
@ -551,6 +555,7 @@ impl OutboundMessage {
tool_call_id: Some(tool_call_id.into()), tool_call_id: Some(tool_call_id.into()),
tool_name: Some(tool_name), tool_name: Some(tool_name),
tool_arguments: None, tool_arguments: None,
reasoning_content: None,
} }
} }
@ -566,19 +571,25 @@ impl OutboundMessage {
"assistant" => { "assistant" => {
if let Some(tool_calls) = &message.tool_calls { if let Some(tool_calls) = &message.tool_calls {
let mut outbound = Vec::new(); let mut outbound = Vec::new();
if !message.content.trim().is_empty() { let has_content_or_reasoning = !message.content.trim().is_empty() || message.reasoning_content.is_some();
outbound.push(Self::assistant( if has_content_or_reasoning {
let mut resp = Self::assistant(
channel.to_string(), channel.to_string(),
chat_id.to_string(), chat_id.to_string(),
session_id.clone(), session_id.clone(),
message.content.clone(), message.content.clone(),
reply_to.clone(), reply_to.clone(),
metadata.clone(), metadata.clone(),
)); );
resp.reasoning_content = message.reasoning_content.clone();
outbound.push(resp);
} }
// AssistantResponse 已携带 reasoning 时ToolCall 不再重复;
// 只有 AssistantResponse 没发时ToolCall 才带 reasoning
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
outbound.extend(tool_calls.iter().map(|tool_call| { outbound.extend(tool_calls.iter().map(|tool_call| {
Self::tool_call( let mut tc = Self::tool_call(
channel.to_string(), channel.to_string(),
chat_id.to_string(), chat_id.to_string(),
session_id.clone(), session_id.clone(),
@ -587,18 +598,22 @@ impl OutboundMessage {
tool_call.arguments.clone(), tool_call.arguments.clone(),
reply_to.clone(), reply_to.clone(),
metadata.clone(), metadata.clone(),
) );
tc.reasoning_content = tc_reasoning.clone();
tc
})); }));
outbound outbound
} else { } else {
vec![Self::assistant( let mut resp = Self::assistant(
channel.to_string(), channel.to_string(),
chat_id.to_string(), chat_id.to_string(),
session_id, session_id,
message.content.clone(), message.content.clone(),
reply_to, reply_to,
metadata.clone(), metadata.clone(),
)] );
resp.reasoning_content = message.reasoning_content.clone();
vec![resp]
} }
} }
"tool" => match message "tool" => match message

View File

@ -79,6 +79,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
}, },
MessageKind::Notification => { MessageKind::Notification => {
// 根据元数据判断具体类型 // 根据元数据判断具体类型
@ -99,6 +100,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
}, },
} }
} else if let Some(session_id) = response.metadata.get("session_id") { } else if let Some(session_id) = response.metadata.get("session_id") {
@ -138,6 +140,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
}, },
} }
} else if let Some(sessions_json) = response.metadata.get("sessions") { } else if let Some(sessions_json) = response.metadata.get("sessions") {
@ -156,6 +159,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
}, },
} }
} else if let Some(topics_json) = response.metadata.get("topics") { } else if let Some(topics_json) = response.metadata.get("topics") {
@ -175,6 +179,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
}, },
} }
} else { } else {
@ -184,6 +189,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
} }
} }
} }
@ -197,6 +203,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), role: "assistant".to_string(),
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()), attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: None,
}, },
}; };
outbounds.push(outbound); outbounds.push(outbound);

View File

@ -102,6 +102,7 @@
- 回答应以帮助用户完成当前目标为中心。 - 回答应以帮助用户完成当前目标为中心。
- 在信息不足时先补关键前提,在信息充分时直接执行。 - 在信息不足时先补关键前提,在信息充分时直接执行。
- Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate并传入对应的 name。 - Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate并传入对应的 name。
- 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么
## 定时任务 ## 定时任务
@ -109,5 +110,3 @@
- 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话 - 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话
- 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具 - 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具
## 注意
- 不要通过一次调用写入一个很长的文件,请分段写入

View File

@ -762,6 +762,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(msg.timestamp / 1000), timestamp: Some(msg.timestamp / 1000),
reasoning_content: msg.reasoning_content.clone(),
}); });
} }
} }
@ -774,6 +775,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(msg.timestamp / 1000), timestamp: Some(msg.timestamp / 1000),
reasoning_content: msg.reasoning_content.clone(),
}) })
} }
"tool" => { "tool" => {
@ -811,6 +813,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: Some(msg.timestamp / 1000), timestamp: Some(msg.timestamp / 1000),
reasoning_content: None,
}), }),
_ => None, _ => None,
} }

View File

@ -139,6 +139,8 @@ pub enum WsOutbound {
topic_id: Option<String>, topic_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>, timestamp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
reasoning_content: Option<String>,
}, },
#[serde(rename = "tool_call")] #[serde(rename = "tool_call")]
ToolCall { ToolCall {
@ -154,6 +156,8 @@ pub enum WsOutbound {
topic_id: Option<String>, topic_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>, timestamp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
reasoning_content: Option<String>,
}, },
#[serde(rename = "tool_result")] #[serde(rename = "tool_result")]
ToolResult { ToolResult {

View File

@ -15,7 +15,8 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
"assistant" => { "assistant" => {
if let Some(tool_calls) = &message.tool_calls { if let Some(tool_calls) = &message.tool_calls {
let mut outbound = Vec::new(); let mut outbound = Vec::new();
if !message.content.trim().is_empty() { let has_content_or_reasoning = !message.content.trim().is_empty() || message.reasoning_content.is_some();
if has_content_or_reasoning {
outbound.push(WsOutbound::AssistantResponse { outbound.push(WsOutbound::AssistantResponse {
id: message.id.clone(), id: message.id.clone(),
content: message.content.clone(), content: message.content.clone(),
@ -24,9 +25,12 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: message.reasoning_content.clone(),
}); });
} }
// AssistantResponse 已携带 reasoning 时ToolCall 不再重复
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall { outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall {
id: message.id.clone(), id: message.id.clone(),
tool_call_id: tool_call.id.clone(), tool_call_id: tool_call.id.clone(),
@ -37,6 +41,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: tc_reasoning.clone(),
})); }));
outbound outbound
} else { } else {
@ -48,6 +53,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
subagent_task_id: None, subagent_task_id: None,
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: message.reasoning_content.clone(),
}] }]
} }
} }
@ -105,6 +111,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
subagent_task_id: message.metadata.get("subagent_task_id").cloned(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
topic_id: message.metadata.get("topic_id").cloned(), topic_id: message.metadata.get("topic_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: message.reasoning_content.clone(),
}] }]
} }
OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall { OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall {
@ -123,6 +130,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
subagent_task_id: message.metadata.get("subagent_task_id").cloned(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
topic_id: message.metadata.get("topic_id").cloned(), topic_id: message.metadata.get("topic_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
reasoning_content: message.reasoning_content.clone(),
}], }],
OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult { OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult {
id: message id: message

View File

@ -438,6 +438,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
tool_call_id: None, tool_call_id: None,
tool_name: None, tool_name: None,
tool_arguments: None, tool_arguments: None,
reasoning_content: None,
}; };
if let Err(e) = bus.publish_outbound(event).await { if let Err(e) = bus.publish_outbound(event).await {

View File

@ -123,6 +123,9 @@ fn test_tool_call_outbound_serialization() {
content: "调用工具: calculator".to_string(), content: "调用工具: calculator".to_string(),
role: "assistant".to_string(), role: "assistant".to_string(),
subagent_task_id: None, subagent_task_id: None,
topic_id: None,
timestamp: None,
reasoning_content: None,
}; };
let json = serde_json::to_string(&msg).unwrap(); let json = serde_json::to_string(&msg).unwrap();
@ -156,6 +159,8 @@ fn test_tool_result_outbound_serialization() {
role: "tool".to_string(), role: "tool".to_string(),
subagent_task_id: None, subagent_task_id: None,
duration_ms: None, duration_ms: None,
topic_id: None,
timestamp: None,
}; };
let json = serde_json::to_string(&msg).unwrap(); let json = serde_json::to_string(&msg).unwrap();

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X } from 'lucide-react' import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain } from 'lucide-react'
import { ChatContainer } from './components/Chat/ChatContainer' import { ChatContainer } from './components/Chat/ChatContainer'
import { TopicList } from './components/Sidebar/TopicList' import { TopicList } from './components/Sidebar/TopicList'
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList' import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
@ -120,6 +120,14 @@ function App() {
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [theme]) }, [theme])
const [showThinking, setShowThinking] = useState(() => {
return localStorage.getItem('picobot-show-thinking') !== 'false'
})
useEffect(() => {
localStorage.setItem('picobot-show-thinking', String(showThinking))
}, [showThinking])
// ---- WebSocket 初始化 ---- // ---- WebSocket 初始化 ----
// Step 1: 连接建立后先请求通道列表 // Step 1: 连接建立后先请求通道列表
@ -443,6 +451,18 @@ function App() {
> >
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} {theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button> </button>
<button
onClick={() => setShowThinking(prev => !prev)}
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-all ${
showThinking
? 'text-purple-400 hover:text-purple-300 bg-purple-500/10 hover:bg-purple-500/20'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
}`}
title={showThinking ? '隐藏思考过程' : '显示思考过程'}
aria-label="Toggle thinking display"
>
<Brain className="h-4 w-4" />
</button>
</div> </div>
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]"> <div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<ChannelSelector <ChannelSelector
@ -589,6 +609,7 @@ function App() {
onSendMessage={subAgentView || schedulerView ? () => {} : handleSendMessage} onSendMessage={subAgentView || schedulerView ? () => {} : handleSendMessage}
onNavigateToSubAgent={handleNavigateToSubAgent} onNavigateToSubAgent={handleNavigateToSubAgent}
onStop={handleStopExecution} onStop={handleStopExecution}
showThinking={showThinking}
/> />
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@ interface ChatContainerProps {
onSendMessage: (content: string, attachments: Attachment[]) => void onSendMessage: (content: string, attachments: Attachment[]) => void
onNavigateToSubAgent?: (taskId: string, description: string) => void onNavigateToSubAgent?: (taskId: string, description: string) => void
onStop?: () => void onStop?: () => void
showThinking?: boolean
} }
export function ChatContainer({ export function ChatContainer({
@ -20,11 +21,12 @@ export function ChatContainer({
onSendMessage, onSendMessage,
onNavigateToSubAgent, onNavigateToSubAgent,
onStop, onStop,
showThinking = true,
}: ChatContainerProps) { }: ChatContainerProps) {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} /> <MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
</div> </div>
<MessageInput <MessageInput
onSend={onSendMessage} onSend={onSendMessage}

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader, X } from 'lucide-react' import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader, X, Brain } from 'lucide-react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol' import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol'
@ -58,6 +58,7 @@ function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pen
interface MessageBubbleProps { interface MessageBubbleProps {
message: ChatMessage message: ChatMessage
onNavigateToSubAgent?: (taskId: string, description: string) => void onNavigateToSubAgent?: (taskId: string, description: string) => void
showThinking?: boolean
} }
function getAttachmentIcon(mediaType: string) { function getAttachmentIcon(mediaType: string) {
@ -236,6 +237,39 @@ function CopyButton({ text, className = '' }: { text: string; className?: string
) )
} }
function ThinkingSection({ content }: { content: string }) {
const [expanded, setExpanded] = useState(false)
return (
<div className="mb-3 rounded-lg border border-[var(--border-color)] bg-[var(--overlay-hover)] overflow-hidden">
<button
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded) }}
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-subtle)] transition-colors cursor-pointer select-none"
>
<Brain className="h-3.5 w-3.5 text-[var(--accent-purple)] flex-shrink-0" />
<span className="font-medium text-[var(--accent-purple)]">Thinking</span>
{expanded ? (
<ChevronDown className="h-3 w-3 ml-auto flex-shrink-0" />
) : (
<ChevronRight className="h-3 w-3 ml-auto flex-shrink-0" />
)}
{!expanded && (
<span className="text-[var(--text-muted)] truncate flex-1 text-left">
{content.slice(0, 60)}{content.length > 60 ? '…' : ''}
</span>
)}
</button>
{expanded && (
<div className="px-3 py-2 border-t border-[var(--border-color)] animate-thinking-reveal">
<div className="text-xs text-[var(--text-secondary)] whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto scrollbar-thin">
{content}
</div>
</div>
)}
</div>
)
}
function parseTaskResult(content: string): TaskToolResult | null { function parseTaskResult(content: string): TaskToolResult | null {
if (!content) return null if (!content) return null
try { try {
@ -255,7 +289,7 @@ function parseTaskResult(content: string): TaskToolResult | null {
} }
} }
export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubbleProps) { export function MessageBubble({ message, onNavigateToSubAgent, showThinking = true }: MessageBubbleProps) {
const isUser = message.role === 'user' const isUser = message.role === 'user'
const isTool = message.role === 'tool' const isTool = message.role === 'tool'
const isMergedTool = message.type === 'merged_tool' const isMergedTool = message.type === 'merged_tool'
@ -404,6 +438,12 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</span> </span>
</div> </div>
{/* 模型思考内容(工具调用时展示) */}
{showThinking && message.reasoningContent && !toolExpanded && (
<div className="px-3 pb-1">
<ThinkingSection content={message.reasoningContent} />
</div>
)}
{/* Collapsed preview */} {/* Collapsed preview */}
{!toolExpanded && ( {!toolExpanded && (
<> <>
@ -462,6 +502,10 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{/* Expanded */} {/* Expanded */}
{toolExpanded && ( {toolExpanded && (
<div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2"> <div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2">
{/* 模型思考内容(展开时也展示) */}
{showThinking && message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
{taskResult ? ( {taskResult ? (
<> <>
{taskPrompt && ( {taskPrompt && (
@ -608,7 +652,12 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
// 用户消息保持纯文本 // 用户消息保持纯文本
<div className="whitespace-pre-wrap text-sm leading-relaxed">{message.content}</div> <div className="whitespace-pre-wrap text-sm leading-relaxed">{message.content}</div>
) : ( ) : (
// AI 和工具消息使用 Markdown 渲染 // 模型思考内容(仅助手消息,非工具消息)
<>
{showThinking && !isTool && message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
{/* AI 和工具消息使用 Markdown 渲染 */}
<div className="markdown-content text-sm leading-relaxed"> <div className="markdown-content text-sm leading-relaxed">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
@ -688,7 +737,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{message.content} {message.content}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
)} </>)}
{message.attachments && message.attachments.length > 0 && ( {message.attachments && message.attachments.length > 0 && (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
{message.attachments.some(att => att.media_type === 'image' && att.content_base64) && ( {message.attachments.some(att => att.media_type === 'image' && att.content_base64) && (

View File

@ -6,9 +6,10 @@ import { Sparkles, ArrowDown, ArrowUp } from 'lucide-react'
interface MessageListProps { interface MessageListProps {
messages: ChatMessage[] messages: ChatMessage[]
onNavigateToSubAgent?: (taskId: string, description: string) => void onNavigateToSubAgent?: (taskId: string, description: string) => void
showThinking?: boolean
} }
export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps) { export function MessageList({ messages, onNavigateToSubAgent, showThinking = true }: MessageListProps) {
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const isAtBottomRef = useRef(true) const isAtBottomRef = useRef(true)
@ -116,7 +117,7 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps
className="h-full overflow-y-auto p-6 space-y-6" className="h-full overflow-y-auto p-6 space-y-6"
> >
{messages.map((message) => ( {messages.map((message) => (
<MessageBubble key={message.id} message={message} onNavigateToSubAgent={onNavigateToSubAgent} /> <MessageBubble key={message.id} message={message} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
))} ))}
<div ref={bottomRef} /> <div ref={bottomRef} />
</div> </div>

View File

@ -195,6 +195,7 @@ export function useChat(): UseChatReturn {
type: 'message', type: 'message',
attachments: msg.attachments, attachments: msg.attachments,
subagentTaskId: msg.subagent_task_id, subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
} }
} }
case 'tool_call': { case 'tool_call': {
@ -209,6 +210,7 @@ export function useChat(): UseChatReturn {
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
arguments: msg.arguments, arguments: msg.arguments,
subagentTaskId: msg.subagent_task_id, subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
} }
} }
case 'tool_result': { case 'tool_result': {
@ -430,6 +432,7 @@ export function useChat(): UseChatReturn {
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
attachments: msg.attachments, attachments: msg.attachments,
reasoningContent: msg.reasoning_content,
}, },
]) ])
setIsLoading(false) setIsLoading(false)
@ -458,6 +461,7 @@ export function useChat(): UseChatReturn {
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
arguments: msg.arguments, arguments: msg.arguments,
subagentTaskId: msg.subagent_task_id, subagentTaskId: msg.subagent_task_id,
reasoningContent: msg.reasoning_content,
}, },
]) ])
break break

View File

@ -258,6 +258,13 @@ body {
.animate-fade-in { animation: fade-in 0.2s ease-out; } .animate-fade-in { animation: fade-in 0.2s ease-out; }
.animate-scale-in { animation: scale-in 0.3s ease-out; } .animate-scale-in { animation: scale-in 0.3s ease-out; }
@keyframes thinking-reveal {
from { max-height: 0; opacity: 0; }
to { max-height: 300px; opacity: 1; }
}
.animate-thinking-reveal { animation: thinking-reveal 0.25s ease-out; }
.typing-indicator span { .typing-indicator span {
animation: typing-dot 1.4s infinite; animation: typing-dot 1.4s infinite;
display: inline-block; display: inline-block;

View File

@ -45,6 +45,7 @@ export interface AssistantResponse {
subagent_task_id?: string subagent_task_id?: string
topic_id?: string topic_id?: string
timestamp?: number timestamp?: number
reasoning_content?: string
} }
export interface ToolCall { export interface ToolCall {
@ -58,6 +59,7 @@ export interface ToolCall {
subagent_task_id?: string subagent_task_id?: string
topic_id?: string topic_id?: string
timestamp?: number timestamp?: number
reasoning_content?: string
} }
export interface ToolResult { export interface ToolResult {
@ -411,6 +413,7 @@ export interface ChatMessage {
callContent?: string callContent?: string
subagentTaskId?: string subagentTaskId?: string
durationMs?: number durationMs?: number
reasoningContent?: string
} }
/** task 工具返回的 JSON 结构 */ /** task 工具返回的 JSON 结构 */