apply feedback - fix CompletionOutputRow & ThinkingRow

This commit is contained in:
abeatrix
2025-12-30 13:58:04 -08:00
parent 309f19e0a3
commit 617f6b4be7
7 changed files with 105 additions and 78 deletions
+55 -50
View File
@@ -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:
</span>
</div>
<div
className="bg-code"
style={{
borderRadius: 3,
overflow: "hidden",
border: "1px solid var(--vscode-editorGroup-border)",
}}>
<div className="bg-code rounded-sm overflow-hidden border border-editor-group-border">
<div
className={cn("text-description flex items-center cursor-pointer select-none", {
className={cn("text-description flex items-center cursor-pointer select-none py-2 px-2.5", {
"cursor-default select-text": isImage,
})}
onClick={() => {
@@ -509,7 +505,7 @@ export const ChatRowContent = memo(
{cleanPathPrefix(tool.path ?? "") + "\u200E"}
</span>
<div className="grow" />
{!isImage && <SquareArrowOutUpRightIcon className="size-3" />}
{!isImage && <SquareArrowOutUpRightIcon className="size-2" />}
</div>
</div>
</>
@@ -1009,24 +1005,25 @@ export const ChatRowContent = memo(
/>
)}
{showCollapsedThinking && (
{showCollapsedThinking && reasoningContent && (
<>
<div
className={cn(
"flex items-center gap-2 select-none mt-0 mb-0 text-description cursor-pointer",
{
"mt-2": apiReqState === "pre",
"mb-2": hasError,
},
)}
<Button
className="flex items-center gap-1 select-none cursor-pointer text-description px-0 w-full"
onClick={handleToggle}
title="Click to view reasoning">
variant="icon">
{isExpanded ? (
<ChevronDownIcon className="opacity-70" />
) : (
<ChevronRightIcon className="opacity-70" />
)}
<span className="font-semibold">Thinking</span>
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
</div>
<span className="italic break-words truncate [direction:rtl] w-full">
{!isExpanded ? reasoningContent : ""}
</span>
</Button>
{isExpanded && reasoningContent && (
<div className="ph-no-capture mt-2 cursor-pointer" onClick={handleToggle}>
{isExpanded && (
<div className="ph-no-capture mt-2 cursor-pointer ml-5" onClick={handleToggle}>
<ThinkingRow
isVisible={true}
reasoningContent={reasoningContent}
@@ -1122,18 +1119,23 @@ export const ChatRowContent = memo(
case "reasoning":
return (
<>
<div
className={cn(
"flex items-center gap-2 select-none mt-0 mb-0 text-description cursor-pointer",
)}
<Button
className="flex items-center gap-1 select-none cursor-pointer text-description px-0 w-full"
onClick={handleToggle}
title="Click to view reasoning">
variant="icon">
{isExpanded ? (
<ChevronDownIcon className="opacity-70" />
) : (
<ChevronRightIcon className="opacity-70" />
)}
<span className="font-semibold">Thinking</span>
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} />
</div>
<span className="italic break-words truncate [direction:rtl] w-full">
{!isExpanded ? message.text : ""}
</span>
</Button>
{isExpanded && message.text && (
<div className="ph-no-capture mt-2 cursor-pointer" onClick={handleToggle}>
{isExpanded && (
<div className="ph-no-capture mt-2 cursor-pointer ml-5" onClick={handleToggle}>
<ThinkingRow
isVisible={true}
reasoningContent={message.text}
@@ -1264,19 +1266,22 @@ export const ChatRowContent = memo(
case "completion_result":
const hasChanges = message.text?.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
const text = hasChanges ? message.text?.slice(0, -COMPLETION_RESULT_CHANGES_FLAG.length) : message.text
return (
<>
<div className="rounded-sm border border-editor-group-border overflow-visible bg-code transition-border duration-300 ease-in-out hover:border-success">
<div className="flex items-center justify-between px-3 py-2 bg-code rounded-0 rounded-tl-sm rounded-tr-sm">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="w-2 h-2 rounded-full bg-success shrink-0" />
<span className="text-success font-semibold text-sm shrink-0">Task Completed</span>
<div>
<div className="rounded-sm border border-editor-group-border overflow-visible bg-success/10 transition-border duration-300 ease-in-out hover:border-success py-2 px-3">
<div className={cn(HEADER_CLASSNAMES, "justify-between")}>
<div className="flex gap-2">
{icon}
{title}
</div>
<CopyButton className="px-0" textToCopy={text || ""} />
<CopyButton textToCopy={text} />
</div>
<CompletionOutputRow
handleQuoteClick={handleQuoteClick}
isOutputFullyExpanded={isCompletionOutputExpanded}
onToggle={() => setIsCompletionOutputExpanded(!isCompletionOutputExpanded)}
quoteButtonState={quoteButtonState}
text={text || ""}
/>
</div>
@@ -1330,7 +1335,7 @@ export const ChatRowContent = memo(
)}
</div>
)}
</>
</div>
)
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 (
<div>
<div className="rounded-sm border border-editor-group-border hover:border-success overflow-visible bg-code transition-all duration-300 ease-in-out">
<div className="flex items-center justify-between py-2 rounded-t-sm bg-code">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="w-2 h-2 rounded-full bg-success flex-shrink-0" />
<span className="text-success font-bold text-sm flex-shrink-0">
Task Completed
</span>
<div className="rounded-sm border border-editor-group-border overflow-visible bg-success/10 transition-border duration-300 ease-in-out hover:border-success py-2 px-3">
<div className={cn(HEADER_CLASSNAMES, "justify-between")}>
<div className="flex gap-2">
{icon}
{title}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<CopyButton textToCopy={text || ""} />
<div className="flex gap-2">
<TaskFeedbackButtons
isFromHistory={
!isLast ||
@@ -1529,11 +1531,14 @@ export const ChatRowContent = memo(
}
messageTs={message.ts}
/>
<CopyButton textToCopy={text} />
</div>
</div>
<CompletionOutputRow
handleQuoteClick={handleQuoteClick}
isOutputFullyExpanded={isCompletionOutputExpanded}
onToggle={() => setIsCompletionOutputExpanded(!isCompletionOutputExpanded)}
quoteButtonState={quoteButtonState}
text={text || ""}
/>
</div>
@@ -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 (
<div
className={cn("bg-code w-full relative pb-0 overflow-visible rounded-b-sm ", {
"pb-2": lineCount > 5,
className={cn("w-full relative pb-0 overflow-visible border-t-1 border-description/20 rounded-b-sm", {
"pb-2": !shouldAutoShow,
})}>
<div
className={cn("completion-output-content scroll-smooth p-3")}
style={{
maxHeight: shouldAutoShow ? "none" : isOutputFullyExpanded ? "400px" : "150px",
overflowY: shouldAutoShow ? "visible" : "auto",
}}>
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,
})}>
<MarkdownRow markdown={text} />
{quoteButtonState.visible && (
<QuoteButton left={quoteButtonState.left} onClick={handleQuoteClick} top={quoteButtonState.top} />
)}
</div>
{/* Show notch only if there's more than 5 lines */}
{lineCount > 5 ? (
<ExpandHandle className="bg-success" isExpanded={isOutputFullyExpanded} onToggle={onToggle} />
{!shouldAutoShow ? (
<ExpandHandle className="bg-success -bottom-4" isExpanded={isOutputFullyExpanded} onToggle={onToggle} />
) : null}
</div>
)
@@ -3,7 +3,7 @@ import MarkdownBlock from "../common/MarkdownBlock"
export const MarkdownRow = memo(({ markdown, showCursor }: { markdown?: string; showCursor?: boolean }) => {
return (
<div className="wrap-anywhere -mb-4 -mt-4 overflow-hidden">
<div className="wrap-anywhere overflow-hidden">
<MarkdownBlock markdown={markdown} showCursor={showCursor} />
</div>
)
@@ -58,7 +58,7 @@ export const ThinkingRow = memo(
</div>
)}
<div
className="flex items-start max-h-[150px] overflow-y-auto text-description leading-normal whitespace-pre-wrap break-words flex-1 [scrollbar-width:none] [-ms-overflow-style:none] pl-2 border-l border-white/10 [&::-webkit-scrollbar]:hidden"
className="flex items-start max-h-[150px] overflow-y-auto text-description leading-normal truncated whitespace-pre-wrap break-words flex-1 [scrollbar-width:none] [-ms-overflow-style:none] pl-2 border-l border-white/10 [&::-webkit-scrollbar]:hidden [direction:ltr]"
ref={scrollRef}>
<span>
{reasoningContent}
@@ -69,3 +69,5 @@ export const ThinkingRow = memo(
)
},
)
ThinkingRow.displayName = "ThinkingRow"
@@ -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 (
<span className="!bg-clip-text text-transparent bg-gradient-to-r from-descriptionvia-foreground to-description animate-shimmer">
<span className="!bg-clip-text text-transparent bg-gradient-to-r from-(--color-description) via-(--color-foreground) to-(--color-description) animate-shimmer">
{text}
</span>
)
@@ -38,6 +38,7 @@ export const MessageRenderer: React.FC<MessageRendererProps> = ({
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<MessageRendererProps> = ({
return (
<BrowserSessionRow
expandedRows={expandedRows}
isLast={index === groupedMessages.length - 1}
isLast={isLastMessage}
key={messageOrGroup[0]?.ts}
lastModifiedMessage={modifiedMessages.at(-1)}
messages={messageOrGroup}
@@ -89,34 +90,35 @@ export const MessageRenderer: React.FC<MessageRendererProps> = ({
)
}
// 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 (
<div
className={cn({
"pb-2.5": isLast,
"pb-2.5": isLastMessage,
})}
data-message-ts={messageOrGroup.ts}>
<ChatRow
inputValue={inputValue}
isExpanded={expandedRows[messageOrGroup.ts] || false}
isLast={isLast}
isLast={isLastMessage}
isRequestInProgress={isRequestInProgress}
key={messageOrGroup.ts}
lastModifiedMessage={modifiedMessages.at(-1)}
@@ -103,7 +103,6 @@ export const ToolGroupRenderer = memo(
isExpandable ? handleItemToggle(tool.ts) : handleOpenFile(info.path)
}
size="icon"
title={reasoning}
variant="text">
<info.icon className="opacity-70 shrink-0" />
<span
@@ -118,7 +117,7 @@ export const ToolGroupRenderer = memo(
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<span>Reason: {reasoning}</span>
{reasoning && <span>Reason: {reasoning}</span>}
</TooltipContent>
</Tooltip>
{/* 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(", ")
}