Merge pull request #73074 from NousResearch/bb/slash-after-command

fix(desktop): keep slash completion alive after a leading command
This commit is contained in:
brooklyn! 2026-07-27 22:16:35 -05:00 committed by GitHub
commit 202140db53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 7 deletions

View file

@ -120,6 +120,29 @@ describe('useComposerTrigger — slash anywhere in the prompt', () => {
expect(hook.result.current.trigger).toBeNull()
})
it('opens the list for a second slash after a leading command', () => {
// `/work /cle`: the command regex's argument tail would otherwise swallow
// `/cle` as an argument to `/work`, and a no-arg command suppresses the
// popover — so every slash after the first went dead.
const editor = mountEditor('/work /cle')
const { hook } = mountTrigger(editor, [item('/clean')])
act(() => hook.result.current.refreshTrigger())
expect(hook.result.current.trigger).toMatchObject({ kind: '/', inline: true, query: 'cle' })
expect(hook.result.current.triggerItems).toHaveLength(1)
})
it('inserts the second command without disturbing the first', () => {
const editor = mountEditor('/work rewrite the composer /cle')
const { hook } = mountTrigger(editor, [item('/clean')])
act(() => hook.result.current.refreshTrigger())
act(() => hook.result.current.replaceTriggerWithChip(item('/clean')))
expect(composerPlainText(editor)).toBe('/work rewrite the composer /clean ')
})
})
describe('useComposerTrigger — free-text slash arguments', () => {

View file

@ -28,6 +28,11 @@ export interface TriggerState {
// there are no args to complete — the trigger is a single token that ends at
// the next space, exactly like `@`.
//
// Only the FIRST slash can be an invocation, so the inline shape is tested
// first: the command regex's argument tail (`(?:\s+\S*)*`) happily swallows a
// later `/skill` as if it were an argument, which killed completion for every
// slash after a leading command (`/work /cle` → nothing).
//
// The inline shape is what makes skills reachable anywhere in a prompt. Both
// shapes need the trailing `$`: detection runs against the text BEFORE the
// caret, so the match must end where the user is typing.
@ -124,14 +129,10 @@ export function textBeforeCaret(editor: HTMLDivElement): string | null {
}
export function detectTrigger(textBefore: string): TriggerState | null {
const command = SLASH_COMMAND_TRIGGER_RE.exec(textBefore)
if (command) {
return { kind: '/', query: command[2], tokenLength: 1 + command[2].length }
}
// An inline `/skill` is a reference dropped into prose, so it carries no args
// and the whole match is the token the chip replaces.
// and the whole match is the token the chip replaces. Checked before the
// anchored command shape so a second slash isn't mistaken for the first
// command's argument.
const inline = SLASH_INLINE_TRIGGER_RE.exec(textBefore)
if (inline) {
@ -140,6 +141,12 @@ export function detectTrigger(textBefore: string): TriggerState | null {
return { inline: true, kind: '/', query, tokenLength: 1 + query.length }
}
const command = SLASH_COMMAND_TRIGGER_RE.exec(textBefore)
if (command) {
return { kind: '/', query: command[2], tokenLength: 1 + command[2].length }
}
const at = AT_TRIGGER_RE.exec(textBefore)
if (at) {