Просмотр исходного кода

Use autocomplete instead for switching modes

Matt Rubens 11 месяцев назад
Родитель
Сommit
ebd9084e56

+ 39 - 9
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -179,6 +179,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					return
 				}
 
+				if (type === ContextMenuOptionType.Mode && value) {
+					// Handle mode selection
+					setMode(value)
+					setInputValue("")
+					setShowContextMenu(false)
+					vscode.postMessage({
+						type: "mode",
+						text: value,
+					})
+					return
+				}
+
 				if (
 					type === ContextMenuOptionType.File ||
 					type === ContextMenuOptionType.Folder ||
@@ -242,7 +254,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						event.preventDefault()
 						setSelectedMenuIndex((prevIndex) => {
 							const direction = event.key === "ArrowUp" ? -1 : 1
-							const options = getContextMenuOptions(searchQuery, selectedType, queryItems)
+							const options = getContextMenuOptions(
+								searchQuery,
+								selectedType,
+								queryItems,
+								getAllModes(customModes),
+							)
 							const optionsLength = options.length
 
 							if (optionsLength === 0) return prevIndex
@@ -272,9 +289,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					}
 					if ((event.key === "Enter" || event.key === "Tab") && selectedMenuIndex !== -1) {
 						event.preventDefault()
-						const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[
-							selectedMenuIndex
-						]
+						const selectedOption = getContextMenuOptions(
+							searchQuery,
+							selectedType,
+							queryItems,
+							getAllModes(customModes),
+						)[selectedMenuIndex]
 						if (
 							selectedOption &&
 							selectedOption.type !== ContextMenuOptionType.URL &&
@@ -340,6 +360,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				setInputValue,
 				justDeletedSpaceAfterMention,
 				queryItems,
+				customModes,
 			],
 		)
 
@@ -360,13 +381,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 				setShowContextMenu(showMenu)
 				if (showMenu) {
-					const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
-					const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
-					setSearchQuery(query)
-					if (query.length > 0) {
+					if (newValue.startsWith("/")) {
+						// Handle slash command
+						const query = newValue
+						setSearchQuery(query)
 						setSelectedMenuIndex(0)
 					} else {
-						setSelectedMenuIndex(3) // Set to "File" option by default
+						// Existing @ mention handling
+						const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
+						const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
+						setSearchQuery(query)
+						if (query.length > 0) {
+							setSelectedMenuIndex(0)
+						} else {
+							setSelectedMenuIndex(3) // Set to "File" option by default
+						}
 					}
 				} else {
 					setSearchQuery("")
@@ -614,6 +643,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							setSelectedIndex={setSelectedMenuIndex}
 							selectedType={selectedType}
 							queryItems={queryItems}
+							modes={getAllModes(customModes)}
 						/>
 					</div>
 				)}

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

@@ -878,7 +878,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 
 	const placeholderText = useMemo(() => {
 		const baseText = task ? "Type a message..." : "Type your task here..."
-		const contextText = "(@ to add context"
+		const contextText = "(@ to add context, / to switch modes"
 		const imageText = shouldDisableImages ? "" : ", hold shift to drag in images"
 		const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})`
 		return baseText + helpText

+ 37 - 11
webview-ui/src/components/chat/ContextMenu.tsx

@@ -1,6 +1,7 @@
 import React, { useEffect, useMemo, useRef } from "react"
 import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../../utils/context-mentions"
 import { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
+import { ModeConfig } from "../../../../src/shared/modes"
 
 interface ContextMenuProps {
 	onSelect: (type: ContextMenuOptionType, value?: string) => void
@@ -10,6 +11,7 @@ interface ContextMenuProps {
 	setSelectedIndex: (index: number) => void
 	selectedType: ContextMenuOptionType | null
 	queryItems: ContextMenuQueryItem[]
+	modes?: ModeConfig[]
 }
 
 const ContextMenu: React.FC<ContextMenuProps> = ({
@@ -20,12 +22,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 	setSelectedIndex,
 	selectedType,
 	queryItems,
+	modes,
 }) => {
 	const menuRef = useRef<HTMLDivElement>(null)
 
 	const filteredOptions = useMemo(
-		() => getContextMenuOptions(searchQuery, selectedType, queryItems),
-		[searchQuery, selectedType, queryItems],
+		() => getContextMenuOptions(searchQuery, selectedType, queryItems, modes),
+		[searchQuery, selectedType, queryItems, modes],
 	)
 
 	useEffect(() => {
@@ -46,6 +49,25 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 
 	const renderOptionContent = (option: ContextMenuQueryItem) => {
 		switch (option.type) {
+			case ContextMenuOptionType.Mode:
+				return (
+					<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
+						<span style={{ lineHeight: "1.2" }}>{option.label}</span>
+						{option.description && (
+							<span
+								style={{
+									opacity: 0.5,
+									fontSize: "0.9em",
+									lineHeight: "1.2",
+									whiteSpace: "nowrap",
+									overflow: "hidden",
+									textOverflow: "ellipsis",
+								}}>
+								{option.description}
+							</span>
+						)}
+					</div>
+				)
 			case ContextMenuOptionType.Problems:
 				return <span>Problems</span>
 			case ContextMenuOptionType.URL:
@@ -101,6 +123,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 
 	const getIconForOption = (option: ContextMenuQueryItem): string => {
 		switch (option.type) {
+			case ContextMenuOptionType.Mode:
+				return "symbol-misc"
 			case ContextMenuOptionType.OpenedFile:
 				return "window"
 			case ContextMenuOptionType.File:
@@ -174,15 +198,17 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 								overflow: "hidden",
 								paddingTop: 0,
 							}}>
-							<i
-								className={`codicon codicon-${getIconForOption(option)}`}
-								style={{
-									marginRight: "6px",
-									flexShrink: 0,
-									fontSize: "14px",
-									marginTop: 0,
-								}}
-							/>
+							{option.type !== ContextMenuOptionType.Mode && getIconForOption(option) && (
+								<i
+									className={`codicon codicon-${getIconForOption(option)}`}
+									style={{
+										marginRight: "6px",
+										flexShrink: 0,
+										fontSize: "14px",
+										marginTop: 0,
+									}}
+								/>
+							)}
 							{renderOptionContent(option)}
 						</div>
 						{(option.type === ContextMenuOptionType.File ||

+ 50 - 0
webview-ui/src/utils/context-mentions.ts

@@ -1,11 +1,20 @@
 import { mentionRegex } from "../../../src/shared/context-mentions"
 import { Fzf } from "fzf"
+import { ModeConfig } from "../../../src/shared/modes"
 
 export function insertMention(
 	text: string,
 	position: number,
 	value: string,
 ): { newValue: string; mentionIndex: number } {
+	// Handle slash command
+	if (text.startsWith("/")) {
+		return {
+			newValue: value,
+			mentionIndex: 0,
+		}
+	}
+
 	const beforeCursor = text.slice(0, position)
 	const afterCursor = text.slice(position)
 
@@ -55,6 +64,7 @@ export enum ContextMenuOptionType {
 	URL = "url",
 	Git = "git",
 	NoResults = "noResults",
+	Mode = "mode", // Add mode type
 }
 
 export interface ContextMenuQueryItem {
@@ -69,7 +79,42 @@ export function getContextMenuOptions(
 	query: string,
 	selectedType: ContextMenuOptionType | null = null,
 	queryItems: ContextMenuQueryItem[],
+	modes?: ModeConfig[],
 ): ContextMenuQueryItem[] {
+	// Handle slash commands for modes
+	if (query.startsWith("/")) {
+		const modeQuery = query.slice(1)
+		if (!modes?.length) return [{ type: ContextMenuOptionType.NoResults }]
+
+		// Create searchable strings array for fzf
+		const searchableItems = modes.map((mode) => ({
+			original: mode,
+			searchStr: mode.name,
+		}))
+
+		// Initialize fzf instance for fuzzy search
+		const fzf = new Fzf(searchableItems, {
+			selector: (item) => item.searchStr,
+		})
+
+		// Get fuzzy matching items
+		const matchingModes = modeQuery
+			? fzf.find(modeQuery).map((result) => ({
+					type: ContextMenuOptionType.Mode,
+					value: result.item.original.slug,
+					label: result.item.original.name,
+					description: result.item.original.roleDefinition.split("\n")[0],
+				}))
+			: modes.map((mode) => ({
+					type: ContextMenuOptionType.Mode,
+					value: mode.slug,
+					label: mode.name,
+					description: mode.roleDefinition.split("\n")[0],
+				}))
+
+		return matchingModes.length > 0 ? matchingModes : [{ type: ContextMenuOptionType.NoResults }]
+	}
+
 	const workingChanges: ContextMenuQueryItem = {
 		type: ContextMenuOptionType.Git,
 		value: "git-changes",
@@ -203,6 +248,11 @@ export function getContextMenuOptions(
 }
 
 export function shouldShowContextMenu(text: string, position: number): boolean {
+	// Handle slash command
+	if (text.startsWith("/")) {
+		return position <= text.length && !text.includes(" ")
+	}
+
 	const beforeCursor = text.slice(0, position)
 	const atIndex = beforeCursor.lastIndexOf("@")