Skip to content
Closed
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
120 changes: 90 additions & 30 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { CheckpointWarning } from "./CheckpointWarning"
import QueuedMessages from "./QueuedMessages"
import { getLatestTodo } from "@roo/todo"
import { QueuedMessage } from "@roo-code/types"
import { ShareButton } from "./ShareButton"

export interface ChatViewProps {
isHidden: boolean
Expand Down Expand Up @@ -1885,38 +1886,97 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</StandardTooltip>
) : (
<>
{primaryButtonText && !isStreaming && (
<StandardTooltip
content={
primaryButtonText === t("chat:retry.title")
? t("chat:retry.tooltip")
: primaryButtonText === t("chat:save.title")
? t("chat:save.tooltip")
: primaryButtonText === t("chat:approve.title")
? t("chat:approve.tooltip")
: primaryButtonText === t("chat:runCommand.title")
? t("chat:runCommand.tooltip")
: primaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: primaryButtonText === t("chat:resumeTask.title")
? t("chat:resumeTask.tooltip")
{(() => {
// Calculate button className based on Daniel's suggestion
const showShareButton =
primaryButtonText === t("chat:startNewTask.title") && currentTaskItem?.id
const buttonClassName =
showShareButton || secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"

return (
<>
{primaryButtonText && !isStreaming && (
<StandardTooltip
content={
primaryButtonText === t("chat:retry.title")
? t("chat:retry.tooltip")
: primaryButtonText === t("chat:save.title")
? t("chat:save.tooltip")
: primaryButtonText === t("chat:approve.title")
? t("chat:approve.tooltip")
: primaryButtonText ===
t("chat:proceedAnyways.title")
? t("chat:proceedAnyways.tooltip")
t("chat:runCommand.title")
? t("chat:runCommand.tooltip")
: primaryButtonText ===
t("chat:proceedWhileRunning.title")
? t("chat:proceedWhileRunning.tooltip")
: undefined
}>
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
onClick={() => handlePrimaryButtonClick()}>
{primaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: primaryButtonText ===
t("chat:resumeTask.title")
? t("chat:resumeTask.tooltip")
: primaryButtonText ===
t("chat:proceedAnyways.title")
? t(
"chat:proceedAnyways.tooltip",
)
: primaryButtonText ===
t(
"chat:proceedWhileRunning.title",
)
? t(
"chat:proceedWhileRunning.tooltip",
)
: undefined
}>
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className={buttonClassName}
onClick={() =>
handlePrimaryButtonClick(inputValue, selectedImages)
}>
{primaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
{primaryButtonText === t("chat:startNewTask.title") &&
currentTaskItem?.id && (
<>
{/* Hidden ShareButton for functionality */}
<div className="hidden">
<ShareButton
item={currentTaskItem}
disabled={!enableButtons}
/>
</div>
{/* Visible VSCodeButton that matches Start New Task */}
<StandardTooltip content={t("chat:task.share")}>
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className="ml-1 w-9 flex items-center justify-center p-0"
onClick={() => {
// Find and click the share button
const shareButtons =
document.querySelectorAll("button")
const shareButton = Array.from(
shareButtons,
).find(
(btn) =>
btn.querySelector(".codicon-link") &&
btn.closest('[role="dialog"]') === null,
)
if (shareButton) {
shareButton.click()
}
}}>
<span className="codicon codicon-link"></span>
</VSCodeButton>
</StandardTooltip>
</>
)}
</>
)
})()}
{(secondaryButtonText || isStreaming) && (
<StandardTooltip
content={
Expand Down
173 changes: 173 additions & 0 deletions webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ vi.mock("../AutoApproveMenu", () => ({
default: () => null,
}))

// Mock ShareButton component
vi.mock("../ShareButton", () => ({
ShareButton: function MockShareButton({ item, disabled }: { item: any; disabled: boolean }) {
// Match the actual ShareButton behavior - don't render if no item ID
if (!item?.id) {
return null
}
return (
<button data-testid="share-button" disabled={disabled} className="share-button">
Share
</button>
)
},
}))

// Mock VersionIndicator - returns null by default to prevent rendering in tests
vi.mock("../../common/VersionIndicator", () => ({
default: vi.fn(() => null),
Expand Down Expand Up @@ -1493,3 +1508,161 @@ describe("ChatView - Message Queueing Tests", () => {
expect(input.getAttribute("data-sending-disabled")).toBe("false")
})
})

describe("ChatView - Share Button Tests", () => {
beforeEach(() => vi.clearAllMocks())

it("displays share button next to Start New Task button when task has completed", async () => {
const { queryAllByTestId, queryByText } = renderChatView()

// Mock a completed task state with currentTaskItem having an ID
mockPostMessage({
currentTaskItem: {
id: "task-123",
task: "Test task",
ts: Date.now(),
},
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now(),
text: "Task completed successfully",
partial: false,
},
],
})

// Wait for the UI to update
await waitFor(() => {
// Check that Start New Task button is present
const startNewTaskButton = queryByText("chat:startNewTask.title")
expect(startNewTaskButton).toBeInTheDocument()

// Check that share buttons are present (one in header, one next to Start New Task)
const shareButtons = queryAllByTestId("share-button")
expect(shareButtons.length).toBeGreaterThan(0)
})
})

it("does not display share button next to Start New Task when currentTaskItem has no ID", async () => {
const { container, queryByText } = renderChatView()

// Mock a completed task state without currentTaskItem ID
mockPostMessage({
currentTaskItem: null,
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now(),
text: "Task completed successfully",
partial: false,
},
],
})

// Wait for the UI to update
await waitFor(() => {
// Check that Start New Task button is present
const startNewTaskButton = queryByText("chat:startNewTask.title")
expect(startNewTaskButton).toBeInTheDocument()

// Check that share button is NOT present next to Start New Task button
// Look specifically in the button area, not the header
const buttonArea = container.querySelector(".flex.h-9.items-center.mb-1")
const shareButtonInButtonArea = buttonArea?.querySelector('[data-testid="share-button"]')
expect(shareButtonInButtonArea).toBeFalsy()
})
})

it("share button respects enableButtons state", async () => {
const { container } = renderChatView()

// Mock a state where buttons should be disabled
mockPostMessage({
currentTaskItem: {
id: "task-123",
task: "Test task",
ts: Date.now(),
},
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "completion_result",
ts: Date.now(),
text: "Task completed successfully",
partial: true, // partial: true should disable buttons
},
],
})

// Wait for the UI to update
await waitFor(() => {
// Check that share button in button area is present but disabled
const buttonArea = container.querySelector(".flex.h-9.items-center.mb-1")
const shareButtonInButtonArea = buttonArea?.querySelector('[data-testid="share-button"]')
expect(shareButtonInButtonArea).toBeTruthy()
expect(shareButtonInButtonArea).toBeDisabled()
})
})

it("share button only appears when primary button is Start New Task", async () => {
const { container, queryByText } = renderChatView()

// Mock a state where primary button is NOT Start New Task
mockPostMessage({
currentTaskItem: {
id: "task-123",
task: "Test task",
ts: Date.now(),
},
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now() - 2000,
text: "Initial task",
},
{
type: "ask",
ask: "api_req_failed",
ts: Date.now(),
text: "API request failed",
partial: false,
},
],
})

// Wait for the UI to update
await waitFor(() => {
// Check that Retry button is present (not Start New Task)
const retryButton = queryByText("chat:retry.title")
expect(retryButton).toBeInTheDocument()

// Check that share button is NOT present next to the primary button
// Look specifically in the button area, not the header
const buttonArea = container.querySelector(".flex.h-9.items-center.mb-1")
const shareButtonInButtonArea = buttonArea?.querySelector('[data-testid="share-button"]')
expect(shareButtonInButtonArea).not.toBeInTheDocument()
})
})
})
Loading