Improve Cmd+K focus reliability on web

This commit is contained in:
Mohamed Boudra
2026-02-04 12:15:36 +07:00
parent d839716e0c
commit 48cecdb6b5
4 changed files with 99 additions and 15 deletions

View File

@@ -406,12 +406,41 @@ export function AgentInputArea({
if (!req) return;
if (req.agentKey !== `${serverId}:${agentId}`) return;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
messageInputRef.current?.focus();
let cancelled = false;
const deadlineMs = Date.now() + 1500;
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();
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]);
// Handle command selection from autocomplete

View File

@@ -62,6 +62,12 @@ export function CommandCenter() {
return filtered;
}, [agents, query]);
const agentKeyFromPathname = useMemo(() => {
const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)/);
if (!match) return null;
return `${match[1]}:${match[2]}`;
}, [pathname]);
useEffect(() => {
const prevOpen = prevOpenRef.current;
prevOpenRef.current = open;
@@ -72,18 +78,43 @@ export function CommandCenter() {
if (prevOpen && !didNavigateRef.current) {
const el = takeFocusRestoreElement();
if (el && el.isConnected) {
// Modal unmount can steal focus; restore on next tick.
const deadlineMs = Date.now() + 1500;
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(() => {
try {
el.focus();
} catch {
// ignore
}
});
requestAnimationFrame(tryRestore);
});
}
};
// Modal unmount can steal focus; restore on next tick (and retry).
requestAnimationFrame(() => {
requestAnimationFrame(tryRestore);
});
}
return;
@@ -95,7 +126,7 @@ export function CommandCenter() {
inputRef.current?.focus();
}, 0);
return () => clearTimeout(id);
}, [open, takeFocusRestoreElement]);
}, [agentKeyFromPathname, open, requestFocusChatInput, takeFocusRestoreElement]);
useEffect(() => {
if (!open) return;

View File

@@ -72,6 +72,11 @@ export interface MessageInputProps {
export interface MessageInputRef {
focus: () => 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;
@@ -134,6 +139,17 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
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 baselineInputHeightRef = useRef<number | null>(null);

View File

@@ -130,8 +130,16 @@ export function useGlobalKeyboardNav({
event.preventDefault();
const s = useKeyboardNavStore.getState();
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;
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);
return;