Skip to content

Commit 343ed6b

Browse files
committed
fix: enable slash command chaining on any line in multiline input
Previously, slash commands only worked on the first line of the textarea. This was because shouldShowContextMenu(), handleChange(), and insertMention() all checked against the full text value rather than the current line where the cursor is positioned. Changes: - shouldShowContextMenu: extract current line from cursor position and check if it starts with "/" instead of checking entire text - ChatTextArea handleChange: same current-line extraction for slash command detection and query building - insertMention: when isSlashCommand is true, replace only the slash token on the current line instead of the entire text content Closes #12028
1 parent 137d3f4 commit 343ed6b

File tree

3 files changed

+65
-11
lines changed

3 files changed

+65
-11
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -598,9 +598,13 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
598598
setShowContextMenu(showMenu)
599599

600600
if (showMenu) {
601-
if (newValue.startsWith("/") && !newValue.includes(" ")) {
602-
// Handle slash command - request fresh commands
603-
const query = newValue
601+
// Extract current line based on cursor position
602+
const lineStart = newValue.lastIndexOf("\n", newCursorPosition - 1) + 1
603+
const currentLineBefore = newValue.slice(lineStart, newCursorPosition)
604+
605+
if (currentLineBefore.startsWith("/") && !currentLineBefore.includes(" ")) {
606+
// Handle slash command on current line - request fresh commands
607+
const query = currentLineBefore
604608
setSearchQuery(query)
605609
// Set to first selectable item (skip section headers)
606610
setSelectedMenuIndex(1) // Section header is at 0, first command is at 1

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

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe("insertMention", () => {
5252

5353
it("should handle slash command replacement", () => {
5454
const result = insertMention("/mode some", 5, "code", true) // Simulating mode selection
55-
expect(result.newValue).toBe("code") // Should replace the whole text
55+
expect(result.newValue).toBe("code some") // Should replace slash token on current line, preserve rest
5656
expect(result.mentionIndex).toBe(0)
5757
})
5858

@@ -106,18 +106,34 @@ describe("insertMention", () => {
106106

107107
// --- Tests for isSlashCommand parameter ---
108108
describe("isSlashCommand parameter", () => {
109-
it("should replace entire text when isSlashCommand is true", () => {
109+
it("should replace slash token on current line when isSlashCommand is true", () => {
110110
const result = insertMention("/cod", 4, "code", true)
111111
expect(result.newValue).toBe("code")
112112
expect(result.mentionIndex).toBe(0)
113113
})
114114

115-
it("should replace entire text even when @ mentions exist and isSlashCommand is true", () => {
115+
it("should replace slash token but preserve text after cursor when isSlashCommand is true", () => {
116116
const result = insertMention("/code @some/file.ts", 5, "debug", true)
117-
expect(result.newValue).toBe("debug")
117+
expect(result.newValue).toBe("debug @some/file.ts")
118118
expect(result.mentionIndex).toBe(0)
119119
})
120120

121+
it("should replace only current line slash token in multiline text", () => {
122+
const text = "/code\n/deb"
123+
const position = 10 // cursor at end of "/deb"
124+
const result = insertMention(text, position, "debug", true)
125+
expect(result.newValue).toBe("/code\ndebug")
126+
expect(result.mentionIndex).toBe(6) // start of second line
127+
})
128+
129+
it("should preserve other lines when inserting slash command on second line", () => {
130+
const text = "/code\nsome text here\n/arc"
131+
const position = 25 // cursor at end of "/arc"
132+
const result = insertMention(text, position, "architect", true)
133+
expect(result.newValue).toBe("/code\nsome text here\narchitect")
134+
expect(result.mentionIndex).toBe(21) // start of third line
135+
})
136+
121137
it("should insert @ mention correctly after slash command when isSlashCommand is false", () => {
122138
const text = "/code @"
123139
const position = 8 // cursor after @
@@ -585,4 +601,29 @@ describe("shouldShowContextMenu", () => {
585601
// This case means the regex wouldn't match anyway, but confirms context menu logic
586602
expect(shouldShowContextMenu("@/path/with space", 13)).toBe(false) // Cursor after unescaped space
587603
})
604+
605+
// --- Multiline slash command tests ---
606+
it("should return true for slash command on second line", () => {
607+
const text = "/code\n/deb"
608+
const position = 10 // cursor at end of "/deb"
609+
expect(shouldShowContextMenu(text, position)).toBe(true)
610+
})
611+
612+
it("should return true for slash command on third line", () => {
613+
const text = "/code\nsome text\n/arc"
614+
const position = 19 // cursor at end of "/arc"
615+
expect(shouldShowContextMenu(text, position)).toBe(true)
616+
})
617+
618+
it("should return false for non-slash text on second line without @", () => {
619+
const text = "/code\nhello"
620+
const position = 11 // cursor at end of "hello"
621+
expect(shouldShowContextMenu(text, position)).toBe(false)
622+
})
623+
624+
it("should return false for slash command with space on second line", () => {
625+
const text = "/code\n/debug something"
626+
const position = 13 // cursor after space in "/debug "
627+
expect(shouldShowContextMenu(text, position)).toBe(false)
628+
})
588629
})

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,16 @@ export function insertMention(
3232
): { newValue: string; mentionIndex: number } {
3333
// Handle slash command selection (only when explicitly selecting a slash command)
3434
if (isSlashCommand) {
35+
const beforeCursor = text.slice(0, position)
36+
const afterCursor = text.slice(position)
37+
// Find the start of the current line
38+
const currentLineStart = beforeCursor.lastIndexOf("\n") + 1
39+
const beforeLine = text.slice(0, currentLineStart)
40+
// Replace slash command token on the current line (from line start to cursor)
41+
const newValue = beforeLine + value + afterCursor
3542
return {
36-
newValue: value,
37-
mentionIndex: 0,
43+
newValue,
44+
mentionIndex: currentLineStart,
3845
}
3946
}
4047

@@ -367,8 +374,10 @@ export function getContextMenuOptions(
367374
export function shouldShowContextMenu(text: string, position: number): boolean {
368375
const beforeCursor = text.slice(0, position)
369376

370-
// Check if we're in a slash command context (at the beginning and no space yet)
371-
if (text.startsWith("/") && !text.includes(" ") && position <= text.length) {
377+
// Check if we're in a slash command context on the current line
378+
const currentLineStart = beforeCursor.lastIndexOf("\n") + 1
379+
const currentLineBefore = beforeCursor.slice(currentLineStart)
380+
if (currentLineBefore.startsWith("/") && !currentLineBefore.includes(" ")) {
372381
return true
373382
}
374383

0 commit comments

Comments
 (0)