diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 1896df486b..6bc1dbe71f 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -21,6 +21,7 @@ import { ShareButton } from "./ShareButton" import { ContextWindowProgress } from "./ContextWindowProgress" import { Mention } from "./Mention" import { TodoListDisplay } from "./TodoListDisplay" +import { ModeBadge } from "@/components/common/ModeBadge" export interface TaskHeaderProps { task: ClineMessage @@ -90,11 +91,12 @@ const TaskHeader = ({
-
+
{t("chat:task.title")} {!isTaskExpanded && ":"} + {currentTaskItem?.mode && } {!isTaskExpanded && ( diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index c04f7e45e5..85b012b6f0 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -33,17 +33,27 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ })) // Mock the ExtensionStateContext +const mockCurrentTaskItem = vi.hoisted(() => ({ + value: { id: "test-task-id", mode: "code" } as any, +})) + vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: () => ({ apiConfiguration: { apiProvider: "anthropic", - apiKey: "test-api-key", // Add relevant fields - apiModelId: "claude-3-opus-20240229", // Add relevant fields - } as ProviderSettings, // Optional: Add type assertion if ProviderSettings is imported - currentTaskItem: { id: "test-task-id" }, + apiKey: "test-api-key", + apiModelId: "claude-3-opus-20240229", + } as ProviderSettings, + currentTaskItem: mockCurrentTaskItem.value, + customModes: [], }), })) +// Mock ModeBadge component +vi.mock("@/components/common/ModeBadge", () => ({ + ModeBadge: ({ modeSlug }: { modeSlug: string }) =>
{modeSlug}
, +})) + describe("TaskHeader", () => { const defaultProps: TaskHeaderProps = { task: { type: "say", ts: Date.now(), text: "Test task", images: [] }, @@ -122,4 +132,25 @@ describe("TaskHeader", () => { fireEvent.click(condenseButton!) expect(handleCondenseContext).not.toHaveBeenCalled() }) + + it("should display mode badge when currentTaskItem has mode", () => { + renderTaskHeader() + expect(screen.getByTestId("mode-badge")).toBeInTheDocument() + expect(screen.getByText("code")).toBeInTheDocument() + }) + + it("should not display mode badge when currentTaskItem has no mode", () => { + // Override the mock for this test + const originalTaskItem = mockCurrentTaskItem.value + mockCurrentTaskItem.value = { + id: "test-task-id", + // No mode property + } + + renderTaskHeader() + expect(screen.queryByTestId("mode-badge")).not.toBeInTheDocument() + + // Restore original mock + mockCurrentTaskItem.value = originalTaskItem + }) }) diff --git a/webview-ui/src/components/common/ModeBadge.tsx b/webview-ui/src/components/common/ModeBadge.tsx new file mode 100644 index 0000000000..655ebd723c --- /dev/null +++ b/webview-ui/src/components/common/ModeBadge.tsx @@ -0,0 +1,36 @@ +import React from "react" +import { getModeBySlug } from "@roo/modes" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { StandardTooltip } from "@/components/ui" +import { cn } from "@/lib/utils" + +interface ModeBadgeProps { + modeSlug?: string + className?: string +} + +export const ModeBadge: React.FC = ({ modeSlug, className }) => { + const { customModes } = useExtensionState() + + if (!modeSlug) { + return null + } + + // Get mode using getModeBySlug which checks custom modes first, then built-in + const mode = getModeBySlug(modeSlug, customModes) + const displayName = mode?.name || modeSlug // Fallback to slug if mode deleted + + return ( + + + {displayName} + + + ) +} diff --git a/webview-ui/src/components/common/__tests__/ModeBadge.spec.tsx b/webview-ui/src/components/common/__tests__/ModeBadge.spec.tsx new file mode 100644 index 0000000000..cf2b0d96a4 --- /dev/null +++ b/webview-ui/src/components/common/__tests__/ModeBadge.spec.tsx @@ -0,0 +1,51 @@ +import { render, screen } from "@/utils/test-utils" +import { ModeBadge } from "../ModeBadge" + +// Mock the shared modes module +vi.mock("@roo/modes", () => ({ + getModeBySlug: vi.fn((slug, _customModes) => { + if (slug === "code") return { slug: "code", name: "💻 Code" } + if (slug === "architect") return { slug: "architect", name: "🏗️ Architect" } + if (slug === "custom") return { slug: "custom", name: "Very Long Custom Mode Name That Should Be Truncated" } + return undefined + }), +})) + +// Mock ExtensionStateContext +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + customModes: [], + }), +})) + +describe("ModeBadge", () => { + it("renders mode name when mode exists", () => { + render() + expect(screen.getByText("💻 Code")).toBeInTheDocument() + }) + + it("renders slug as fallback when mode not found", () => { + render() + expect(screen.getByText("deleted-mode")).toBeInTheDocument() + }) + + it("returns null when no mode slug provided", () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it("truncates long mode names", () => { + render() + const badge = screen.getByText(/Very Long Custom Mode Name/) + expect(badge).toHaveClass("truncate") + expect(badge).toHaveClass("max-w-[120px]") + }) + + it("shows full name in tooltip", () => { + render() + // The tooltip content is set via the StandardTooltip's content prop + // We verify the text is rendered in the badge itself + const badge = screen.getByText(/Very Long Custom Mode Name/) + expect(badge).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/history/TaskItemHeader.tsx b/webview-ui/src/components/history/TaskItemHeader.tsx index bdddb090c8..ac28d42d94 100644 --- a/webview-ui/src/components/history/TaskItemHeader.tsx +++ b/webview-ui/src/components/history/TaskItemHeader.tsx @@ -3,6 +3,7 @@ import type { HistoryItem } from "@roo-code/types" import { formatDate } from "@/utils/format" import { DeleteButton } from "./DeleteButton" import { cn } from "@/lib/utils" +import { ModeBadge } from "@/components/common/ModeBadge" export interface TaskItemHeaderProps { item: HistoryItem @@ -22,6 +23,7 @@ const TaskItemHeader: React.FC = ({ item, isSelectionMode, {formatDate(item.ts)} + {item.mode && }
{/* Action Buttons */} diff --git a/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx index 090bf2521f..94bb5800fe 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx @@ -8,6 +8,11 @@ vi.mock("@src/i18n/TranslationContext", () => ({ }), })) +// Mock ModeBadge component +vi.mock("@/components/common/ModeBadge", () => ({ + ModeBadge: ({ modeSlug }: { modeSlug: string }) =>
{modeSlug}
, +})) + const mockItem = { id: "1", number: 1, @@ -32,4 +37,20 @@ describe("TaskItemHeader", () => { expect(screen.getByRole("button")).toBeInTheDocument() }) + + it("shows mode badge when item has mode", () => { + const itemWithMode = { ...mockItem, mode: "code" } + render() + + // ModeBadge would be mocked in the test + expect(screen.getByTestId("mode-badge")).toBeInTheDocument() + expect(screen.getByText("code")).toBeInTheDocument() + }) + + it("does not show mode badge when item has no mode", () => { + render() + + // Verify no mode badge is rendered + expect(screen.queryByTestId("mode-badge")).not.toBeInTheDocument() + }) })