Skip to content

Commit dbec3f5

Browse files
committed
UI tweaks
1 parent aae9cfa commit dbec3f5

File tree

23 files changed

+162
-162
lines changed

23 files changed

+162
-162
lines changed

webview-ui/src/__tests__/command-autocomplete.spec.ts

Lines changed: 37 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,18 @@ describe("Command Autocomplete", () => {
1616
{ type: ContextMenuOptionType.Problems, value: "problems" },
1717
]
1818

19-
// Mock translation function
20-
const mockT = (key: string, options?: { name?: string }) => {
21-
if (key === "chat:command.triggerDescription") {
22-
return `Trigger the ${options?.name || "command"} command`
23-
}
24-
return key
25-
}
26-
2719
describe("slash command command suggestions", () => {
2820
it('should return all commands when query is just "/"', () => {
29-
const options = getContextMenuOptions("/", "/", mockT, null, mockQueryItems, [], [], mockCommands)
21+
const options = getContextMenuOptions("/", "/", null, mockQueryItems, [], [], mockCommands)
3022

31-
expect(options).toHaveLength(5)
32-
expect(options.every((option) => option.type === ContextMenuOptionType.Command)).toBe(true)
23+
// Should have 6 items: 1 section header + 5 commands
24+
expect(options).toHaveLength(6)
3325

34-
const commandNames = options.map((option) => option.value)
26+
// Filter out section headers to check commands
27+
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
28+
expect(commandOptions).toHaveLength(5)
29+
30+
const commandNames = commandOptions.map((option) => option.value)
3531
expect(commandNames).toContain("setup")
3632
expect(commandNames).toContain("build")
3733
expect(commandNames).toContain("deploy")
@@ -40,7 +36,7 @@ describe("Command Autocomplete", () => {
4036
})
4137

4238
it("should filter commands based on fuzzy search", () => {
43-
const options = getContextMenuOptions("/set", "/set", mockT, null, mockQueryItems, [], [], mockCommands)
39+
const options = getContextMenuOptions("/set", "/set", null, mockQueryItems, [], [], mockCommands)
4440

4541
// Should match 'setup' (fuzzy search behavior may vary)
4642
expect(options.length).toBeGreaterThan(0)
@@ -50,18 +46,17 @@ describe("Command Autocomplete", () => {
5046
})
5147

5248
it("should return commands with correct format", () => {
53-
const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], mockCommands)
49+
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands)
5450

5551
const setupOption = options.find((option) => option.value === "setup")
5652
expect(setupOption).toBeDefined()
5753
expect(setupOption!.type).toBe(ContextMenuOptionType.Command)
58-
expect(setupOption!.label).toBe("setup")
59-
expect(setupOption!.description).toBe("Trigger the setup command")
60-
expect(setupOption!.icon).toBe("$(play)")
54+
expect(setupOption!.slashCommand).toBe("/setup")
55+
expect(setupOption!.value).toBe("setup")
6156
})
6257

6358
it("should handle empty command list", () => {
64-
const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], [])
59+
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], [])
6560

6661
// Should return NoResults when no commands match
6762
expect(options).toHaveLength(1)
@@ -72,7 +67,6 @@ describe("Command Autocomplete", () => {
7267
const options = getContextMenuOptions(
7368
"/nonexistent",
7469
"/nonexistent",
75-
mockT,
7670
null,
7771
mockQueryItems,
7872
[],
@@ -86,7 +80,7 @@ describe("Command Autocomplete", () => {
8680
})
8781

8882
it("should not return command suggestions for non-slash queries", () => {
89-
const options = getContextMenuOptions("setup", "setup", mockT, null, mockQueryItems, [], [], mockCommands)
83+
const options = getContextMenuOptions("setup", "setup", null, mockQueryItems, [], [], mockCommands)
9084

9185
// Should not contain command options for non-slash queries
9286
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
@@ -100,24 +94,15 @@ describe("Command Autocomplete", () => {
10094
{ name: "deploy.prod", source: "global" },
10195
]
10296

103-
const options = getContextMenuOptions(
104-
"/setup",
105-
"/setup",
106-
mockT,
107-
null,
108-
mockQueryItems,
109-
[],
110-
[],
111-
specialCommands,
112-
)
97+
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], specialCommands)
11398

11499
const setupDevOption = options.find((option) => option.value === "setup-dev")
115100
expect(setupDevOption).toBeDefined()
116-
expect(setupDevOption!.label).toBe("setup-dev")
101+
expect(setupDevOption!.slashCommand).toBe("/setup-dev")
117102
})
118103

119104
it("should handle case-insensitive fuzzy matching", () => {
120-
const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], mockCommands)
105+
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands)
121106

122107
const commandNames = options.map((option) => option.value)
123108
expect(commandNames).toContain("setup")
@@ -133,20 +118,20 @@ describe("Command Autocomplete", () => {
133118
const options = getContextMenuOptions(
134119
"/test",
135120
"/test",
136-
mockT,
137121
null,
138122
mockQueryItems,
139123
[],
140124
[],
141125
commandsWithSimilarNames,
142126
)
143127

144-
// 'test' should be first due to exact match
145-
expect(options[0].value).toBe("test")
128+
// Filter out section headers and check the first command
129+
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
130+
expect(commandOptions[0].value).toBe("test")
146131
})
147132

148133
it("should handle partial matches correctly", () => {
149-
const options = getContextMenuOptions("/te", "/te", mockT, null, mockQueryItems, [], [], mockCommands)
134+
const options = getContextMenuOptions("/te", "/te", null, mockQueryItems, [], [], mockCommands)
150135

151136
// Should match 'test-suite'
152137
const commandNames = options.map((option) => option.value)
@@ -173,7 +158,7 @@ describe("Command Autocomplete", () => {
173158
] as any[]
174159

175160
it("should return both modes and commands for slash commands", () => {
176-
const options = getContextMenuOptions("/", "/", mockT, null, mockQueryItems, [], mockModes, mockCommands)
161+
const options = getContextMenuOptions("/", "/", null, mockQueryItems, [], mockModes, mockCommands)
177162

178163
const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode)
179164
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
@@ -183,16 +168,7 @@ describe("Command Autocomplete", () => {
183168
})
184169

185170
it("should filter both modes and commands based on query", () => {
186-
const options = getContextMenuOptions(
187-
"/co",
188-
"/co",
189-
mockT,
190-
null,
191-
mockQueryItems,
192-
[],
193-
mockModes,
194-
mockCommands,
195-
)
171+
const options = getContextMenuOptions("/co", "/co", null, mockQueryItems, [], mockModes, mockCommands)
196172

197173
// Should match 'code' mode and possibly some commands (fuzzy search may match)
198174
const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode)
@@ -207,28 +183,30 @@ describe("Command Autocomplete", () => {
207183

208184
describe("command source indication", () => {
209185
it("should not expose source information in autocomplete", () => {
210-
const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], mockCommands)
186+
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands)
211187

212188
const setupOption = options.find((option) => option.value === "setup")
213189
expect(setupOption).toBeDefined()
214190

215191
// Source should not be exposed in the UI
216-
expect(setupOption!.description).not.toContain("project")
217-
expect(setupOption!.description).not.toContain("global")
218-
expect(setupOption!.description).toBe("Trigger the setup command")
192+
if (setupOption!.description) {
193+
expect(setupOption!.description).not.toContain("project")
194+
expect(setupOption!.description).not.toContain("global")
195+
expect(setupOption!.description).toBe("Trigger the setup command")
196+
}
219197
})
220198
})
221199

222200
describe("edge cases", () => {
223201
it("should handle undefined commands gracefully", () => {
224-
const options = getContextMenuOptions("/setup", "/setup", mockT, null, mockQueryItems, [], [], undefined)
202+
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], undefined)
225203

226204
expect(options).toHaveLength(1)
227205
expect(options[0].type).toBe(ContextMenuOptionType.NoResults)
228206
})
229207

230208
it("should handle empty query with commands", () => {
231-
const options = getContextMenuOptions("", "", mockT, null, mockQueryItems, [], [], mockCommands)
209+
const options = getContextMenuOptions("", "", null, mockQueryItems, [], [], mockCommands)
232210

233211
// Should not return command options for empty query
234212
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
@@ -240,19 +218,12 @@ describe("Command Autocomplete", () => {
240218
{ name: "very-long-command-name-that-exceeds-normal-length", source: "project" },
241219
]
242220

243-
const options = getContextMenuOptions(
244-
"/very",
245-
"/very",
246-
mockT,
247-
null,
248-
mockQueryItems,
249-
[],
250-
[],
251-
longNameCommands,
252-
)
221+
const options = getContextMenuOptions("/very", "/very", null, mockQueryItems, [], [], longNameCommands)
253222

254-
expect(options.length).toBe(1)
255-
expect(options[0].value).toBe("very-long-command-name-that-exceeds-normal-length")
223+
// Should have 2 items: 1 section header + 1 command
224+
expect(options.length).toBe(2)
225+
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
226+
expect(commandOptions[0].value).toBe("very-long-command-name-that-exceeds-normal-length")
256227
})
257228

258229
it("should handle commands with numeric names", () => {
@@ -262,7 +233,7 @@ describe("Command Autocomplete", () => {
262233
{ name: "123test", source: "project" },
263234
]
264235

265-
const options = getContextMenuOptions("/v", "/v", mockT, null, mockQueryItems, [], [], numericCommands)
236+
const options = getContextMenuOptions("/v", "/v", null, mockQueryItems, [], [], numericCommands)
266237

267238
const commandNames = options.map((option) => option.value)
268239
expect(commandNames).toContain("v2-setup")

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
396396
const options = getContextMenuOptions(
397397
searchQuery,
398398
inputValue,
399-
t,
400399
selectedType,
401400
queryItems,
402401
fileSearchResults,
@@ -435,7 +434,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
435434
const selectedOption = getContextMenuOptions(
436435
searchQuery,
437436
inputValue,
438-
t,
439437
selectedType,
440438
queryItems,
441439
fileSearchResults,
@@ -529,7 +527,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
529527
handleHistoryNavigation,
530528
resetHistoryNavigation,
531529
commands,
532-
t,
533530
],
534531
)
535532

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react"
22
import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons"
3-
import { useTranslation } from "react-i18next"
43

54
import type { ModeConfig } from "@roo-code/types"
65
import type { Command } from "@roo/ExtensionMessage"
@@ -43,20 +42,18 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
4342
}) => {
4443
const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("")
4544
const menuRef = useRef<HTMLDivElement>(null)
46-
const { t } = useTranslation()
4745

4846
const filteredOptions = useMemo(() => {
4947
return getContextMenuOptions(
5048
searchQuery,
5149
inputValue,
52-
t,
5350
selectedType,
5451
queryItems,
5552
dynamicSearchResults,
5653
modes,
5754
commands,
5855
)
59-
}, [searchQuery, inputValue, t, selectedType, queryItems, dynamicSearchResults, modes, commands])
56+
}, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes, commands])
6057

6158
useEffect(() => {
6259
if (menuRef.current) {
@@ -82,10 +79,25 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
8279

8380
const renderOptionContent = (option: ContextMenuQueryItem) => {
8481
switch (option.type) {
82+
case ContextMenuOptionType.SectionHeader:
83+
return (
84+
<span
85+
style={{
86+
fontWeight: "bold",
87+
fontSize: "0.85em",
88+
opacity: 0.8,
89+
textTransform: "uppercase",
90+
letterSpacing: "0.5px",
91+
}}>
92+
{option.label}
93+
</span>
94+
)
8595
case ContextMenuOptionType.Mode:
8696
return (
8797
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
88-
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
98+
<div style={{ lineHeight: "1.2" }}>
99+
<span>{option.slashCommand}</span>
100+
</div>
89101
{option.description && (
90102
<span
91103
style={{
@@ -104,7 +116,9 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
104116
case ContextMenuOptionType.Command:
105117
return (
106118
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
107-
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
119+
<div style={{ lineHeight: "1.2" }}>
120+
<span>{option.slashCommand}</span>
121+
</div>
108122
{option.description && (
109123
<span
110124
style={{
@@ -229,7 +243,11 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
229243
}
230244

231245
const isOptionSelectable = (option: ContextMenuQueryItem): boolean => {
232-
return option.type !== ContextMenuOptionType.NoResults && option.type !== ContextMenuOptionType.URL
246+
return (
247+
option.type !== ContextMenuOptionType.NoResults &&
248+
option.type !== ContextMenuOptionType.URL &&
249+
option.type !== ContextMenuOptionType.SectionHeader
250+
)
233251
}
234252

235253
return (
@@ -252,21 +270,30 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
252270
zIndex: 1000,
253271
display: "flex",
254272
flexDirection: "column",
255-
maxHeight: "200px",
273+
maxHeight: "300px",
256274
overflowY: "auto",
275+
overflowX: "hidden",
257276
}}>
258277
{filteredOptions && filteredOptions.length > 0 ? (
259278
filteredOptions.map((option, index) => (
260279
<div
261280
key={`${option.type}-${option.value || index}`}
262281
onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)}
263282
style={{
264-
padding: "4px 6px",
283+
padding:
284+
option.type === ContextMenuOptionType.SectionHeader ? "8px 6px 4px 6px" : "4px 6px",
265285
cursor: isOptionSelectable(option) ? "pointer" : "default",
266286
color: "var(--vscode-dropdown-foreground)",
267287
display: "flex",
268288
alignItems: "center",
269289
justifyContent: "space-between",
290+
position: "relative",
291+
...(option.type === ContextMenuOptionType.SectionHeader
292+
? {
293+
borderBottom: "1px solid var(--vscode-editorGroup-border)",
294+
marginBottom: "2px",
295+
}
296+
: {}),
270297
...(index === selectedIndex && isOptionSelectable(option)
271298
? {
272299
backgroundColor: "var(--vscode-list-activeSelectionBackground)",
@@ -283,6 +310,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
283310
minWidth: 0,
284311
overflow: "hidden",
285312
paddingTop: 0,
313+
position: "relative",
286314
}}>
287315
{(option.type === ContextMenuOptionType.File ||
288316
option.type === ContextMenuOptionType.Folder ||
@@ -299,9 +327,11 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
299327
/>
300328
)}
301329
{option.type !== ContextMenuOptionType.Mode &&
330+
option.type !== ContextMenuOptionType.Command &&
302331
option.type !== ContextMenuOptionType.File &&
303332
option.type !== ContextMenuOptionType.Folder &&
304333
option.type !== ContextMenuOptionType.OpenedFile &&
334+
option.type !== ContextMenuOptionType.SectionHeader &&
305335
getIconForOption(option) && (
306336
<i
307337
className={`codicon codicon-${getIconForOption(option)}`}

webview-ui/src/i18n/locales/ca/chat.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)