mirror of
https://github.com/cline/cline.git
synced 2026-09-21 05:10:09 +08:00
apply feedback - fix CompletionOutputRow & ThinkingRow
This commit is contained in:
@@ -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(", ")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user