fix(desktop): keep slash/@ completion menu navigable and Esc-dismissable
The desktop composer's `onKeyUp` handler unconditionally re-ran `refreshTrigger` on every keyup, including the Arrow/Enter/Tab/Escape keys the open-trigger `onKeyDown` branch had already fully handled. Because `refreshTrigger` re-detects the trigger and resets the active index to 0, this produced two bugs in the `/` (and `@`) completion popover: - ArrowDown/ArrowUp moved the highlight on keydown, then keyup snapped it straight back to the top — so the user could never cycle past the first couple of items. - Escape closed the menu on keydown, then keyup re-detected the still-present `/` and immediately reopened it — so Esc appeared to do nothing. Fix: skip the keyup-driven refresh for the navigation/control keys while a trigger menu is open (they never edit text, so refreshing is pointless), and only reset the highlight in `refreshTrigger` when the detected trigger query actually changed. Applied to both the main composer (chat/composer/index.tsx) and the message-edit composer (assistant-ui/thread.tsx), which shared the same bug. New `shouldSkipTriggerRefreshOnKeyUp` helper is unit-tested.
This commit is contained in:
@@ -44,7 +44,7 @@ import {
|
||||
renderComposerContents,
|
||||
RICH_INPUT_SLOT
|
||||
} from '@/app/chat/composer/rich-editor'
|
||||
import { detectTrigger, textBeforeCaret, type TriggerState } from '@/app/chat/composer/text-utils'
|
||||
import { detectTrigger, shouldSkipTriggerRefreshOnKeyUp, textBeforeCaret, type TriggerState } from '@/app/chat/composer/text-utils'
|
||||
import { ComposerTriggerPopover } from '@/app/chat/composer/trigger-popover'
|
||||
import { extractDroppedFiles, HERMES_PATHS_MIME } from '@/app/chat/hooks/use-composer-actions'
|
||||
import { ClarifyTool } from '@/components/assistant-ui/clarify-tool'
|
||||
@@ -997,8 +997,15 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
|
||||
}
|
||||
|
||||
setTrigger(detected)
|
||||
setTriggerActive(0)
|
||||
}, [])
|
||||
|
||||
// Only reset the highlight when the trigger actually changed (opened, or
|
||||
// the query/kind differs). Re-detecting the *same* trigger — e.g. on a
|
||||
// caret move (mouseup) or a stray refresh — must preserve the user's
|
||||
// current selection instead of snapping back to the first item.
|
||||
if (detected?.kind !== trigger?.kind || detected?.query !== trigger?.query) {
|
||||
setTriggerActive(0)
|
||||
}
|
||||
}, [trigger])
|
||||
|
||||
const closeTrigger = useCallback(() => {
|
||||
setTrigger(null)
|
||||
@@ -1275,6 +1282,18 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
// Arrow/Enter/Tab/Escape while the trigger menu is open are fully handled
|
||||
// in keydown and never edit text. Refreshing the trigger here would reset
|
||||
// the highlight to the top (breaking ArrowDown/ArrowUp) and re-open a menu
|
||||
// that Escape just closed, so skip it.
|
||||
if (shouldSkipTriggerRefreshOnKeyUp(event.key, trigger !== null)) {
|
||||
return
|
||||
}
|
||||
|
||||
window.setTimeout(refreshTrigger, 0)
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposerPrimitive.Root className="contents" data-slot="aui_edit-composer-root">
|
||||
<StickyHumanMessageContainer>
|
||||
@@ -1325,7 +1344,7 @@ const UserEditComposer: FC<UserEditComposerProps> = ({ cwd, gateway, sessionId }
|
||||
onFocus={() => markActiveComposer('edit')}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={() => window.setTimeout(refreshTrigger, 0)}
|
||||
onKeyUp={handleKeyUp}
|
||||
onMouseUp={refreshTrigger}
|
||||
onPaste={handlePaste}
|
||||
ref={editorRef}
|
||||
|
||||
Reference in New Issue
Block a user