Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,21 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
})
}
}

// Mark the last tool-approval ask as answered when user approves (or auto-approval)
if (askResponse === "yesButtonClicked") {
const lastToolAskIndex = findLastIndex(
this.clineMessages,
(msg) => msg.type === "ask" && msg.ask === "tool" && !msg.isAnswered,
)
if (lastToolAskIndex !== -1) {
this.clineMessages[lastToolAskIndex].isAnswered = true
void this.updateClineMessage(this.clineMessages[lastToolAskIndex])
this.saveClineMessages().catch((error) => {
console.error("Failed to save answered tool-ask state:", error)
})
}
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/__tests__/App.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe("App", () => {
const chatView = screen.getByTestId("chat-view")
expect(chatView).toBeInTheDocument()
expect(chatView.getAttribute("data-hidden")).toBe("false")
})
}, 10000)

it("switches to settings view when receiving settingsButtonClicked action", async () => {
render(<AppWithProviders />)
Expand Down
175 changes: 175 additions & 0 deletions webview-ui/src/__tests__/FileChangesPanel.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React from "react"
import { fireEvent, render, screen } from "@/utils/test-utils"
import type { ClineMessage } from "@roo-code/types"
import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"
import FileChangesPanel from "../components/chat/FileChangesPanel"

const mockPostMessage = vi.fn()

vi.mock("@src/utils/vscode", () => ({
vscode: {
postMessage: (...args: unknown[]) => mockPostMessage(...args),
},
}))

// Mock i18n to return readable header with count
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, opts?: { count?: number }) => {
if (key === "chat:fileChangesInConversation.header" && opts?.count != null) {
return `${opts.count} file(s) changed in this conversation`
}
return key
},
}),
}))

// Lightweight mock so we don't pull in CodeBlock/DiffView
vi.mock("@src/components/common/CodeAccordian", () => ({
default: ({
path,
isExpanded,
onToggleExpand,
}: {
path?: string
isExpanded: boolean
onToggleExpand: () => void
}) => (
<div data-testid="code-accordian">
<span data-testid="accordian-path">{path}</span>
<button type="button" onClick={onToggleExpand} data-testid="accordian-toggle">
{isExpanded ? "expanded" : "collapsed"}
</button>
</div>
),
}))

function createFileEditMessage(path: string, diff: string): ClineMessage {
return {
type: "ask",
ask: "tool",
ts: Date.now(),
partial: false,
isAnswered: true,
text: JSON.stringify({
tool: "appliedDiff",
path,
diff,
}),
}
}

function renderPanel(messages: ClineMessage[] | undefined) {
return render(
<TranslationProvider>
<FileChangesPanel clineMessages={messages} />
</TranslationProvider>,
)
}

describe("FileChangesPanel", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("renders nothing when clineMessages is undefined", () => {
const { container } = renderPanel(undefined)
expect(container.firstChild).toBeNull()
})

it("renders nothing when clineMessages is empty", () => {
const { container } = renderPanel([])
expect(container.firstChild).toBeNull()
})

it("renders nothing when there are no file-edit messages", () => {
const messages: ClineMessage[] = [
{
type: "say",
say: "text",
ts: Date.now(),
partial: false,
text: "hello",
},
{
type: "ask",
ask: "tool",
ts: Date.now(),
partial: false,
text: JSON.stringify({ tool: "read_file", path: "x.ts" }),
},
]
const { container } = renderPanel(messages)
expect(container.firstChild).toBeNull()
})

it("renders nothing when file-edit ask tool is not approved (isAnswered false or missing)", () => {
const messages: ClineMessage[] = [
{
type: "ask",
ask: "tool",
ts: Date.now(),
partial: false,
text: JSON.stringify({
tool: "appliedDiff",
path: "src/foo.ts",
diff: "+line",
}),
},
]
const { container } = renderPanel(messages)
expect(container.firstChild).toBeNull()
})

it("renders panel with header when there is one file edit", () => {
const messages = [createFileEditMessage("src/foo.ts", "@@ -1 +1 @@\n+line")]
renderPanel(messages)

expect(screen.getByText("1 file(s) changed in this conversation")).toBeInTheDocument()
// Expand panel so file row is in DOM (CollapsibleContent may not render when closed in some setups)
fireEvent.click(screen.getByText("1 file(s) changed in this conversation").closest("button")!)
expect(screen.getByTestId("accordian-path")).toHaveTextContent("src/foo.ts")
})

it("renders one row per unique path when multiple files edited", () => {
const messages = [createFileEditMessage("src/a.ts", "diff a"), createFileEditMessage("src/b.ts", "diff b")]
renderPanel(messages)

expect(screen.getByText("2 file(s) changed in this conversation")).toBeInTheDocument()
// Expand panel so file rows are rendered
fireEvent.click(screen.getByText("2 file(s) changed in this conversation").closest("button")!)
const paths = screen.getAllByTestId("accordian-path")
expect(paths).toHaveLength(2)
expect(paths.map((el) => el.textContent)).toEqual(expect.arrayContaining(["src/a.ts", "src/b.ts"]))
})

it("collapsed by default: panel trigger shows chevron and expanding reveals file rows", () => {
const messages = [createFileEditMessage("src/foo.ts", "diff")]
renderPanel(messages)

// Header visible
const headerText = screen.getByText("1 file(s) changed in this conversation")
expect(headerText).toBeInTheDocument()
// Trigger is the button that contains the header text
const trigger = headerText.closest("button")
expect(trigger).toBeInTheDocument()

// Expand panel
fireEvent.click(trigger!)
expect(screen.getByTestId("accordian-path")).toHaveTextContent("src/foo.ts")
})

it("toggling a file row expand calls onToggleExpand", () => {
const messages = [createFileEditMessage("src/foo.ts", "diff")]
renderPanel(messages)

// Expand panel first so the file row is rendered
const headerText = screen.getByText("1 file(s) changed in this conversation")
fireEvent.click(headerText.closest("button")!)

const accordianToggle = screen.getByTestId("accordian-toggle")
expect(accordianToggle).toHaveTextContent("collapsed")
fireEvent.click(accordianToggle)
expect(accordianToggle).toHaveTextContent("expanded")
})
})
Loading
Loading