mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add mode display indicators on task cards
- Create reusable ModeBadge component that displays mode name from slug - Update TaskItemHeader to show mode badge next to timestamp in history view - Update TaskHeader to show mode badge in main chat view task header - Handle cases where mode is undefined (no badge displayed) - Handle deleted custom modes gracefully - Implement text truncation for long mode names with CSS - Add comprehensive unit tests for ModeBadge component - Use existing Badge component with outline variant for consistent styling Fixes #6493
This commit is contained in:
parent
74672fafcb
commit
2905191390
4 changed files with 230 additions and 4 deletions
|
|
@ -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 = ({
|
|||
<span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
|
||||
</div>
|
||||
<div className="ml-1.5 whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
|
||||
<span className="font-bold">
|
||||
{t("chat:task.title")}
|
||||
{!isTaskExpanded && ":"}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold">
|
||||
{t("chat:task.title")}
|
||||
{!isTaskExpanded && ":"}
|
||||
</span>
|
||||
{currentTaskItem?.mode && <ModeBadge modeSlug={currentTaskItem.mode} />}
|
||||
</div>
|
||||
{!isTaskExpanded && (
|
||||
<span className="ml-1">
|
||||
<Mention text={task.text} />
|
||||
|
|
|
|||
51
webview-ui/src/components/common/ModeBadge.tsx
Normal file
51
webview-ui/src/components/common/ModeBadge.tsx
Normal file
|
|
@ -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<ModeBadgeProps> = ({ 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 = (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs font-medium max-w-24 truncate ${className || ""}`}
|
||||
title={modeName} // Fallback tooltip
|
||||
>
|
||||
{modeName}
|
||||
</Badge>
|
||||
)
|
||||
|
||||
// If the mode name is longer than ~20 characters, wrap with tooltip
|
||||
if (modeName.length > 20) {
|
||||
return <StandardTooltip content={modeName}>{badgeContent}</StandardTooltip>
|
||||
}
|
||||
|
||||
return badgeContent
|
||||
}
|
||||
|
||||
export default ModeBadge
|
||||
169
webview-ui/src/components/common/__tests__/ModeBadge.spec.tsx
Normal file
169
webview-ui/src/components/common/__tests__/ModeBadge.spec.tsx
Normal file
|
|
@ -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(<ModeBadge modeSlug="code" />)
|
||||
|
||||
const badge = screen.getByText("💻 Code")
|
||||
expect(badge).toBeInTheDocument()
|
||||
expect(badge).toHaveClass("border-vscode-input-border")
|
||||
})
|
||||
|
||||
it("renders mode badge for architect mode", () => {
|
||||
render(<ModeBadge modeSlug="architect" />)
|
||||
|
||||
expect(screen.getByText("🏗️ Architect")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode badge for ask mode", () => {
|
||||
render(<ModeBadge modeSlug="ask" />)
|
||||
|
||||
expect(screen.getByText("❓ Ask")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode badge for debug mode", () => {
|
||||
render(<ModeBadge modeSlug="debug" />)
|
||||
|
||||
expect(screen.getByText("🪲 Debug")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders mode badge for orchestrator mode", () => {
|
||||
render(<ModeBadge modeSlug="orchestrator" />)
|
||||
|
||||
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(<ModeBadge modeSlug="custom-test" />)
|
||||
|
||||
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(<ModeBadge modeSlug="plain-mode" />)
|
||||
|
||||
expect(screen.getByText("Plain Mode")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("returns null for undefined mode", () => {
|
||||
const { container } = render(<ModeBadge modeSlug="non-existent-mode" />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("returns null for deleted custom mode", () => {
|
||||
// Simulate a mode that was deleted but still referenced in history
|
||||
render(<ModeBadge modeSlug="deleted-custom-mode" />)
|
||||
|
||||
const { container } = render(<ModeBadge modeSlug="deleted-custom-mode" />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(<ModeBadge modeSlug="code" className="custom-class" />)
|
||||
|
||||
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(<ModeBadge modeSlug="very-long-mode" />)
|
||||
|
||||
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(<ModeBadge modeSlug="code" />)
|
||||
|
||||
const badge = screen.getByText("💻 Code")
|
||||
expect(badge).toHaveAttribute("title", "💻 Code")
|
||||
})
|
||||
|
||||
it("uses outline variant for consistent styling", () => {
|
||||
render(<ModeBadge modeSlug="code" />)
|
||||
|
||||
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(<ModeBadge modeSlug="code" />)
|
||||
|
||||
// Should show the custom mode name, not the built-in one
|
||||
expect(screen.getByText("🔧 Custom Code")).toBeInTheDocument()
|
||||
expect(screen.queryByText("💻 Code")).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -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<TaskItemHeaderProps> = ({ item, isSelectionMode,
|
|||
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
{item.mode && <ModeBadge modeSlug={item.mode} />}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue