feat(ui): view_logs/LogDetailsDrawer tree blocks to shadcn

- SimpleToolCallBlock: antd Typography \u2192 plain span + semantic
  bg-muted/border-border/text-muted-foreground tokens. Function
  badge preserved.
- SimpleMessageBlock: antd Typography \u2192 plain span; preserves
  label/content/toolCalls layout.
- CollapsibleMessage / HistoryTree: antd Typography + @ant-design
  Down/RightOutlined \u2192 plain span + lucide ChevronDown/ChevronRight.
  Smooth max-height expand animation preserved via Tailwind's
  transition-[max-height,opacity] utility.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 11:47:49 +00:00
parent cf30ba43ef
commit baefdd512c
No known key found for this signature in database
4 changed files with 65 additions and 174 deletions

View file

@ -1,13 +1,5 @@
/**
* CollapsibleMessage - Collapsible message with arrow and char count
* Used for system messages
*/
import { useState } from 'react';
import { Typography } from 'antd';
import { DownOutlined, RightOutlined } from '@ant-design/icons';
const { Text } = Typography;
import { useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
interface CollapsibleMessageProps {
label: string;
@ -15,13 +7,12 @@ interface CollapsibleMessageProps {
defaultExpanded?: boolean;
}
export function CollapsibleMessage({
label,
content,
defaultExpanded = false
export function CollapsibleMessage({
label,
content,
defaultExpanded = false,
}: CollapsibleMessageProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [isHovered, setIsHovered] = useState(false);
const charCount = content?.length || 0;
if (!content || charCount === 0) {
@ -29,57 +20,32 @@ export function CollapsibleMessage({
}
return (
<div style={{ marginBottom: 8 }}>
{/* Clickable Header with hover state */}
<div className="mb-2">
<div
onClick={() => setIsExpanded(!isExpanded)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
cursor: 'pointer',
padding: '4px 0',
borderRadius: 4,
background: isHovered ? '#f5f5f5' : 'transparent',
transition: 'background 0.15s ease',
marginBottom: isExpanded ? 4 : 0,
}}
className="flex items-center gap-1.5 cursor-pointer py-1 rounded hover:bg-muted transition-colors"
>
{isExpanded ? (
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<RightOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
<Text type="secondary" style={{ fontSize: 10, letterSpacing: '0.5px', textTransform: 'uppercase' }}>
<span className="text-[10px] tracking-wider uppercase text-muted-foreground">
{label}
</Text>
<Text type="secondary" style={{ fontSize: 10 }}>
</span>
<span className="text-[10px] text-muted-foreground">
({charCount.toLocaleString()} chars)
</Text>
</span>
</div>
{/* Content with smooth animation */}
<div
className="overflow-hidden transition-[max-height,opacity] duration-200 ease-out"
style={{
maxHeight: isExpanded ? '2000px' : '0px',
overflow: 'hidden',
transition: 'max-height 0.2s ease-out, opacity 0.2s ease-out',
maxHeight: isExpanded ? "2000px" : "0px",
opacity: isExpanded ? 1 : 0,
}}
>
<div
style={{
paddingLeft: 16,
fontSize: 13,
lineHeight: 1.7,
color: '#262626',
borderLeft: '1px solid #f0f0f0',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
<div className="pl-4 text-[13px] leading-7 text-foreground border-l border-border whitespace-pre-wrap break-words">
{content}
</div>
</div>

View file

@ -1,15 +1,7 @@
/**
* HistoryTree - Collapsible tree view for message history
* Shows arrow indicator and message count
*/
import { useState } from 'react';
import { Typography } from 'antd';
import { DownOutlined, RightOutlined } from '@ant-design/icons';
import { ParsedMessage } from './prettyMessagesTypes';
import { SimpleMessageBlock } from './SimpleMessageBlock';
const { Text } = Typography;
import { useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { ParsedMessage } from "./prettyMessagesTypes";
import { SimpleMessageBlock } from "./SimpleMessageBlock";
interface HistoryTreeProps {
messages: ParsedMessage[];
@ -17,56 +9,35 @@ interface HistoryTreeProps {
export function HistoryTree({ messages }: HistoryTreeProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [isHovered, setIsHovered] = useState(false);
if (messages.length === 0) {
return null;
}
return (
<div style={{ marginBottom: 8 }}>
{/* Clickable Header with hover state */}
<div className="mb-2">
<div
onClick={() => setIsExpanded(!isExpanded)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
cursor: 'pointer',
padding: '4px 0',
borderRadius: 4,
background: isHovered ? '#f5f5f5' : 'transparent',
transition: 'background 0.15s ease',
marginBottom: isExpanded ? 4 : 0,
}}
className="flex items-center gap-1.5 cursor-pointer py-1 rounded hover:bg-muted transition-colors"
>
{isExpanded ? (
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<RightOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
<Text type="secondary" style={{ fontSize: 10, letterSpacing: '0.5px', textTransform: 'uppercase' }}>
HISTORY ({messages.length} message{messages.length !== 1 ? 's' : ''})
</Text>
<span className="text-[10px] tracking-wider uppercase text-muted-foreground">
HISTORY ({messages.length} message{messages.length !== 1 ? "s" : ""})
</span>
</div>
{/* Expanded Tree Content with smooth animation */}
<div
className="overflow-hidden transition-[max-height,opacity] duration-200 ease-out"
style={{
maxHeight: isExpanded ? '2000px' : '0px',
overflow: 'hidden',
transition: 'max-height 0.2s ease-out, opacity 0.2s ease-out',
maxHeight: isExpanded ? "2000px" : "0px",
opacity: isExpanded ? 1 : 0,
}}
>
<div
style={{
paddingLeft: 16,
borderLeft: '1px solid #f0f0f0',
}}
>
<div className="pl-4 border-l border-border">
{messages.map((msg, index) => (
<SimpleMessageBlock
key={index}

View file

@ -1,13 +1,6 @@
/**
* SimpleMessageBlock - Simple message display without collapsing
* Used for messages in tree view and last user message
*/
import { Typography } from 'antd';
import { ToolCall } from './prettyMessagesTypes';
import { SimpleToolCallBlock } from './SimpleToolCallBlock';
const { Text } = Typography;
import { ToolCall } from "./prettyMessagesTypes";
import { SimpleToolCallBlock } from "./SimpleToolCallBlock";
import { cn } from "@/lib/utils";
interface SimpleMessageBlockProps {
label: string;
@ -16,56 +9,45 @@ interface SimpleMessageBlockProps {
isCompact?: boolean;
}
export function SimpleMessageBlock({
label,
content,
toolCalls,
isCompact = false
export function SimpleMessageBlock({
label,
content,
toolCalls,
isCompact = false,
}: SimpleMessageBlockProps) {
// Don't show "null" for empty content
const displayContent = content && content !== 'null' && content.length > 0 ? content : null;
const displayContent =
content && content !== "null" && content.length > 0 ? content : null;
const hasToolCalls = toolCalls && toolCalls.length > 0;
// If no content and no tool calls, don't render
if (!displayContent && !hasToolCalls) {
return null;
}
return (
<div style={{ marginBottom: isCompact ? 8 : 0 }}>
<Text
type="secondary"
style={{
fontSize: 10,
letterSpacing: '0.5px',
textTransform: 'uppercase',
display: 'block',
marginBottom: 3
}}
>
<div className={cn(isCompact ? "mb-2" : "")}>
<span className="block text-[10px] tracking-wider uppercase text-muted-foreground mb-0.5">
{label}
</Text>
</span>
{displayContent && (
<div
style={{
fontSize: 13,
lineHeight: 1.7,
color: '#262626',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
marginBottom: hasToolCalls ? 6 : 0,
}}
className={cn(
"text-[13px] leading-7 text-foreground whitespace-pre-wrap break-words",
hasToolCalls ? "mb-1.5" : "",
)}
>
{displayContent}
</div>
)}
{/* Inline tool calls for assistant messages in history */}
{hasToolCalls && (
<div>
{toolCalls.map((tc, index) => (
<SimpleToolCallBlock key={tc.id || index} tool={tc} compact={isCompact} />
<SimpleToolCallBlock
key={tc.id || index}
tool={tc}
compact={isCompact}
/>
))}
</div>
)}

View file

@ -1,61 +1,33 @@
/**
* SimpleToolCallBlock - Simple tool call display without copy button
* Used in compact/tree views
*/
import { Typography } from 'antd';
import { ToolCall } from './prettyMessagesTypes';
const { Text } = Typography;
import { ToolCall } from "./prettyMessagesTypes";
interface SimpleToolCallBlockProps {
tool: ToolCall;
compact?: boolean;
}
export function SimpleToolCallBlock({ tool, compact = false }: SimpleToolCallBlockProps) {
export function SimpleToolCallBlock({
tool,
compact = false,
}: SimpleToolCallBlockProps) {
return (
<div
className="bg-muted border border-border rounded-md mt-2 font-mono text-xs relative"
style={{
background: '#f8f9fa',
border: '1px solid #e9ecef',
borderRadius: 6,
padding: compact ? '6px 10px' : '10px 14px',
marginTop: 8,
fontFamily: 'monospace',
fontSize: 12,
position: 'relative',
padding: compact ? "6px 10px" : "10px 14px",
}}
>
{/* Function badge */}
<div
style={{
position: 'absolute',
top: -8,
left: 12,
background: '#fff',
padding: '0 6px',
fontSize: 10,
color: '#8c8c8c',
border: '1px solid #e9ecef',
borderRadius: 3,
}}
>
<div className="absolute -top-2 left-3 bg-background px-1.5 text-[10px] text-muted-foreground border border-border rounded">
function
</div>
<Text strong style={{ fontSize: 13, display: 'block', marginBottom: 6 }}>
{tool.name}
</Text>
<span className="block font-bold text-[13px] mb-1.5">{tool.name}</span>
{Object.keys(tool.arguments).length > 0 && (
<div>
{Object.entries(tool.arguments).map(([key, value]) => (
<div key={key} style={{ marginBottom: 2 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
{key}:{' '}
</Text>
<Text style={{ fontSize: 12 }}>{JSON.stringify(value)}</Text>
<div key={key} className="mb-0.5">
<span className="text-xs text-muted-foreground">{key}: </span>
<span className="text-xs">{JSON.stringify(value)}</span>
</div>
))}
</div>