From 40ec9834b2af2b5700510d131d55f2b6dee540ce Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 1 Aug 2026 01:17:41 -0500 Subject: [PATCH] fix(tui): keep slash completion alive after a leading command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing a second slash command went dead whenever the message started with one: `/work /cle` offered nothing while `do /work then /cle` completed fine, which reads as an intermittent glitch rather than a rule. Only the first slash can be an invocation, so detect the inline shape first. The leading-command branch claimed the whole line and handed it to the backend's completer, which has nothing to say about a slash sitting in a command's argument tail. The inline trigger requires a whitespace-preceded slash at the caret, so ordinary argument completion (`/cron ad`, `/personality alic`) is untouched — it fires only where completion was already dead. --- ui-tui/src/__tests__/inlineSlashSkill.test.ts | 22 +++++++++++++++++++ ui-tui/src/hooks/useCompletion.ts | 20 +++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/ui-tui/src/__tests__/inlineSlashSkill.test.ts b/ui-tui/src/__tests__/inlineSlashSkill.test.ts index d2f81b7059f04..61578fce682ac 100644 --- a/ui-tui/src/__tests__/inlineSlashSkill.test.ts +++ b/ui-tui/src/__tests__/inlineSlashSkill.test.ts @@ -64,6 +64,28 @@ describe('completionRequestForInput — inline skill references', () => { }) }) + it('completes a second slash in a line that starts with a command', () => { + // Only the first slash is an invocation. Routing the whole line to the + // completer offered nothing, so `/work /cle` went dead while + // `do /work then /cle` completed fine. + expect(completionRequestForInput('/work /cle')).toMatchObject({ + method: 'complete.slash', + params: { text: '/cle' }, + replaceFrom: 7, + skillsOnly: true + }) + }) + + it('leaves a command own arguments to the command', () => { + for (const input of ['/personality alic', '/cron ad', '/details ']) { + expect(completionRequestForInput(input)).toEqual({ + method: 'complete.slash', + params: { text: input }, + replaceFrom: 1 + }) + } + }) + it('routes a real mid-message path to path completion, not skills', () => { expect(completionRequestForInput('open src/foo/ba')).toMatchObject({ method: 'complete.path' }) expect(completionRequestForInput('open /usr/lo')).toMatchObject({ method: 'complete.path' }) diff --git a/ui-tui/src/hooks/useCompletion.ts b/ui-tui/src/hooks/useCompletion.ts index 468c8995b0330..cb9572f261500 100644 --- a/ui-tui/src/hooks/useCompletion.ts +++ b/ui-tui/src/hooks/useCompletion.ts @@ -41,15 +41,13 @@ export function completionRequestForInput( return null } - if (isSlashCommand) { - return { method: 'complete.slash', params: { text: input }, replaceFrom: 1 } - } - - // A `/token` mid-message is a skill reference dropped into prose. It's only - // reachable here because the path branch below would otherwise claim it: a - // bare `/cle` matches TAB_PATH_RE as an absolute path. Skills win that tie — - // the moment a second `/` is typed the inline trigger stops matching and - // path completion takes back over. + // A `/token` mid-message is a skill reference dropped into prose. Detected + // BEFORE the leading-command shape because only the first slash can be an + // invocation — `/help /cle` is a command whose argument names a skill, and + // routing the whole line to the backend's completer offered nothing at all. + // It only matches a whitespace-preceded slash sitting at the caret, so + // ordinary argument completion (`/cron ad`, `/personality alic`) is + // untouched. const inline = inlineSlashTrigger(input) if (inline) { @@ -61,6 +59,10 @@ export function completionRequestForInput( } } + if (isSlashCommand) { + return { method: 'complete.slash', params: { text: input }, replaceFrom: 1 } + } + if (!pathWord) { return null }