Compare commits

...
Author SHA1 Message Date
Jose R. Perez 9d45b4d073 Merge branch 'main' into jose/ui-refresh-01b 2025-05-06 11:33:15 -04:00
root 4bc23c152f Fix: Hide Approve/Reject buttons in Plan mode when not streaming 2025-05-05 22:39:59 -04:00
root 8a399580cf Fix: Hide Approve/Reject buttons in Plan mode when not streaming 2025-05-05 22:37:53 -04:00
root 1c555e768e feat: enhance SendButton and remove Cancel button
- Create dedicated SendButton component with VSCode Codicons
- Add custom SVG for Resume Task state with mode-specific colors
- Remove Cancel button during streaming per request
- Add tooltip and hover effects for improved UX
2025-05-05 12:45:43 -04:00
root 3286f831ef fix: reset didClickCancel when streaming starts to fix Cancel button disappearing bug
- Add useEffect hook to reset didClickCancel when streaming starts
- This ensures the Cancel button reappears when a new task is started after canceling a previous task
2025-05-05 12:12:53 -04:00
root 9ea5ee4335 feat: enhance SendButton with custom SVG for Resume Task state
- Replace HeroUI icons with VSCode Codicons for play/pause button
- Add custom SVG for Resume Task state with mode-specific colors
- Make button 3px bigger in Resume Task state
- Add hover effect with brightness filter
- Add tooltip for Resume Task state
- Hide original Resume Task and Cancel buttons
- Match colors with Plan/Act mode toggle
2025-05-05 12:05:00 -04:00
root a1ea9799ab UI Refresh: Modernize Chat Interface with Personalized Welcome Screen 2025-05-02 10:48:33 -04:00
root 7d5fce3e37 UI Refresh: Modernize Chat Interface with Personalized Welcome Screen 2025-05-02 10:42:50 -04:00
8 changed files with 245 additions and 66 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
This UI refresh transforms Cline's interface from a technical, feature-focused experience to a personal, welcoming environment that greets users by name and invites them to start coding with the friendly tagline "Let the vibe coding begin." Visual improvements including rounded corners and distinct color-coding for Plan/Act modes create a more modern aesthetic while improving usability through better visual distinction between different states. By shifting from a capabilities-focused introduction to a personalized greeting with the Cline logo, the experience feels more human and approachable, potentially increasing user engagement and reducing the intimidation factor for those new to AI coding assistants.
+31
View File
@@ -0,0 +1,31 @@
---
"claude-dev": minor
---
# Summary of Changes
1. __Created a New SendButton Component__
- Created a dedicated SendButton component in `webview-ui/src/components/chat/SendButton.tsx`
- Implemented VSCode Codicons instead of HeroUI icons
- Added dynamic behavior to switch between play/send and pause/cancel states
2. __Added Mode-Specific Styling__
- Implemented custom SVG for the Resume Task state
- Added color matching with Plan/Act mode toggle (purple for Plan mode, green for Act mode)
- Created hover effects with brightness filter
3. __Fixed Cancel Button Issues__
- Initially attempted to fix the Cancel button's visibility during streaming
- Ultimately removed the Cancel button completely during streaming per request
- Modified the condition to only show secondary buttons when not streaming
4. __Improved User Experience__
- Added tooltip for the Resume Task state
- Made the button 3px bigger in Resume Task state
- Ensured proper cursor behavior during streaming
These changes have enhanced the UI with native VSCode styling while addressing the issues with the Cancel button's behavior during streaming.
+3
View File
@@ -0,0 +1,3 @@
<svg width="115" height="121" viewBox="0 0 115 121" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M114.317 68.0068L107.099 53.5136V45.1668C107.099 31.3312 95.9942 20.1262 82.2992 20.1262H69.9623C70.8552 18.2924 71.3456 16.231 71.3456 14.0557C71.3456 6.36649 65.1583 0.144287 57.5123 0.144287C49.8662 0.144287 43.6789 6.36649 43.6789 14.0557C43.6789 16.231 44.1693 18.2924 45.0622 20.1262H32.7254C19.0303 20.1262 7.9259 31.3312 7.9259 45.1668V53.5136L0.556479 67.9689C-0.185493 69.4232 -0.185493 71.1558 0.556479 72.6102L7.9259 86.9011V95.2479C7.9259 109.083 19.0303 120.289 32.7254 120.289H82.2992C95.9942 120.289 107.099 109.083 107.099 95.2479V86.9011L114.305 72.5596C115.021 71.1306 115.021 69.4485 114.317 68.0068ZM49.9668 79.8189C49.9668 86.1043 44.8987 91.201 38.6486 91.201C32.3984 91.201 27.3303 86.1043 27.3303 79.8189V59.5841C27.3303 53.2986 32.3984 48.202 38.6486 48.202C44.8987 48.202 49.9668 53.2986 49.9668 59.5841V79.8189ZM86.4366 79.8189C86.4366 86.1043 81.3685 91.201 75.1184 91.201C68.8682 91.201 63.8002 86.1043 63.8002 79.8189V59.5841C63.8002 53.2986 68.8682 48.202 75.1184 48.202C81.3685 48.202 86.4366 53.2986 86.4366 59.5841V79.8189Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+32 -28
View File
@@ -1,5 +1,6 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import { SendButton } from "./SendButton"
import DynamicTextArea from "react-textarea-autosize"
import { useClickAway, useEvent, useWindowSize } from "react-use"
import styled from "styled-components"
@@ -51,6 +52,10 @@ interface ChatTextAreaProps {
onSelectImages: () => void
shouldDisableImages: boolean
onHeightChange?: (height: number) => void
isStreaming?: boolean
didClickCancel?: boolean
setDidClickCancel?: (value: boolean) => void
clineAsk?: string
onFocusChange?: (isFocused: boolean) => void
}
@@ -61,7 +66,8 @@ interface GitCommit {
description: string
}
const PLAN_MODE_COLOR = "var(--vscode-inputValidation-warningBorder)"
const PLAN_MODE_COLOR = "#955CF1" // Purple color for plan mode
const ACT_MODE_COLOR = "var(--vscode-terminal-ansiGreen)"
const SwitchOption = styled.div<{ isActive: boolean }>`
padding: 2px 8px;
@@ -96,7 +102,7 @@ const Slider = styled.div<{ isAct: boolean; isPlan?: boolean }>`
position: absolute;
height: 100%;
width: 50%;
background-color: ${(props) => (props.isPlan ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)")};
background-color: ${(props) => (props.isPlan ? PLAN_MODE_COLOR : props.isAct ? ACT_MODE_COLOR : "var(--vscode-focusBorder)")};
transition: transform 0.2s ease;
transform: translateX(${(props) => (props.isAct ? "100%" : "0%")});
`
@@ -236,6 +242,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onSelectImages,
shouldDisableImages,
onHeightChange,
isStreaming,
didClickCancel,
setDidClickCancel,
clineAsk,
onFocusChange,
},
ref,
@@ -1238,6 +1248,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
display: "flex",
// Drag-over styles moved to DynamicTextArea
transition: "background-color 0.1s ease-in-out, border 0.1s ease-in-out",
cursor: isStreaming && !didClickCancel ? "default" : undefined,
}}
onDrop={onDrop}
onDragOver={onDragOver}
@@ -1250,7 +1261,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
borderRadius: 4,
display: "flex",
alignItems: "center",
justifyContent: "center",
@@ -1300,7 +1311,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
position: "absolute",
inset: "10px 15px",
border: "1px solid var(--vscode-input-border)",
borderRadius: 2,
borderRadius: 4,
pointerEvents: "none",
zIndex: 5,
}}
@@ -1323,7 +1334,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
fontFamily: "var(--vscode-font-family)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
borderRadius: 2,
borderRadius: 4,
borderLeft: 0,
borderRight: 0,
borderTop: 0,
@@ -1366,14 +1377,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}}
placeholder={showUnsupportedFileError ? "" : placeholderText}
maxRows={10}
minRows={3}
autoFocus={true}
style={{
width: "100%",
boxSizing: "border-box",
backgroundColor: "transparent",
color: "var(--vscode-input-foreground)",
//border: "1px solid var(--vscode-input-border)",
borderRadius: 2,
borderRadius: 4,
fontFamily: "var(--vscode-font-family)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
@@ -1381,26 +1392,20 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
overflowX: "hidden",
overflowY: "scroll",
scrollbarWidth: "none",
// Since we have maxRows, when text is long enough it starts to overflow the bottom padding, appearing behind the thumbnails. To fix this, we use a transparent border to push the text up instead. (https://stackoverflow.com/questions/42631947/maintaining-a-padding-inside-of-text-area/52538410#52538410)
// borderTop: "9px solid transparent",
borderLeft: 0,
borderRight: 0,
borderTop: 0,
borderBottom: `${thumbnailsHeight + 6}px solid transparent`,
borderColor: "transparent",
// borderRight: "54px solid transparent",
// borderLeft: "9px solid transparent", // NOTE: react-textarea-autosize doesn't calculate correct height when using borderLeft/borderRight so we need to use horizontal padding instead
// Instead of using boxShadow, we use a div with a border to better replicate the behavior when the textarea is focused
// boxShadow: "0px 0px 0px 1px var(--vscode-input-border)",
padding: "9px 28px 3px 9px",
cursor: textAreaDisabled ? "not-allowed" : undefined,
cursor: textAreaDisabled && !isStreaming ? "not-allowed" : undefined,
flex: 1,
zIndex: 1,
outline:
isDraggingOver && !showUnsupportedFileError // Only show drag outline if not showing error
? "2px dashed var(--vscode-focusBorder)"
: isTextAreaFocused
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : "var(--vscode-focusBorder)"}`
? `1px solid ${chatSettings.mode === "plan" ? PLAN_MODE_COLOR : ACT_MODE_COLOR}`
: "none",
outlineOffset: isDraggingOver && !showUnsupportedFileError ? "1px" : "0px", // Add offset for drag-over outline
}}
@@ -1424,11 +1429,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<div
style={{
position: "absolute",
right: 23,
right: 20, // Changed from 23 to 20 (3 pixels to the right)
display: "flex",
alignItems: "flex-center",
height: textAreaBaseHeight || 31,
bottom: 9.5, // should be 10 but doesn't look good on mac
height: 31,
bottom: 14.5, // Changed from 9.5 to 14.5 (5 pixels from the bottom)
zIndex: 2,
}}>
<div
@@ -1449,16 +1454,15 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
fontSize: 16.5,
}}
/> */}
<div
data-testid="send-button"
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
onClick={() => {
if (!textAreaDisabled) {
setIsTextAreaFocused(false)
onSend()
}
}}
style={{ fontSize: 15 }}></div>
<SendButton
textAreaDisabled={textAreaDisabled}
isStreaming={isStreaming}
didClickCancel={didClickCancel}
setDidClickCancel={setDidClickCancel}
setIsTextAreaFocused={setIsTextAreaFocused}
onSend={onSend}
clineAsk={clineAsk}
/>
</div>
</div>
</div>
+77 -38
View File
@@ -1,4 +1,5 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
import debounce from "debounce"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useDeepCompareEffect, useEvent, useMount } from "react-use"
@@ -64,7 +65,15 @@ async function convertHtmlToMarkdown(html: string) {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetrySetting } = useExtensionState()
const {
version,
clineMessages: messages,
taskHistory,
apiConfiguration,
telemetrySetting,
userInfo,
chatSettings,
} = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
@@ -346,6 +355,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return false
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
// Reset didClickCancel when streaming starts
useEffect(() => {
if (isStreaming && didClickCancel) {
setDidClickCancel(false)
}
}, [isStreaming, didClickCancel])
const handleSendMessage = useCallback(
async (text: string, images: string[]) => {
let messageToSend = text.trim()
@@ -945,22 +961,30 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
paddingBottom: "10px",
}}>
{telemetrySetting === "unset" && <TelemetryBanner />}
<div
style={{
padding: "30px 20px",
flexShrink: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
}}>
<div style={{ marginBottom: "15px" }}>
<ClineLogoWhite style={{ width: "50px", height: "50px", marginBottom: "15px" }} />
</div>
<div style={{ marginBottom: "5px" }}>
<p style={{ fontSize: "16px", fontWeight: "500", margin: "0", color: "var(--vscode-foreground)" }}>
{userInfo?.displayName ? `Hello ${userInfo.displayName.split(" ")[0]}!` : "Hello there!"}
</p>
<p style={{ fontSize: "14px", color: "var(--vscode-descriptionForeground)", margin: "5px 0 0 0" }}>
Let the vibe coding begin.
</p>
</div>
</div>
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<p>
Thanks to{" "}
<VSCodeLink href="https://www.anthropic.com/claude/sonnet" style={{ display: "inline" }}>
Claude 3.7 Sonnet's
</VSCodeLink>
agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools
that let me create & edit files, explore complex projects, use a browser, and execute terminal
commands (after you grant permission), I can assist you in ways that go beyond code completion or tech
support. I can even use MCP to create new tools and extend my own capabilities.
</p>
</div>
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
)}
@@ -1039,6 +1063,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
</div>
) : (
<div
className={clineAsk === "resume_task" && !isStreaming ? "cline-resume-task-buttons" : ""}
style={{
opacity:
primaryButtonText || secondaryButtonText || isStreaming
@@ -1049,30 +1074,40 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
display: "flex",
padding: `${primaryButtonText || secondaryButtonText || isStreaming ? "10" : "0"}px 15px 0px 15px`,
}}>
{primaryButtonText && !isStreaming && (
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
style={{
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
)}
{(secondaryButtonText || isStreaming) && (
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
style={{
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}
{/* Hide buttons when in resume_task state, when task is complete, or when in plan mode */}
{clineAsk !== "resume_task" &&
!(clineAsk === "completion_result" && !isStreaming) &&
!(primaryButtonText === "Start New Task" && !isStreaming) &&
!(lastMessage?.say === "completion_result" && !isStreaming) &&
!(chatSettings.mode === "plan" && !isStreaming) && (
<>
{primaryButtonText && !isStreaming && primaryButtonText !== "Start New Task" && (
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
style={{
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
)}
{/* Cancel button commented out as requested */}
{secondaryButtonText && !isStreaming && (
<VSCodeButton
appearance="secondary"
disabled={!enableButtons}
style={{
flex: 1,
marginLeft: "6px",
}}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{secondaryButtonText}
</VSCodeButton>
)}
</>
)}
</div>
)}
</>
@@ -1108,6 +1143,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
scrollToBottomAuto()
}
}}
isStreaming={isStreaming}
didClickCancel={didClickCancel}
setDidClickCancel={setDidClickCancel}
clineAsk={clineAsk}
/>
</div>
)
@@ -0,0 +1,9 @@
.resume-play-button:hover {
filter: brightness(0.9); /* Makes the button slightly darker on hover */
}
/* Hide the "Resume Task" and "Cancel" buttons when in the resume task state */
/* We're hiding these buttons because we've implemented the resume functionality in the play button */
.cline-resume-task-buttons .vscode-button {
display: none !important;
}
@@ -0,0 +1,88 @@
import React from "react"
import { TaskServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import "./SendButton.css"
// Color constants for plan/act modes - using exact colors specified by the user
const PLAN_MODE_COLOR = "#955CF1" // Purple color for plan mode
const ACT_MODE_COLOR = "#0dbc79" // Green color for act mode - exact color specified by the user
interface SendButtonProps {
textAreaDisabled: boolean
isStreaming?: boolean
didClickCancel?: boolean
setDidClickCancel?: (value: boolean) => void
setIsTextAreaFocused: (value: boolean) => void
onSend: () => void
clineAsk?: string
}
export const SendButton: React.FC<SendButtonProps> = ({
textAreaDisabled,
isStreaming,
didClickCancel,
setDidClickCancel,
setIsTextAreaFocused,
onSend,
clineAsk,
}) => {
const { chatSettings } = useExtensionState()
const modeColor = chatSettings.mode === "plan" ? PLAN_MODE_COLOR : ACT_MODE_COLOR
// Only show resume task styling if we're actually in a resumable state
// This prevents the resume task button from showing up at the end of a task
const isResumeTask = clineAsk === "resume_task" && !isStreaming
return (
<div
data-testid="send-button"
title={isResumeTask ? "Resume Task" : undefined}
className={`input-icon-button ${textAreaDisabled && !(isStreaming && !didClickCancel) && !isResumeTask ? "disabled" : ""} codicon ${
isStreaming && !didClickCancel ? "codicon-debug-pause" : "codicon-play"
} ${isResumeTask ? "resume-play-button" : ""}`}
onClick={() => {
if (isStreaming && !didClickCancel) {
// Cancel task functionality
TaskServiceClient.cancelTask({})
setDidClickCancel?.(true)
} else if (isResumeTask) {
// Resume task functionality
// For empty input, directly send yesButtonClicked to resume the task
if (!document.querySelector("textarea")?.value.trim()) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
setIsTextAreaFocused(false)
} else {
// For user input, use the normal send flow
setIsTextAreaFocused(false)
onSend()
}
} else if (!textAreaDisabled) {
// Send message functionality
setIsTextAreaFocused(false)
onSend()
}
}}
style={{
fontSize: isResumeTask ? 25 : 22, // Reduced overall size by 3px, but resume state still 3px bigger
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: isStreaming && !didClickCancel ? "pointer" : undefined,
color: isResumeTask ? modeColor : undefined,
opacity: isResumeTask ? 1 : undefined, // Ensure no opacity effect in resume state
...(isResumeTask
? {
backgroundImage: `url("data:image/svg+xml,${encodeURIComponent(`<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="${chatSettings.mode === "plan" ? "#955CF1" : "#0dbc79"}" d="M3.5 2.5v11l9-5.5z"/></svg>`)}")`,
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
backgroundSize: "100%",
color: "transparent", // Hide the original icon
}
: {}),
}}
/>
)
}