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
This commit is contained in:
Roo Code 2026-03-29 00:30:10 +00:00
parent 137d3f4fd8
commit 343ed6be42
3 changed files with 65 additions and 11 deletions

View file

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

View file

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

View file

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