diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e72c1726f35..1b436bca7d7 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -598,9 +598,13 @@ export const ChatTextArea = forwardRef( setShowContextMenu(showMenu) if (showMenu) { - if (newValue.startsWith("/") && !newValue.includes(" ")) { - // Handle slash command - request fresh commands - const query = newValue + // Extract current line based on cursor position + const lineStart = newValue.lastIndexOf("\n", newCursorPosition - 1) + 1 + const currentLineBefore = newValue.slice(lineStart, newCursorPosition) + + if (currentLineBefore.startsWith("/") && !currentLineBefore.includes(" ")) { + // Handle slash command on current line - request fresh commands + const query = currentLineBefore setSearchQuery(query) // Set to first selectable item (skip section headers) setSelectedMenuIndex(1) // Section header is at 0, first command is at 1 diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index 48b03907c34..efc72aa7771 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -52,7 +52,7 @@ describe("insertMention", () => { it("should handle slash command replacement", () => { const result = insertMention("/mode some", 5, "code", true) // Simulating mode selection - expect(result.newValue).toBe("code") // Should replace the whole text + expect(result.newValue).toBe("code some") // Should replace slash token on current line, preserve rest expect(result.mentionIndex).toBe(0) }) @@ -106,18 +106,34 @@ describe("insertMention", () => { // --- Tests for isSlashCommand parameter --- describe("isSlashCommand parameter", () => { - it("should replace entire text when isSlashCommand is true", () => { + it("should replace slash token on current line when isSlashCommand is true", () => { const result = insertMention("/cod", 4, "code", true) expect(result.newValue).toBe("code") expect(result.mentionIndex).toBe(0) }) - it("should replace entire text even when @ mentions exist and isSlashCommand is true", () => { + it("should replace slash token but preserve text after cursor when isSlashCommand is true", () => { const result = insertMention("/code @some/file.ts", 5, "debug", true) - expect(result.newValue).toBe("debug") + expect(result.newValue).toBe("debug @some/file.ts") expect(result.mentionIndex).toBe(0) }) + it("should replace only current line slash token in multiline text", () => { + const text = "/code\n/deb" + const position = 10 // cursor at end of "/deb" + const result = insertMention(text, position, "debug", true) + expect(result.newValue).toBe("/code\ndebug") + expect(result.mentionIndex).toBe(6) // start of second line + }) + + it("should preserve other lines when inserting slash command on second line", () => { + const text = "/code\nsome text here\n/arc" + const position = 25 // cursor at end of "/arc" + const result = insertMention(text, position, "architect", true) + expect(result.newValue).toBe("/code\nsome text here\narchitect") + expect(result.mentionIndex).toBe(21) // start of third line + }) + it("should insert @ mention correctly after slash command when isSlashCommand is false", () => { const text = "/code @" const position = 8 // cursor after @ @@ -585,4 +601,29 @@ describe("shouldShowContextMenu", () => { // This case means the regex wouldn't match anyway, but confirms context menu logic expect(shouldShowContextMenu("@/path/with space", 13)).toBe(false) // Cursor after unescaped space }) + + // --- Multiline slash command tests --- + it("should return true for slash command on second line", () => { + const text = "/code\n/deb" + const position = 10 // cursor at end of "/deb" + expect(shouldShowContextMenu(text, position)).toBe(true) + }) + + it("should return true for slash command on third line", () => { + const text = "/code\nsome text\n/arc" + const position = 19 // cursor at end of "/arc" + expect(shouldShowContextMenu(text, position)).toBe(true) + }) + + it("should return false for non-slash text on second line without @", () => { + const text = "/code\nhello" + const position = 11 // cursor at end of "hello" + expect(shouldShowContextMenu(text, position)).toBe(false) + }) + + it("should return false for slash command with space on second line", () => { + const text = "/code\n/debug something" + const position = 13 // cursor after space in "/debug " + expect(shouldShowContextMenu(text, position)).toBe(false) + }) }) diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 22dba8864fc..40124212e91 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -32,9 +32,16 @@ export function insertMention( ): { newValue: string; mentionIndex: number } { // Handle slash command selection (only when explicitly selecting a slash command) if (isSlashCommand) { + const beforeCursor = text.slice(0, position) + const afterCursor = text.slice(position) + // Find the start of the current line + const currentLineStart = beforeCursor.lastIndexOf("\n") + 1 + const beforeLine = text.slice(0, currentLineStart) + // Replace slash command token on the current line (from line start to cursor) + const newValue = beforeLine + value + afterCursor return { - newValue: value, - mentionIndex: 0, + newValue, + mentionIndex: currentLineStart, } } @@ -367,8 +374,10 @@ export function getContextMenuOptions( export function shouldShowContextMenu(text: string, position: number): boolean { const beforeCursor = text.slice(0, position) - // Check if we're in a slash command context (at the beginning and no space yet) - if (text.startsWith("/") && !text.includes(" ") && position <= text.length) { + // Check if we're in a slash command context on the current line + const currentLineStart = beforeCursor.lastIndexOf("\n") + 1 + const currentLineBefore = beforeCursor.slice(currentLineStart) + if (currentLineBefore.startsWith("/") && !currentLineBefore.includes(" ")) { return true }