feat: auto-expand last reasoning block and collapse when no longer last

- Initialize reasoning block as expanded when it is the last message
- Add useEffect to auto-collapse reasoning blocks when they are no longer the last message
- Add comprehensive tests for the new behavior

Fixes #6309
This commit is contained in:
Roo Code 2025-07-28 20:07:03 +00:00
parent 785fcac625
commit ddef20401c
2 changed files with 204 additions and 1 deletions

View file

@ -115,7 +115,8 @@ export const ChatRowContent = ({
}: ChatRowContentProps) => {
const { t } = useTranslation()
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
// Initialize reasoning block as expanded if it's the last message and is a reasoning message
const [reasoningCollapsed, setReasoningCollapsed] = useState(!(isLast && message.say === "reasoning"))
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
@ -124,6 +125,13 @@ export const ChatRowContent = ({
const [editImages, setEditImages] = useState<string[]>([])
const { copyWithFeedback } = useCopyToClipboard()
// Auto-collapse reasoning blocks when they're no longer the last message
useEffect(() => {
if (message.say === "reasoning" && !isLast) {
setReasoningCollapsed(true)
}
}, [isLast, message.say])
// Handle message events for image selection during edit mode
useEffect(() => {
const handleMessage = (event: MessageEvent) => {

View file

@ -0,0 +1,195 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi, beforeEach } from "vitest"
import ChatRow from "../ChatRow"
import { ClineMessage } from "@roo-code/types"
// Mock the dependencies
vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
mcpServers: [],
alwaysAllowMcp: false,
currentCheckpoint: null,
mode: "code",
}),
}))
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
initReactI18next: {
type: "3rdParty",
init: () => {},
},
}))
vi.mock("@src/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))
vi.mock("react-use", () => ({
useSize: (element: any) => [element, { height: 100 }],
useMount: (fn: () => void) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { useEffect } = require("react")
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(fn, [])
},
}))
vi.mock("@src/utils/clipboard", () => ({
useCopyToClipboard: () => ({
copyWithFeedback: vi.fn().mockResolvedValue(true),
}),
}))
describe("ChatRow", () => {
const mockOnToggleExpand = vi.fn()
const mockOnHeightChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it("should render reasoning block expanded when it's the last message", () => {
const reasoningMessage: ClineMessage = {
ts: Date.now(),
type: "say",
say: "reasoning",
text: "This is my reasoning process...",
partial: false,
}
render(
<ChatRow
message={reasoningMessage}
isExpanded={false}
isLast={true}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onHeightChange={mockOnHeightChange}
/>,
)
// Check that the reasoning content is visible (expanded)
expect(screen.getByText("This is my reasoning process...")).toBeInTheDocument()
})
it("should render reasoning block collapsed when it's not the last message", () => {
const reasoningMessage: ClineMessage = {
ts: Date.now(),
type: "say",
say: "reasoning",
text: "This is my reasoning process...",
partial: false,
}
render(
<ChatRow
message={reasoningMessage}
isExpanded={false}
isLast={false}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onHeightChange={mockOnHeightChange}
/>,
)
// Check that the reasoning content is not visible (collapsed)
expect(screen.queryByText("This is my reasoning process...")).not.toBeInTheDocument()
})
it("should auto-collapse reasoning block when isLast changes from true to false", () => {
const reasoningMessage: ClineMessage = {
ts: Date.now(),
type: "say",
say: "reasoning",
text: "This is my reasoning process...",
partial: false,
}
const { rerender } = render(
<ChatRow
message={reasoningMessage}
isExpanded={false}
isLast={true}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onHeightChange={mockOnHeightChange}
/>,
)
// Initially expanded (last message)
expect(screen.getByText("This is my reasoning process...")).toBeInTheDocument()
// Rerender with isLast=false
rerender(
<ChatRow
message={reasoningMessage}
isExpanded={false}
isLast={false}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onHeightChange={mockOnHeightChange}
/>,
)
// Should be collapsed now
expect(screen.queryByText("This is my reasoning process...")).not.toBeInTheDocument()
})
it("should not affect non-reasoning messages", () => {
const textMessage: ClineMessage = {
ts: Date.now(),
type: "say",
say: "text",
text: "This is a regular text message",
partial: false,
}
render(
<ChatRow
message={textMessage}
isExpanded={false}
isLast={true}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onHeightChange={mockOnHeightChange}
/>,
)
// Regular text messages should always be visible
expect(screen.getByText("This is a regular text message")).toBeInTheDocument()
})
it("should allow manual toggle of reasoning block", () => {
const reasoningMessage: ClineMessage = {
ts: Date.now(),
type: "say",
say: "reasoning",
text: "This is my reasoning process...",
partial: false,
}
render(
<ChatRow
message={reasoningMessage}
isExpanded={false}
isLast={false}
isStreaming={false}
onToggleExpand={mockOnToggleExpand}
onHeightChange={mockOnHeightChange}
/>,
)
// Find the reasoning block header and click it
const reasoningHeader = screen.getByText("chat:reasoning.thinking")
fireEvent.click(reasoningHeader.closest("div")!)
// The content should now be visible after manual toggle
expect(screen.getByText("This is my reasoning process...")).toBeInTheDocument()
})
})