mirror of
https://github.com/cline/cline.git
synced 2026-09-16 21:01:52 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d934f9a9c |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user