Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
21 changes: 14 additions & 7 deletions apps/desktop/src/app/chat/composer/text-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Loading