From d0c79f6a0a323fc0d2bfa63b26301251b973ef4f Mon Sep 17 00:00:00 2001 From: Tao Yiping Date: Thu, 23 Apr 2026 11:49:54 +0800 Subject: [PATCH] feat(chat): optimize ui for execution graph card (#885) --- src/pages/Chat/ExecutionGraphCard.tsx | 78 +++++++++++++++------------ src/pages/Chat/index.tsx | 2 +- src/pages/Chat/task-visualization.ts | 10 +++- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/pages/Chat/ExecutionGraphCard.tsx b/src/pages/Chat/ExecutionGraphCard.tsx index 911a1a7..2941587 100644 --- a/src/pages/Chat/ExecutionGraphCard.tsx +++ b/src/pages/Chat/ExecutionGraphCard.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { CheckCircle2, ChevronDown, ChevronRight, CircleDashed, GitBranch, MessageSquare, Wrench, XCircle } from 'lucide-react'; +import { CheckCircle2, ChevronDown, ChevronRight, CircleDashed, GitBranch, Link, MessageSquare, Wrench, XCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import type { TaskStep } from './task-visualization'; @@ -47,18 +47,17 @@ function StepDetailCard({ step }: { step: TaskStep }) { const isNarration = step.kind === 'message'; const isTool = step.kind === 'tool'; const isThinking = step.kind === 'thinking'; - const showRunningDots = isTool && step.status === 'running'; + const showRunningDots = (isTool || isThinking) && step.status === 'running'; const hideStatusText = isTool && step.status === 'completed'; const detailPreview = step.detail?.replace(/\s+/g, ' ').trim(); const canExpand = hasDetail; - const usePlainExpandedDetail = isTool || isThinking; - const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label; + const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label; return (
- {!isNarration && ( + {(!isNarration && !isThinking || expanded) && (

{displayLabel}

+ {isTool && step.label === 'web_fetch' && step.url && ( + e.stopPropagation()} + className="shrink-0 text-muted-foreground hover:text-foreground" + title={step.url} + > + + + )} {isTool && detailPreview && !expanded && (

{detailPreview} @@ -104,10 +115,8 @@ function StepDetailCard({ step }: { step: TaskStep }) { className={cn( 'text-muted-foreground', isThinking - ? 'mt-0.5 text-[12px] leading-5 line-clamp-1' - : isNarration - ? 'text-[13px] leading-6 text-muted-foreground line-clamp-2' - : 'mt-0.5 text-[12px] leading-5 line-clamp-2', + ? 'mt-0.5 text-[13px] leading-5 line-clamp-2' + : 'text-[13px] leading-6 text-muted-foreground line-clamp-2', )} > {step.detail} @@ -120,29 +129,30 @@ function StepDetailCard({ step }: { step: TaskStep }) { )} - {step.detail && expanded && canExpand && ( - usePlainExpandedDetail ? ( -

-            {step.detail}
-          
- ) : ( -
-
-            {step.detail}
-          
-
- ) - )} + {step.detail && expanded && canExpand && isTool && (() => { + let formatted = step.detail; + try { + formatted = JSON.stringify(JSON.parse(step.detail), null, 2); + } catch { /* not valid JSON */ } + return ( +
+
+                  {formatted}
+                
+
+ ); + })()} + {step.detail && expanded && canExpand && (isNarration || isThinking) && ( +
+
+                {step.detail}
+              
+
+ )}
); } @@ -260,7 +270,7 @@ export function ExecutionGraphCard({ )} > {step.kind === 'thinking' - ? + ? : step.kind === 'tool' ? : step.kind === 'message' diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index 0bb6d48..3daaf61 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -443,7 +443,7 @@ export function Chat() { keys.add(runKey); } return keys; - }, [currentSessionKey, messages, replyTextOverrides, userRunCards]); + }, [currentSessionKey, messages, userRunCards]); useEffect(() => { if (userRunCards.length === 0) return; diff --git a/src/pages/Chat/task-visualization.ts b/src/pages/Chat/task-visualization.ts index 60fa5c6..0f1aaae 100644 --- a/src/pages/Chat/task-visualization.ts +++ b/src/pages/Chat/task-visualization.ts @@ -11,6 +11,8 @@ export interface TaskStep { detail?: string; depth: number; parentId?: string; + /** Extracted URL for web_fetch tool, used to render a clickable link icon. */ + url?: string; } /** @@ -56,7 +58,7 @@ export interface SubagentCompletionInfo { function normalizeText(text: string | null | undefined): string | undefined { if (!text) return undefined; - const normalized = text.replace(/\s+/g, ' ').trim(); + const normalized = text.replace(/[ \t]+/g, ' ').trim(); if (!normalized) return undefined; return normalized; } @@ -271,6 +273,8 @@ export function deriveTaskSteps({ }); toolUses.forEach((tool, index) => { + const input = tool.input as Record; + const url = tool.name === 'web_fetch' && typeof input?.url === 'string' ? input.url : undefined; upsertStep({ id: tool.id || makeToolId(`history-tool-${message.id || messageIndex}`, tool.name, index), label: tool.name, @@ -278,6 +282,7 @@ export function deriveTaskSteps({ kind: 'tool', detail: normalizeText(JSON.stringify(tool.input, null, 2)), depth: 1, + url, }); }); } @@ -334,6 +339,8 @@ export function deriveTaskSteps({ extractToolUse(streamMessage).forEach((tool, index) => { const id = tool.id || makeToolId('stream-tool', tool.name, index); if (activeToolIds.has(id) || activeToolNamesWithoutIds.has(tool.name)) return; + const input = tool.input as Record; + const url = tool.name === 'web_fetch' && typeof input?.url === 'string' ? input.url : undefined; upsertStep({ id, label: tool.name, @@ -341,6 +348,7 @@ export function deriveTaskSteps({ kind: 'tool', detail: normalizeText(JSON.stringify(tool.input, null, 2)), depth: 1, + url, }); }); }