From 617f6b4be74d134d9c570365a2655d3325670676 Mon Sep 17 00:00:00 2001 From: abeatrix Date: Tue, 30 Dec 2025 13:58:04 -0800 Subject: [PATCH] apply feedback - fix CompletionOutputRow & ThinkingRow --- webview-ui/src/components/chat/ChatRow.tsx | 105 +++++++++--------- .../components/chat/CompletionOutputRow.tsx | 37 ++++-- .../src/components/chat/MarkdownRow.tsx | 2 +- .../src/components/chat/ThinkingRow.tsx | 4 +- .../src/components/chat/TypewriterText.tsx | 2 +- .../components/messages/MessageRenderer.tsx | 20 ++-- .../components/messages/ToolGroupRenderer.tsx | 13 ++- 7 files changed, 105 insertions(+), 78 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 7ace02bdbf..a2d17d7469 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -13,6 +13,8 @@ import { BooleanRequest, Int64Request, StringRequest } from "@shared/proto/cline import { Mode } from "@shared/storage/types" import deepEqual from "fast-deep-equal" import { + ChevronDownIcon, + ChevronRightIcon, CircleXIcon, FilePlus2Icon, FoldVerticalIcon, @@ -81,7 +83,7 @@ interface ChatRowProps { isRequestInProgress?: boolean } -interface QuoteButtonState { +export interface QuoteButtonState { visible: boolean top: number left: number @@ -485,15 +487,9 @@ export const ChatRowContent = memo( Cline wants to read this file: -
+
{ @@ -509,7 +505,7 @@ export const ChatRowContent = memo( {cleanPathPrefix(tool.path ?? "") + "\u200E"}
- {!isImage && } + {!isImage && }
@@ -1009,24 +1005,25 @@ export const ChatRowContent = memo( /> )} - {showCollapsedThinking && ( + {showCollapsedThinking && reasoningContent && ( <> -
+ variant="icon"> + {isExpanded ? ( + + ) : ( + + )} Thinking - -
+ + {!isExpanded ? reasoningContent : ""} + + - {isExpanded && reasoningContent && ( -
+ {isExpanded && ( +
-
+ variant="icon"> + {isExpanded ? ( + + ) : ( + + )} Thinking - -
+ + {!isExpanded ? message.text : ""} + + - {isExpanded && message.text && ( -
+ {isExpanded && ( +
-
-
-
-
- Task Completed +
+
+
+
+ {icon} + {title}
- +
setIsCompletionOutputExpanded(!isCompletionOutputExpanded)} + quoteButtonState={quoteButtonState} text={text || ""} />
@@ -1330,7 +1335,7 @@ export const ChatRowContent = memo( )}
)} - +
) case "shell_integration_warning": return ( @@ -1511,16 +1516,13 @@ export const ChatRowContent = memo( const text = hasChanges ? message.text.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text return (
-
-
-
-
- - Task Completed - +
+
+
+ {icon} + {title}
-
- +
+
setIsCompletionOutputExpanded(!isCompletionOutputExpanded)} + quoteButtonState={quoteButtonState} text={text || ""} />
diff --git a/webview-ui/src/components/chat/CompletionOutputRow.tsx b/webview-ui/src/components/chat/CompletionOutputRow.tsx index e681832133..d5d3d8414d 100644 --- a/webview-ui/src/components/chat/CompletionOutputRow.tsx +++ b/webview-ui/src/components/chat/CompletionOutputRow.tsx @@ -2,30 +2,47 @@ import { memo } from "react" import { cn } from "@/lib/utils" import { MarkdownRow } from "./MarkdownRow" import "./CompletionOutputRow.css" +import { QuoteButtonState } from "./ChatRow" import ExpandHandle from "./ExpandHandle" +import QuoteButton from "./QuoteButton" export const CompletionOutputRow = memo( - ({ text, isOutputFullyExpanded, onToggle }: { text: string; isOutputFullyExpanded: boolean; onToggle: () => void }) => { + ({ + text, + isOutputFullyExpanded, + onToggle, + quoteButtonState, + handleQuoteClick, + }: { + text: string + isOutputFullyExpanded: boolean + onToggle: () => void + quoteButtonState: QuoteButtonState + handleQuoteClick: () => void + }) => { const outputLines = text.split("\n") const lineCount = outputLines.length const shouldAutoShow = lineCount <= 5 return (
5, + className={cn("w-full relative pb-0 overflow-visible border-t-1 border-description/20 rounded-b-sm", { + "pb-2": !shouldAutoShow, })}>
+ className={cn("completion-output-content", "scroll-smooth p-2 overflow-y-auto w-full", { + "max-h-[400px]": !shouldAutoShow && isOutputFullyExpanded, + "max-h-[150px]": !shouldAutoShow && !isOutputFullyExpanded, + "overflow-y-visible": shouldAutoShow, + })}> + {quoteButtonState.visible && ( + + )}
{/* Show notch only if there's more than 5 lines */} - {lineCount > 5 ? ( - + {!shouldAutoShow ? ( + ) : null}
) diff --git a/webview-ui/src/components/chat/MarkdownRow.tsx b/webview-ui/src/components/chat/MarkdownRow.tsx index a2ef4c5e4e..48ce9c39fe 100644 --- a/webview-ui/src/components/chat/MarkdownRow.tsx +++ b/webview-ui/src/components/chat/MarkdownRow.tsx @@ -3,7 +3,7 @@ import MarkdownBlock from "../common/MarkdownBlock" export const MarkdownRow = memo(({ markdown, showCursor }: { markdown?: string; showCursor?: boolean }) => { return ( -
+
) diff --git a/webview-ui/src/components/chat/ThinkingRow.tsx b/webview-ui/src/components/chat/ThinkingRow.tsx index bd94ae2669..f0c3fd186f 100644 --- a/webview-ui/src/components/chat/ThinkingRow.tsx +++ b/webview-ui/src/components/chat/ThinkingRow.tsx @@ -58,7 +58,7 @@ export const ThinkingRow = memo(
)}
{reasoningContent} @@ -69,3 +69,5 @@ export const ThinkingRow = memo( ) }, ) + +ThinkingRow.displayName = "ThinkingRow" diff --git a/webview-ui/src/components/chat/TypewriterText.tsx b/webview-ui/src/components/chat/TypewriterText.tsx index 85cd8c385c..3171dae8ad 100644 --- a/webview-ui/src/components/chat/TypewriterText.tsx +++ b/webview-ui/src/components/chat/TypewriterText.tsx @@ -25,7 +25,7 @@ export const TypewriterText = memo(({ text, speed = 30 }: { text: string; speed? // After typing completes, show shimmer effect instead of blinking cursor if (isComplete) { return ( - + {text} ) diff --git a/webview-ui/src/components/chat/chat-view/components/messages/MessageRenderer.tsx b/webview-ui/src/components/chat/chat-view/components/messages/MessageRenderer.tsx index e0dbc50423..9e53af328c 100644 --- a/webview-ui/src/components/chat/chat-view/components/messages/MessageRenderer.tsx +++ b/webview-ui/src/components/chat/chat-view/components/messages/MessageRenderer.tsx @@ -38,6 +38,7 @@ export const MessageRenderer: React.FC = ({ messageHandlers, }) => { const { mode } = useExtensionState() + const isLastMessage = index === groupedMessages?.length - 1 // Get reasoning content and response status for api_req_started messages const reasoningData = useMemo(() => { @@ -78,7 +79,7 @@ export const MessageRenderer: React.FC = ({ return ( = ({ ) } + // Determine if this is the last message for status display purposes + const nextMessage = index < groupedMessages.length - 1 && groupedMessages[index + 1] + const isNextCheckpoint = !Array.isArray(nextMessage) && nextMessage && nextMessage?.say === "checkpoint_created" + const isLastMessageGroup = isNextCheckpoint && index === groupedMessages.length - 2 + const isLastMessageOrGroup = isLastMessage || isLastMessageGroup + // Deterministic flash fix: // If this api_req_started is meant to be absorbed into a low-stakes tool group, // never render it as a standalone row. // BUT: Only absorb if this isn't the last/only message (to avoid hiding completed task api_reqs) if ( messageOrGroup.say === "api_req_started" && - index < groupedMessages.length - 1 && + !isLastMessageOrGroup && isApiReqAbsorbable(messageOrGroup.ts, modifiedMessages) ) { return null } - // Determine if this is the last message for status display purposes - const nextMessage = index < groupedMessages.length - 1 && groupedMessages[index + 1] - const isNextCheckpoint = !Array.isArray(nextMessage) && nextMessage && nextMessage?.say === "checkpoint_created" - const isLastMessageGroup = isNextCheckpoint && index === groupedMessages.length - 2 - const isLast = index === groupedMessages.length - 1 || isLastMessageGroup // Regular message return (
- Reason: {reasoning} + {reasoning && Reason: {reasoning}} {/* Expanded content for folders/search/definitions - raw text */} @@ -252,18 +251,20 @@ function getToolGroupSummary(messages: ClineMessage[]): string { } const parts: string[] = [] + const action = counts.read > 0 || counts.list > 0 ? " read " : " " + if (counts.read > 0) { parts.push(`${counts.read} file${counts.read > 1 ? "s" : ""}`) } if (counts.list > 0) { parts.push(`${counts.list} folder${counts.list > 1 ? "s" : ""}`) } - if (counts.search > 0) { - parts.push(`${counts.search} search${counts.search > 1 ? "es" : ""}`) - } if (counts.def > 0) { parts.push(`${counts.def} definition${counts.def > 1 ? "s" : ""}`) } + if (counts.search > 0) { + parts.push(`performed ${counts.search} search${counts.search > 1 ? "es" : ""}`) + } - return parts.length === 0 ? "Context" : parts.join(", ") + " in context" + return parts.length === 0 ? "Context" : "Cline" + action + parts.join(", ") }