mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dff0f9d151 | |||
| c240504e00 | |||
| 0a0c52db50 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Redesigned checkpoint UI to declutter chat view by using a subtle indicator line that expands to a full control panel on hover, with a new date indicator for when it was created
|
||||
+63
-39
@@ -890,6 +890,7 @@ export class Task {
|
||||
let responseImages: string[] | undefined
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
responseText = text
|
||||
responseImages = images
|
||||
}
|
||||
@@ -1146,6 +1147,7 @@ export class Task {
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
||||
await this.saveCheckpoint()
|
||||
return [
|
||||
true,
|
||||
formatResponse.toolResult(
|
||||
@@ -1563,6 +1565,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
this.didRejectTool = true // Prevent further tool uses in this message
|
||||
return false
|
||||
@@ -1571,6 +1574,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1644,7 +1648,7 @@ export class Task {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1700,6 +1704,7 @@ export class Task {
|
||||
)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} else if (content) {
|
||||
@@ -1762,14 +1767,14 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (block.name === "write_to_file" && !content) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1828,6 +1833,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
@@ -1837,12 +1843,14 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
|
||||
if (!didApprove) {
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1903,7 +1911,7 @@ export class Task {
|
||||
await handleError("writing file", error)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1931,7 +1939,7 @@ export class Task {
|
||||
if (!relPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1939,7 +1947,7 @@ export class Task {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1961,6 +1969,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
@@ -1973,12 +1982,12 @@ export class Task {
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
pushToolResult(content)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("reading file", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2008,7 +2017,7 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2039,18 +2048,19 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("listing files", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2078,7 +2088,7 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2106,18 +2116,19 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("parsing source code definitions", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2149,13 +2160,13 @@ export class Task {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!regex) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2185,18 +2196,19 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, false)
|
||||
break
|
||||
}
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, false, true)
|
||||
}
|
||||
pushToolResult(results)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("searching files", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2212,6 +2224,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "action"))
|
||||
await this.browserSession.closeBrowser()
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -2255,7 +2268,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2271,6 +2284,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
|
||||
const didApprove = await askApproval("browser_action_launch", url)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2297,7 +2311,7 @@ export class Task {
|
||||
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
|
||||
)
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break // can't be within an inner switch
|
||||
}
|
||||
}
|
||||
@@ -2306,7 +2320,7 @@ export class Task {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
|
||||
await this.browserSession.closeBrowser()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2355,7 +2369,7 @@ export class Task {
|
||||
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
case "close":
|
||||
pushToolResult(
|
||||
@@ -2363,7 +2377,7 @@ export class Task {
|
||||
`The browser has been closed. You may now proceed to using other tools.`,
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2372,7 +2386,7 @@ export class Task {
|
||||
} catch (error) {
|
||||
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
|
||||
await handleError("executing browser action", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2400,7 +2414,7 @@ export class Task {
|
||||
if (!command) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!requiresApprovalRaw) {
|
||||
@@ -2408,7 +2422,7 @@ export class Task {
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2456,6 +2470,7 @@ export class Task {
|
||||
`${this.shouldAutoApproveTool(block.name) && requiresApprovalPerLLM ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
|
||||
)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2491,7 +2506,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing command", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2521,13 +2536,13 @@ export class Task {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!tool_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// arguments are optional, but if they are provided they must be valid JSON
|
||||
@@ -2551,7 +2566,7 @@ export class Task {
|
||||
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
|
||||
),
|
||||
)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2579,6 +2594,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2614,7 +2630,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing MCP tool", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2642,13 +2658,13 @@ export class Task {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
if (!uri) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2669,6 +2685,7 @@ export class Task {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2688,12 +2705,12 @@ export class Task {
|
||||
.join("\n\n") || "(Empty response)"
|
||||
await this.say("mcp_server_response", resourceResultPretty)
|
||||
pushToolResult(formatResponse.toolResult(resourceResultPretty))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("accessing MCP resource", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2712,7 +2729,7 @@ export class Task {
|
||||
if (!question) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2749,12 +2766,12 @@ export class Task {
|
||||
}
|
||||
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("asking question", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2768,6 +2785,7 @@ export class Task {
|
||||
if (!context) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2796,10 +2814,12 @@ export class Task {
|
||||
formatResponse.toolResult(`The user has created a new task with the provided context.`),
|
||||
)
|
||||
}
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("creating new task", error)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2860,6 +2880,7 @@ export class Task {
|
||||
if (text || images?.length) {
|
||||
telemetryService.captureOptionsIgnored(this.taskId, options.length, "plan")
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2991,12 +3012,14 @@ export class Task {
|
||||
// complete command message
|
||||
const didApprove = await askApproval("command", command)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
|
||||
if (userRejected) {
|
||||
this.didRejectTool = true
|
||||
pushToolResult(execCommandResult)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// user didn't reject, but the command may have output
|
||||
@@ -3015,6 +3038,7 @@ export class Task {
|
||||
break
|
||||
}
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
if (commandResult) {
|
||||
@@ -3043,7 +3067,7 @@ export class Task {
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("attempting completion", error)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,19 +275,10 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
consoleLogs: currentPage?.currentState.consoleLogs,
|
||||
screenshot: currentPage?.currentState.screenshot,
|
||||
}
|
||||
const [rowIndex, setRowIndex] = useState<number>(0)
|
||||
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null)
|
||||
const [actionContent, { height: actionHeight }] = useSize(
|
||||
<div>
|
||||
{currentPage?.nextAction?.messages.map((message) => (
|
||||
<BrowserSessionRowContent
|
||||
key={message.ts}
|
||||
{...props}
|
||||
message={message}
|
||||
setMaxActionHeight={setMaxActionHeight}
|
||||
rowIndex={rowIndex}
|
||||
hoveredRowIndex={hoveredRowIndex}
|
||||
/>
|
||||
<BrowserSessionRowContent key={message.ts} {...props} message={message} setMaxActionHeight={setMaxActionHeight} />
|
||||
))}
|
||||
{!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && (
|
||||
<BrowserActionBox action={"launch"} text={initialUrl} />
|
||||
@@ -481,8 +472,6 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
|
||||
message: ClineMessage
|
||||
setMaxActionHeight: (height: number) => void
|
||||
rowIndex: number
|
||||
hoveredRowIndex: number | null
|
||||
}
|
||||
|
||||
const BrowserSessionRowContent = ({
|
||||
@@ -492,8 +481,6 @@ const BrowserSessionRowContent = ({
|
||||
lastModifiedMessage,
|
||||
isLast,
|
||||
setMaxActionHeight,
|
||||
rowIndex,
|
||||
hoveredRowIndex,
|
||||
}: BrowserSessionRowContentProps) => {
|
||||
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
|
||||
return (
|
||||
@@ -516,8 +503,6 @@ const BrowserSessionRowContent = ({
|
||||
return (
|
||||
<div style={chatRowContentContainerStyle}>
|
||||
<ChatRowContent
|
||||
rowIndex={rowIndex}
|
||||
hoveredRowIndex={hoveredRowIndex}
|
||||
message={message}
|
||||
isExpanded={isExpanded(message.ts)}
|
||||
onToggleExpand={() => {
|
||||
|
||||
@@ -48,18 +48,12 @@ interface ChatRowProps {
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
lastModifiedMessage?: ClineMessage
|
||||
isFirst: boolean
|
||||
isLast: boolean
|
||||
onHeightChange: (isTaller: boolean) => void
|
||||
rowIndex: number
|
||||
hoveredRowIndex: number | null
|
||||
setHoveredRowIndex: React.Dispatch<React.SetStateAction<number | null>>
|
||||
}
|
||||
|
||||
interface ChatRowContentProps
|
||||
extends Omit<ChatRowProps, "onHeightChange" | "rowIndex" | "hoveredRowIndex" | "setHoveredRowIndex"> {
|
||||
rowIndex: number
|
||||
hoveredRowIndex: number | null
|
||||
}
|
||||
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange" | "isFirst"> {}
|
||||
|
||||
export const ProgressIndicator = () => (
|
||||
<div
|
||||
@@ -92,23 +86,40 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message, lastModifiedMessage, rowIndex, hoveredRowIndex, setHoveredRowIndex } = props
|
||||
const { isLast, isFirst, onHeightChange, message } = props
|
||||
// Store the previous height to compare with the current height
|
||||
// This allows us to detect changes without causing re-renders
|
||||
const prevHeightRef = useRef(0)
|
||||
// Calculate dynamic styles using the custom hook
|
||||
const { padding, minHeight } = useChatRowStyles(message, hoveredRowIndex, rowIndex)
|
||||
const { ...chatRowStyles } = useChatRowStyles(message)
|
||||
|
||||
const isCheckpointMessage = message.say === "checkpoint_created"
|
||||
|
||||
// Special handling for first row checkpoint
|
||||
const checkpointStyles = useMemo(() => {
|
||||
if (isCheckpointMessage) {
|
||||
// Apply additional styles for first row checkpoints
|
||||
if (isFirst) {
|
||||
return {
|
||||
...chatRowStyles,
|
||||
marginTop: "3px", // Add a small margin to ensure visibility
|
||||
}
|
||||
}
|
||||
return chatRowStyles
|
||||
}
|
||||
return chatRowStyles
|
||||
}, [chatRowStyles, isCheckpointMessage, isFirst])
|
||||
|
||||
// ChatRowContainer with updated styles
|
||||
const [chatrow, { height }] = useSize(
|
||||
<ChatRowContainer
|
||||
style={{ padding, minHeight }}
|
||||
onMouseEnter={() => setHoveredRowIndex(rowIndex)}
|
||||
onMouseLeave={() => setHoveredRowIndex(null)}>
|
||||
<ChatRowContent {...props} rowIndex={rowIndex} hoveredRowIndex={hoveredRowIndex} />
|
||||
<ChatRowContainer style={checkpointStyles}>
|
||||
<ChatRowContent {...props} />
|
||||
</ChatRowContainer>,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Skip height change effects for checkpoint messages
|
||||
if (isCheckpointMessage) return
|
||||
|
||||
// used for partials command output etc.
|
||||
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
|
||||
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
|
||||
@@ -119,7 +130,7 @@ const ChatRow = memo(
|
||||
}
|
||||
prevHeightRef.current = height
|
||||
}
|
||||
}, [height, isLast, onHeightChange, message])
|
||||
}, [height, isLast, onHeightChange, message, isCheckpointMessage])
|
||||
|
||||
// we cannot return null as virtuoso does not support it so we use a separate visibleMessages array to filter out messages that should not be rendered
|
||||
return chatrow
|
||||
@@ -130,15 +141,7 @@ const ChatRow = memo(
|
||||
|
||||
export default ChatRow
|
||||
|
||||
export const ChatRowContent = ({
|
||||
message,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
lastModifiedMessage,
|
||||
isLast,
|
||||
rowIndex,
|
||||
hoveredRowIndex,
|
||||
}: ChatRowContentProps) => {
|
||||
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
|
||||
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
|
||||
@@ -988,15 +991,12 @@ export const ChatRowContent = ({
|
||||
</>
|
||||
)
|
||||
case "checkpoint_created":
|
||||
// Determine if the hover is near the checkpoint marker's visual position (either on the preceding row or the checkpoint row itself)
|
||||
const isHoveredNearCheckpoint = hoveredRowIndex === rowIndex - 1 || hoveredRowIndex === rowIndex
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckmarkControl
|
||||
messageTs={message.ts}
|
||||
isCheckpointCheckedOut={message.isCheckpointCheckedOut}
|
||||
isHoveredNearCheckpoint={isHoveredNearCheckpoint}
|
||||
isLastRow={isLast}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -77,7 +77,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const disableAutoScrollRef = useRef(false)
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null)
|
||||
|
||||
// UI layout depends on the last 2 messages
|
||||
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
|
||||
@@ -780,22 +779,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
onToggleExpand={() => toggleRowExpansion(messageOrGroup.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === groupedMessages.length - 1}
|
||||
isFirst={index === 0}
|
||||
onHeightChange={handleRowHeightChange}
|
||||
rowIndex={index}
|
||||
hoveredRowIndex={hoveredRowIndex}
|
||||
setHoveredRowIndex={setHoveredRowIndex}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[
|
||||
expandedRows,
|
||||
modifiedMessages,
|
||||
groupedMessages.length,
|
||||
toggleRowExpansion,
|
||||
handleRowHeightChange,
|
||||
hoveredRowIndex,
|
||||
setHoveredRowIndex,
|
||||
],
|
||||
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useState, useEffect } from "react"
|
||||
import { useCallback, useRef, useState, useEffect, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
@@ -11,19 +11,20 @@ import { useFloating, offset, flip, shift } from "@floating-ui/react"
|
||||
interface CheckmarkControlProps {
|
||||
messageTs?: number
|
||||
isCheckpointCheckedOut?: boolean
|
||||
/** Determines if the hover is near the checkpoint marker's visual position (either on the preceding row or the checkpoint row itself) */
|
||||
isHoveredNearCheckpoint: boolean
|
||||
/** Whether this is the last row in the chat */
|
||||
isLastRow?: boolean
|
||||
}
|
||||
|
||||
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isHoveredNearCheckpoint }: CheckmarkControlProps) => {
|
||||
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isLastRow = false }: CheckmarkControlProps) => {
|
||||
const [compareDisabled, setCompareDisabled] = useState(false)
|
||||
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
|
||||
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
|
||||
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
|
||||
const [hasMouseEntered, setHasMouseEntered] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
const [isComponentHovered, setIsComponentHovered] = useState(false)
|
||||
const [isLineHovered, setIsLineHovered] = useState(false)
|
||||
|
||||
const { refs, floatingStyles, update, placement } = useFloating({
|
||||
placement: "bottom-end",
|
||||
@@ -37,6 +38,65 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isHoveredN
|
||||
],
|
||||
})
|
||||
|
||||
// Simple time formatter if date-fns is not available
|
||||
const getSimpleRelativeTime = (date: Date) => {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
|
||||
const seconds = Math.floor(diffMs / 1000)
|
||||
// Handle "just now" case in the fallback too
|
||||
if (seconds < 180) return "now" // 3 minutes = 180 seconds
|
||||
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${minutes}m`
|
||||
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}h`
|
||||
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d`
|
||||
}
|
||||
|
||||
// Format the timestamp for relative time display
|
||||
const relativeTime = useMemo(() => {
|
||||
if (!messageTs) return ""
|
||||
|
||||
// Create a Date object from the timestamp
|
||||
const date = new Date(messageTs)
|
||||
const now = new Date()
|
||||
|
||||
// Calculate time difference in milliseconds
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
||||
|
||||
// Show "just now" if less than 3 minutes
|
||||
if (diffMinutes < 3) {
|
||||
return "now"
|
||||
}
|
||||
|
||||
// Fallback formatting if date-fns errors
|
||||
return getSimpleRelativeTime(date)
|
||||
}, [messageTs])
|
||||
|
||||
// Format the full timestamp for the detailed display (without year)
|
||||
const formattedTime = useMemo(() => {
|
||||
if (!messageTs) return ""
|
||||
const date = new Date(messageTs)
|
||||
return date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
}, [messageTs])
|
||||
|
||||
// Combined time format with relative time and full date
|
||||
const combinedTimeFormat = useMemo(() => {
|
||||
if (!relativeTime || !formattedTime) return ""
|
||||
return `${relativeTime} · ${formattedTime}`
|
||||
}, [relativeTime, formattedTime])
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
update()
|
||||
@@ -121,206 +181,334 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isHoveredN
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
// Hide checkpoint if it is not the currently restored one AND the user is not hovering near it.
|
||||
// This keeps the UI clean but ensures the checkpoint appear on hover for interaction.
|
||||
const shouldHideCheckpoint = !isCheckpointCheckedOut && !isHoveredNearCheckpoint
|
||||
if (shouldHideCheckpoint) {
|
||||
return null
|
||||
}
|
||||
// Modified: Only show the expanded UI on hover (not permanently for checked out checkpoints)
|
||||
// This way checked out checkpoints only show the line indicator unless hovered
|
||||
const showExpandedUI =
|
||||
(isLineHovered || isComponentHovered || showRestoreConfirm) &&
|
||||
!(showRestoreConfirm === false && isComponentHovered === false && isLineHovered === false)
|
||||
|
||||
// The line should still be highlighted when the checkpoint is checked out
|
||||
const shouldShowHoveredLine = isCheckpointCheckedOut || isLineHovered || isComponentHovered || showRestoreConfirm
|
||||
|
||||
return (
|
||||
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
|
||||
<i
|
||||
className="codicon codicon-bookmark"
|
||||
style={{
|
||||
color: isCheckpointCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
flexShrink: 0,
|
||||
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{/* Line indicator is still styled differently for checked out checkpoints */}
|
||||
<CheckpointIndicator
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
$isHovered={shouldShowHoveredLine}
|
||||
onMouseEnter={() => setIsLineHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setTimeout(() => {
|
||||
if (!isComponentHovered && !showRestoreConfirm) {
|
||||
setIsLineHovered(false)
|
||||
}
|
||||
}, 50)
|
||||
}}
|
||||
/>
|
||||
<Label $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{isCheckpointCheckedOut ? "Checkpoint (restored)" : "Checkpoint"}
|
||||
</Label>
|
||||
<DottedLine $isCheckedOut={isCheckpointCheckedOut} />
|
||||
<ButtonGroup>
|
||||
<CustomButton
|
||||
|
||||
<HoverArea
|
||||
onMouseEnter={() => setIsLineHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
if (!isComponentHovered && !showRestoreConfirm) {
|
||||
setIsLineHovered(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{showExpandedUI && (
|
||||
<ExpandedUI
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
disabled={compareDisabled}
|
||||
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
|
||||
onClick={() => {
|
||||
setCompareDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointDiff",
|
||||
number: messageTs,
|
||||
})
|
||||
$isLastRow={isLastRow}
|
||||
onMouseEnter={() => {
|
||||
setIsComponentHovered(true)
|
||||
setIsLineHovered(true)
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!showRestoreConfirm) {
|
||||
setIsComponentHovered(false)
|
||||
setIsLineHovered(false)
|
||||
} else {
|
||||
handleControlsMouseLeave(e)
|
||||
}
|
||||
}}>
|
||||
Compare
|
||||
</CustomButton>
|
||||
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
|
||||
<div ref={refs.setReference} style={{ position: "relative", marginTop: -2 }}>
|
||||
<CustomButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
isActive={showRestoreConfirm}
|
||||
onClick={() => setShowRestoreConfirm(true)}>
|
||||
Restore
|
||||
</CustomButton>
|
||||
{showRestoreConfirm &&
|
||||
createPortal(
|
||||
<RestoreConfirmTooltip
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
data-placement={placement}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files
|
||||
</VSCodeButton>
|
||||
<p>
|
||||
Restores your project's files back to a snapshot taken at this point (use "Compare" to see
|
||||
what will be reverted)
|
||||
</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreTask}
|
||||
disabled={restoreTaskDisabled}
|
||||
style={{
|
||||
cursor: restoreTaskDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Task Only
|
||||
</VSCodeButton>
|
||||
<p>Deletes messages after this point (does not affect workspace files)</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreBoth}
|
||||
disabled={restoreBothDisabled}
|
||||
style={{
|
||||
cursor: restoreBothDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files & Task
|
||||
</VSCodeButton>
|
||||
<p>Restores your project's files and deletes all messages after this point</p>
|
||||
</RestoreOption>
|
||||
</RestoreConfirmTooltip>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
<DottedLine small $isCheckedOut={isCheckpointCheckedOut} />
|
||||
</ButtonGroup>
|
||||
<SimpleLayout>
|
||||
<LabelColumn>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<i
|
||||
className="codicon codicon-bookmark"
|
||||
style={{
|
||||
color: isCheckpointCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
marginRight: "6px",
|
||||
}}
|
||||
/>
|
||||
<Label $isCheckedOut={isCheckpointCheckedOut}>
|
||||
{isCheckpointCheckedOut ? "Checkpoint (restored)" : "Checkpoint"}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<TimeLabel $isCheckedOut={isCheckpointCheckedOut}>{combinedTimeFormat}</TimeLabel>
|
||||
</LabelColumn>
|
||||
|
||||
<ButtonsWrapper>
|
||||
<EnhancedButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
disabled={compareDisabled}
|
||||
style={{ cursor: compareDisabled ? "wait" : "pointer" }}
|
||||
onClick={() => {
|
||||
setCompareDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "checkpointDiff",
|
||||
number: messageTs,
|
||||
})
|
||||
}}>
|
||||
Compare
|
||||
</EnhancedButton>
|
||||
|
||||
<div ref={refs.setReference} style={{ position: "relative" }}>
|
||||
<EnhancedButton
|
||||
$isCheckedOut={isCheckpointCheckedOut}
|
||||
isActive={showRestoreConfirm}
|
||||
onClick={() => setShowRestoreConfirm(true)}>
|
||||
Restore
|
||||
</EnhancedButton>
|
||||
{showRestoreConfirm &&
|
||||
createPortal(
|
||||
<RestoreConfirmTooltip
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
data-placement={placement}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreWorkspace}
|
||||
disabled={restoreWorkspaceDisabled}
|
||||
style={{
|
||||
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files
|
||||
</VSCodeButton>
|
||||
<p>
|
||||
Restores your project's files back to a snapshot taken at this point (use
|
||||
"Compare" to see what will be reverted)
|
||||
</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreTask}
|
||||
disabled={restoreTaskDisabled}
|
||||
style={{
|
||||
cursor: restoreTaskDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Task Only
|
||||
</VSCodeButton>
|
||||
<p>Deletes messages after this point (does not affect workspace files)</p>
|
||||
</RestoreOption>
|
||||
<RestoreOption>
|
||||
<VSCodeButton
|
||||
onClick={handleRestoreBoth}
|
||||
disabled={restoreBothDisabled}
|
||||
style={{
|
||||
cursor: restoreBothDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Restore Files & Task
|
||||
</VSCodeButton>
|
||||
<p>Restores your project's files and deletes all messages after this point</p>
|
||||
</RestoreOption>
|
||||
</RestoreConfirmTooltip>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
</ButtonsWrapper>
|
||||
</SimpleLayout>
|
||||
</ExpandedUI>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div<{ isMenuOpen?: boolean; $isCheckedOut?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
margin-top: -10px;
|
||||
margin-bottom: -10px;
|
||||
opacity: ${(props) => (props.$isCheckedOut ? 1 : props.isMenuOpen ? 1 : 0.5)};
|
||||
// Updated Container styling - doesn't need to handle as many hover events
|
||||
const Container = styled.div<{
|
||||
isMenuOpen?: boolean
|
||||
$isCheckedOut?: boolean
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 0;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
// Invisible hover area just around the line indicator
|
||||
const HoverArea = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -6px;
|
||||
width: 20px; /* Wide enough to easily hover */
|
||||
height: 15px; /* Tall enough to catch hover events */
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
// Make the highlighted line more visible for checked out checkpoints
|
||||
const CheckpointIndicator = styled.div<{
|
||||
$isCheckedOut?: boolean
|
||||
$isHovered?: boolean
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
/* Make checked out checkpoints have a more visible line */
|
||||
width: ${(props) =>
|
||||
props.$isCheckedOut ? "10px" /* Wider default for checked out checkpoints */ : props.$isHovered ? "12px" : "8px"};
|
||||
height: 3px;
|
||||
background-color: ${(props) =>
|
||||
props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"};
|
||||
opacity: ${(props) => (props.$isCheckedOut ? 1 /* Always full opacity for checked out */ : props.$isHovered ? 1 : 0.6)};
|
||||
transition:
|
||||
opacity 0.15s ease-in-out,
|
||||
width 0.15s ease-in-out;
|
||||
cursor: pointer;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
z-index: 5;
|
||||
`
|
||||
|
||||
// Added label column component for consistent height
|
||||
const LabelColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
// Updated SimpleLayout with center alignment
|
||||
const SimpleLayout = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center; /* Center align items vertically */
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
// Updated ButtonsWrapper with center alignment
|
||||
const ButtonsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px; /* Reduced gap */
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
// Container for the expanded UI that appears on hover
|
||||
const ExpandedUI = styled.div<{
|
||||
$isCheckedOut?: boolean
|
||||
$isLastRow?: boolean
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
right: 7px;
|
||||
${
|
||||
(props) =>
|
||||
props.$isLastRow
|
||||
? "bottom: -3px;" // Position above for last row
|
||||
: "top: -3px;" // Position below for normal rows
|
||||
}
|
||||
background-color: var(--vscode-editor-background);
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
z-index: 20;
|
||||
animation: ${(props) => (props.$isLastRow ? "fadeInUp" : "fadeIn")} 0.15s ease-in-out;
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Updated Label with slightly smaller font size
|
||||
const Label = styled.span<{ $isCheckedOut?: boolean }>`
|
||||
color: ${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
font-size: 9px;
|
||||
font-size: 11px; // Reduced from 12px
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const DottedLine = styled.div<{ small?: boolean; $isCheckedOut?: boolean }>`
|
||||
flex: ${(props) => (props.small ? "0 0 5px" : "1")};
|
||||
min-width: ${(props) => (props.small ? "5px" : "5px")};
|
||||
height: 1px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")} 50%,
|
||||
transparent 50%
|
||||
);
|
||||
background-size: 4px 1px;
|
||||
background-repeat: repeat-x;
|
||||
// Updated TimeLabel with word-break enabled
|
||||
const TimeLabel = styled.span<{ $isCheckedOut?: boolean }>`
|
||||
font-size: 10px;
|
||||
color: ${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
margin-top: 3px;
|
||||
margin-left: 20px;
|
||||
word-break: break-word; /* Allow breaking words */
|
||||
white-space: normal; /* Changed from nowrap to allow text to wrap */
|
||||
max-width: 100%;
|
||||
display: block; /* Ensure it takes full width for proper breaking */
|
||||
`
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CustomButton = styled.button<{ disabled?: boolean; isActive?: boolean; $isCheckedOut?: boolean }>`
|
||||
// Simplified button styling with minimal padding and no extra styling
|
||||
const EnhancedButton = styled.button<{
|
||||
disabled?: boolean
|
||||
isActive?: boolean
|
||||
$isCheckedOut?: boolean
|
||||
}>`
|
||||
background: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? props.$isCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)"
|
||||
: "transparent"};
|
||||
border: none;
|
||||
: "var(--vscode-editor-background)"};
|
||||
border: 1px solid
|
||||
${(props) => (props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)")};
|
||||
color: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? "var(--vscode-editor-background)"
|
||||
: props.$isCheckedOut
|
||||
? "var(--vscode-textLink-foreground)"
|
||||
: "var(--vscode-descriptionForeground)"};
|
||||
padding: 2px 6px;
|
||||
font-size: 9px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 1px;
|
||||
background-image: ${(props) =>
|
||||
props.isActive || props.disabled
|
||||
? "none"
|
||||
: `linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
|
||||
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
|
||||
background-size: ${(props) => (props.isActive || props.disabled ? "auto" : `4px 1px, 1px 4px, 4px 1px, 1px 4px`)};
|
||||
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
|
||||
background-position:
|
||||
0 0,
|
||||
100% 0,
|
||||
0 100%,
|
||||
0 0;
|
||||
}
|
||||
border-radius: 3px;
|
||||
padding: 3px 4px; /* Minimal padding */
|
||||
font-size: 10px;
|
||||
cursor: ${(props) => (props.disabled ? "wait" : "pointer")};
|
||||
line-height: 1;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${(props) =>
|
||||
props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"};
|
||||
color: var(--vscode-editor-background);
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { useMemo } from "react"
|
||||
import { CSSProperties, useMemo } from "react"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
|
||||
/**
|
||||
* Custom hook to determine the dynamic styles for a ChatRowContainer.
|
||||
*
|
||||
* This hook calculates the padding and minimum height for a chat row based on
|
||||
* whether it represents a checkpoint message and its current hover state.
|
||||
* The goal is to visually collapse checkpoint markers when they are not checked out
|
||||
* and not being hovered over, while ensuring they remain interactable.
|
||||
* This hook provides styles for checkpoint messages that control their
|
||||
* visual appearance based on hover state and whether they're checked out.
|
||||
*
|
||||
* @param message - The chat message object for the current row.
|
||||
* @param hoveredRowIndex - The index of the currently hovered row, or null if none.
|
||||
* @param rowIndex - The index of the current row being rendered.
|
||||
* @returns An object containing the calculated style properties (padding and minHeight).
|
||||
* @returns An object containing the calculated style properties.
|
||||
*/
|
||||
export const useChatRowStyles = (
|
||||
message: ClineMessage,
|
||||
hoveredRowIndex: number | null,
|
||||
rowIndex: number,
|
||||
): { padding: number | undefined; minHeight: number | undefined } => {
|
||||
export const useChatRowStyles = (message: ClineMessage): CSSProperties => {
|
||||
return useMemo(() => {
|
||||
// Check if the current message is a checkpoint creation message.
|
||||
const isCheckpointMessage = message.say === "checkpoint_created"
|
||||
|
||||
// Determine if the hover state is relevant to this row or the one immediately preceding it.
|
||||
// This is because the checkpoint marker is visually associated with the row *before* the checkpoint message,
|
||||
// but its visibility is controlled by the hover state of *both* the preceding row and the checkpoint message row itself.
|
||||
const isHoverRelevant = hoveredRowIndex === rowIndex - 1 || hoveredRowIndex === rowIndex
|
||||
// For checkpoint messages, make the row take up minimal space
|
||||
// The checkpoint component will be absolutely positioned
|
||||
if (isCheckpointMessage) {
|
||||
return {
|
||||
padding: 0,
|
||||
// we can't set height to 0 because virtuoso needs a height to render the row, we can't set to 1 because it results in a visual artifact bug, so we use this hack to make the row almost invisible (and the checkpoint indicator positioned absolutely at this point in the list)
|
||||
height: 8,
|
||||
marginTop: -4,
|
||||
marginBottom: -4,
|
||||
overflow: "visible", // Allow the absolutely positioned content to be visible
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate styles based on checkpoint status and hover relevance.
|
||||
// If it's a checkpoint message, not currently checked out, and not relevantly hovered,
|
||||
// reset padding to 0 and set minHeight to 1px to visually collapse it.
|
||||
// Otherwise, use default styles (undefined, letting CSS handle it).
|
||||
const padding = isCheckpointMessage && !message.isCheckpointCheckedOut && !isHoverRelevant ? 0 : undefined
|
||||
const minHeight = isCheckpointMessage && !message.isCheckpointCheckedOut && !isHoverRelevant ? 1 : undefined
|
||||
|
||||
return { padding, minHeight }
|
||||
}, [message.say, message.isCheckpointCheckedOut, hoveredRowIndex, rowIndex])
|
||||
// For non-checkpoint messages, return normal styling
|
||||
return {
|
||||
padding: undefined,
|
||||
minHeight: undefined,
|
||||
height: undefined,
|
||||
overflow: undefined,
|
||||
position: undefined,
|
||||
}
|
||||
}, [message.say])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user