Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -598,9 +598,13 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
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
Expand Down
49 changes: 45 additions & 4 deletions webview-ui/src/utils/__tests__/context-mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down Expand Up @@ -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 @
Expand Down Expand Up @@ -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)
})
})
17 changes: 13 additions & 4 deletions webview-ui/src/utils/context-mentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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
}

Expand Down
Loading