mirror of
https://github.com/cline/cline.git
synced 2026-09-12 00:50:27 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d745fb100 | ||
|
|
d1ed2e9e02 | ||
|
|
07fe6ca363 |
@@ -35,6 +35,9 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
tooManyMistakes: (feedback?: string) =>
|
||||
`You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
autoApprovalMaxReached: (feedback?: string) =>
|
||||
`Auto-approval limit reached. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
missingToolParameterError: (paramName: string) =>
|
||||
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,
|
||||
|
||||
|
||||
+35
-2
@@ -1456,7 +1456,10 @@ export class Task {
|
||||
try {
|
||||
const { response, text, images, files } = await this.ask("command_output", chunk)
|
||||
if (response === "yesButtonClicked") {
|
||||
// proceed while running
|
||||
// proceed while running - but still capture user feedback if provided
|
||||
if (text || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
} else {
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
@@ -1947,12 +1950,42 @@ export class Task {
|
||||
message: `Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests.`,
|
||||
})
|
||||
}
|
||||
await this.ask(
|
||||
const { response, text, images, files } = await this.ask(
|
||||
"auto_approval_max_req_reached",
|
||||
`Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests. Would you like to reset the count and proceed with the task?`,
|
||||
)
|
||||
// if we get past the promise it means the user approved and did not start a new task
|
||||
this.taskState.consecutiveAutoApprovedRequestsCount = 0
|
||||
|
||||
// Process user feedback if provided
|
||||
if (response === "messageResponse") {
|
||||
// Display the user's message in the chat UI
|
||||
await this.say("user_feedback", text, images, files)
|
||||
|
||||
// This userContent is for the *next* API call.
|
||||
const feedbackUserContent: UserContent = []
|
||||
feedbackUserContent.push({
|
||||
type: "text",
|
||||
text: formatResponse.autoApprovalMaxReached(text),
|
||||
})
|
||||
if (images && images.length > 0) {
|
||||
feedbackUserContent.push(...formatResponse.imageBlocks(images))
|
||||
}
|
||||
|
||||
let fileContentString = ""
|
||||
if (files && files.length > 0) {
|
||||
fileContentString = await processFilesIntoText(files)
|
||||
}
|
||||
|
||||
if (fileContentString) {
|
||||
feedbackUserContent.push({
|
||||
type: "text",
|
||||
text: fileContentString,
|
||||
})
|
||||
}
|
||||
|
||||
userContent = feedbackUserContent
|
||||
}
|
||||
}
|
||||
|
||||
// get previous api req's index to check token usage and determine if we need to truncate conversation history
|
||||
|
||||
@@ -1551,7 +1551,10 @@ export const ChatRowContent = memo(
|
||||
<OptionsButtons
|
||||
options={options}
|
||||
selected={selected}
|
||||
isActive={isLast && lastModifiedMessage?.ask === "followup"}
|
||||
isActive={
|
||||
(isLast && lastModifiedMessage?.ask === "followup") ||
|
||||
(!selected && options && options.length > 0)
|
||||
}
|
||||
inputValue={inputValue}
|
||||
/>
|
||||
{quoteButtonState.visible && (
|
||||
@@ -1640,7 +1643,10 @@ export const ChatRowContent = memo(
|
||||
<OptionsButtons
|
||||
options={options}
|
||||
selected={selected}
|
||||
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_respond"}
|
||||
isActive={
|
||||
(isLast && lastModifiedMessage?.ask === "plan_mode_respond") ||
|
||||
(!selected && options && options.length > 0)
|
||||
}
|
||||
inputValue={inputValue}
|
||||
/>
|
||||
{quoteButtonState.visible && (
|
||||
|
||||
@@ -66,7 +66,7 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
|
||||
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
key={task.ts} // trick to make sure virtuoso re-renders when task changes
|
||||
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
|
||||
className="scrollable"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
@@ -75,11 +75,12 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
|
||||
components={{
|
||||
Footer: () => <div style={{ height: 5 }} />, // Add empty padding at the bottom
|
||||
}}
|
||||
// increasing top by 3_000 to prevent jumping around when user collapses a row
|
||||
increaseViewportBy={{
|
||||
top: 3_000,
|
||||
bottom: Number.MAX_SAFE_INTEGER,
|
||||
}}
|
||||
data={groupedMessages}
|
||||
}} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
||||
data={groupedMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
|
||||
itemContent={itemContent}
|
||||
atBottomStateChange={(isAtBottom) => {
|
||||
setIsAtBottom(isAtBottom)
|
||||
@@ -88,7 +89,7 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
|
||||
}
|
||||
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
|
||||
}}
|
||||
atBottomThreshold={10}
|
||||
atBottomThreshold={10} // anything lower causes issues with followOutput
|
||||
initialTopMostItemIndex={groupedMessages.length - 1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -39,55 +39,3 @@ export const useIsStreaming = (
|
||||
return false
|
||||
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that shows a visual streaming indicator
|
||||
* Can be used to show loading states, typing indicators, etc.
|
||||
*/
|
||||
export const StreamingVisualIndicator: React.FC<{ isStreaming: boolean }> = ({ isStreaming }) => {
|
||||
if (!isStreaming) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px 16px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "4px",
|
||||
marginRight: "8px",
|
||||
}}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: "4px",
|
||||
height: "4px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--vscode-progressBar-background)",
|
||||
animation: `pulse 1.4s infinite ease-in-out ${i * 0.16}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span>Cline is thinking...</span>
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
0%, 80%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
*/
|
||||
|
||||
export { MessageRenderer, createMessageRenderer } from "./MessageRenderer"
|
||||
export { useIsStreaming, StreamingVisualIndicator } from "./StreamingIndicator"
|
||||
export { useIsStreaming } from "./StreamingIndicator"
|
||||
|
||||
@@ -57,6 +57,8 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
case "api_req_failed":
|
||||
case "new_task":
|
||||
case "condense":
|
||||
case "report_bug":
|
||||
@@ -111,34 +113,69 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
// For approval buttons, if there's input content, send it as a proper user message
|
||||
// If there's no input content, just approve the action
|
||||
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
// Send as a regular message so it appears in the conversation
|
||||
await handleSendMessage(trimmedInput || "", images || [], files || [])
|
||||
} else {
|
||||
// No input content, just approve the action
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
}),
|
||||
)
|
||||
// Clear input state after sending (only when no content was sent as a message)
|
||||
setInputValue("")
|
||||
setActiveQuote(null)
|
||||
setSelectedImages([])
|
||||
setSelectedFiles([])
|
||||
}
|
||||
break
|
||||
case "mistake_limit_reached":
|
||||
case "auto_approval_max_req_reached":
|
||||
case "command_output":
|
||||
// For proceed buttons, if there's input content, send it as a proper user message
|
||||
// If there's no input content, just proceed with the action
|
||||
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
files: files,
|
||||
}),
|
||||
)
|
||||
// Send as a regular message so it appears in the conversation
|
||||
await handleSendMessage(trimmedInput || "", images || [], files || [])
|
||||
} else {
|
||||
// No input content, just proceed with the action
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
}),
|
||||
)
|
||||
// Clear input state after sending (only when no content was sent as a message)
|
||||
setInputValue("")
|
||||
setActiveQuote(null)
|
||||
setSelectedImages([])
|
||||
setSelectedFiles([])
|
||||
}
|
||||
break
|
||||
case "resume_task":
|
||||
// For resume_task, if there's input content, send it as a proper user message
|
||||
// If there's no input content, just resume the task
|
||||
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
// Send as a regular message so it appears in the conversation
|
||||
await handleSendMessage(trimmedInput || "", images || [], files || [])
|
||||
} else {
|
||||
// No input content, just resume the task
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
}),
|
||||
)
|
||||
// Clear input state after sending (only when no content was sent as a message)
|
||||
setInputValue("")
|
||||
setActiveQuote(null)
|
||||
setSelectedImages([])
|
||||
setSelectedFiles([])
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setActiveQuote(null)
|
||||
setSelectedImages([])
|
||||
setSelectedFiles([])
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
@@ -185,6 +222,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
|
||||
setSendingDisabled,
|
||||
setEnableButtons,
|
||||
chatState,
|
||||
handleSendMessage,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ export function useScrollBehavior(
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [isAtBottom, setIsAtBottom] = useState(false)
|
||||
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
|
||||
|
||||
// Smooth scroll to bottom with debounce
|
||||
const scrollToBottomSmooth = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
@@ -49,7 +47,7 @@ export function useScrollBehavior(
|
||||
[],
|
||||
)
|
||||
|
||||
// Instant scroll to bottom
|
||||
// Smooth scroll to bottom with debounce
|
||||
const scrollToBottomAuto = useCallback(() => {
|
||||
virtuosoRef.current?.scrollTo({
|
||||
top: Number.MAX_SAFE_INTEGER,
|
||||
@@ -57,7 +55,6 @@ export function useScrollBehavior(
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Scroll to specific message
|
||||
const scrollToMessage = useCallback(
|
||||
(messageIndex: number) => {
|
||||
setPendingScrollToMessage(messageIndex)
|
||||
@@ -113,7 +110,7 @@ export function useScrollBehavior(
|
||||
[messages, visibleMessages, groupedMessages],
|
||||
)
|
||||
|
||||
// Toggle row expansion with scroll handling
|
||||
// scroll when user toggles certain rows
|
||||
const toggleRowExpansion = useCallback(
|
||||
(ts: number) => {
|
||||
const isCollapsing = expandedRows[ts] ?? false
|
||||
@@ -165,10 +162,9 @@ export function useScrollBehavior(
|
||||
}
|
||||
}
|
||||
},
|
||||
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom, setExpandedRows],
|
||||
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom],
|
||||
)
|
||||
|
||||
// Handle row height changes
|
||||
const handleRowHeightChange = useCallback(
|
||||
(isTaller: boolean) => {
|
||||
if (!disableAutoScrollRef.current) {
|
||||
@@ -184,23 +180,21 @@ export function useScrollBehavior(
|
||||
[scrollToBottomSmooth, scrollToBottomAuto],
|
||||
)
|
||||
|
||||
// Auto-scroll when new messages arrive
|
||||
useEffect(() => {
|
||||
if (!disableAutoScrollRef.current) {
|
||||
setTimeout(() => {
|
||||
scrollToBottomSmooth()
|
||||
}, 50)
|
||||
// return () => clearTimeout(timer) // dont cleanup since if visibleMessages.length changes it cancels.
|
||||
}
|
||||
}, [groupedMessages.length, scrollToBottomSmooth])
|
||||
|
||||
// Handle pending scroll to message
|
||||
useEffect(() => {
|
||||
if (pendingScrollToMessage !== null) {
|
||||
scrollToMessage(pendingScrollToMessage)
|
||||
}
|
||||
}, [pendingScrollToMessage, groupedMessages, scrollToMessage])
|
||||
|
||||
// Handle wheel events to detect manual scrolling
|
||||
const handleWheel = useCallback((event: Event) => {
|
||||
const wheelEvent = event as WheelEvent
|
||||
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
|
||||
@@ -210,8 +204,7 @@ export function useScrollBehavior(
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("wheel", handleWheel, window, { passive: true })
|
||||
useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
|
||||
|
||||
return {
|
||||
virtuosoRef,
|
||||
|
||||
@@ -20,23 +20,23 @@ export function filterVisibleMessages(messages: ClineMessage[]): ClineMessage[]
|
||||
return messages.filter((message) => {
|
||||
switch (message.ask) {
|
||||
case "completion_result":
|
||||
// don't show a chat row for a completion_result ask without text
|
||||
// don't show a chat row for a completion_result ask without text. This specific type of message only occurs if cline wants to execute a command as part of its completion result, in which case we interject the completion_result tool with the execute_command tool.
|
||||
if (message.text === "") {
|
||||
return false
|
||||
}
|
||||
break
|
||||
case "api_req_failed":
|
||||
case "api_req_failed": // this message is used to update the latest api_req_started that the request failed
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
return false
|
||||
}
|
||||
switch (message.say) {
|
||||
case "api_req_finished":
|
||||
case "api_req_retried":
|
||||
case "deleted_api_reqs":
|
||||
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
|
||||
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
|
||||
case "deleted_api_reqs": // aggregated api_req metrics from deleted messages
|
||||
return false
|
||||
case "text":
|
||||
// Sometimes cline returns an empty text message, we don't want to render these
|
||||
// Sometimes cline returns an empty text message, we don't want to render these. (We also use a say text for user messages, so in case they just sent images we still render that)
|
||||
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user