Skip to content

Commit 7612005

Browse files
committed
feat: add mode display indicators on task cards (#6493)
1 parent 3803c29 commit 7612005

File tree

6 files changed

+161
-5
lines changed

6 files changed

+161
-5
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ShareButton } from "./ShareButton"
2121
import { ContextWindowProgress } from "./ContextWindowProgress"
2222
import { Mention } from "./Mention"
2323
import { TodoListDisplay } from "./TodoListDisplay"
24+
import { ModeBadge } from "@/components/common/ModeBadge"
2425

2526
export interface TaskHeaderProps {
2627
task: ClineMessage
@@ -90,11 +91,12 @@ const TaskHeader = ({
9091
<div className="flex items-center shrink-0">
9192
<span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
9293
</div>
93-
<div className="ml-1.5 whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
94+
<div className="ml-1.5 whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0 flex items-center gap-2">
9495
<span className="font-bold">
9596
{t("chat:task.title")}
9697
{!isTaskExpanded && ":"}
9798
</span>
99+
{currentTaskItem?.mode && <ModeBadge modeSlug={currentTaskItem.mode} />}
98100
{!isTaskExpanded && (
99101
<span className="ml-1">
100102
<Mention text={task.text} />

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

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,33 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
3333
}))
3434

3535
// Mock the ExtensionStateContext
36+
const { mockGetCurrentTaskItem, mockSetCurrentTaskItem } = vi.hoisted(() => {
37+
let currentTaskItem: any = { id: "test-task-id", mode: "code" }
38+
return {
39+
mockGetCurrentTaskItem: () => currentTaskItem,
40+
mockSetCurrentTaskItem: (newItem: any) => {
41+
currentTaskItem = newItem
42+
},
43+
}
44+
})
45+
3646
vi.mock("@src/context/ExtensionStateContext", () => ({
3747
useExtensionState: () => ({
3848
apiConfiguration: {
3949
apiProvider: "anthropic",
40-
apiKey: "test-api-key", // Add relevant fields
41-
apiModelId: "claude-3-opus-20240229", // Add relevant fields
42-
} as ProviderSettings, // Optional: Add type assertion if ProviderSettings is imported
43-
currentTaskItem: { id: "test-task-id" },
50+
apiKey: "test-api-key",
51+
apiModelId: "claude-3-opus-20240229",
52+
} as ProviderSettings,
53+
currentTaskItem: mockGetCurrentTaskItem(),
54+
customModes: [],
4455
}),
4556
}))
4657

58+
// Mock ModeBadge component
59+
vi.mock("@/components/common/ModeBadge", () => ({
60+
ModeBadge: ({ modeSlug }: { modeSlug: string }) => <div data-testid="mode-badge">{modeSlug}</div>,
61+
}))
62+
4763
describe("TaskHeader", () => {
4864
const defaultProps: TaskHeaderProps = {
4965
task: { type: "say", ts: Date.now(), text: "Test task", images: [] },
@@ -122,4 +138,31 @@ describe("TaskHeader", () => {
122138
fireEvent.click(condenseButton!)
123139
expect(handleCondenseContext).not.toHaveBeenCalled()
124140
})
141+
142+
it("should display mode badge when currentTaskItem has mode", () => {
143+
renderTaskHeader()
144+
expect(screen.getByTestId("mode-badge")).toBeInTheDocument()
145+
expect(screen.getByText("code")).toBeInTheDocument()
146+
})
147+
148+
it("should not display mode badge when currentTaskItem has no mode", () => {
149+
// Override the mock for this test
150+
const originalTaskItem = mockGetCurrentTaskItem()
151+
mockSetCurrentTaskItem({
152+
id: "test-task-id",
153+
number: 1,
154+
ts: Date.now(),
155+
task: "Test task",
156+
tokensIn: 100,
157+
tokensOut: 50,
158+
totalCost: 0.05,
159+
// No mode property
160+
})
161+
162+
renderTaskHeader()
163+
expect(screen.queryByTestId("mode-badge")).not.toBeInTheDocument()
164+
165+
// Restore original mock
166+
mockSetCurrentTaskItem(originalTaskItem)
167+
})
125168
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react"
2+
import { findModeBySlug, getAllModes } from "@roo/modes"
3+
import { useExtensionState } from "@/context/ExtensionStateContext"
4+
import { StandardTooltip } from "@/components/ui"
5+
import { cn } from "@/lib/utils"
6+
7+
interface ModeBadgeProps {
8+
modeSlug?: string
9+
className?: string
10+
}
11+
12+
export const ModeBadge: React.FC<ModeBadgeProps> = ({ modeSlug, className }) => {
13+
const { customModes } = useExtensionState()
14+
15+
if (!modeSlug) {
16+
return null
17+
}
18+
19+
// Get all modes (built-in + custom)
20+
const allModes = getAllModes(customModes)
21+
const mode = findModeBySlug(modeSlug, allModes)
22+
const displayName = mode?.name || modeSlug // Fallback to slug if mode deleted
23+
24+
return (
25+
<StandardTooltip content={displayName}>
26+
<span
27+
className={cn(
28+
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
29+
"bg-vscode-badge-background text-vscode-badge-foreground",
30+
"max-w-[120px] truncate",
31+
className,
32+
)}>
33+
{displayName}
34+
</span>
35+
</StandardTooltip>
36+
)
37+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { render, screen } from "@/utils/test-utils"
2+
import { ModeBadge } from "../ModeBadge"
3+
4+
// Mock the shared modes module
5+
vi.mock("@roo/modes", () => ({
6+
findModeBySlug: vi.fn((slug, _modes) => {
7+
if (slug === "code") return { slug: "code", name: "💻 Code" }
8+
if (slug === "architect") return { slug: "architect", name: "🏗️ Architect" }
9+
if (slug === "custom") return { slug: "custom", name: "Very Long Custom Mode Name That Should Be Truncated" }
10+
return undefined
11+
}),
12+
getAllModes: vi.fn(() => []),
13+
}))
14+
15+
// Mock ExtensionStateContext
16+
vi.mock("@/context/ExtensionStateContext", () => ({
17+
useExtensionState: () => ({
18+
customModes: [],
19+
}),
20+
}))
21+
22+
describe("ModeBadge", () => {
23+
it("renders mode name when mode exists", () => {
24+
render(<ModeBadge modeSlug="code" />)
25+
expect(screen.getByText("💻 Code")).toBeInTheDocument()
26+
})
27+
28+
it("renders slug as fallback when mode not found", () => {
29+
render(<ModeBadge modeSlug="deleted-mode" />)
30+
expect(screen.getByText("deleted-mode")).toBeInTheDocument()
31+
})
32+
33+
it("returns null when no mode slug provided", () => {
34+
const { container } = render(<ModeBadge />)
35+
expect(container.firstChild).toBeNull()
36+
})
37+
38+
it("truncates long mode names", () => {
39+
render(<ModeBadge modeSlug="custom" />)
40+
const badge = screen.getByText(/Very Long Custom Mode Name/)
41+
expect(badge).toHaveClass("truncate")
42+
expect(badge).toHaveClass("max-w-[120px]")
43+
})
44+
45+
it("shows full name in tooltip", async () => {
46+
render(<ModeBadge modeSlug="custom" />)
47+
// Tooltip content would be tested with user interaction
48+
// This is a simplified test
49+
expect(screen.getByText(/Very Long Custom Mode Name/)).toBeInTheDocument()
50+
})
51+
})

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 */}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ vi.mock("@src/i18n/TranslationContext", () => ({
88
}),
99
}))
1010

11+
// Mock ModeBadge component
12+
vi.mock("@/components/common/ModeBadge", () => ({
13+
ModeBadge: ({ modeSlug }: { modeSlug: string }) => <div data-testid="mode-badge">{modeSlug}</div>,
14+
}))
15+
1116
const mockItem = {
1217
id: "1",
1318
number: 1,
@@ -32,4 +37,20 @@ describe("TaskItemHeader", () => {
3237

3338
expect(screen.getByRole("button")).toBeInTheDocument()
3439
})
40+
41+
it("shows mode badge when item has mode", () => {
42+
const itemWithMode = { ...mockItem, mode: "code" }
43+
render(<TaskItemHeader item={itemWithMode} isSelectionMode={false} onDelete={vi.fn()} />)
44+
45+
// ModeBadge would be mocked in the test
46+
expect(screen.getByTestId("mode-badge")).toBeInTheDocument()
47+
expect(screen.getByText("code")).toBeInTheDocument()
48+
})
49+
50+
it("does not show mode badge when item has no mode", () => {
51+
render(<TaskItemHeader item={mockItem} isSelectionMode={false} onDelete={vi.fn()} />)
52+
53+
// Verify no mode badge is rendered
54+
expect(screen.queryByTestId("mode-badge")).not.toBeInTheDocument()
55+
})
3556
})

0 commit comments

Comments
 (0)