diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index bc9c36a6c2..9a811a0fd3 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -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 @@ -1885,38 +1886,97 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) : ( <> - {primaryButtonText && !isStreaming && ( - { + // 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 && ( + - handlePrimaryButtonClick()}> - {primaryButtonText} - - - )} + 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 + }> + + handlePrimaryButtonClick(inputValue, selectedImages) + }> + {primaryButtonText} + + + )} + {primaryButtonText === t("chat:startNewTask.title") && + currentTaskItem?.id && ( + <> + {/* Hidden ShareButton for functionality */} +
+ +
+ {/* Visible VSCodeButton that matches Start New Task */} + + { + // 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() + } + }}> + + + + + )} + + ) + })()} {(secondaryButtonText || isStreaming) && ( ({ 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 ( + + ) + }, +})) + // Mock VersionIndicator - returns null by default to prevent rendering in tests vi.mock("../../common/VersionIndicator", () => ({ default: vi.fn(() => null), @@ -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() + }) + }) +})