mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0029df1f2 | |||
| 2c9ebd80e8 | |||
| 771c993543 | |||
| e1fbeec461 | |||
| 0ad867db42 | |||
| a772c7f0cd |
@@ -1,4 +1,4 @@
|
||||
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEvent, useSize } from "react-use"
|
||||
@@ -47,6 +47,8 @@ interface ChatRowProps {
|
||||
lastModifiedMessage?: ClineMessage
|
||||
isLast: boolean
|
||||
onHeightChange: (isTaller: boolean) => void
|
||||
hasMessageBeenSentAfterCompletion?: boolean
|
||||
isYesNoButtonsHighlighted?: boolean
|
||||
}
|
||||
|
||||
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
|
||||
@@ -133,9 +135,18 @@ const ChatRow = memo(
|
||||
|
||||
export default ChatRow
|
||||
|
||||
export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => {
|
||||
export const ChatRowContent = ({
|
||||
message,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
lastModifiedMessage,
|
||||
isLast,
|
||||
hasMessageBeenSentAfterCompletion,
|
||||
isYesNoButtonsHighlighted,
|
||||
}: ChatRowContentProps) => {
|
||||
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
const [selectedButton, setSelectedButton] = useState<"yes" | "no" | null>(null)
|
||||
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||
if (message.text != null && message.say === "api_req_started") {
|
||||
@@ -1008,24 +1019,27 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
|
||||
}}>
|
||||
<Markdown markdown={text} />
|
||||
</div>
|
||||
{message.partial !== true && hasChanges && (
|
||||
<div style={{ paddingTop: 17 }}>
|
||||
<SuccessButton
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
}}>
|
||||
<i className="codicon codicon-new-file" style={{ marginRight: 6 }} />
|
||||
See new changes
|
||||
</SuccessButton>
|
||||
{message.partial !== true && (
|
||||
<div style={{ paddingTop: 17, display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{hasChanges && (
|
||||
<SuccessButton
|
||||
disabled={seeNewChangesDisabled}
|
||||
onClick={() => {
|
||||
setSeeNewChangesDisabled(true)
|
||||
vscode.postMessage({
|
||||
type: "taskCompletionViewChanges",
|
||||
number: message.ts,
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
cursor: seeNewChangesDisabled ? "wait" : "pointer",
|
||||
width: "100%",
|
||||
}}>
|
||||
<i className="codicon codicon-new-file" style={{ marginRight: 6 }} />
|
||||
See new changes
|
||||
</SuccessButton>
|
||||
)}
|
||||
{/* Yes/No buttons moved to ChatView.tsx */}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -35,6 +35,7 @@ interface ChatTextAreaProps {
|
||||
onSelectImages: () => void
|
||||
shouldDisableImages: boolean
|
||||
onHeightChange?: (height: number) => void
|
||||
onDisabledClick?: () => void
|
||||
}
|
||||
|
||||
const PLAN_MODE_COLOR = "var(--vscode-inputValidation-warningBorder)"
|
||||
@@ -211,6 +212,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onSelectImages,
|
||||
shouldDisableImages,
|
||||
onHeightChange,
|
||||
onDisabledClick,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -890,6 +892,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}>
|
||||
{/* Overlay div that captures clicks when input is disabled */}
|
||||
{textAreaDisabled && onDisabledClick && (
|
||||
<div
|
||||
onClick={onDisabledClick}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 10,
|
||||
cursor: "not-allowed",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showContextMenu && (
|
||||
<div ref={contextMenuContainerRef}>
|
||||
<ContextMenu
|
||||
@@ -964,6 +981,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onPaste={handlePaste}
|
||||
onSelect={updateCursorPosition}
|
||||
onMouseUp={updateCursorPosition}
|
||||
onClick={() => {
|
||||
if (textAreaDisabled && onDisabledClick) {
|
||||
onDisabledClick()
|
||||
}
|
||||
}}
|
||||
onHeightChange={(height) => {
|
||||
if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
||||
setTextAreaBaseHeight(height)
|
||||
|
||||
@@ -64,6 +64,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [textAreaDisabled, setTextAreaDisabled] = useState(false)
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([])
|
||||
const [taskCompletionStatus, setTaskCompletionStatus] = useState<"pending" | "completed" | "not_completed">("pending")
|
||||
const [hasMessageBeenSentAfterCompletion, setHasMessageBeenSentAfterCompletion] = useState(false)
|
||||
const buttonContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
|
||||
const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
|
||||
@@ -77,6 +80,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const disableAutoScrollRef = useRef(false)
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [isYesNoButtonsHighlighted, setIsYesNoButtonsHighlighted] = useState(false) // This will be passed to ChatRow
|
||||
const [selectedButton, setSelectedButton] = useState<"yes" | "no" | 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
|
||||
@@ -173,7 +178,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
break
|
||||
case "completion_result":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
setTextAreaDisabled(isPartial)
|
||||
setTextAreaDisabled(true) // Always disable text area until user selects Yes or No
|
||||
setClineAsk("completion_result")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Start New Task")
|
||||
@@ -295,10 +300,23 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
case "mistake_limit_reached":
|
||||
// Add prefix based on task completion status
|
||||
let messageText = text
|
||||
if (taskCompletionStatus === "not_completed") {
|
||||
messageText = `> The user indicated that the task is not completed:\n\n-----\n\n${text}`
|
||||
} else if (taskCompletionStatus === "completed") {
|
||||
messageText = `> The user indicated that the task is completed:\n\n-----\n\n${text}`
|
||||
}
|
||||
|
||||
// Set flag that a message has been sent after completion
|
||||
if (taskCompletionStatus !== "pending") {
|
||||
setHasMessageBeenSentAfterCompletion(true)
|
||||
}
|
||||
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
text,
|
||||
text: messageText,
|
||||
images,
|
||||
})
|
||||
break
|
||||
@@ -310,16 +328,21 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setSelectedImages([])
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
setTaskCompletionStatus("pending") // Reset task completion status after sending a message
|
||||
setHasMessageBeenSentAfterCompletion(false) // Reset the flag
|
||||
setSelectedButton(null) // Reset button selection state
|
||||
// setPrimaryButtonText(undefined)
|
||||
// setSecondaryButtonText(undefined)
|
||||
disableAutoScrollRef.current = false
|
||||
}
|
||||
},
|
||||
[messages.length, clineAsk],
|
||||
[messages.length, clineAsk, taskCompletionStatus],
|
||||
)
|
||||
|
||||
const startNewTask = useCallback(() => {
|
||||
vscode.postMessage({ type: "clearTask" })
|
||||
setHasMessageBeenSentAfterCompletion(false)
|
||||
setSelectedButton(null) // Reset button selection state
|
||||
}, [])
|
||||
|
||||
/*
|
||||
@@ -434,6 +457,23 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
const shouldDisableImages =
|
||||
!selectedModelInfo.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
||||
|
||||
// Listen for task completion status events
|
||||
useEffect(() => {
|
||||
const handleTaskCompletionStatus = (e: CustomEvent<{ status: "completed" | "not_completed" }>) => {
|
||||
setTaskCompletionStatus(e.detail.status)
|
||||
setTextAreaDisabled(false)
|
||||
setTimeout(() => {
|
||||
textAreaRef.current?.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
window.addEventListener("taskCompletionStatus", handleTaskCompletionStatus as EventListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("taskCompletionStatus", handleTaskCompletionStatus as EventListener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
@@ -705,6 +745,23 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
[scrollToBottomSmooth, scrollToBottomAuto],
|
||||
)
|
||||
|
||||
const handleDisabledInputClick = useCallback(() => {
|
||||
// Only highlight the Yes/No buttons if the input is disabled due to completion result
|
||||
if (textAreaDisabled && clineAsk === "completion_result" && !isStreaming) {
|
||||
setIsYesNoButtonsHighlighted(true)
|
||||
|
||||
// Set a timeout to turn off the highlight after a short duration
|
||||
setTimeout(() => {
|
||||
setIsYesNoButtonsHighlighted(false)
|
||||
}, 1500)
|
||||
|
||||
// Scroll to the Yes/No buttons if needed
|
||||
if (buttonContainerRef.current) {
|
||||
buttonContainerRef.current.scrollIntoView({ behavior: "smooth", block: "center" })
|
||||
}
|
||||
}
|
||||
}, [textAreaDisabled, clineAsk, isStreaming, setIsYesNoButtonsHighlighted])
|
||||
|
||||
useEffect(() => {
|
||||
if (!disableAutoScrollRef.current) {
|
||||
setTimeout(() => {
|
||||
@@ -762,10 +819,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === groupedMessages.length - 1}
|
||||
onHeightChange={handleRowHeightChange}
|
||||
hasMessageBeenSentAfterCompletion={hasMessageBeenSentAfterCompletion}
|
||||
isYesNoButtonsHighlighted={isYesNoButtonsHighlighted}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange],
|
||||
[
|
||||
expandedRows,
|
||||
modifiedMessages,
|
||||
groupedMessages.length,
|
||||
toggleRowExpansion,
|
||||
handleRowHeightChange,
|
||||
hasMessageBeenSentAfterCompletion,
|
||||
isYesNoButtonsHighlighted,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -881,6 +948,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
/>
|
||||
</div>
|
||||
<AutoApproveMenu />
|
||||
{/* Yes/No buttons container is now only in the button container div below */}
|
||||
|
||||
{showScrollToBottom ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -897,6 +966,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={buttonContainerRef}
|
||||
className="button-container"
|
||||
style={{
|
||||
opacity:
|
||||
primaryButtonText || secondaryButtonText || isStreaming
|
||||
@@ -907,34 +978,90 @@ 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}
|
||||
{/* Yes/No buttons for task completion feedback */}
|
||||
{clineAsk === "completion_result" && !isStreaming && (
|
||||
<div
|
||||
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>
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
border: isYesNoButtonsHighlighted
|
||||
? "2px solid var(--vscode-focusBorder)"
|
||||
: "2px solid transparent",
|
||||
boxShadow: isYesNoButtonsHighlighted ? "0 0 8px var(--vscode-focusBorder)" : "none",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
{isYesNoButtonsHighlighted ? (
|
||||
<span>Click on Yes or No to continue typing</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="codicon codicon-question" style={{ marginRight: "5px" }} />
|
||||
Did I complete this task successfully?
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginTop: "10px",
|
||||
width: "100%",
|
||||
borderRadius: "4px",
|
||||
gap: "10px",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{
|
||||
flex: 1,
|
||||
opacity: selectedButton === "yes" ? 0.5 : 1,
|
||||
border:
|
||||
selectedButton === "no" ? "2px solid var(--vscode-focusBorder)" : undefined,
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedButton("no")
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("taskCompletionStatus", {
|
||||
detail: { status: "not_completed" },
|
||||
}),
|
||||
)
|
||||
}}>
|
||||
{selectedButton === "no" && (
|
||||
<span className="codicon codicon-check" style={{ marginRight: "5px" }}></span>
|
||||
)}
|
||||
No, let's Talk...
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{
|
||||
flex: 1,
|
||||
opacity: selectedButton === "no" ? 0.5 : 1,
|
||||
border:
|
||||
selectedButton === "yes" ? "2px solid var(--vscode-focusBorder)" : undefined,
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedButton("yes")
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("taskCompletionStatus", {
|
||||
detail: { status: "completed" },
|
||||
}),
|
||||
)
|
||||
}}>
|
||||
{selectedButton === "yes" && (
|
||||
<span className="codicon codicon-check" style={{ marginRight: "5px" }}></span>
|
||||
)}
|
||||
Yes!
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ChatTextArea
|
||||
ref={textAreaRef}
|
||||
inputValue={inputValue}
|
||||
@@ -951,11 +1078,22 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
scrollToBottomAuto()
|
||||
}
|
||||
}}
|
||||
onDisabledClick={handleDisabledInputClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const YesNoButtonContainer = styled.div<{ isHighlighted: boolean }>`
|
||||
display: flex;
|
||||
padding: 10px 15px 0px 15px;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease-in-out;
|
||||
border: ${(props) => (props.isHighlighted ? "2px solid var(--vscode-focusBorder)" : "2px solid transparent")};
|
||||
box-shadow: ${(props) => (props.isHighlighted ? "0 0 8px var(--vscode-focusBorder)" : "none")};
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
const ScrollToBottomButton = styled.div`
|
||||
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 55%, transparent);
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -173,3 +173,36 @@ vscode-dropdown::part(listbox) {
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
|
||||
}
|
||||
|
||||
/* Flash animation for yes/no buttons */
|
||||
@keyframes button-container-flash {
|
||||
0% {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
20% {
|
||||
background-color: color-mix(in srgb, var(--vscode-focusBorder) 50%, transparent);
|
||||
box-shadow: 0 0 8px var(--vscode-focusBorder);
|
||||
}
|
||||
40% {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
60% {
|
||||
background-color: color-mix(in srgb, var(--vscode-focusBorder) 40%, transparent);
|
||||
box-shadow: 0 0 6px var(--vscode-focusBorder);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container-flash {
|
||||
animation: button-container-flash 1.2s ease-out;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user