mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] settings: the shortcut recorder owns the keyboard while armed (window-level capture, hotkeys suppressed), so rebinding no longer snaps back to fn
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
||||
@@ -26,17 +26,25 @@ export function comboDisplay(combo: string): string {
|
||||
.join(IS_MAC ? '' : '+');
|
||||
}
|
||||
|
||||
/** Click-to-record shortcut chip: click arms it, the next non-modifier keydown becomes the combo ("Meta+Shift+d" parts format, same as new_agent_shortcut). */
|
||||
/** Click-to-record shortcut chip: click arms it, the next non-modifier keydown becomes the combo
|
||||
* ("Meta+Shift+d" parts format, same as new_agent_shortcut). While armed, the WINDOW owns the
|
||||
* keyboard at capture phase and app hotkeys are suppressed: the old chip-local listener lost the
|
||||
* keys to global shortcuts (pressing the current dictation combo started a dictation, stole focus,
|
||||
* and snapped the chip back before any combo could land, ENG-183). */
|
||||
const ShortcutRecorderChip: React.FC<{ value: string; onChange: (combo: string) => void }> = ({ value, onChange }) => {
|
||||
const c = useClaudeTokens();
|
||||
const [recording, setRecording] = useState(false);
|
||||
return (
|
||||
<Box
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (!recording) return;
|
||||
if (['Meta', 'Control', 'Shift', 'Alt'].includes(e.key)) return;
|
||||
const hostRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!recording) return undefined;
|
||||
const w = window as unknown as Record<string, unknown>;
|
||||
w.__OSW_SHORTCUT_RECORDING__ = true;
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
// Swallow EVERYTHING while armed so no app shortcut fires mid-recording.
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (['Meta', 'Control', 'Shift', 'Alt'].includes(e.key)) return;
|
||||
if (e.key === 'Escape') { setRecording(false); return; }
|
||||
const parts: string[] = [];
|
||||
if (e.metaKey) parts.push('Meta');
|
||||
@@ -46,11 +54,25 @@ const ShortcutRecorderChip: React.FC<{ value: string; onChange: (combo: string)
|
||||
parts.push(e.key.length === 1 ? e.key.toLowerCase() : e.key);
|
||||
onChange(parts.join('+'));
|
||||
setRecording(false);
|
||||
}}
|
||||
onBlur={() => setRecording(false)}
|
||||
// Arming without taking focus meant the next blur disarmed it before any key could land, so
|
||||
// the chip snapped back to its old value and looked like it refused to be rebound.
|
||||
onClick={(e) => { (e.currentTarget as HTMLElement).focus(); setRecording(true); }}
|
||||
};
|
||||
// Click-away cancels; blur alone must not (a global hotkey stealing focus was the snap-back).
|
||||
const onPointerDown = (e: PointerEvent): void => {
|
||||
if (hostRef.current && e.target instanceof Node && hostRef.current.contains(e.target)) return;
|
||||
setRecording(false);
|
||||
};
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
window.addEventListener('pointerdown', onPointerDown, true);
|
||||
return () => {
|
||||
w.__OSW_SHORTCUT_RECORDING__ = false;
|
||||
window.removeEventListener('keydown', onKey, true);
|
||||
window.removeEventListener('pointerdown', onPointerDown, true);
|
||||
};
|
||||
}, [recording, onChange]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={hostRef}
|
||||
onClick={() => setRecording(true)}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -62,6 +62,9 @@ export function VoiceDictationProvider({ children }: { children: React.ReactNode
|
||||
const latchedRef = useRef(false);
|
||||
|
||||
const pressStart = useCallback((): void => {
|
||||
// The Settings recorder chip owns the keyboard while it's armed: without this, pressing the
|
||||
// CURRENT hotkey to rebind it started a dictation, which stole focus and snapped the chip back.
|
||||
if ((window as unknown as Record<string, unknown>).__OSW_SHORTCUT_RECORDING__) return;
|
||||
if (holdMode) {
|
||||
// A TAP while recording must stop: the quick tap's press-end fires before the async start
|
||||
// flips state to 'recording', so without this the mic could be started by a click but never
|
||||
|
||||
Reference in New Issue
Block a user