diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 1896df486b..dbce906a7c 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -15,6 +15,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import Thumbnails from "../common/Thumbnails" +import ModeBadge from "../common/ModeBadge" import { TaskActions } from "./TaskActions" import { ShareButton } from "./ShareButton" @@ -91,10 +92,13 @@ const TaskHeader = ({
- - {t("chat:task.title")} - {!isTaskExpanded && ":"} - +
+ + {t("chat:task.title")} + {!isTaskExpanded && ":"} + + {currentTaskItem?.mode && } +
{!isTaskExpanded && ( diff --git a/webview-ui/src/components/common/ModeBadge.tsx b/webview-ui/src/components/common/ModeBadge.tsx new file mode 100644 index 0000000000..9782e39490 --- /dev/null +++ b/webview-ui/src/components/common/ModeBadge.tsx @@ -0,0 +1,51 @@ +import React from "react" +import { getModeBySlug } from "../../../../src/shared/modes" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { Badge } from "@/components/ui/badge" +import { StandardTooltip } from "@/components/ui/standard-tooltip" + +export interface ModeBadgeProps { + modeSlug: string + className?: string +} + +/** + * ModeBadge component displays a badge showing the mode name for a given mode slug. + * It handles cases where the mode is undefined or deleted gracefully. + * For long mode names, it truncates with ellipsis and shows full name in tooltip. + */ +const ModeBadge: React.FC = ({ modeSlug, className }) => { + const { customModes } = useExtensionState() + + // Get mode details using the existing getModeBySlug function + const mode = getModeBySlug(modeSlug, customModes) + + // If mode is not found, don't render anything + if (!mode) { + return null + } + + // Extract just the mode name (without emoji if present) + // Mode names can be like "๐Ÿ’ป Code" or just "Code" + const modeName = mode.name + + // For very long mode names, we'll let CSS handle truncation + const badgeContent = ( + + {modeName} + + ) + + // If the mode name is longer than ~20 characters, wrap with tooltip + if (modeName.length > 20) { + return {badgeContent} + } + + return badgeContent +} + +export default ModeBadge 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..b76d2ee529 --- /dev/null +++ b/webview-ui/src/components/common/__tests__/ModeBadge.spec.tsx @@ -0,0 +1,169 @@ +// npx vitest run src/components/common/__tests__/ModeBadge.spec.tsx + +import { render, screen } from "@/utils/test-utils" +import { DEFAULT_MODES, type ModeConfig } from "@roo-code/types" + +import ModeBadge from "../ModeBadge" + +// Mock the shared modes module +vi.mock("../../../../../src/shared/modes", () => ({ + getModeBySlug: vi.fn((slug: string, customModes?: any[]) => { + // First check custom modes + const customMode = customModes?.find((mode) => mode.slug === slug) + if (customMode) { + return customMode + } + // Then check built-in modes + return DEFAULT_MODES.find((mode) => mode.slug === slug) + }), +})) + +// Mock the extension state context +const mockExtensionState = { + customModes: [] as ModeConfig[], +} + +vi.mock("../../../context/ExtensionStateContext", () => ({ + useExtensionState: () => mockExtensionState, +})) + +describe("ModeBadge", () => { + beforeEach(() => { + // Reset custom modes before each test + mockExtensionState.customModes = [] + }) + + it("renders mode badge for built-in mode", () => { + render() + + const badge = screen.getByText("๐Ÿ’ป Code") + expect(badge).toBeInTheDocument() + expect(badge).toHaveClass("border-vscode-input-border") + }) + + it("renders mode badge for architect mode", () => { + render() + + expect(screen.getByText("๐Ÿ—๏ธ Architect")).toBeInTheDocument() + }) + + it("renders mode badge for ask mode", () => { + render() + + expect(screen.getByText("โ“ Ask")).toBeInTheDocument() + }) + + it("renders mode badge for debug mode", () => { + render() + + expect(screen.getByText("๐Ÿชฒ Debug")).toBeInTheDocument() + }) + + it("renders mode badge for orchestrator mode", () => { + render() + + expect(screen.getByText("๐Ÿชƒ Orchestrator")).toBeInTheDocument() + }) + + it("renders mode badge for custom mode", () => { + // Add a custom mode to the mock state + mockExtensionState.customModes = [ + { + slug: "custom-test", + name: "๐Ÿงช Test Mode", + roleDefinition: "Test role", + groups: ["read"], + }, + ] + + render() + + expect(screen.getByText("๐Ÿงช Test Mode")).toBeInTheDocument() + }) + + it("renders mode badge for custom mode without emoji", () => { + // Add a custom mode without emoji + mockExtensionState.customModes = [ + { + slug: "plain-mode", + name: "Plain Mode", + roleDefinition: "Plain role", + groups: ["read"], + }, + ] + + render() + + expect(screen.getByText("Plain Mode")).toBeInTheDocument() + }) + + it("returns null for undefined mode", () => { + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it("returns null for deleted custom mode", () => { + // Simulate a mode that was deleted but still referenced in history + render() + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it("applies custom className", () => { + render() + + const badge = screen.getByText("๐Ÿ’ป Code") + expect(badge).toHaveClass("custom-class") + }) + + it("truncates long mode names with CSS", () => { + // Add a custom mode with a very long name + mockExtensionState.customModes = [ + { + slug: "very-long-mode", + name: "This is a very long mode name that should be truncated", + roleDefinition: "Long role", + groups: ["read"], + }, + ] + + render() + + const badge = screen.getByText("This is a very long mode name that should be truncated") + expect(badge).toHaveClass("max-w-24", "truncate") + }) + + it("has proper title attribute for accessibility", () => { + render() + + const badge = screen.getByText("๐Ÿ’ป Code") + expect(badge).toHaveAttribute("title", "๐Ÿ’ป Code") + }) + + it("uses outline variant for consistent styling", () => { + render() + + const badge = screen.getByText("๐Ÿ’ป Code") + expect(badge).toHaveClass("border-vscode-input-border") + }) + + it("handles custom mode overriding built-in mode", () => { + // Add a custom mode that overrides a built-in mode + mockExtensionState.customModes = [ + { + slug: "code", + name: "๐Ÿ”ง Custom Code", + roleDefinition: "Custom code role", + groups: ["read", "edit"], + }, + ] + + render() + + // Should show the custom mode name, not the built-in one + expect(screen.getByText("๐Ÿ”ง Custom Code")).toBeInTheDocument() + expect(screen.queryByText("๐Ÿ’ป Code")).not.toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/history/TaskItemHeader.tsx b/webview-ui/src/components/history/TaskItemHeader.tsx index bdddb090c8..a6916bcc46 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 "../common/ModeBadge" export interface TaskItemHeaderProps { item: HistoryItem @@ -22,6 +23,7 @@ const TaskItemHeader: React.FC = ({ item, isSelectionMode, {formatDate(item.ts)} + {item.mode && }
{/* Action Buttons */}