Skip to content

Commit 2ef4e56

Browse files
authored
ENG-377 Changing Checkpoint UI to take less real space on the chat interface (RooCodeInc#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
1 parent e26d001 commit 2ef4e56

File tree

5 files changed

+113
-28
lines changed

5 files changed

+113
-28
lines changed

webview-ui/src/components/chat/BrowserSessionRow.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,11 +275,19 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
275275
consoleLogs: currentPage?.currentState.consoleLogs,
276276
screenshot: currentPage?.currentState.screenshot,
277277
}
278-
278+
const [rowIndex, setRowIndex] = useState<number>(0)
279+
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null)
279280
const [actionContent, { height: actionHeight }] = useSize(
280281
<div>
281282
{currentPage?.nextAction?.messages.map((message) => (
282-
<BrowserSessionRowContent key={message.ts} {...props} message={message} setMaxActionHeight={setMaxActionHeight} />
283+
<BrowserSessionRowContent
284+
key={message.ts}
285+
{...props}
286+
message={message}
287+
setMaxActionHeight={setMaxActionHeight}
288+
rowIndex={rowIndex}
289+
hoveredRowIndex={hoveredRowIndex}
290+
/>
283291
))}
284292
{!isBrowsing && messages.some((m) => m.say === "browser_action_result") && currentPageIndex === 0 && (
285293
<BrowserActionBox action={"launch"} text={initialUrl} />
@@ -473,6 +481,8 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
473481
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
474482
message: ClineMessage
475483
setMaxActionHeight: (height: number) => void
484+
rowIndex: number
485+
hoveredRowIndex: number | null
476486
}
477487

478488
const BrowserSessionRowContent = ({
@@ -482,6 +492,8 @@ const BrowserSessionRowContent = ({
482492
lastModifiedMessage,
483493
isLast,
484494
setMaxActionHeight,
495+
rowIndex,
496+
hoveredRowIndex,
485497
}: BrowserSessionRowContentProps) => {
486498
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
487499
return (
@@ -504,6 +516,8 @@ const BrowserSessionRowContent = ({
504516
return (
505517
<div style={chatRowContentContainerStyle}>
506518
<ChatRowContent
519+
rowIndex={rowIndex}
520+
hoveredRowIndex={hoveredRowIndex}
507521
message={message}
508522
isExpanded={isExpanded(message.ts)}
509523
onToggleExpand={() => {

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
1717
import { useExtensionState } from "@/context/ExtensionStateContext"
1818
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
1919
import { vscode } from "@/utils/vscode"
20+
import { useChatRowStyles } from "@/hooks/useChatRowStyles"
2021
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
2122
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
2223
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
@@ -49,9 +50,16 @@ interface ChatRowProps {
4950
lastModifiedMessage?: ClineMessage
5051
isLast: boolean
5152
onHeightChange: (isTaller: boolean) => void
53+
rowIndex: number
54+
hoveredRowIndex: number | null
55+
setHoveredRowIndex: React.Dispatch<React.SetStateAction<number | null>>
5256
}
5357

54-
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
58+
interface ChatRowContentProps
59+
extends Omit<ChatRowProps, "onHeightChange" | "rowIndex" | "hoveredRowIndex" | "setHoveredRowIndex"> {
60+
rowIndex: number
61+
hoveredRowIndex: number | null
62+
}
5563

5664
export const ProgressIndicator = () => (
5765
<div
@@ -84,32 +92,19 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
8492

8593
const ChatRow = memo(
8694
(props: ChatRowProps) => {
87-
const { isLast, onHeightChange, message, lastModifiedMessage } = props
95+
const { isLast, onHeightChange, message, lastModifiedMessage, rowIndex, hoveredRowIndex, setHoveredRowIndex } = props
8896
// Store the previous height to compare with the current height
8997
// This allows us to detect changes without causing re-renders
9098
const prevHeightRef = useRef(0)
91-
92-
// NOTE: for tools that are interrupted and not responded to (approved or rejected) there won't be a checkpoint hash
93-
let shouldShowCheckpoints =
94-
message.lastCheckpointHash != null &&
95-
(message.say === "tool" ||
96-
message.ask === "tool" ||
97-
message.say === "command" ||
98-
message.ask === "command" ||
99-
// message.say === "completion_result" ||
100-
// message.ask === "completion_result" ||
101-
message.say === "use_mcp_server" ||
102-
message.ask === "use_mcp_server")
103-
104-
if (shouldShowCheckpoints && isLast) {
105-
shouldShowCheckpoints =
106-
lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
107-
}
99+
// Calculate dynamic styles using the custom hook
100+
const { padding, minHeight } = useChatRowStyles(message, hoveredRowIndex, rowIndex)
108101

109102
const [chatrow, { height }] = useSize(
110-
<ChatRowContainer>
111-
<ChatRowContent {...props} />
112-
{shouldShowCheckpoints && <CheckpointOverlay messageTs={message.ts} />}
103+
<ChatRowContainer
104+
style={{ padding, minHeight }}
105+
onMouseEnter={() => setHoveredRowIndex(rowIndex)}
106+
onMouseLeave={() => setHoveredRowIndex(null)}>
107+
<ChatRowContent {...props} rowIndex={rowIndex} hoveredRowIndex={hoveredRowIndex} />
113108
</ChatRowContainer>,
114109
)
115110

@@ -135,7 +130,15 @@ const ChatRow = memo(
135130

136131
export default ChatRow
137132

138-
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
133+
export const ChatRowContent = ({
134+
message,
135+
isExpanded,
136+
onToggleExpand,
137+
lastModifiedMessage,
138+
isLast,
139+
rowIndex,
140+
hoveredRowIndex,
141+
}: ChatRowContentProps) => {
139142
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
140143
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
141144

@@ -985,9 +988,16 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
985988
</>
986989
)
987990
case "checkpoint_created":
991+
// Determine if the hover is near the checkpoint marker's visual position (either on the preceding row or the checkpoint row itself)
992+
const isHoveredNearCheckpoint = hoveredRowIndex === rowIndex - 1 || hoveredRowIndex === rowIndex
993+
988994
return (
989995
<>
990-
<CheckmarkControl messageTs={message.ts} isCheckpointCheckedOut={message.isCheckpointCheckedOut} />
996+
<CheckmarkControl
997+
messageTs={message.ts}
998+
isCheckpointCheckedOut={message.isCheckpointCheckedOut}
999+
isHoveredNearCheckpoint={isHoveredNearCheckpoint}
1000+
/>
9911001
</>
9921002
)
9931003
case "completion_result":

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
7777
const disableAutoScrollRef = useRef(false)
7878
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
7979
const [isAtBottom, setIsAtBottom] = useState(false)
80+
const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null)
8081

8182
// UI layout depends on the last 2 messages
8283
// (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
780781
lastModifiedMessage={modifiedMessages.at(-1)}
781782
isLast={index === groupedMessages.length - 1}
782783
onHeightChange={handleRowHeightChange}
784+
rowIndex={index}
785+
hoveredRowIndex={hoveredRowIndex}
786+
setHoveredRowIndex={setHoveredRowIndex}
783787
/>
784788
)
785789
},
786-
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
790+
[
791+
expandedRows,
792+
modifiedMessages,
793+
groupedMessages.length,
794+
toggleRowExpansion,
795+
handleRowHeightChange,
796+
hoveredRowIndex,
797+
setHoveredRowIndex,
798+
],
787799
)
788800

789801
return (

webview-ui/src/components/common/CheckmarkControl.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import { useFloating, offset, flip, shift } from "@floating-ui/react"
1111
interface CheckmarkControlProps {
1212
messageTs?: number
1313
isCheckpointCheckedOut?: boolean
14+
/** Determines if the hover is near the checkpoint marker's visual position (either on the preceding row or the checkpoint row itself) */
15+
isHoveredNearCheckpoint: boolean
1416
}
1517

16-
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: CheckmarkControlProps) => {
18+
export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut, isHoveredNearCheckpoint }: CheckmarkControlProps) => {
1719
const [compareDisabled, setCompareDisabled] = useState(false)
1820
const [restoreTaskDisabled, setRestoreTaskDisabled] = useState(false)
1921
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
@@ -119,6 +121,13 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
119121

120122
useEvent("message", handleMessage)
121123

124+
// Hide checkpoint if it is not the currently restored one AND the user is not hovering near it.
125+
// This keeps the UI clean but ensures the checkpoint appear on hover for interaction.
126+
const shouldHideCheckpoint = !isCheckpointCheckedOut && !isHoveredNearCheckpoint
127+
if (shouldHideCheckpoint) {
128+
return null
129+
}
130+
122131
return (
123132
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
124133
<i
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useMemo } from "react"
2+
import { ClineMessage } from "@shared/ExtensionMessage"
3+
4+
/**
5+
* Custom hook to determine the dynamic styles for a ChatRowContainer.
6+
*
7+
* This hook calculates the padding and minimum height for a chat row based on
8+
* whether it represents a checkpoint message and its current hover state.
9+
* The goal is to visually collapse checkpoint markers when they are not checked out
10+
* and not being hovered over, while ensuring they remain interactable.
11+
*
12+
* @param message - The chat message object for the current row.
13+
* @param hoveredRowIndex - The index of the currently hovered row, or null if none.
14+
* @param rowIndex - The index of the current row being rendered.
15+
* @returns An object containing the calculated style properties (padding and minHeight).
16+
*/
17+
export const useChatRowStyles = (
18+
message: ClineMessage,
19+
hoveredRowIndex: number | null,
20+
rowIndex: number,
21+
): { padding: number | undefined; minHeight: number | undefined } => {
22+
return useMemo(() => {
23+
// Check if the current message is a checkpoint creation message.
24+
const isCheckpointMessage = message.say === "checkpoint_created"
25+
26+
// Determine if the hover state is relevant to this row or the one immediately preceding it.
27+
// This is because the checkpoint marker is visually associated with the row *before* the checkpoint message,
28+
// but its visibility is controlled by the hover state of *both* the preceding row and the checkpoint message row itself.
29+
const isHoverRelevant = hoveredRowIndex === rowIndex - 1 || hoveredRowIndex === rowIndex
30+
31+
// Calculate styles based on checkpoint status and hover relevance.
32+
// If it's a checkpoint message, not currently checked out, and not relevantly hovered,
33+
// reset padding to 0 and set minHeight to 1px to visually collapse it.
34+
// Otherwise, use default styles (undefined, letting CSS handle it).
35+
const padding = isCheckpointMessage && !message.isCheckpointCheckedOut && !isHoverRelevant ? 0 : undefined
36+
const minHeight = isCheckpointMessage && !message.isCheckpointCheckedOut && !isHoverRelevant ? 1 : undefined
37+
38+
return { padding, minHeight }
39+
}, [message.say, message.isCheckpointCheckedOut, hoveredRowIndex, rowIndex])
40+
}

0 commit comments

Comments
 (0)