Compare commits

...

6 Commits

Author SHA1 Message Date
pashpashpash e0029df1f2 moving to chatview 2025-03-27 02:38:13 -07:00
pashpashpash 2c9ebd80e8 moving to chatview 2025-03-27 02:24:57 -07:00
pashpashpash 771c993543 moving to chatview 2025-03-27 02:17:48 -07:00
pashpashpash e1fbeec461 glow effect 2025-03-27 01:28:51 -07:00
pashpashpash 0ad867db42 ui 2025-03-26 23:55:20 -07:00
pashpashpash a772c7f0cd disabling input before selection 2025-03-26 23:23:45 -07:00
4 changed files with 253 additions and 46 deletions
+34 -20
View File
@@ -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)
+164 -26
View File
@@ -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;
+33
View File
@@ -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;
}