Improve Cmd+K focus reliability on web
This commit is contained in:
@@ -406,12 +406,41 @@ export function AgentInputArea({
|
|||||||
if (!req) return;
|
if (!req) return;
|
||||||
if (req.agentKey !== `${serverId}:${agentId}`) return;
|
if (req.agentKey !== `${serverId}:${agentId}`) return;
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
let cancelled = false;
|
||||||
requestAnimationFrame(() => {
|
const deadlineMs = Date.now() + 1500;
|
||||||
messageInputRef.current?.focus();
|
|
||||||
|
const tryFocus = () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const ref = messageInputRef.current;
|
||||||
|
ref?.focus();
|
||||||
|
|
||||||
|
const el = ref?.getNativeElement?.() ?? null;
|
||||||
|
const active = typeof document !== "undefined" ? document.activeElement : null;
|
||||||
|
const didFocus = !!el && active === el;
|
||||||
|
|
||||||
|
if (didFocus) {
|
||||||
clearFocusChatInputRequest();
|
clearFocusChatInputRequest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() >= deadlineMs) {
|
||||||
|
// Don't keep stealing focus forever; allow other interactions.
|
||||||
|
clearFocusChatInputRequest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(tryFocus);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(tryFocus);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [agentId, clearFocusChatInputRequest, focusChatInputRequest, serverId]);
|
}, [agentId, clearFocusChatInputRequest, focusChatInputRequest, serverId]);
|
||||||
|
|
||||||
// Handle command selection from autocomplete
|
// Handle command selection from autocomplete
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export function CommandCenter() {
|
|||||||
return filtered;
|
return filtered;
|
||||||
}, [agents, query]);
|
}, [agents, query]);
|
||||||
|
|
||||||
|
const agentKeyFromPathname = useMemo(() => {
|
||||||
|
const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)/);
|
||||||
|
if (!match) return null;
|
||||||
|
return `${match[1]}:${match[2]}`;
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prevOpen = prevOpenRef.current;
|
const prevOpen = prevOpenRef.current;
|
||||||
prevOpenRef.current = open;
|
prevOpenRef.current = open;
|
||||||
@@ -72,18 +78,43 @@ export function CommandCenter() {
|
|||||||
|
|
||||||
if (prevOpen && !didNavigateRef.current) {
|
if (prevOpen && !didNavigateRef.current) {
|
||||||
const el = takeFocusRestoreElement();
|
const el = takeFocusRestoreElement();
|
||||||
if (el && el.isConnected) {
|
const deadlineMs = Date.now() + 1500;
|
||||||
// Modal unmount can steal focus; restore on next tick.
|
const tryRestore = () => {
|
||||||
|
if (el && el.isConnected) {
|
||||||
|
try {
|
||||||
|
el.focus();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
el &&
|
||||||
|
typeof document !== "undefined" &&
|
||||||
|
document.activeElement === el
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() >= deadlineMs) {
|
||||||
|
// If we failed to restore focus to the original element (RN web can
|
||||||
|
// remount textareas), fall back to focusing the chat input for the
|
||||||
|
// current agent route.
|
||||||
|
if (agentKeyFromPathname) {
|
||||||
|
requestFocusChatInput(agentKeyFromPathname);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(tryRestore);
|
||||||
try {
|
|
||||||
el.focus();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Modal unmount can steal focus; restore on next tick (and retry).
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(tryRestore);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -95,7 +126,7 @@ export function CommandCenter() {
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}, 0);
|
}, 0);
|
||||||
return () => clearTimeout(id);
|
return () => clearTimeout(id);
|
||||||
}, [open, takeFocusRestoreElement]);
|
}, [agentKeyFromPathname, open, requestFocusChatInput, takeFocusRestoreElement]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ export interface MessageInputProps {
|
|||||||
export interface MessageInputRef {
|
export interface MessageInputRef {
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
blur: () => void;
|
blur: () => void;
|
||||||
|
/**
|
||||||
|
* Web-only: return the underlying DOM element for focus assertions/retries.
|
||||||
|
* May return null if not mounted or on native.
|
||||||
|
*/
|
||||||
|
getNativeElement?: () => HTMLElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_INPUT_HEIGHT = 30;
|
const MIN_INPUT_HEIGHT = 30;
|
||||||
@@ -134,6 +139,17 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
blur: () => {
|
blur: () => {
|
||||||
textInputRef.current?.blur?.();
|
textInputRef.current?.blur?.();
|
||||||
},
|
},
|
||||||
|
getNativeElement: () => {
|
||||||
|
if (!IS_WEB) return null;
|
||||||
|
const current = textInputRef.current as
|
||||||
|
| (TextInput & { getNativeRef?: () => unknown })
|
||||||
|
| null;
|
||||||
|
const native =
|
||||||
|
typeof current?.getNativeRef === "function"
|
||||||
|
? current.getNativeRef()
|
||||||
|
: current;
|
||||||
|
return native instanceof HTMLElement ? native : null;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
|
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
|
||||||
const baselineInputHeightRef = useRef<number | null>(null);
|
const baselineInputHeightRef = useRef<number | null>(null);
|
||||||
|
|||||||
@@ -130,8 +130,16 @@ export function useGlobalKeyboardNav({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const s = useKeyboardNavStore.getState();
|
const s = useKeyboardNavStore.getState();
|
||||||
if (!s.commandCenterOpen) {
|
if (!s.commandCenterOpen) {
|
||||||
|
const target =
|
||||||
|
event.target instanceof Element ? (event.target as Element) : null;
|
||||||
|
const targetEl =
|
||||||
|
target?.closest?.("textarea, input, [contenteditable='true']") ??
|
||||||
|
(target instanceof HTMLElement ? target : null);
|
||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
s.setFocusRestoreElement(active instanceof HTMLElement ? active : null);
|
const activeEl = active instanceof HTMLElement ? active : null;
|
||||||
|
s.setFocusRestoreElement(
|
||||||
|
(targetEl as HTMLElement | null) ?? activeEl ?? null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
s.setCommandCenterOpen(!s.commandCenterOpen);
|
s.setCommandCenterOpen(!s.commandCenterOpen);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user