From 3c74f463d0b392746c16069bbb7ec39ee8a75e79 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 18:02:54 -0500 Subject: [PATCH] fix(desktop): keep /resume's free-text search typeable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /resume was classified as an options command, but its argument is a free-text query the picker fuzzy-matches against session titles and previews. Its completion list also always ends in a "Browse all sessions…" action row, so Space-to-accept never fell through to an empty list: the first space in a multi-word query emptied the composer and threw the user into the overlay, query and all. Classify it as mixed so spaces type through, matches keep narrowing as you refine, and Tab or arrow-then-Enter still accept a highlighted session. --- .../composer/hooks/use-composer-trigger.test.ts | 14 ++++++++++++++ apps/desktop/src/lib/desktop-slash-commands.ts | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/chat/composer/hooks/use-composer-trigger.test.ts b/apps/desktop/src/app/chat/composer/hooks/use-composer-trigger.test.ts index a4ab38b3b3a..4239f781afa 100644 --- a/apps/desktop/src/app/chat/composer/hooks/use-composer-trigger.test.ts +++ b/apps/desktop/src/app/chat/composer/hooks/use-composer-trigger.test.ts @@ -172,6 +172,20 @@ describe('useComposerTrigger — free-text slash arguments', () => { expect(hook.result.current.triggerActiveExplicit).toBe(false) }) + it('keeps a multi-word /resume search typeable instead of firing the picker action', () => { + // The session list always ends in a "Browse all sessions…" action row, so + // an accept here doesn't insert a chip — it empties the composer and opens + // the overlay, taking the half-typed query with it. + const editor = mountEditor('/resume my new') + const { hook } = mountTrigger(editor, [item('/resume', 'Sessions')]) + + act(() => hook.result.current.refreshTrigger()) + + expect(hook.result.current.slashFreeTextArgStage).toBe(true) + expect(hook.result.current.commitTypedSlashDirective()).toBe(false) + expect(composerPlainText(editor)).toBe('/resume my new') + }) + it('still commits a fully typed finite option as one directive chip', () => { const editor = mountEditor('/personality creative') const { hook } = mountTrigger(editor, []) diff --git a/apps/desktop/src/lib/desktop-slash-commands.ts b/apps/desktop/src/lib/desktop-slash-commands.ts index 81e4a694c01..62f2e0d5bd7 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -188,7 +188,13 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [ description: 'Resume a saved session', aliases: ['/sessions', '/switch'], surface: picker('session'), - argumentMode: 'options' + // `mixed`, not `options`: the argument is a free-text search the picker + // fuzzy-matches against titles and previews, so multi-word queries have to + // stay typeable. Its completion list also always carries a trailing + // "Browse all sessions…" action row, which meant Space-to-accept could + // never fall through — the first space wiped the composer and threw the + // user into the overlay. + argumentMode: 'mixed' }, // Backend-executed commands that render useful inline output.