Skip to content

Commit f258f49

Browse files
committed
feat: Add mode display indicators on task cards (#6493)
- Created ModeBadge component to display mode names with proper styling - Updated TaskItemHeader to show mode badge next to timestamp in history view - Updated TaskHeader to display mode badge in main chat view - Added comprehensive unit tests for ModeBadge component - Handles custom modes, deleted modes, and long mode names with truncation - Only displays badge when mode information exists (no empty badges)
1 parent 3803c29 commit f258f49

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
1515
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
1616

1717
import Thumbnails from "../common/Thumbnails"
18+
import { ModeBadge } from "../common/ModeBadge"
1819

1920
import { TaskActions } from "./TaskActions"
2021
import { ShareButton } from "./ShareButton"
@@ -101,6 +102,11 @@ const TaskHeader = ({
101102
</span>
102103
)}
103104
</div>
105+
{currentTaskItem?.mode && (
106+
<div className="ml-2 flex-shrink-0">
107+
<ModeBadge modeSlug={currentTaskItem.mode} />
108+
</div>
109+
)}
104110
</div>
105111
<StandardTooltip content={t("chat:task.closeAndStart")}>
106112
<Button variant="ghost" size="icon" onClick={onClose} className="shrink-0 w-5 h-5">
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react"
2+
import { getModeBySlug } from "@roo/modes"
3+
import { Badge } from "@/components/ui/badge"
4+
import { StandardTooltip } from "@/components/ui"
5+
import { cn } from "@/lib/utils"
6+
import { useExtensionState } from "@/context/ExtensionStateContext"
7+
8+
interface ModeBadgeProps {
9+
modeSlug: string | undefined
10+
className?: string
11+
}
12+
13+
export const ModeBadge: React.FC<ModeBadgeProps> = ({ modeSlug, className }) => {
14+
const { customModes } = useExtensionState()
15+
16+
if (!modeSlug) {
17+
return null
18+
}
19+
20+
const mode = getModeBySlug(modeSlug, customModes)
21+
22+
// If mode is not found (e.g., deleted custom mode), show the slug as fallback
23+
const displayName = mode?.name || modeSlug
24+
25+
// Truncate long mode names
26+
const truncatedName = displayName.length > 20 ? `${displayName.substring(0, 17)}...` : displayName
27+
28+
return (
29+
<StandardTooltip content={displayName}>
30+
<Badge
31+
variant="outline"
32+
className={cn(
33+
"text-xs font-normal px-1.5 py-0 h-5",
34+
"bg-vscode-badge-background text-vscode-badge-foreground",
35+
"border-vscode-badge-background",
36+
className,
37+
)}>
38+
{truncatedName}
39+
</Badge>
40+
</StandardTooltip>
41+
)
42+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { render, screen } from "@testing-library/react"
2+
import { ModeBadge } from "../ModeBadge"
3+
import { getModeBySlug } from "@roo/modes"
4+
import { useExtensionState } from "@/context/ExtensionStateContext"
5+
6+
// Mock dependencies
7+
vi.mock("@roo/modes")
8+
vi.mock("@/context/ExtensionStateContext")
9+
vi.mock("@/components/ui", () => ({
10+
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
11+
Badge: ({ children, className }: any) => <div className={className}>{children}</div>,
12+
}))
13+
14+
const mockGetModeBySlug = vi.mocked(getModeBySlug)
15+
const mockUseExtensionState = vi.mocked(useExtensionState)
16+
17+
describe("ModeBadge", () => {
18+
beforeEach(() => {
19+
vi.clearAllMocks()
20+
mockUseExtensionState.mockReturnValue({
21+
customModes: [],
22+
} as any)
23+
})
24+
25+
it("should not render when modeSlug is undefined", () => {
26+
const { container } = render(<ModeBadge modeSlug={undefined} />)
27+
expect(container.firstChild).toBeNull()
28+
})
29+
30+
it("should render mode name for built-in mode", () => {
31+
mockGetModeBySlug.mockReturnValue({
32+
slug: "code",
33+
name: "💻 Code",
34+
roleDefinition: "You are a code assistant",
35+
groups: ["read", "edit"] as const,
36+
} as any)
37+
38+
render(<ModeBadge modeSlug="code" />)
39+
40+
expect(screen.getByText("💻 Code")).toBeInTheDocument()
41+
expect(mockGetModeBySlug).toHaveBeenCalledWith("code", [])
42+
})
43+
44+
it("should render mode name for custom mode", () => {
45+
const customModes = [
46+
{
47+
slug: "custom-mode",
48+
name: "🎨 Custom Mode",
49+
roleDefinition: "Custom role",
50+
groups: ["read"] as const,
51+
},
52+
]
53+
54+
mockUseExtensionState.mockReturnValue({
55+
customModes,
56+
} as any)
57+
58+
mockGetModeBySlug.mockReturnValue(customModes[0] as any)
59+
60+
render(<ModeBadge modeSlug="custom-mode" />)
61+
62+
expect(screen.getByText("🎨 Custom Mode")).toBeInTheDocument()
63+
expect(mockGetModeBySlug).toHaveBeenCalledWith("custom-mode", customModes)
64+
})
65+
66+
it("should render mode slug as fallback for deleted custom mode", () => {
67+
mockGetModeBySlug.mockReturnValue(undefined)
68+
69+
render(<ModeBadge modeSlug="deleted-mode" />)
70+
71+
expect(screen.getByText("deleted-mode")).toBeInTheDocument()
72+
})
73+
74+
it("should truncate long mode names", () => {
75+
mockGetModeBySlug.mockReturnValue({
76+
slug: "long-mode",
77+
name: "This is a very long mode name that should be truncated",
78+
roleDefinition: "Long mode",
79+
groups: ["read"] as const,
80+
} as any)
81+
82+
render(<ModeBadge modeSlug="long-mode" />)
83+
84+
expect(screen.getByText("This is a very lo...")).toBeInTheDocument()
85+
})
86+
87+
it("should show full name in tooltip", () => {
88+
const longName = "This is a very long mode name that should be truncated"
89+
mockGetModeBySlug.mockReturnValue({
90+
slug: "long-mode",
91+
name: longName,
92+
roleDefinition: "Long mode",
93+
groups: ["read"] as const,
94+
} as any)
95+
96+
render(<ModeBadge modeSlug="long-mode" />)
97+
98+
// The StandardTooltip component should have the full name as content
99+
const badge = screen.getByText("This is a very lo...")
100+
expect(badge.closest("[title]")).toHaveAttribute("title", longName)
101+
})
102+
103+
it("should apply custom className", () => {
104+
mockGetModeBySlug.mockReturnValue({
105+
slug: "code",
106+
name: "Code",
107+
roleDefinition: "Code mode",
108+
groups: ["read"] as const,
109+
} as any)
110+
111+
render(<ModeBadge modeSlug="code" className="custom-class" />)
112+
113+
const badge = screen.getByText("Code")
114+
expect(badge).toHaveClass("custom-class")
115+
})
116+
117+
it("should handle mode without emoji", () => {
118+
mockGetModeBySlug.mockReturnValue({
119+
slug: "plain-mode",
120+
name: "Plain Mode",
121+
roleDefinition: "Plain mode",
122+
groups: ["read"] as const,
123+
} as any)
124+
125+
render(<ModeBadge modeSlug="plain-mode" />)
126+
127+
expect(screen.getByText("Plain Mode")).toBeInTheDocument()
128+
})
129+
130+
it("should use correct styling classes", () => {
131+
mockGetModeBySlug.mockReturnValue({
132+
slug: "test-mode",
133+
name: "Test Mode",
134+
roleDefinition: "Test mode",
135+
groups: ["read"] as const,
136+
} as any)
137+
138+
render(<ModeBadge modeSlug="test-mode" />)
139+
140+
const badge = screen.getByText("Test Mode")
141+
expect(badge).toHaveClass("bg-vscode-badge-background")
142+
expect(badge).toHaveClass("text-vscode-badge-foreground")
143+
expect(badge).toHaveClass("border-vscode-badge-background")
144+
expect(badge).toHaveClass("text-xs")
145+
expect(badge).toHaveClass("font-normal")
146+
expect(badge).toHaveClass("px-1.5")
147+
expect(badge).toHaveClass("py-0")
148+
expect(badge).toHaveClass("h-5")
149+
})
150+
151+
it("should handle all built-in modes", () => {
152+
const builtInModes = [
153+
{ slug: "architect", name: "🏗️ Architect" },
154+
{ slug: "code", name: "💻 Code" },
155+
{ slug: "ask", name: "❓ Ask" },
156+
{ slug: "debug", name: "🪲 Debug" },
157+
{ slug: "test", name: "🧪 Test" },
158+
]
159+
160+
builtInModes.forEach((mode) => {
161+
mockGetModeBySlug.mockReturnValue({
162+
...mode,
163+
roleDefinition: "Test",
164+
groups: ["read"] as const,
165+
} as any)
166+
167+
const { unmount } = render(<ModeBadge modeSlug={mode.slug} />)
168+
expect(screen.getByText(mode.name)).toBeInTheDocument()
169+
unmount()
170+
})
171+
})
172+
})

webview-ui/src/components/history/TaskItemHeader.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { HistoryItem } from "@roo-code/types"
33
import { formatDate } from "@/utils/format"
44
import { DeleteButton } from "./DeleteButton"
55
import { cn } from "@/lib/utils"
6+
import { ModeBadge } from "@/components/common/ModeBadge"
67

78
export interface TaskItemHeaderProps {
89
item: HistoryItem
@@ -22,6 +23,7 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
2223
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
2324
{formatDate(item.ts)}
2425
</span>
26+
{item.mode && <ModeBadge modeSlug={item.mode} />}
2527
</div>
2628

2729
{/* Action Buttons */}

0 commit comments

Comments
 (0)