authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-03 17:04:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 17:04:39-07:00
log6382268323f86e01b2d40206bcf33beccb3fb64a
treeffde84f8f29388705b40b0e2bf64e328a0ba655d
parentf8a14b28ff80a92d8f4df9a9aa53bfacb8195199
signature Signed by PGP key B5690EEEBB952194

fix(web): let paste expand a resting composer (#9498)

Co-authored-by: Claude Code <noreply@anthropic.com>

1 files changed, 45 insertions(+), 5 deletions(-)

apps/web/src/components/ChatView.tsx+45-5
...@@ -571,14 +571,24 @@ function eventPathContainsSelector(event: Event, selector: string): boolean {...@@ -571,14 +571,24 @@ function eventPathContainsSelector(event: Event, selector: string): boolean {
571 return path.some((target) => target instanceof Element && target.closest(selector));571 return path.some((target) => target instanceof Element && target.closest(selector));
572}572}
573573
574function shouldTypeToFocusComposer(event: KeyboardEvent): boolean {574/**
575 if (event.defaultPrevented || event.isComposing) return false;575 * Whether input that landed outside any editable or interactive element
576 if (event.metaKey || event.ctrlKey || event.altKey) return false;576 * should be redirected into the composer. Shared by type-to-focus and
577 if (event.key.length !== 1) return false;577 * paste-to-focus so both honour the same surfaces.
578578 */
579function shouldRedirectInputToComposer(event: Event): boolean {
580 if (event.defaultPrevented) return false;
579 if (eventPathContainsSelector(event, TYPE_TO_FOCUS_EDITABLE_SELECTOR)) return false;581 if (eventPathContainsSelector(event, TYPE_TO_FOCUS_EDITABLE_SELECTOR)) return false;
580 if (eventPathContainsSelector(event, TYPE_TO_FOCUS_INTERACTIVE_SELECTOR)) return false;582 if (eventPathContainsSelector(event, TYPE_TO_FOCUS_INTERACTIVE_SELECTOR)) return false;
581 if (document.querySelector(TYPE_TO_FOCUS_FLOATING_LAYER_SELECTOR)) return false;583 if (document.querySelector(TYPE_TO_FOCUS_FLOATING_LAYER_SELECTOR)) return false;
584 return true;
585}
586
587function shouldTypeToFocusComposer(event: KeyboardEvent): boolean {
588 if (event.isComposing) return false;
589 if (event.metaKey || event.ctrlKey || event.altKey) return false;
590 if (event.key.length !== 1) return false;
591 if (!shouldRedirectInputToComposer(event)) return false;
582592
583 // The right-panel surface launcher claims its shortcut letters while it is593 // The right-panel surface launcher claims its shortcut letters while it is
584 // visible (data attribute set in RightPanelTabs); those keys open surfaces594 // visible (data attribute set in RightPanelTabs); those keys open surfaces
...@@ -591,6 +601,17 @@ function shouldTypeToFocusComposer(event: KeyboardEvent): boolean {...@@ -591,6 +601,17 @@ function shouldTypeToFocusComposer(event: KeyboardEvent): boolean {
591 return true;601 return true;
592}602}
593603
604/**
605 * Plain text pasted with nothing editable focused, such as after the resting
606 * composer blurred. Files are left to the composer's own paste handler.
607 */
608function pasteTextToFocusComposer(event: ClipboardEvent): string | null {
609 if (!event.clipboardData || event.clipboardData.files.length > 0) return null;
610 if (!shouldRedirectInputToComposer(event)) return null;
611 const text = event.clipboardData.getData("text/plain");
612 return text.length > 0 ? text : null;
613}
614
594function formatOutgoingPrompt(params: {615function formatOutgoingPrompt(params: {
595 provider: ProviderDriverKind;616 provider: ProviderDriverKind;
596 model: string | null;617 model: string | null;
...@@ -5859,6 +5880,25 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5859,6 +5880,25 @@ function ChatViewContent(props: ChatViewProps) {
5859 composerRef,5880 composerRef,
5860 ]);5881 ]);
58615882
5883 // Paste-to-focus: the resting composer blurs on a click into the timeline,
5884 // so a paste that follows has no editable target and would be dropped.
5885 // Route it to the composer like a typed key, which also expands it.
5886 useEffect(() => {
5887 const handler = (event: ClipboardEvent) => {
5888 if (!activeThreadId || isCommandPaletteOpen()) return;
5889 if (getTerminalFocusOwner() !== null) return;
5890 if (composerRef.current?.isModelPickerOpen()) return;
5891 const text = pasteTextToFocusComposer(event);
5892 if (text === null) return;
5893 if (composerRef.current?.insertTextAtEnd(text)) {
5894 event.preventDefault();
5895 event.stopPropagation();
5896 }
5897 };
5898 window.addEventListener("paste", handler, true);
5899 return () => window.removeEventListener("paste", handler, true);
5900 }, [activeThreadId, composerRef]);
5901
5862 const onRevertToTurnCount = useCallback(5902 const onRevertToTurnCount = useCallback(
5863 async (turnCount: number) => {5903 async (turnCount: number) => {
5864 const localApi = readLocalApi();5904 const localApi = readLocalApi();