fix(desktop): keep the @ popover open while typing a path

`AT_TRIGGER_RE` excluded `/` from the query, so the trigger died on the
first separator: `@/desk`, `@./www`, `@~/Desktop` and even `@file:src/foo`
all stopped matching the moment a path appeared. The gateway already
answered those queries correctly — the composer just never asked.

A `/` inside an `@` token is navigation, not a delimiter. The token stays
whitespace-bounded, which is what actually ends it.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 15:24:44 -05:00
parent b378cc0a72
commit de0b376cc9
2 changed files with 39 additions and 2 deletions

View file

@ -51,6 +51,39 @@ describe('detectTrigger', () => {
expect(detectTrigger('and/or')).toBeNull()
})
it('keeps the at-mention live while walking into subfolders', () => {
// A `/` inside the query is path navigation, not the end of the token —
// the popover has to stay open so the next directory level can load.
expect(detectTrigger('@./')).toEqual({ kind: '@', query: './', tokenLength: 3 })
expect(detectTrigger('@./src')).toEqual({ kind: '@', query: './src', tokenLength: 6 })
expect(detectTrigger('@~/Desktop/')).toEqual({ kind: '@', query: '~/Desktop/', tokenLength: 11 })
expect(detectTrigger('@/usr/local')).toEqual({ kind: '@', query: '/usr/local', tokenLength: 11 })
expect(detectTrigger('@apps/desktop/src')).toEqual({
kind: '@',
query: 'apps/desktop/src',
tokenLength: 17
})
})
it('keeps the at-mention live for a typed ref kind with a path', () => {
expect(detectTrigger('@file:src/main.tsx')).toEqual({
kind: '@',
query: 'file:src/main.tsx',
tokenLength: 18
})
expect(detectTrigger('@folder:apps/')).toEqual({ kind: '@', query: 'folder:apps/', tokenLength: 13 })
})
it('still ends the at-mention token at whitespace', () => {
// The token is whitespace-delimited; a path doesn't change that.
expect(detectTrigger('@./src and more')).toBeNull()
expect(detectTrigger('look at @apps/desktop')).toEqual({
kind: '@',
query: 'apps/desktop',
tokenLength: 13
})
})
it('treats a mid-message slash as an inline reference', () => {
// Skills have to be reachable anywhere in a prompt, not just at position 0.
expect(detectTrigger('hello /')).toEqual({ kind: '/', inline: true, query: '', tokenLength: 1 })

View file

@ -10,7 +10,11 @@ export interface TriggerState {
}
// `@` triggers stop at the first whitespace — `@file:path` and `@diff` are
// single tokens. Restricting the slash command name to `[a-zA-Z][\w-]*` avoids
// single tokens, and a path is part of that token: `@./src/`, `@~/Desktop/`,
// and `@file:src/foo` all have to keep the popover live while the user walks
// into subdirectories. Excluding `/` from the query class would end the token
// at the first separator, which is exactly the "can't browse into a folder"
// bug. Restricting the slash command name to `[a-zA-Z][\w-]*` avoids
// matching file paths like `src/foo/bar`.
//
// `/` triggers fire in two shapes, because a slash means two different things
@ -27,7 +31,7 @@ export interface TriggerState {
// 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.
const AT_TRIGGER_RE = /(?:^|[\s])(@)([^\s@/]*)$/
const AT_TRIGGER_RE = /(?:^|[\s])(@)([^\s@]*)$/
const SLASH_COMMAND_TRIGGER_RE = /^(\/)((?:[a-zA-Z][\w-]*(?:\s+\S*)*)?)$/
const SLASH_INLINE_TRIGGER_RE = /[\s](\/)([a-zA-Z][\w-]*)?$/