Skip to content

Commit 4ea6164

Browse files
committed
feat: conditionally show search bar in ModeSelector based on item count
- Search bar now only appears when there are more than 6 modes - When 6 or fewer modes, display info blurb instead of search bar - Hide info icon when search bar is not visible - Add comprehensive tests for the new behavior
1 parent 2b0017e commit 4ea6164

File tree

2 files changed

+125
-25
lines changed

2 files changed

+125
-25
lines changed

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

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -182,26 +182,32 @@ export const ModeSelector = ({
182182
container={portalContainer}
183183
className="p-0 overflow-hidden min-w-80 max-w-9/10">
184184
<div className="flex flex-col w-full">
185-
{/* Search input only */}
186-
<div className="relative p-2 border-b border-vscode-dropdown-border">
187-
<input
188-
aria-label="Search modes"
189-
ref={searchInputRef}
190-
value={searchValue}
191-
onChange={(e) => setSearchValue(e.target.value)}
192-
placeholder={t("chat:modeSelector.searchPlaceholder")}
193-
className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0"
194-
data-testid="mode-search-input"
195-
/>
196-
{searchValue.length > 0 && (
197-
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
198-
<X
199-
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
200-
onClick={onClearSearch}
201-
/>
202-
</div>
203-
)}
204-
</div>
185+
{/* Show search bar only when there are more than 6 items, otherwise show info blurb */}
186+
{modes.length > 6 ? (
187+
<div className="relative p-2 border-b border-vscode-dropdown-border">
188+
<input
189+
aria-label="Search modes"
190+
ref={searchInputRef}
191+
value={searchValue}
192+
onChange={(e) => setSearchValue(e.target.value)}
193+
placeholder={t("chat:modeSelector.searchPlaceholder")}
194+
className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0"
195+
data-testid="mode-search-input"
196+
/>
197+
{searchValue.length > 0 && (
198+
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
199+
<X
200+
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
201+
onClick={onClearSearch}
202+
/>
203+
</div>
204+
)}
205+
</div>
206+
) : (
207+
<div className="p-3 border-b border-vscode-dropdown-border">
208+
<p className="m-0 text-xs text-vscode-descriptionForeground">{instructionText}</p>
209+
</div>
210+
)}
205211

206212
{/* Mode List */}
207213
<div className="max-h-[300px] overflow-y-auto">
@@ -269,11 +275,13 @@ export const ModeSelector = ({
269275
/>
270276
</div>
271277

272-
{/* Info icon and title on the right with matching spacing */}
278+
{/* Info icon and title on the right - only show info icon when search bar is visible */}
273279
<div className="flex items-center gap-1 pr-1">
274-
<StandardTooltip content={instructionText}>
275-
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
276-
</StandardTooltip>
280+
{modes.length > 6 && (
281+
<StandardTooltip content={instructionText}>
282+
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
283+
</StandardTooltip>
284+
)}
277285
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
278286
{t("chat:modeSelector.title")}
279287
</h4>

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

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from "react"
2-
import { render, screen } from "@/utils/test-utils"
2+
import { render, screen, fireEvent } from "@/utils/test-utils"
33
import { describe, test, expect, vi } from "vitest"
44
import ModeSelector from "../ModeSelector"
55
import { Mode } from "@roo/modes"
6+
import { ModeConfig } from "@roo-code/types"
67

78
// Mock the dependencies
89
vi.mock("@/utils/vscode", () => ({
@@ -28,6 +29,23 @@ vi.mock("@/components/ui/hooks/useRooPortal", () => ({
2829
useRooPortal: () => document.body,
2930
}))
3031

32+
vi.mock("@/utils/TelemetryClient", () => ({
33+
telemetryClient: {
34+
capture: vi.fn(),
35+
},
36+
}))
37+
38+
// Create a variable to control what getAllModes returns
39+
let mockModes: ModeConfig[] = []
40+
41+
vi.mock("@roo/modes", async () => {
42+
const actual = await vi.importActual<typeof import("@roo/modes")>("@roo/modes")
43+
return {
44+
...actual,
45+
getAllModes: () => mockModes,
46+
}
47+
})
48+
3149
describe("ModeSelector", () => {
3250
test("shows custom description from customModePrompts", () => {
3351
const customModePrompts = {
@@ -55,4 +73,78 @@ describe("ModeSelector", () => {
5573
// The component should be rendered
5674
expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
5775
})
76+
77+
test("shows search bar when there are more than 6 modes", () => {
78+
// Set up mock to return 7 modes
79+
mockModes = Array.from({ length: 7 }, (_, i) => ({
80+
slug: `mode-${i}`,
81+
name: `Mode ${i}`,
82+
description: `Description for mode ${i}`,
83+
roleDefinition: "Role definition",
84+
groups: ["read", "edit"],
85+
}))
86+
87+
render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
88+
89+
// Click to open the popover
90+
fireEvent.click(screen.getByTestId("mode-selector-trigger"))
91+
92+
// Search input should be visible
93+
expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
94+
95+
// Info icon should be visible
96+
expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument()
97+
const infoIcon = document.querySelector(".codicon-info")
98+
expect(infoIcon).toBeInTheDocument()
99+
})
100+
101+
test("shows info blurb instead of search bar when there are 6 or fewer modes", () => {
102+
// Set up mock to return 5 modes
103+
mockModes = Array.from({ length: 5 }, (_, i) => ({
104+
slug: `mode-${i}`,
105+
name: `Mode ${i}`,
106+
description: `Description for mode ${i}`,
107+
roleDefinition: "Role definition",
108+
groups: ["read", "edit"],
109+
}))
110+
111+
render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
112+
113+
// Click to open the popover
114+
fireEvent.click(screen.getByTestId("mode-selector-trigger"))
115+
116+
// Search input should NOT be visible
117+
expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
118+
119+
// Info blurb should be visible
120+
expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
121+
122+
// Info icon should NOT be visible
123+
const infoIcon = document.querySelector(".codicon-info")
124+
expect(infoIcon).not.toBeInTheDocument()
125+
})
126+
127+
test("filters modes correctly when searching", () => {
128+
// Set up mock to return 7 modes to enable search
129+
mockModes = Array.from({ length: 7 }, (_, i) => ({
130+
slug: `mode-${i}`,
131+
name: `Mode ${i}`,
132+
description: `Description for mode ${i}`,
133+
roleDefinition: "Role definition",
134+
groups: ["read", "edit"],
135+
}))
136+
137+
render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
138+
139+
// Click to open the popover
140+
fireEvent.click(screen.getByTestId("mode-selector-trigger"))
141+
142+
// Type in search
143+
const searchInput = screen.getByTestId("mode-search-input")
144+
fireEvent.change(searchInput, { target: { value: "Mode 3" } })
145+
146+
// Should show filtered results
147+
const modeItems = screen.getAllByTestId("mode-selector-item")
148+
expect(modeItems.length).toBeLessThan(7) // Should have filtered some out
149+
})
58150
})

0 commit comments

Comments
 (0)