Compare commits

...
Author SHA1 Message Date
Cline Evaluation 6d934f9a9c Improve tasktimeline 2025-05-15 23:02:44 +04:00
3 changed files with 309 additions and 383 deletions
+2 -2
View File
@@ -40,8 +40,8 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
return extractTextFromIPYNB(filePath)
default:
const fileBuffer = await fs.readFile(filePath)
if (fileBuffer.byteLength > 20 * 1000 * 1024) {
// 20MB limit (20 * 1000 * 1024 bytes, decimal MB)
if (fileBuffer.byteLength > 300 * 1024) {
// (300 *1024 bytes)
throw new Error(`File is too large to read into context.`)
}
const encoding = await detectEncoding(fileBuffer, fileExtension)
+148 -114
View File
@@ -1,169 +1,155 @@
import React, { useMemo, useState, useRef, useEffect, useCallback } from "react"
import { Virtuoso } from "react-virtuoso"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import { ClineMessage } from "@shared/ExtensionMessage"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import TaskTimelineTooltip from "./TaskTimelineTooltip"
import { COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN } from "./colors"
// getBlockColor will be defined in this file now.
// Color constants (moved from TaskTimelineTooltip.tsx or a shared file)
const COLOR_WHITE = "#FFFFFF"
const COLOR_GRAY = "#808080"
const COLOR_DARK_GRAY = "#A9A9A9"
const COLOR_BEIGE = "#F5F5DC"
const COLOR_BLUE = "#ADD8E6"
const COLOR_RED = "#FFC0CB" // Example, adjust as needed
const COLOR_PURPLE = "#E6E6FA" // Example, adjust as needed
const COLOR_GREEN = "#90EE90" // Example, adjust as needed
// Timeline dimensions and spacing
const TIMELINE_HEIGHT = "18px"
const BLOCK_WIDTH = "9px"
const BLOCK_GAP = "3px"
const TOOLTIP_MARGIN = 32 // 32px margin on each side
// const TOOLTIP_MARGIN = 32;
interface TaskTimelineProps {
messages: ClineMessage[]
}
// Moved getBlockColor function here
const getBlockColor = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
case "task":
return COLOR_WHITE // White for system prompt
return COLOR_WHITE
case "user_feedback":
return COLOR_WHITE // White for user feedback
return COLOR_WHITE
case "text":
return COLOR_GRAY // Gray for assistant responses
return COLOR_GRAY
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return COLOR_BLUE // Blue for file edit/create operations
}
[
"readFile",
"listFilesTopLevel",
"listFilesRecursive",
"listCodeDefinitionNames",
"searchFiles",
].includes(toolData.tool)
)
return COLOR_BEIGE
if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") return COLOR_BLUE
} catch (e) {
// JSON parse error here
/* fallback */
}
}
return COLOR_BEIGE // Default beige for tool use
return COLOR_BEIGE
case "command":
case "command_output":
return COLOR_PURPLE // Red for terminal commands
return COLOR_RED // Using defined COLOR_RED
case "browser_action":
case "browser_action_result":
return COLOR_PURPLE // Purple for browser actions
return COLOR_PURPLE
case "completion_result":
return COLOR_GREEN // Green for task success
return COLOR_GREEN
default:
return COLOR_DARK_GRAY // Dark gray for unknown
return COLOR_DARK_GRAY
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return COLOR_GRAY // Gray for user messages
return COLOR_GRAY
case "plan_mode_respond":
return COLOR_GRAY // Gray for planning responses
return COLOR_GRAY
case "tool":
// Match the color of the tool approval with the tool type
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return COLOR_BLUE // Blue for file edit/create operations
}
[
"readFile",
"listFilesTopLevel",
"listFilesRecursive",
"listCodeDefinitionNames",
"searchFiles",
].includes(toolData.tool)
)
return COLOR_BEIGE
if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") return COLOR_BLUE
} catch (e) {
// JSON parse error here
/* fallback */
}
}
return COLOR_BEIGE // Default beige for tool approvals
return COLOR_BEIGE
case "command":
return COLOR_PURPLE // Red for command approvals (same as terminal commands)
return COLOR_RED // Using defined COLOR_RED
case "browser_action_launch":
return COLOR_PURPLE // Purple for browser launch approvals (same as browser actions)
return COLOR_PURPLE
default:
return COLOR_DARK_GRAY // Dark gray for unknown
return COLOR_DARK_GRAY
}
}
return COLOR_WHITE // Default color
}
// Define an interface for messages that include the pre-calculated _blockColor
interface ProcessedClineMessage extends ClineMessage {
_blockColor: string
}
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
const containerRef = useRef<HTMLDivElement>(null)
const scrollableRef = useRef<HTMLDivElement>(null)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const taskTimelinePropsMessages = useMemo(() => {
const [hoveredMessage, setHoveredMessage] = useState<ProcessedClineMessage | null>(null) // Use ProcessedClineMessage
const [tooltipPosition, setTooltipPosition] = useState<{ top: number; left: number } | null>(null)
const taskTimelinePropsMessages: ProcessedClineMessage[] = useMemo(() => {
// Ensure type
if (messages.length <= 1) return []
const processed = combineApiRequests(combineCommandSequences(messages.slice(1)))
return processed.filter((msg) => {
// Filter out standard "say" events we don't want to show
if (
msg.type === "say" &&
(msg.say === "api_req_started" ||
msg.say === "api_req_finished" ||
msg.say === "api_req_retried" ||
msg.say === "deleted_api_reqs" ||
msg.say === "checkpoint_created" ||
(msg.say === "text" && (!msg.text || msg.text.trim() === "")))
) {
return false
}
// Filter out "ask" events we don't want to show, including the duplicate completion_result
if (
msg.type === "ask" &&
(msg.ask === "resume_task" || msg.ask === "resume_completed_task" || msg.ask === "completion_result") // Filter out the duplicate completion_result "ask" message
) {
return false
}
return true
})
return processed
.filter((msg) => {
if (
msg.type === "say" &&
(msg.say === "api_req_started" ||
msg.say === "api_req_finished" ||
msg.say === "api_req_retried" ||
msg.say === "deleted_api_reqs" ||
msg.say === "checkpoint_created" ||
(msg.say === "text" && (!msg.text || msg.text.trim() === "")))
) {
return false
}
if (
msg.type === "ask" &&
(msg.ask === "resume_task" || msg.ask === "resume_completed_task" || msg.ask === "completion_result")
) {
return false
}
return true
})
.map(
(msg): ProcessedClineMessage => ({
// Explicitly type the mapped object
...msg,
_blockColor: getBlockColor(msg),
}),
)
}, [messages])
useEffect(() => {
if (scrollableRef.current && taskTimelinePropsMessages.length > 0) {
scrollableRef.current.scrollLeft = scrollableRef.current.scrollWidth
}
}, [taskTimelinePropsMessages])
// Calculate the item size (width of block + gap)
const itemWidth = parseInt(BLOCK_WIDTH.replace("px", "")) + parseInt(BLOCK_GAP.replace("px", ""))
// Virtuoso requires a reference to scroll to the end
const virtuosoRef = useRef<any>(null)
// Render a timeline block
const TimelineBlock = useCallback(
(index: number) => {
const message = taskTimelinePropsMessages[index]
return (
<TaskTimelineTooltip message={message}>
<div
style={{
width: BLOCK_WIDTH,
height: "100%",
backgroundColor: getBlockColor(message),
flexShrink: 0,
cursor: "pointer",
marginRight: BLOCK_GAP,
}}
/>
</TaskTimelineTooltip>
)
},
[taskTimelinePropsMessages],
)
// Scroll to the end when messages change
useEffect(() => {
if (virtuosoRef.current && taskTimelinePropsMessages.length > 0) {
virtuosoRef.current.scrollToIndex({
@@ -173,6 +159,47 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
}
}, [taskTimelinePropsMessages])
const TimelineBlock = useCallback(
(index: number) => {
const message = taskTimelinePropsMessages[index]
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {
setHoveredMessage(message)
if (containerRef.current) {
const blockRect = event.currentTarget.getBoundingClientRect()
const containerRect = containerRef.current.getBoundingClientRect()
const tooltipHeightEstimate = 60 // Approximate tooltip height
const gap = 5
let top = blockRect.top - containerRect.top - tooltipHeightEstimate - gap
let left = blockRect.left - containerRect.left + blockRect.width / 2
setTooltipPosition({ top, left })
}
}
const handleMouseLeave = () => {
setHoveredMessage(null)
}
return (
<div
style={{
width: BLOCK_WIDTH,
height: "100%",
backgroundColor: message._blockColor, // Use pre-calculated color
flexShrink: 0,
cursor: "pointer",
marginRight: BLOCK_GAP,
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
)
},
[taskTimelinePropsMessages],
)
if (taskTimelinePropsMessages.length === 0) {
return null
}
@@ -189,17 +216,10 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
}}>
<style>
{`
/* Hide scrollbar for Chrome, Safari and Opera */
.timeline-virtuoso::-webkit-scrollbar {
display: none;
}
.timeline-virtuoso {
scrollbar-width: none;
-ms-overflow-style: none;
}
.timeline-virtuoso::-webkit-scrollbar { display: none; }
.timeline-virtuoso { scrollbar-width: none; -ms-overflow-style: none; }
`}
</style>
<Virtuoso
ref={virtuosoRef}
className="timeline-virtuoso"
@@ -211,10 +231,24 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
itemContent={TimelineBlock}
horizontalDirection={true}
increaseViewportBy={12}
fixedItemHeight={itemWidth}
// fixedItemHeight is for vertical lists; for horizontal, Virtuoso uses item width
/>
{hoveredMessage && tooltipPosition && (
<div
style={{
position: "absolute",
top: `${tooltipPosition.top}px`,
left: `${tooltipPosition.left}px`,
transform: "translateX(-50%)", // Center the tooltip
zIndex: 1000,
pointerEvents: "none",
}}>
{/* Pass the pre-calculated _blockColor to TaskTimelineTooltip */}
<TaskTimelineTooltip message={hoveredMessage} blockColor={hoveredMessage._blockColor} />
</div>
)}
</div>
)
}
export default TaskTimeline
export default React.memo(TaskTimeline)
@@ -1,290 +1,182 @@
import React from "react"
import { ClineMessage } from "@shared/ExtensionMessage"
import { COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN } from "./colors"
import { Tooltip } from "@heroui/react"
// Color mapping for different message types
// COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN are now defined in TaskTimeline.tsx or a shared colors.ts file
// For this diff, we assume they are imported if this file were to be standalone, but they will be removed.
interface TaskTimelineTooltipProps {
message: ClineMessage
children: React.ReactNode
// No 'children' prop as it's no longer a wrapper
// Add a prop for the pre-calculated color
blockColor: string
}
const TaskTimelineTooltip = ({ message, children }: TaskTimelineTooltipProps) => {
const getMessageDescription = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
// TODO: Need to confirm these classifcations with design
case "task":
return "Task Message"
case "user_feedback":
return "User Message"
case "text":
return "Assistant Response"
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return `File Read: ${toolData.tool}`
} else if (toolData.tool === "editedExistingFile") {
return `File Edit: ${toolData.path || "Unknown file"}`
} else if (toolData.tool === "newFileCreated") {
return `New File: ${toolData.path || "Unknown file"}`
}
return `Tool: ${toolData.tool}`
} catch (e) {
return "Tool Use"
}
const getMessageDescription = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
case "task":
return "Task Message"
case "user_feedback":
return "User Message"
case "text":
return "Assistant Response"
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
[
"readFile",
"listFilesTopLevel",
"listFilesRecursive",
"listCodeDefinitionNames",
"searchFiles",
].includes(toolData.tool)
)
return `File Read: ${toolData.tool}`
if (toolData.tool === "editedExistingFile") return `File Edit: ${toolData.path || "Unknown file"}`
if (toolData.tool === "newFileCreated") return `New File: ${toolData.path || "Unknown file"}`
return `Tool: ${toolData.tool}`
} catch (e) {
/* fallback */
}
return "Tool Use"
case "command":
return "Terminal Command"
case "command_output":
return "Terminal Output"
case "browser_action":
return "Browser Action"
case "browser_action_result":
return "Browser Result"
case "completion_result":
return "Task Completed"
case "checkpoint_created":
return "Checkpoint Created"
default:
return message.say || "Unknown"
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "User Message"
case "plan_mode_respond":
return "Planning Response"
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return `File Read Approval: ${toolData.tool}`
} else if (toolData.tool === "editedExistingFile") {
return `File Edit Approval: ${toolData.path || "Unknown file"}`
} else if (toolData.tool === "newFileCreated") {
return `New File Approval: ${toolData.path || "Unknown file"}`
}
return `Tool Approval: ${toolData.tool}`
} catch (e) {
return "Tool Approval"
}
}
return "Tool Approval"
case "command":
return "Terminal Command Approval"
case "browser_action_launch":
return "Browser Action Approval"
default:
return message.ask || "Unknown"
}
}
return "Unknown Message Type"
}
const getMessageContent = (message: ClineMessage): string => {
if (message.text) {
if (message.type === "ask" && message.ask === "plan_mode_respond" && message.text) {
try {
const planData = JSON.parse(message.text)
return planData.response || message.text
} catch (e) {
return message.text
}
} else if (message.type === "say" && message.say === "tool" && message.text) {
try {
const toolData = JSON.parse(message.text)
return JSON.stringify(toolData, null, 2)
} catch (e) {
return message.text
return "Tool Use"
case "command":
return "Terminal Command"
case "command_output":
return "Terminal Output"
case "browser_action":
return "Browser Action"
case "browser_action_result":
return "Browser Result"
case "completion_result":
return "Task Completed"
case "checkpoint_created":
return "Checkpoint Created"
default:
return message.say || "Unknown"
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "User Message"
case "plan_mode_respond":
return "Planning Response"
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
[
"readFile",
"listFilesTopLevel",
"listFilesRecursive",
"listCodeDefinitionNames",
"searchFiles",
].includes(toolData.tool)
)
return `File Read Approval: ${toolData.tool}`
if (toolData.tool === "editedExistingFile")
return `File Edit Approval: ${toolData.path || "Unknown file"}`
if (toolData.tool === "newFileCreated") return `New File Approval: ${toolData.path || "Unknown file"}`
return `Tool Approval: ${toolData.tool}`
} catch (e) {
/* fallback */
}
}
}
if (message.text.length > 200) {
return message.text.substring(0, 200) + "..."
}
return message.text
return "Tool Approval"
case "command":
return "Terminal Command Approval"
case "browser_action_launch":
return "Browser Action Approval"
default:
return message.ask || "Unknown"
}
return ""
}
return "Unknown Message Type"
}
const getTimestamp = (message: ClineMessage): string => {
if (message.ts) {
const messageDate = new Date(message.ts)
const today = new Date()
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const messageDateOnly = new Date(messageDate.getFullYear(), messageDate.getMonth(), messageDate.getDate())
const time = messageDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", hour12: true })
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
const monthName = monthNames[messageDate.getMonth()]
if (messageDateOnly.getTime() === todayDate.getTime()) {
return `${time}`
} else if (messageDate.getFullYear() === today.getFullYear()) {
return `${monthName} ${messageDate.getDate()} ${time}`
} else {
return `${monthName} ${messageDate.getDate()}, ${messageDate.getFullYear()} ${time}`
const getMessageContent = (message: ClineMessage): string => {
if (message.text) {
if (message.type === "ask" && message.ask === "plan_mode_respond") {
try {
const planData = JSON.parse(message.text)
return planData.response || message.text
} catch (e) {
return message.text
}
} else if (message.type === "say" && message.say === "tool") {
try {
const toolData = JSON.parse(message.text)
return JSON.stringify(toolData, null, 2)
} catch (e) {
return message.text
}
}
return ""
return message.text.length > 200 ? message.text.substring(0, 200) + "..." : message.text
}
return ""
}
// Get color for the indicator based on message type
const getMessageColor = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
case "task":
return COLOR_WHITE // White for system prompt
case "user_feedback":
return COLOR_WHITE // White for user feedback
case "text":
return COLOR_GRAY // Gray for assistant responses
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return COLOR_BLUE // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return COLOR_BEIGE // Default beige for tool use
case "command":
case "command_output":
return COLOR_PURPLE // Red for terminal commands
case "browser_action":
case "browser_action_result":
return COLOR_PURPLE // Purple for browser actions
case "completion_result":
return COLOR_GREEN // Green for task success
default:
return COLOR_DARK_GRAY // Dark gray for unknown
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return COLOR_GRAY // Gray for user messages
case "plan_mode_respond":
return COLOR_GRAY // Gray for planning responses
case "tool":
// Match the color of the tool approval with the tool type
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return COLOR_BLUE // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return COLOR_BEIGE // Default beige for tool approvals
case "command":
return COLOR_PURPLE // Red for command approvals (same as terminal commands)
case "browser_action_launch":
return COLOR_PURPLE // Purple for browser launch approvals (same as browser actions)
default:
return COLOR_DARK_GRAY // Dark gray for unknown
}
}
return COLOR_DARK_GRAY // Default dark gray
}
const getTimestamp = (message: ClineMessage): string => {
if (!message.ts) return ""
const msgDate = new Date(message.ts)
const today = new Date()
const isToday = msgDate.toDateString() === today.toDateString()
const isThisYear = msgDate.getFullYear() === today.getFullYear()
const time = msgDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", hour12: true })
if (isToday) return time
if (isThisYear) return `${msgDate.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`
return `${msgDate.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" })} ${time}`
}
// getMessageColor is removed from here and will be in TaskTimeline.tsx or a shared utility.
const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message, blockColor }) => {
return (
<Tooltip
content={
<div className="flex flex-col">
<div className="flex flex-wrap items-center font-bold mb-1">
<div className="mr-4 mb-0.5">
<div
style={{
width: "10px",
height: "10px",
minWidth: "10px", // Ensure fixed width
minHeight: "10px", // Ensure fixed height
borderRadius: "50%",
backgroundColor: getMessageColor(message),
marginRight: "8px",
display: "inline-block",
flexShrink: 0, // Prevent shrinking when space is limited
}}
/>
{getMessageDescription(message)}
</div>
{getTimestamp(message) && (
<span className="font-normal text-tiny" style={{ fontWeight: "normal", fontSize: "10px" }}>
{getTimestamp(message)}
</span>
)}
</div>
{getMessageContent(message) && (
<div
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxHeight: "150px",
overflowY: "auto",
fontSize: "11px",
fontFamily: "var(--vscode-editor-font-family)",
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: "4px",
borderRadius: "2px",
scrollbarWidth: "none",
}}>
{getMessageContent(message)}
</div>
)}
<div className="flex flex-col bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border border-[var(--vscode-widget-border)] py-1 px-2 rounded-[3px] shadow-md text-xs max-w-xs">
<div className="flex flex-wrap items-center font-bold mb-1">
<div className="mr-2 mb-0.5 flex items-center">
<div
style={{
width: "10px",
height: "10px",
borderRadius: "50%",
backgroundColor: blockColor, // Use passed blockColor
marginRight: "6px",
display: "inline-block",
flexShrink: 0,
}}
/>
<span className="truncate max-w-[150px]">{getMessageDescription(message)}</span>
</div>
}
classNames={{
base: "bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--vscode-widget-border)] py-1 rounded-[3px] max-w-[calc(100dvw-2rem)] text-xs",
}}
shadow="sm"
placement="bottom"
disableAnimation
closeDelay={100}
isKeyboardDismissDisabled={true}>
{children}
</Tooltip>
{getTimestamp(message) && (
<span className="font-normal text-tiny" style={{ fontSize: "10px", marginLeft: "auto" }}>
{getTimestamp(message)}
</span>
)}
</div>
{getMessageContent(message) && (
<div
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxHeight: "150px",
overflowY: "auto",
fontSize: "11px",
fontFamily: "var(--vscode-editor-font-family)",
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: "4px",
borderRadius: "2px",
scrollbarWidth: "thin",
scrollbarColor: "var(--vscode-scrollbarSlider-background) var(--vscode-editorWidget-background)",
}}
className="timeline-tooltip-content">
{getMessageContent(message)}
</div>
)}
</div>
)
}
export default TaskTimelineTooltip
export default React.memo(TaskTimelineTooltip)