ENG-377 Changing Checkpoint UI to take less real space on the chat interface (#2752)

* Enhance chat component interactivity by adding row index and hover state management. Updated BrowserSessionRow, ChatRow, and CheckmarkControl to support row-specific hover effects and state tracking, improving user experience during interactions.

* Adding hovered row index

* Adding hovered row index

* Adding hovered row index
This commit is contained in:
Ara 2025-04-10 02:23:10 +05:30 committed by GitHub
parent e26d001585
commit 2ef4e56bca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 113 additions and 28 deletions

View File

@ -275,11 +275,19 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
consoleLogs: currentPage?.currentState.consoleLogs, consoleLogs: currentPage?.currentState.consoleLogs,
screenshot: currentPage?.currentState.screenshot, screenshot: currentPage?.currentState.screenshot,
} }
const [rowIndex, setRowIndex] = useState<number>(0)
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null)
const [actionContent, { height: actionHeight }] = useSize( const [actionContent, { height: actionHeight }] = useSize(
<div> <div>
{currentPage?.nextAction?.messages.map((message) => ( {currentPage?.nextAction?.messages.map((message) => (
<BrowserSessionRowContent key={message.ts} {...props} message={message} setMaxActionHeight={setMaxActionHeight} /> <BrowserSessionRowContent
key={message.ts}
{...props}
message={message}
setMaxActionHeight={setMaxActionHeight}
rowIndex={rowIndex}
hoveredRowIndex={hoveredRowIndex}
/>
))} ))}
{!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && ( {!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && (
<BrowserActionBox action={"launch"} text={initialUrl} /> <BrowserActionBox action={"launch"} text={initialUrl} />
@ -473,6 +481,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> { interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
message: ClineMessage message: ClineMessage
setMaxActionHeight: (height: number) => void setMaxActionHeight: (height: number) => void
rowIndex: number
hoveredRowIndex: number | null
} }
const BrowserSessionRowContent = ({ const BrowserSessionRowContent = ({
@ -482,6 +492,8 @@ const BrowserSessionRowContent = ({
lastModifiedMessage, lastModifiedMessage,
isLast, isLast,
setMaxActionHeight, setMaxActionHeight,
rowIndex,
hoveredRowIndex,
}: BrowserSessionRowContentProps) => { }: BrowserSessionRowContentProps) => {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") { if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
return ( return (
@ -504,6 +516,8 @@ const BrowserSessionRowContent = ({
return ( return (
<div style={chatRowContentContainerStyle}> <div style={chatRowContentContainerStyle}>
<ChatRowContent <ChatRowContent
rowIndex={rowIndex}
hoveredRowIndex={hoveredRowIndex}
message={message} message={message}
isExpanded={isExpanded(message.ts)} isExpanded={isExpanded(message.ts)}
onToggleExpand={() => { onToggleExpand={() => {

View File

@ -17,6 +17,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
import { useExtensionState } from "@/context/ExtensionStateContext" import { useExtensionState } from "@/context/ExtensionStateContext"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp" import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
import { vscode } from "@/utils/vscode" import { vscode } from "@/utils/vscode"
import { useChatRowStyles } from "@/hooks/useChatRowStyles"
import { CheckmarkControl } from "@/components/common/CheckmarkControl" import { CheckmarkControl } from "@/components/common/CheckmarkControl"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls" import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian" import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
@ -49,9 +50,16 @@ interface ChatRowProps {
lastModifiedMessage?: ClineMessage lastModifiedMessage?: ClineMessage
isLast: boolean isLast: boolean
onHeightChange: (isTaller: boolean) => void onHeightChange: (isTaller: boolean) => void
rowIndex: number
hoveredRowIndex: number | null
setHoveredRowIndex: React.Dispatch<React.SetStateAction<number | null>>
} }
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {} interface ChatRowContentProps
extends Omit<ChatRowProps, "onHeightChange" | "rowIndex" | "hoveredRowIndex" | "setHoveredRowIndex"> {
rowIndex: number
hoveredRowIndex: number | null
}
export const ProgressIndicator = () => ( export const ProgressIndicator = () => (
<div <div
@ -84,32 +92,19 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
const ChatRow = memo( const ChatRow = memo(
(props: ChatRowProps) => { (props: ChatRowProps) => {
const { isLast, onHeightChange, message, lastModifiedMessage } = props const { isLast, onHeightChange, message, lastModifiedMessage, rowIndex, hoveredRowIndex, setHoveredRowIndex } = props
// Store the previous height to compare with the current height // Store the previous height to compare with the current height
// This allows us to detect changes without causing re-renders // This allows us to detect changes without causing re-renders
const prevHeightRef = useRef(0) const prevHeightRef = useRef(0)
// Calculate dynamic styles using the custom hook
// NOTE: for tools that are interrupted and not responded to (approved or rejected) there won't be a checkpoint hash const { padding, minHeight } = useChatRowStyles(message, hoveredRowIndex, rowIndex)
let shouldShowCheckpoints =
message.lastCheckpointHash != null &&
(message.say === "tool" ||
message.ask === "tool" ||
message.say === "command" ||
message.ask === "command" ||
// message.say === "completion_result" ||
// message.ask === "completion_result" ||
message.say === "use_mcp_server" ||
message.ask === "use_mcp_server")
if (shouldShowCheckpoints && isLast) {
shouldShowCheckpoints =
lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
}
const [chatrow, { height }] = useSize( const [chatrow, { height }] = useSize(
<ChatRowContainer> <ChatRowContainer
<ChatRowContent {...props} /> style={{ padding, minHeight }}
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />} onMouseEnter={() => setHoveredRowIndex(rowIndex)}
onMouseLeave={() => setHoveredRowIndex(null)}>
<ChatRowContent {...props} rowIndex={rowIndex} hoveredRowIndex={hoveredRowIndex} />
</ChatRowContainer>, </ChatRowContainer>,
) )
@ -135,7 +130,15 @@ const ChatRow = memo(
export default ChatRow export default ChatRow
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => { export const ChatRowContent = ({
message,
isExpanded,
onToggleExpand,
lastModifiedMessage,
isLast,
rowIndex,
hoveredRowIndex,
}: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState() const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false) const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
@ -985,9 +988,16 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</> </>
) )
case "checkpoint_created": 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 ( return (
<> <>
<CheckmarkControl messageTs={message.ts} isCheckpointCheckedOut={message.isCheckpointCheckedOut} /> <CheckmarkControl
messageTs={message.ts}
isCheckpointCheckedOut={message.isCheckpointCheckedOut}
isHoveredNearCheckpoint={isHoveredNearCheckpoint}
/>
</> </>
) )
case "completion_result": case "completion_result":

View File

@ -77,6 +77,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const disableAutoScrollRef = useRef(false) const disableAutoScrollRef = useRef(false)
const [showScrollToBottom, setShowScrollToBottom] = useState(false) const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [isAtBottom, setIsAtBottom] = useState(false) const [isAtBottom, setIsAtBottom] = useState(false)
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null)
// UI layout depends on the last 2 messages // 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 // (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,10 +781,21 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
lastModifiedMessage={modifiedMessages.at(-1)} lastModifiedMessage={modifiedMessages.at(-1)}
isLast={index === groupedMessages.length - 1} isLast={index === groupedMessages.length - 1}
onHeightChange={handleRowHeightChange} onHeightChange={handleRowHeightChange}
rowIndex={index}
hoveredRowIndex={hoveredRowIndex}
setHoveredRowIndex={setHoveredRowIndex}
/> />
) )
}, },
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange], [
expandedRows,
modifiedMessages,
groupedMessages.length,
toggleRowExpansion,
handleRowHeightChange,
hoveredRowIndex,
setHoveredRowIndex,
],
) )
return ( return (

View File

@ -11,9 +11,11 @@ import { useFloating, offset, flip, shift } from "@floating-ui/react"
interface CheckmarkControlProps { interface CheckmarkControlProps {
messageTs?: number messageTs?: number
isCheckpointCheckedOut?: boolean 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
} }
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: CheckmarkControlProps) => { export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isHoveredNearCheckpoint }: CheckmarkControlProps) => {
const [compareDisabled, setCompareDisabled] = useState(false) const [compareDisabled, setCompareDisabled] = useState(false)
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false) const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false) const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
@ -119,6 +121,13 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
useEvent("message", handleMessage) 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
}
return ( return (
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}> <Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
<i <i

View File

@ -0,0 +1,40 @@
import { 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.
*
* @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).
*/
export const useChatRowStyles = (
message: ClineMessage,
hoveredRowIndex: number | null,
rowIndex: number,
): { padding: number | undefined; minHeight: number | undefined } => {
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
// 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])
}