feat: 添加详细工具视图和状态图标,支持放大查看功能

This commit is contained in:
oudecheng 2026-06-12 16:49:20 +08:00
parent 3c889caacf
commit 50d0b92336
5 changed files with 62 additions and 12 deletions

View File

@ -54,6 +54,12 @@ impl CommandHandler for ListTodosCommandHandler {
.list_todos(scope_key) .list_todos(scope_key)
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?; .map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
tracing::info!(
scope_key = %scope_key,
record_count = records.len(),
"list_todos handler: reading from store"
);
let summaries: Vec<TodoItemSummary> = records let summaries: Vec<TodoItemSummary> = records
.into_iter() .into_iter()
.map(|r| TodoItemSummary { .map(|r| TodoItemSummary {

View File

@ -190,13 +190,11 @@ impl AgentExecutionService {
// 只有当是最新回合时才触发历史压缩 // 只有当是最新回合时才触发历史压缩
let should_schedule_compaction = is_current_turn; let should_schedule_compaction = is_current_turn;
// 拦截 todo_write 结果:持久化 + 前端推送 // 拦截 todo_write 结果:持久化 + 前端推送(不受 is_current_turn 限制)
if is_current_turn { session.intercept_todo_write_results(
session.intercept_todo_write_results( &request.result.emitted_messages,
&request.result.emitted_messages, request.chat_id,
request.chat_id, );
);
}
Ok(FinalizedAgentResult { Ok(FinalizedAgentResult {
outbound_messages, outbound_messages,

View File

@ -525,7 +525,9 @@ async fn handle_inbound(
// 处理 Todo 列表 // 处理 Todo 列表
if let Some(todos_json) = response.metadata.get("todos") { if let Some(todos_json) = response.metadata.get("todos") {
if let Ok(todos) = serde_json::from_str::<Vec<crate::protocol::TodoItemSummary>>(todos_json) { if let Ok(todos) = serde_json::from_str::<Vec<crate::protocol::TodoItemSummary>>(todos_json) {
let _ = sender.send(WsOutbound::TodoList { todos, scope_key: String::new() }).await; let scope_key = response.metadata.get("todos_scope_key").cloned().unwrap_or_default();
tracing::info!(todo_count = todos.len(), %scope_key, "list_todos command response");
let _ = sender.send(WsOutbound::TodoList { todos, scope_key }).await;
} }
} }

View File

@ -1,8 +1,9 @@
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, Brain } 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, Maximize2 } 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'
import { ToolDetailModal } from './ToolDetailModal'
// 状态图标组件 // 状态图标组件
function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pending' | 'success' | 'failed' | 'timeout', size?: number }) { function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pending' | 'success' | 'failed' | 'timeout', size?: number }) {
@ -294,6 +295,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
const isTool = message.role === 'tool' const isTool = message.role === 'tool'
const isMergedTool = message.type === 'merged_tool' const isMergedTool = message.type === 'merged_tool'
const [toolExpanded, setToolExpanded] = useState(false) const [toolExpanded, setToolExpanded] = useState(false)
const [showDetailModal, setShowDetailModal] = useState(false)
const [lightboxImage, setLightboxImage] = useState<{ base64: string; mimeType: string; fileName?: string } | null>(null) const [lightboxImage, setLightboxImage] = useState<{ base64: string; mimeType: string; fileName?: string } | null>(null)
const lightboxElement = lightboxImage ? ( const lightboxElement = lightboxImage ? (
@ -429,6 +431,13 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
</span> </span>
)} )}
{hasResult && <CopyButton text={taskResult ? taskResult.output : displayContent} />} {hasResult && <CopyButton text={taskResult ? taskResult.output : displayContent} />}
<button
onClick={(e) => { e.stopPropagation(); setShowDetailModal(true) }}
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 rounded hover:bg-[var(--overlay-subtle)] flex-shrink-0"
title="放大查看"
>
<Maximize2 className="h-3 w-3 text-[var(--text-muted)]" />
</button>
<span className="ml-auto flex-shrink-0"> <span className="ml-auto flex-shrink-0">
{toolExpanded ? ( {toolExpanded ? (
<ChevronDown className="h-3.5 w-3.5 text-[var(--text-muted)]" /> <ChevronDown className="h-3.5 w-3.5 text-[var(--text-muted)]" />
@ -585,6 +594,18 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
</div> </div>
</div> </div>
{lightboxElement} {lightboxElement}
{showDetailModal && (
<ToolDetailModal
toolName={message.toolName || 'Tool'}
status={status}
statusLabel={status === 'calling' ? '执行中' : status === 'result' ? '已完成' : status === 'pending' ? '待确认' : status}
arguments={message.arguments}
resultContent={message.resultContent || ''}
callContent={message.callContent || ''}
durationMs={message.durationMs}
onClose={() => setShowDetailModal(false)}
/>
)}
</div> </div>
) )
} }

View File

@ -1,6 +1,7 @@
import { ChevronDown, ChevronRight, Play, Check, AlertTriangle, Terminal } from 'lucide-react' import { ChevronDown, ChevronRight, Play, Check, AlertTriangle, Terminal, Maximize2 } from 'lucide-react'
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import type { ChatMessage } from '../../types/protocol' import type { ChatMessage } from '../../types/protocol'
import { ToolDetailModal } from '../Chat/ToolDetailModal'
interface ToolPanelProps { interface ToolPanelProps {
messages: ChatMessage[] messages: ChatMessage[]
@ -77,6 +78,7 @@ function mergeToolMessages(messages: ChatMessage[]): ToolCallItem[] {
export function ToolPanel({ messages }: ToolPanelProps) { export function ToolPanel({ messages }: ToolPanelProps) {
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set()) const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set())
const [detailModalTool, setDetailModalTool] = useState<ToolCallItem | null>(null)
const toolCalls = useMemo(() => mergeToolMessages(messages), [messages]) const toolCalls = useMemo(() => mergeToolMessages(messages), [messages])
@ -143,6 +145,7 @@ export function ToolPanel({ messages }: ToolPanelProps) {
} }
return ( return (
<>
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<style>{animStyles}</style> <style>{animStyles}</style>
<div className="border-b border-[var(--border-color)] p-4 font-semibold text-[var(--text-primary)] flex items-center gap-2"> <div className="border-b border-[var(--border-color)] p-4 font-semibold text-[var(--text-primary)] flex items-center gap-2">
@ -187,13 +190,20 @@ export function ToolPanel({ messages }: ToolPanelProps) {
</span> </span>
)} )}
</div> </div>
<span className="flex-shrink-0 ml-2"> <div className="flex items-center gap-1 ml-2 flex-shrink-0">
<button
onClick={(e) => { e.stopPropagation(); setDetailModalTool(tool) }}
className="p-0.5 rounded hover:bg-[var(--overlay-subtle)] transition-colors"
title="放大查看"
>
<Maximize2 className="h-3.5 w-3.5 text-[var(--text-muted)]" />
</button>
{isExpanded ? ( {isExpanded ? (
<ChevronDown className="h-4 w-4 text-[var(--text-muted)]" /> <ChevronDown className="h-4 w-4 text-[var(--text-muted)]" />
) : ( ) : (
<ChevronRight className="h-4 w-4 text-[var(--text-muted)]" /> <ChevronRight className="h-4 w-4 text-[var(--text-muted)]" />
)} )}
</span> </div>
</button> </button>
{/* 结果预览区 — 始终可见 */} {/* 结果预览区 — 始终可见 */}
@ -234,6 +244,19 @@ export function ToolPanel({ messages }: ToolPanelProps) {
</div> </div>
</div> </div>
</div> </div>
{detailModalTool && (
<ToolDetailModal
toolName={detailModalTool.toolName}
status={detailModalTool.status}
statusLabel={detailModalTool.status === 'calling' ? '执行中' : detailModalTool.status === 'result' ? '已完成' : detailModalTool.status === 'pending' ? '待确认' : detailModalTool.status}
arguments={detailModalTool.arguments}
resultContent={detailModalTool.resultContent}
callContent={detailModalTool.callContent}
durationMs={detailModalTool.durationMs}
onClose={() => setDetailModalTool(null)}
/>
)}
</>
) )
} }