mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 04:46:43 +08:00
fix(webview): preserve existing text when selecting slash command at start (#11728)
* fix(webview): preserve existing text when selecting slash command at input start * remove unwanted check not related to pr * fix(webview): update test mocks for new select() cursor usage and add trailing text tests * chore: add changeset for slash command trailing text fix * fix(webview): add slashEnd signal to fix cursor-corruption bug when selecting slash commands * chore(test): fix redundant double type assertion in use-slash-command.test.ts * address the pr review comments * fix(test): use full text length as onInput cursor in review slash test * fix: preserve trailing text on no-argument /memory commands * refactor: drop review commit and review pr slash suggestions * fix(webview): place caret at start when preserving trailing text for action commands * fix(kilo-memory): preserve trailing text for /memory show like other no-arg commands * fix(kilo-vscode): update mirrored /memory show fixture for rest field * fix(kilo-vscode): format prompt-input-utils.ts to satisfy prettier * fix(kilo-vscode): place caret at start for preserved /memory text
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Typing extra text after the `/memory show` command (for example `/memory show my notes`) used to clear that text from the chat input when the command ran. The text now stays in the input so you don't lose what you typed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve existing text when selecting a slash command at the start of the chat input. Previously, typing `/new` or another slash command with text before the cursor would wipe the existing content entirely.
|
||||
@@ -38,6 +38,7 @@ type Help = {
|
||||
|
||||
type Show = {
|
||||
kind: "show"
|
||||
rest?: string
|
||||
}
|
||||
|
||||
type Operation =
|
||||
@@ -64,6 +65,7 @@ type Operation =
|
||||
| {
|
||||
kind: "operation"
|
||||
operation: Exclude<MemoryOperation, "remember" | "correct" | "forget" | "purge" | "auto">
|
||||
rest?: string
|
||||
}
|
||||
|
||||
type Usage = {
|
||||
@@ -93,10 +95,10 @@ function usage(reason: string): ParsedMemoryCommand {
|
||||
}
|
||||
|
||||
function operation(verb: string, text: string): ParsedMemoryCommand | undefined {
|
||||
if (verb === "on" || verb === "enable") return { kind: "operation", operation: "enable" }
|
||||
if (verb === "off" || verb === "disable") return { kind: "operation", operation: "disable" }
|
||||
if (verb === "on" || verb === "enable") return { kind: "operation", operation: "enable", rest: text }
|
||||
if (verb === "off" || verb === "disable") return { kind: "operation", operation: "disable", rest: text }
|
||||
if (verb === "status" || verb === "inspect" || verb === "rebuild") {
|
||||
return { kind: "operation", operation: verb }
|
||||
return { kind: "operation", operation: verb, rest: text }
|
||||
}
|
||||
if (verb === "purge") {
|
||||
if (text.toLowerCase() === "confirm") return { kind: "operation", operation: "purge", confirm: true }
|
||||
@@ -138,7 +140,7 @@ export function parseMemoryCommand(input: string): ParsedMemoryCommand | undefin
|
||||
const parts = split(picked.rest)
|
||||
const verb = parts.head
|
||||
if (!verb) return { kind: "help" }
|
||||
if (verb === "show") return { kind: "show" }
|
||||
if (verb === "show") return { kind: "show", rest: parts.tail }
|
||||
|
||||
const op = operation(verb, parts.tail)
|
||||
if (op) return op
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
"input": "/memory project show",
|
||||
"result": "show"
|
||||
},
|
||||
{
|
||||
"name": "show preserves trailing text",
|
||||
"input": "/memory show draft notes",
|
||||
"result": "show",
|
||||
"rest": "draft notes"
|
||||
},
|
||||
{
|
||||
"name": "status operation",
|
||||
"input": "/memory status",
|
||||
@@ -61,6 +67,27 @@
|
||||
"result": "operation",
|
||||
"operation": "rebuild"
|
||||
},
|
||||
{
|
||||
"name": "rebuild keeps trailing text",
|
||||
"input": "/memory rebuild hello",
|
||||
"result": "operation",
|
||||
"operation": "rebuild",
|
||||
"rest": "hello"
|
||||
},
|
||||
{
|
||||
"name": "enable keeps trailing text",
|
||||
"input": "/memory on hello",
|
||||
"result": "operation",
|
||||
"operation": "enable",
|
||||
"rest": "hello"
|
||||
},
|
||||
{
|
||||
"name": "project scope rebuild keeps trailing text",
|
||||
"input": "/memory project rebuild hello",
|
||||
"result": "operation",
|
||||
"operation": "rebuild",
|
||||
"rest": "hello"
|
||||
},
|
||||
{
|
||||
"name": "purge requires confirmation",
|
||||
"input": "/memory purge",
|
||||
|
||||
@@ -11,6 +11,7 @@ type Case = {
|
||||
text?: string
|
||||
query?: string
|
||||
reason?: string
|
||||
rest?: string
|
||||
}
|
||||
|
||||
const cases = (await Bun.file(new URL("./command-cases.json", import.meta.url)).json()) as Case[]
|
||||
@@ -18,7 +19,7 @@ const cases = (await Bun.file(new URL("./command-cases.json", import.meta.url)).
|
||||
function expected(item: Case): ParsedMemoryCommand | undefined {
|
||||
if (item.result === "none") return
|
||||
if (item.result === "help") return { kind: "help" }
|
||||
if (item.result === "show") return { kind: "show" }
|
||||
if (item.result === "show") return { kind: "show", rest: item.rest ?? "" }
|
||||
if (item.result === "usage") return { kind: "usage", reason: item.reason ?? "" }
|
||||
if (!item.operation) throw new Error(`Missing operation for fixture: ${item.name}`)
|
||||
if (item.operation === "remember" || item.operation === "correct") {
|
||||
@@ -37,7 +38,7 @@ function expected(item: Case): ParsedMemoryCommand | undefined {
|
||||
if (item.confirm !== true) throw new Error(`Missing confirmation for fixture: ${item.name}`)
|
||||
return { kind: "operation", operation: item.operation, confirm: true }
|
||||
}
|
||||
return { kind: "operation", operation: item.operation }
|
||||
return { kind: "operation", operation: item.operation, rest: item.rest ?? "" }
|
||||
}
|
||||
|
||||
describe("memory commands", () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ type Case = {
|
||||
text?: string
|
||||
query?: string
|
||||
reason?: string
|
||||
rest?: string
|
||||
}
|
||||
|
||||
const cases = (await Bun.file(
|
||||
@@ -31,7 +32,7 @@ const cases = (await Bun.file(
|
||||
function expected(item: Case): ParsedMemoryCommand | undefined {
|
||||
if (item.result === "none") return
|
||||
if (item.result === "help") return { kind: "help" }
|
||||
if (item.result === "show") return { kind: "show" }
|
||||
if (item.result === "show") return { kind: "show", rest: item.rest ?? "" }
|
||||
if (item.result === "usage") return { kind: "usage", reason: item.reason ?? "" }
|
||||
if (!item.operation) throw new Error(`Missing operation for fixture: ${item.name}`)
|
||||
if (item.operation === "remember" || item.operation === "correct") {
|
||||
@@ -50,7 +51,7 @@ function expected(item: Case): ParsedMemoryCommand | undefined {
|
||||
if (item.confirm !== true) throw new Error(`Missing confirmation for fixture: ${item.name}`)
|
||||
return { kind: "operation", operation: item.operation, confirm: true }
|
||||
}
|
||||
return { kind: "operation", operation: item.operation }
|
||||
return { kind: "operation", operation: item.operation, rest: item.rest ?? "" }
|
||||
}
|
||||
|
||||
describe("parseMemoryCommand", () => {
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
isPathMention,
|
||||
applySandboxState,
|
||||
applySandboxStates,
|
||||
memoryRest,
|
||||
} from "../../webview-ui/src/components/chat/prompt-input-utils"
|
||||
import { parseMemoryCommand } from "../../webview-ui/src/utils/memory-command"
|
||||
|
||||
describe("applySandboxState", () => {
|
||||
const state = (enabled: boolean, revision: number, sessionID = "ses_1", directory = "/repo") => ({
|
||||
@@ -329,3 +331,33 @@ describe("isPathMention", () => {
|
||||
expect(isPathMention("src/foo.ts")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("memoryRest", () => {
|
||||
it("keeps trailing text in the input after a no-argument memory command", () => {
|
||||
// /memory rebuild hello -> rebuild executes, "hello" stays in the input.
|
||||
// This is the submit-path half of the trailing-text bug: handleSend sets
|
||||
// the input to memoryRest(parsed), so a regression would drop "hello".
|
||||
const memory = parseMemoryCommand("/memory rebuild hello")
|
||||
expect(memory).not.toBeUndefined()
|
||||
expect(memoryRest(memory!)).toBe("hello")
|
||||
})
|
||||
|
||||
it("keeps trailing text through the project scope", () => {
|
||||
expect(memoryRest(parseMemoryCommand("/memory project rebuild hello")!)).toBe("hello")
|
||||
})
|
||||
|
||||
it("returns empty string when a no-argument command has no trailing text", () => {
|
||||
expect(memoryRest(parseMemoryCommand("/memory rebuild")!)).toBe("")
|
||||
})
|
||||
|
||||
it("keeps trailing text in the input after the show command", () => {
|
||||
// /memory show draft notes -> show executes, "draft notes" stays in the input.
|
||||
expect(memoryRest(parseMemoryCommand("/memory show draft notes")!)).toBe("draft notes")
|
||||
})
|
||||
|
||||
it("returns empty string for argument-taking operations", () => {
|
||||
// remember/correct/forget/auto/purge consume their text, so nothing remains.
|
||||
expect(memoryRest(parseMemoryCommand("/memory remember hello")!)).toBe("")
|
||||
expect(memoryRest(parseMemoryCommand("/memory auto on")!)).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,14 +60,14 @@ describe("useSlashCommand sandbox action", () => {
|
||||
|
||||
it("opens project memory actions from the top-level command", () => {
|
||||
const ctx = setup(() => {})
|
||||
const state = { text: "/memory" }
|
||||
const state = { text: "/mem" }
|
||||
const textarea = {
|
||||
value: state.text,
|
||||
setSelectionRange: () => {},
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
|
||||
ctx.slash.onInput("/mem", 4)
|
||||
ctx.slash.onInput(state.text, state.text.length)
|
||||
|
||||
expect(ctx.slash.results()).toContainEqual(
|
||||
expect.objectContaining({ name: "memory", description: "Manage project memory", hints: ["mem"] }),
|
||||
@@ -131,7 +131,11 @@ describe("useSlashCommand sandbox action", () => {
|
||||
it("runs the sandbox toggle as a client command", () => {
|
||||
const state = { toggles: 0, text: "/sandbox", prevented: 0 }
|
||||
const ctx = setup(() => state.toggles++)
|
||||
const textarea = { value: state.text } as HTMLTextAreaElement
|
||||
const textarea = {
|
||||
value: state.text,
|
||||
selectionStart: state.text.length,
|
||||
setSelectionRange: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const event = {
|
||||
key: "Enter",
|
||||
isComposing: false,
|
||||
@@ -153,7 +157,11 @@ describe("useSlashCommand sandbox action", () => {
|
||||
it("keeps the command text when the sandbox control is disabled", () => {
|
||||
const state = { toggles: 0, text: "/sandbox" }
|
||||
const ctx = setup(() => state.toggles++, { enabled: () => false })
|
||||
const textarea = { value: state.text } as HTMLTextAreaElement
|
||||
const textarea = {
|
||||
value: state.text,
|
||||
selectionStart: state.text.length,
|
||||
setSelectionRange: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const event = {
|
||||
key: "Enter",
|
||||
isComposing: false,
|
||||
@@ -198,7 +206,7 @@ describe("useSlashCommand sandbox action", () => {
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
|
||||
ctx.slash.onInput("/rev", 4)
|
||||
ctx.slash.onInput(state.text, state.text.length)
|
||||
|
||||
expect(ctx.slash.results()).toContainEqual(
|
||||
expect.objectContaining({ name: "review", description: expect.stringContaining("Review code changes") }),
|
||||
@@ -261,3 +269,223 @@ describe("useSlashCommand sandbox action", () => {
|
||||
ctx.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe("select", () => {
|
||||
it("preserves trailing text for action commands", () => {
|
||||
let actionCalls = 0
|
||||
let currentText = "existing text"
|
||||
const ctx = setup(() => {})
|
||||
|
||||
const textarea = {
|
||||
value: "/newexisting text",
|
||||
selectionStart: 4,
|
||||
setSelectionRange: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
ctx.slash.select(
|
||||
{
|
||||
name: "new",
|
||||
description: "Start a new session",
|
||||
hints: [],
|
||||
action: () => {
|
||||
actionCalls++
|
||||
},
|
||||
},
|
||||
textarea,
|
||||
setText,
|
||||
)
|
||||
|
||||
expect(textarea.value).toBe("existing text")
|
||||
expect(currentText).toBe("existing text")
|
||||
expect(actionCalls).toBe(1)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
it("preserves trailing text for server commands and sets cursor", () => {
|
||||
const ctx = setup(() => {})
|
||||
let currentText = ""
|
||||
let selectionStart = 0
|
||||
|
||||
const textarea = {
|
||||
value: "/docmdexisting text",
|
||||
selectionStart: 6,
|
||||
setSelectionRange: (start: number, end: number) => {
|
||||
selectionStart = start
|
||||
},
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
ctx.slash.select({ name: "docmd", description: "Run doc command", hints: [] }, textarea, setText)
|
||||
|
||||
expect(textarea.value).toBe("/docmd existing text")
|
||||
expect(currentText).toBe("/docmd existing text")
|
||||
expect(selectionStart).toBe("/docmd ".length)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
it("uses slashEnd for server commands when onInput fired before select", () => {
|
||||
const ctx = setup(() => {})
|
||||
let currentText = ""
|
||||
let selectionStart = 0
|
||||
|
||||
ctx.slash.onInput("/docmdexisting text", 6)
|
||||
|
||||
const textarea = {
|
||||
value: "/docmdexisting text",
|
||||
selectionStart: 2,
|
||||
setSelectionRange: (start: number, end: number) => {
|
||||
selectionStart = start
|
||||
},
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
ctx.slash.select({ name: "docmd", description: "Run doc command", hints: [] }, textarea, setText)
|
||||
|
||||
expect(textarea.value).toBe("/docmd existing text")
|
||||
expect(currentText).toBe("/docmd existing text")
|
||||
expect(selectionStart).toBe("/docmd ".length)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
it("preserves trailing text even when cursor moves after typing slash command", () => {
|
||||
let actionCalls = 0
|
||||
let currentText = "existing text"
|
||||
const ctx = setup(() => {})
|
||||
|
||||
// Type slash command: cursor at 4, slashEnd stored as 4
|
||||
ctx.slash.onInput("/newexisting text", 4)
|
||||
|
||||
// Simulate user moving cursor (e.g. ArrowLeft twice)
|
||||
const textarea = {
|
||||
value: "/newexisting text",
|
||||
selectionStart: 2,
|
||||
setSelectionRange: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
ctx.slash.select(
|
||||
{
|
||||
name: "new",
|
||||
description: "Start a new session",
|
||||
hints: [],
|
||||
action: () => {
|
||||
actionCalls++
|
||||
},
|
||||
},
|
||||
textarea,
|
||||
setText,
|
||||
)
|
||||
|
||||
// Should preserve trailing text from original slashEnd (4), not stale selectionStart (2)
|
||||
expect(textarea.value).toBe("existing text")
|
||||
expect(currentText).toBe("existing text")
|
||||
expect(actionCalls).toBe(1)
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
it("preserves trailing text for memory commands when cursor moves", () => {
|
||||
let currentText = ""
|
||||
const ctx = setup(() => {})
|
||||
|
||||
// Type /memory rem: matches memory pattern, slashEnd set to end of text
|
||||
const typed = "/memory rem"
|
||||
ctx.slash.onInput(typed, typed.length)
|
||||
|
||||
// Simulate user moving cursor (no onInput fires for arrow keys)
|
||||
const textarea = {
|
||||
value: typed,
|
||||
selectionStart: 3,
|
||||
setSelectionRange: () => {},
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
// Find the memory remember command (nested, no action)
|
||||
const remembered = ctx.slash.results().find((c) => c.name === "memory remember")
|
||||
expect(remembered).toBeDefined()
|
||||
|
||||
ctx.slash.select(remembered!, textarea, setText)
|
||||
|
||||
// Trailing text from slashEnd (end of typed text) — empty — should be preserved correctly
|
||||
expect(currentText).toBe("/memory remember ")
|
||||
expect(textarea.value).toBe("/memory remember ")
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
it("preserves trailing text through the two-step nested command path", () => {
|
||||
let currentText = ""
|
||||
const ctx = setup(() => {})
|
||||
|
||||
ctx.slash.onInput("/mem", 4)
|
||||
|
||||
const textarea = {
|
||||
value: "/mem",
|
||||
selectionStart: 4,
|
||||
setSelectionRange: () => {},
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
const memory = ctx.slash.results().find((c) => c.name === "memory")
|
||||
expect(memory).toBeDefined()
|
||||
ctx.slash.select(memory!, textarea, setText)
|
||||
expect(currentText).toBe("/memory ")
|
||||
expect(textarea.value).toBe("/memory ")
|
||||
|
||||
const remember = ctx.slash.results().find((c) => c.name === "memory remember")
|
||||
expect(remember).toBeDefined()
|
||||
ctx.slash.select(remember!, textarea, setText)
|
||||
expect(currentText).toBe("/memory remember ")
|
||||
expect(textarea.value).toBe("/memory remember ")
|
||||
ctx.dispose()
|
||||
})
|
||||
|
||||
it("keeps trailing text and cursor before it through nested selection", () => {
|
||||
const ctx = setup(() => {})
|
||||
let currentText = "hello"
|
||||
const textarea = {
|
||||
value: "hello",
|
||||
selectionStart: 0,
|
||||
setSelectionRange: (start: number, _end: number) => {
|
||||
textarea.selectionStart = start
|
||||
},
|
||||
focus: () => {},
|
||||
} as unknown as HTMLTextAreaElement
|
||||
const setText = (text: string) => {
|
||||
currentText = text
|
||||
}
|
||||
|
||||
textarea.value = "/memhello"
|
||||
textarea.selectionStart = 4
|
||||
ctx.slash.onInput("/memhello", 4)
|
||||
|
||||
const memory = ctx.slash.results().find((c) => c.name === "memory")
|
||||
expect(memory).toBeDefined()
|
||||
ctx.slash.select(memory!, textarea, setText)
|
||||
expect(textarea.value).toBe("/memory hello")
|
||||
expect(textarea.selectionStart).toBe("/memory ".length)
|
||||
|
||||
const rebuild = ctx.slash.results().find((c) => c.name === "memory rebuild")
|
||||
expect(rebuild).toBeDefined()
|
||||
ctx.slash.select(rebuild!, textarea, setText)
|
||||
|
||||
expect(textarea.value).toBe("/memory rebuild hello")
|
||||
expect(textarea.selectionStart).toBe("/memory rebuild ".length)
|
||||
ctx.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
isPromptBusy,
|
||||
isPathMention,
|
||||
applySandboxStates,
|
||||
memoryRest,
|
||||
type SandboxDefaultState,
|
||||
type SandboxState,
|
||||
} from "./prompt-input-utils"
|
||||
@@ -80,7 +81,7 @@ import {
|
||||
import { ReviewComments } from "./ReviewComments"
|
||||
import { partReview, reviewBody } from "../../../../src/shared/review-comments"
|
||||
import { isEnterKeyCommitNotIme } from "../../utils/ime-enter"
|
||||
import { parseMemoryCommand } from "../../utils/memory-command"
|
||||
import { parseMemoryCommand, type ParsedMemoryCommand } from "../../utils/memory-command"
|
||||
import { useMemory } from "../../context/memory"
|
||||
|
||||
function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]): ReviewComment[] {
|
||||
@@ -1124,6 +1125,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const setMemoryText = (memory: ParsedMemoryCommand) => {
|
||||
const rest = memoryRest(memory)
|
||||
setText(rest)
|
||||
if (textareaRef) {
|
||||
textareaRef.value = rest
|
||||
textareaRef.setSelectionRange(0, 0)
|
||||
textareaRef.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
const draft = text().trim()
|
||||
|
||||
@@ -1131,7 +1142,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (memory) {
|
||||
if (!runMemory(memory)) return
|
||||
history.append(draft)
|
||||
setText("")
|
||||
setMemoryText(memory)
|
||||
clearReviewComments()
|
||||
imageAttach.clear()
|
||||
mention.closeMention()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type ParsedMemoryCommand } from "../../utils/memory-command"
|
||||
|
||||
export type SandboxDefaultState = {
|
||||
desired: boolean
|
||||
enabled: boolean
|
||||
@@ -150,3 +152,15 @@ export function isPathMention(text: string): boolean {
|
||||
const path = text.replace(/^@/, "")
|
||||
return path !== "terminal" && path !== "git-changes"
|
||||
}
|
||||
|
||||
/**
|
||||
* The text that should remain in the prompt input after a memory command is
|
||||
* submitted. No-argument memory operations (e.g. rebuild, on, status, inspect)
|
||||
* typed with trailing free text (e.g. "/memory rebuild hello") keep that text in
|
||||
* the input instead of discarding it; the parser reports the unconsumed
|
||||
* remainder as `rest`. Argument-taking operations (remember, correct, forget,
|
||||
* auto, purge) consume their text, so nothing remains.
|
||||
*/
|
||||
export function memoryRest(cmd: ParsedMemoryCommand): string {
|
||||
return "rest" in cmd ? (cmd.rest ?? "") : ""
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export function useSlashCommand(
|
||||
const [query, setQuery] = createSignal<string | null>(null)
|
||||
const [index, setIndex] = createSignal(0)
|
||||
const [requested, setRequested] = createSignal(false)
|
||||
const [slashEnd, setSlashEnd] = createSignal<number | null>(null)
|
||||
const open = (name: string) => {
|
||||
window.dispatchEvent(new CustomEvent(name, { detail: { source: scope } }))
|
||||
}
|
||||
@@ -287,6 +288,7 @@ export function useSlashCommand(
|
||||
|
||||
const close = () => {
|
||||
setQuery(null)
|
||||
setSlashEnd(null)
|
||||
}
|
||||
|
||||
const onInput = (val: string, cursor: number) => {
|
||||
@@ -296,6 +298,7 @@ export function useSlashCommand(
|
||||
request()
|
||||
setQuery(match[1])
|
||||
setIndex(0)
|
||||
setSlashEnd(cursor)
|
||||
return
|
||||
}
|
||||
const memory = before.match(/^\/(?:memory|mem)\s+([^\n]*)$/i)
|
||||
@@ -305,6 +308,7 @@ export function useSlashCommand(
|
||||
request()
|
||||
setQuery(value)
|
||||
setIndex(0)
|
||||
setSlashEnd(cursor)
|
||||
return
|
||||
}
|
||||
const review = before.match(/^\/review\s+([^\n]*)$/i)
|
||||
@@ -314,6 +318,7 @@ export function useSlashCommand(
|
||||
request()
|
||||
setQuery(value)
|
||||
setIndex(0)
|
||||
setSlashEnd(cursor)
|
||||
return
|
||||
}
|
||||
return close()
|
||||
@@ -325,24 +330,31 @@ export function useSlashCommand(
|
||||
setText: (text: string) => void,
|
||||
onSelect?: () => void,
|
||||
) => {
|
||||
const cursor = slashEnd() ?? textarea.selectionStart ?? 0
|
||||
// trailingText holds text after the slash command.
|
||||
// slashEnd is the cursor position from onInput when the slash pattern was matched.
|
||||
const trailingText = textarea.value.substring(cursor)
|
||||
|
||||
if (cmd.action) {
|
||||
if (cmd.enabled && !cmd.enabled()) return
|
||||
textarea.value = ""
|
||||
setText("")
|
||||
textarea.value = trailingText
|
||||
setText(trailingText)
|
||||
textarea.setSelectionRange(0, 0)
|
||||
close()
|
||||
onSelect?.()
|
||||
cmd.action()
|
||||
return
|
||||
}
|
||||
const text = `/${cmd.name} `
|
||||
textarea.value = text
|
||||
setText(text)
|
||||
const pos = text.length
|
||||
textarea.setSelectionRange(pos, pos)
|
||||
const commandText = `/${cmd.name} `
|
||||
const updatedText = commandText + trailingText
|
||||
textarea.value = updatedText
|
||||
setText(updatedText)
|
||||
textarea.setSelectionRange(commandText.length, commandText.length)
|
||||
textarea.focus()
|
||||
if (cmd.nested) {
|
||||
setQuery(`${cmd.name} `)
|
||||
setIndex(0)
|
||||
setSlashEnd(commandText.length)
|
||||
}
|
||||
if (!cmd.nested) close()
|
||||
onSelect?.()
|
||||
|
||||
Reference in New Issue
Block a user