Skip to content

Commit ebd9084

Browse files
committed
Use autocomplete instead for switching modes
1 parent 5297cad commit ebd9084

File tree

4 files changed

+127
-21
lines changed

4 files changed

+127
-21
lines changed

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
179179
return
180180
}
181181

182+
if (type === ContextMenuOptionType.Mode && value) {
183+
// Handle mode selection
184+
setMode(value)
185+
setInputValue("")
186+
setShowContextMenu(false)
187+
vscode.postMessage({
188+
type: "mode",
189+
text: value,
190+
})
191+
return
192+
}
193+
182194
if (
183195
type === ContextMenuOptionType.File ||
184196
type === ContextMenuOptionType.Folder ||
@@ -242,7 +254,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
242254
event.preventDefault()
243255
setSelectedMenuIndex((prevIndex) => {
244256
const direction = event.key === "ArrowUp" ? -1 : 1
245-
const options = getContextMenuOptions(searchQuery, selectedType, queryItems)
257+
const options = getContextMenuOptions(
258+
searchQuery,
259+
selectedType,
260+
queryItems,
261+
getAllModes(customModes),
262+
)
246263
const optionsLength = options.length
247264

248265
if (optionsLength === 0) return prevIndex
@@ -272,9 +289,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
272289
}
273290
if ((event.key === "Enter" || event.key === "Tab") && selectedMenuIndex !== -1) {
274291
event.preventDefault()
275-
const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[
276-
selectedMenuIndex
277-
]
292+
const selectedOption = getContextMenuOptions(
293+
searchQuery,
294+
selectedType,
295+
queryItems,
296+
getAllModes(customModes),
297+
)[selectedMenuIndex]
278298
if (
279299
selectedOption &&
280300
selectedOption.type !== ContextMenuOptionType.URL &&
@@ -340,6 +360,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
340360
setInputValue,
341361
justDeletedSpaceAfterMention,
342362
queryItems,
363+
customModes,
343364
],
344365
)
345366

@@ -360,13 +381,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
360381

361382
setShowContextMenu(showMenu)
362383
if (showMenu) {
363-
const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
364-
const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
365-
setSearchQuery(query)
366-
if (query.length > 0) {
384+
if (newValue.startsWith("/")) {
385+
// Handle slash command
386+
const query = newValue
387+
setSearchQuery(query)
367388
setSelectedMenuIndex(0)
368389
} else {
369-
setSelectedMenuIndex(3) // Set to "File" option by default
390+
// Existing @ mention handling
391+
const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
392+
const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
393+
setSearchQuery(query)
394+
if (query.length > 0) {
395+
setSelectedMenuIndex(0)
396+
} else {
397+
setSelectedMenuIndex(3) // Set to "File" option by default
398+
}
370399
}
371400
} else {
372401
setSearchQuery("")
@@ -614,6 +643,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
614643
setSelectedIndex={setSelectedMenuIndex}
615644
selectedType={selectedType}
616645
queryItems={queryItems}
646+
modes={getAllModes(customModes)}
617647
/>
618648
</div>
619649
)}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
878878

879879
const placeholderText = useMemo(() => {
880880
const baseText = task ? "Type a message..." : "Type your task here..."
881-
const contextText = "(@ to add context"
881+
const contextText = "(@ to add context, / to switch modes"
882882
const imageText = shouldDisableImages ? "" : ", hold shift to drag in images"
883883
const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})`
884884
return baseText + helpText

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

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useMemo, useRef } from "react"
22
import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../../utils/context-mentions"
33
import { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
4+
import { ModeConfig } from "../../../../src/shared/modes"
45

56
interface ContextMenuProps {
67
onSelect: (type: ContextMenuOptionType, value?: string) => void
@@ -10,6 +11,7 @@ interface ContextMenuProps {
1011
setSelectedIndex: (index: number) => void
1112
selectedType: ContextMenuOptionType | null
1213
queryItems: ContextMenuQueryItem[]
14+
modes?: ModeConfig[]
1315
}
1416

1517
const ContextMenu: React.FC<ContextMenuProps> = ({
@@ -20,12 +22,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
2022
setSelectedIndex,
2123
selectedType,
2224
queryItems,
25+
modes,
2326
}) => {
2427
const menuRef = useRef<HTMLDivElement>(null)
2528

2629
const filteredOptions = useMemo(
27-
() => getContextMenuOptions(searchQuery, selectedType, queryItems),
28-
[searchQuery, selectedType, queryItems],
30+
() => getContextMenuOptions(searchQuery, selectedType, queryItems, modes),
31+
[searchQuery, selectedType, queryItems, modes],
2932
)
3033

3134
useEffect(() => {
@@ -46,6 +49,25 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
4649

4750
const renderOptionContent = (option: ContextMenuQueryItem) => {
4851
switch (option.type) {
52+
case ContextMenuOptionType.Mode:
53+
return (
54+
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
55+
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
56+
{option.description && (
57+
<span
58+
style={{
59+
opacity: 0.5,
60+
fontSize: "0.9em",
61+
lineHeight: "1.2",
62+
whiteSpace: "nowrap",
63+
overflow: "hidden",
64+
textOverflow: "ellipsis",
65+
}}>
66+
{option.description}
67+
</span>
68+
)}
69+
</div>
70+
)
4971
case ContextMenuOptionType.Problems:
5072
return <span>Problems</span>
5173
case ContextMenuOptionType.URL:
@@ -101,6 +123,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
101123

102124
const getIconForOption = (option: ContextMenuQueryItem): string => {
103125
switch (option.type) {
126+
case ContextMenuOptionType.Mode:
127+
return "symbol-misc"
104128
case ContextMenuOptionType.OpenedFile:
105129
return "window"
106130
case ContextMenuOptionType.File:
@@ -174,15 +198,17 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
174198
overflow: "hidden",
175199
paddingTop: 0,
176200
}}>
177-
<i
178-
className={`codicon codicon-${getIconForOption(option)}`}
179-
style={{
180-
marginRight: "6px",
181-
flexShrink: 0,
182-
fontSize: "14px",
183-
marginTop: 0,
184-
}}
185-
/>
201+
{option.type !== ContextMenuOptionType.Mode && getIconForOption(option) && (
202+
<i
203+
className={`codicon codicon-${getIconForOption(option)}`}
204+
style={{
205+
marginRight: "6px",
206+
flexShrink: 0,
207+
fontSize: "14px",
208+
marginTop: 0,
209+
}}
210+
/>
211+
)}
186212
{renderOptionContent(option)}
187213
</div>
188214
{(option.type === ContextMenuOptionType.File ||

webview-ui/src/utils/context-mentions.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import { mentionRegex } from "../../../src/shared/context-mentions"
22
import { Fzf } from "fzf"
3+
import { ModeConfig } from "../../../src/shared/modes"
34

45
export function insertMention(
56
text: string,
67
position: number,
78
value: string,
89
): { newValue: string; mentionIndex: number } {
10+
// Handle slash command
11+
if (text.startsWith("/")) {
12+
return {
13+
newValue: value,
14+
mentionIndex: 0,
15+
}
16+
}
17+
918
const beforeCursor = text.slice(0, position)
1019
const afterCursor = text.slice(position)
1120

@@ -55,6 +64,7 @@ export enum ContextMenuOptionType {
5564
URL = "url",
5665
Git = "git",
5766
NoResults = "noResults",
67+
Mode = "mode", // Add mode type
5868
}
5969

6070
export interface ContextMenuQueryItem {
@@ -69,7 +79,42 @@ export function getContextMenuOptions(
6979
query: string,
7080
selectedType: ContextMenuOptionType | null = null,
7181
queryItems: ContextMenuQueryItem[],
82+
modes?: ModeConfig[],
7283
): ContextMenuQueryItem[] {
84+
// Handle slash commands for modes
85+
if (query.startsWith("/")) {
86+
const modeQuery = query.slice(1)
87+
if (!modes?.length) return [{ type: ContextMenuOptionType.NoResults }]
88+
89+
// Create searchable strings array for fzf
90+
const searchableItems = modes.map((mode) => ({
91+
original: mode,
92+
searchStr: mode.name,
93+
}))
94+
95+
// Initialize fzf instance for fuzzy search
96+
const fzf = new Fzf(searchableItems, {
97+
selector: (item) => item.searchStr,
98+
})
99+
100+
// Get fuzzy matching items
101+
const matchingModes = modeQuery
102+
? fzf.find(modeQuery).map((result) => ({
103+
type: ContextMenuOptionType.Mode,
104+
value: result.item.original.slug,
105+
label: result.item.original.name,
106+
description: result.item.original.roleDefinition.split("\n")[0],
107+
}))
108+
: modes.map((mode) => ({
109+
type: ContextMenuOptionType.Mode,
110+
value: mode.slug,
111+
label: mode.name,
112+
description: mode.roleDefinition.split("\n")[0],
113+
}))
114+
115+
return matchingModes.length > 0 ? matchingModes : [{ type: ContextMenuOptionType.NoResults }]
116+
}
117+
73118
const workingChanges: ContextMenuQueryItem = {
74119
type: ContextMenuOptionType.Git,
75120
value: "git-changes",
@@ -203,6 +248,11 @@ export function getContextMenuOptions(
203248
}
204249

205250
export function shouldShowContextMenu(text: string, position: number): boolean {
251+
// Handle slash command
252+
if (text.startsWith("/")) {
253+
return position <= text.length && !text.includes(" ")
254+
}
255+
206256
const beforeCursor = text.slice(0, position)
207257
const atIndex = beforeCursor.lastIndexOf("@")
208258

0 commit comments

Comments
 (0)