feat(chat): optimize ui for execution graph card (#885)

This commit is contained in:
Tao Yiping
2026-04-23 11:49:54 +08:00
committed by GitHub
parent eda34ad9ce
commit d0c79f6a0a
3 changed files with 54 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; 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 { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { TaskStep } from './task-visualization'; import type { TaskStep } from './task-visualization';
@@ -47,18 +47,17 @@ function StepDetailCard({ step }: { step: TaskStep }) {
const isNarration = step.kind === 'message'; const isNarration = step.kind === 'message';
const isTool = step.kind === 'tool'; const isTool = step.kind === 'tool';
const isThinking = step.kind === 'thinking'; 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 hideStatusText = isTool && step.status === 'completed';
const detailPreview = step.detail?.replace(/\s+/g, ' ').trim(); const detailPreview = step.detail?.replace(/\s+/g, ' ').trim();
const canExpand = hasDetail; const canExpand = hasDetail;
const usePlainExpandedDetail = isTool || isThinking; const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label;
const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label;
return ( return (
<div <div
className={cn( className={cn(
'min-w-0 flex-1 text-muted-foreground', 'min-w-0 flex-1 text-muted-foreground',
isTool || isNarration isTool || isNarration || isThinking
? 'px-0 py-0' ? 'px-0 py-0'
: 'rounded-xl border border-black/10 bg-white/40 px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]', : 'rounded-xl border border-black/10 bg-white/40 px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]',
)} )}
@@ -76,9 +75,21 @@ function StepDetailCard({ step }: { step: TaskStep }) {
}} }}
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{!isNarration && ( {(!isNarration && !isThinking || expanded) && (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<p className="shrink-0 text-sm font-medium text-muted-foreground">{displayLabel}</p> <p className="shrink-0 text-sm font-medium text-muted-foreground">{displayLabel}</p>
{isTool && step.label === 'web_fetch' && step.url && (
<a
href={step.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="shrink-0 text-muted-foreground hover:text-foreground"
title={step.url}
>
<Link className="h-3.5 w-3.5" />
</a>
)}
{isTool && detailPreview && !expanded && ( {isTool && detailPreview && !expanded && (
<p className="min-w-0 truncate text-[12px] leading-4 text-muted-foreground/80"> <p className="min-w-0 truncate text-[12px] leading-4 text-muted-foreground/80">
{detailPreview} {detailPreview}
@@ -104,10 +115,8 @@ function StepDetailCard({ step }: { step: TaskStep }) {
className={cn( className={cn(
'text-muted-foreground', 'text-muted-foreground',
isThinking isThinking
? 'mt-0.5 text-[12px] leading-5 line-clamp-1' ? 'mt-0.5 text-[13px] leading-5 line-clamp-2'
: isNarration : 'text-[13px] leading-6 text-muted-foreground line-clamp-2',
? 'text-[13px] leading-6 text-muted-foreground line-clamp-2'
: 'mt-0.5 text-[12px] leading-5 line-clamp-2',
)} )}
> >
{step.detail} {step.detail}
@@ -120,29 +129,30 @@ function StepDetailCard({ step }: { step: TaskStep }) {
</span> </span>
)} )}
</button> </button>
{step.detail && expanded && canExpand && ( {step.detail && expanded && canExpand && isTool && (() => {
usePlainExpandedDetail ? ( let formatted = step.detail;
<pre try {
className={cn( formatted = JSON.stringify(JSON.parse(step.detail), null, 2);
'mt-0.5 whitespace-pre-wrap text-[12px] leading-5 text-muted-foreground', } catch { /* not valid JSON */ }
isTool ? 'break-all' : 'break-words', return (
)} <div className="mt-3 rounded-lg border border-black/10 bg-black/[0.03] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]">
> <pre
{step.detail} className="whitespace-pre-wrap text-[12px] leading-5 text-muted-foreground"
</pre> >
) : ( {formatted}
<div className="mt-3 rounded-lg border border-black/10 bg-black/[0.03] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]"> </pre>
<pre </div>
className={cn( );
'whitespace-pre-wrap text-[12px] leading-5', })()}
isNarration ? 'text-muted-foreground' : 'break-all text-muted-foreground', {step.detail && expanded && canExpand && (isNarration || isThinking) && (
)} <div className="mt-3 rounded-lg border border-black/10 bg-black/[0.03] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]">
> <pre
{step.detail} className="whitespace-pre-wrap break-words text-[12px] leading-5 text-muted-foreground"
</pre> >
</div> {step.detail}
) </pre>
)} </div>
)}
</div> </div>
); );
} }
@@ -260,7 +270,7 @@ export function ExecutionGraphCard({
)} )}
> >
{step.kind === 'thinking' {step.kind === 'thinking'
? <AnimatedDots className="text-[14px]" /> ? <MessageSquare className="h-3.5 w-3.5" />
: step.kind === 'tool' : step.kind === 'tool'
? <Wrench className="h-3.5 w-3.5" /> ? <Wrench className="h-3.5 w-3.5" />
: step.kind === 'message' : step.kind === 'message'

View File

@@ -443,7 +443,7 @@ export function Chat() {
keys.add(runKey); keys.add(runKey);
} }
return keys; return keys;
}, [currentSessionKey, messages, replyTextOverrides, userRunCards]); }, [currentSessionKey, messages, userRunCards]);
useEffect(() => { useEffect(() => {
if (userRunCards.length === 0) return; if (userRunCards.length === 0) return;

View File

@@ -11,6 +11,8 @@ export interface TaskStep {
detail?: string; detail?: string;
depth: number; depth: number;
parentId?: string; 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 { function normalizeText(text: string | null | undefined): string | undefined {
if (!text) return undefined; if (!text) return undefined;
const normalized = text.replace(/\s+/g, ' ').trim(); const normalized = text.replace(/[ \t]+/g, ' ').trim();
if (!normalized) return undefined; if (!normalized) return undefined;
return normalized; return normalized;
} }
@@ -271,6 +273,8 @@ export function deriveTaskSteps({
}); });
toolUses.forEach((tool, index) => { toolUses.forEach((tool, index) => {
const input = tool.input as Record<string, unknown>;
const url = tool.name === 'web_fetch' && typeof input?.url === 'string' ? input.url : undefined;
upsertStep({ upsertStep({
id: tool.id || makeToolId(`history-tool-${message.id || messageIndex}`, tool.name, index), id: tool.id || makeToolId(`history-tool-${message.id || messageIndex}`, tool.name, index),
label: tool.name, label: tool.name,
@@ -278,6 +282,7 @@ export function deriveTaskSteps({
kind: 'tool', kind: 'tool',
detail: normalizeText(JSON.stringify(tool.input, null, 2)), detail: normalizeText(JSON.stringify(tool.input, null, 2)),
depth: 1, depth: 1,
url,
}); });
}); });
} }
@@ -334,6 +339,8 @@ export function deriveTaskSteps({
extractToolUse(streamMessage).forEach((tool, index) => { extractToolUse(streamMessage).forEach((tool, index) => {
const id = tool.id || makeToolId('stream-tool', tool.name, index); const id = tool.id || makeToolId('stream-tool', tool.name, index);
if (activeToolIds.has(id) || activeToolNamesWithoutIds.has(tool.name)) return; if (activeToolIds.has(id) || activeToolNamesWithoutIds.has(tool.name)) return;
const input = tool.input as Record<string, unknown>;
const url = tool.name === 'web_fetch' && typeof input?.url === 'string' ? input.url : undefined;
upsertStep({ upsertStep({
id, id,
label: tool.name, label: tool.name,
@@ -341,6 +348,7 @@ export function deriveTaskSteps({
kind: 'tool', kind: 'tool',
detail: normalizeText(JSON.stringify(tool.input, null, 2)), detail: normalizeText(JSON.stringify(tool.input, null, 2)),
depth: 1, depth: 1,
url,
}); });
}); });
} }