Skip to content

Commit cfad872

Browse files
committed
fix(web-ui): let the '@' reference picker own its keys while open
The picker is an overlay layer, so the overlay coordinator routes its keyboard from the document after the React handlers run. The composer key handler therefore consumed ArrowDown as history navigation and Enter as send before the picker could move or accept a row, which left the list reachable by ArrowUp only and made Enter submit the draft. Release the keys the picker owns while it is open instead of relying on event order, and keep the composer path for every other key.
1 parent af4c657 commit cfad872

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎src/web-ui/src/flow_chat/components/ChatInput.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ import {
217217
replaceLeadingSlashCommandWithSkillToken,
218218
} from '../utils/skillPromptReference';
219219
import { resolveChatInputQuickSkillShortcuts } from '../utils/chatInputQuickSkills';
220+
import { contextPickerOwnsKey } from '../utils/chatInputKeyOwnership';
220221
import { useDeepReviewConsent } from './DeepReviewConsentDialog';
221222
import { useSessionReviewActivity } from '../hooks/useSessionReviewActivity';
222223
import { shouldBlockReviewCommand } from '../utils/deepReviewCommandGuard';
@@ -5619,6 +5620,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({
56195620
return;
56205621
}
56215622

5623+
// The '@' reference picker owns its navigation and acceptance keys through
5624+
// its overlay layer, which the coordinator routes after React handlers.
5625+
if (contextPickerOwnsKey({ contextPickerActive: contextTriggerState.isActive, key: e.key })) {
5626+
return;
5627+
}
5628+
56225629
if (slashCommandState.isActive) {
56235630
const items = getActiveSlashPickerItems();
56245631
const maxIndex = Math.max(0, items.length - 1);
@@ -5813,7 +5820,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
58135820
handleSendOrCancel();
58145821
}
58155822

5816-
}, [canUseThreadGoal, handleSendOrCancel, submitBtwFromInput, submitGoalFromInput, derivedState, dispatchInput, slashCommandState, getActiveSlashPickerItems, selectSlashCommandAction, selectSlashExternalPromptCommand, selectSlashPromptCommand, selectSlashAcpCommand, selectSlashSkill, getRichTextTriggerController, historyIndex, inputHistory, savedDraft, inputState.value, hasSendableInput, currentSessionId, isBtwSession, showTargetSwitcher, setInputTarget, removeContext, t]);
5823+
}, [canUseThreadGoal, handleSendOrCancel, submitBtwFromInput, submitGoalFromInput, derivedState, dispatchInput, slashCommandState, contextTriggerState.isActive, getActiveSlashPickerItems, selectSlashCommandAction, selectSlashExternalPromptCommand, selectSlashPromptCommand, selectSlashAcpCommand, selectSlashSkill, getRichTextTriggerController, historyIndex, inputHistory, savedDraft, inputState.value, hasSendableInput, currentSessionId, isBtwSession, showTargetSwitcher, setInputTarget, removeContext, t]);
58175824

58185825
const handleImeCompositionStart = useCallback(() => {
58195826
isImeComposingRef.current = true;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { contextPickerOwnsKey } from './chatInputKeyOwnership';
4+
5+
describe('context picker key ownership', () => {
6+
it('releases navigation and acceptance keys so the open picker handles them', () => {
7+
for (const key of ['ArrowUp', 'ArrowDown', 'Enter', 'Tab']) {
8+
expect(contextPickerOwnsKey({ contextPickerActive: true, key })).toBe(true);
9+
}
10+
});
11+
12+
it('keeps history navigation and send while the picker is closed', () => {
13+
for (const key of ['ArrowUp', 'ArrowDown', 'Enter', 'Tab']) {
14+
expect(contextPickerOwnsKey({ contextPickerActive: false, key })).toBe(false);
15+
}
16+
});
17+
18+
it('leaves typing and caret keys with the editor', () => {
19+
for (const key of ['a', 'Backspace', 'ArrowLeft', 'ArrowRight', 'Escape']) {
20+
expect(contextPickerOwnsKey({ contextPickerActive: true, key })).toBe(false);
21+
}
22+
});
23+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Keys the '@' reference picker consumes while it is open.
3+
*/
4+
const CONTEXT_PICKER_OWNED_KEYS: ReadonlySet<string> = new Set([
5+
'ArrowUp',
6+
'ArrowDown',
7+
'Enter',
8+
'Tab',
9+
]);
10+
11+
/**
12+
* The '@' reference picker is an overlay layer, so the overlay coordinator routes
13+
* its keyboard from the document after React handlers. The composer must release
14+
* every key the picker consumes instead of relying on event order; otherwise the
15+
* picker loses ArrowDown to history navigation and Enter to send.
16+
*/
17+
export function contextPickerOwnsKey(params: {
18+
contextPickerActive: boolean;
19+
key: string;
20+
}): boolean {
21+
return params.contextPickerActive && CONTEXT_PICKER_OWNED_KEYS.has(params.key);
22+
}

0 commit comments

Comments
 (0)