mirror of
https://github.com/cline/cline.git
synced 2025-06-03 03:59:07 +00:00
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:
parent
e26d001585
commit
2ef4e56bca
@ -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={() => {
|
||||||
|
@ -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":
|
||||||
|
@ -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 (
|
||||||
|
@ -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
|
||||||
|
40
webview-ui/src/hooks/useChatRowStyles.ts
Normal file
40
webview-ui/src/hooks/useChatRowStyles.ts
Normal 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])
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user