Use autocomplete instead for switching modes

This commit is contained in:
Matt Rubens 2025-02-04 10:07:38 -05:00
parent 5297cad5e2
commit ebd9084e56
4 changed files with 127 additions and 21 deletions

View file

@ -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>
)}

View file

@ -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

View file

@ -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 ||

View file

@ -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("@")