| ... | ... | @@ -571,14 +571,24 @@ function eventPathContainsSelector(event: Event, selector: string): boolean { |
| 571 | 571 | return path.some((target) => target instanceof Element && target.closest(selector)); |
| 572 | 572 | } |
| 573 | 573 | |
| 574 | | function shouldTypeToFocusComposer(event: KeyboardEvent): boolean { |
| 575 | | if (event.defaultPrevented || event.isComposing) return false; |
| 576 | | if (event.metaKey || event.ctrlKey || event.altKey) return false; |
| 577 | | if (event.key.length !== 1) return false; |
| 578 | | |
| 574 | /** |
| 575 | * Whether input that landed outside any editable or interactive element |
| 576 | * should be redirected into the composer. Shared by type-to-focus and |
| 577 | * paste-to-focus so both honour the same surfaces. |
| 578 | */ |
| 579 | function shouldRedirectInputToComposer(event: Event): boolean { |
| 580 | if (event.defaultPrevented) return false; |
| 579 | 581 | if (eventPathContainsSelector(event, TYPE_TO_FOCUS_EDITABLE_SELECTOR)) return false; |
| 580 | 582 | if (eventPathContainsSelector(event, TYPE_TO_FOCUS_INTERACTIVE_SELECTOR)) return false; |
| 581 | 583 | if (document.querySelector(TYPE_TO_FOCUS_FLOATING_LAYER_SELECTOR)) return false; |
| 584 | return true; |
| 585 | } |
| 586 | |
| 587 | function 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; |
| 582 | 592 | |
| 583 | 593 | // The right-panel surface launcher claims its shortcut letters while it is |
| 584 | 594 | // visible (data attribute set in RightPanelTabs); those keys open surfaces |
| ... | ... | @@ -591,6 +601,17 @@ function shouldTypeToFocusComposer(event: KeyboardEvent): boolean { |
| 591 | 601 | return true; |
| 592 | 602 | } |
| 593 | 603 | |
| 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 | */ |
| 608 | function 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 | |
| 594 | 615 | function formatOutgoingPrompt(params: { |
| 595 | 616 | provider: ProviderDriverKind; |
| 596 | 617 | model: string | null; |
| ... | ... | @@ -5859,6 +5880,25 @@ function ChatViewContent(props: ChatViewProps) { |
| 5859 | 5880 | composerRef, |
| 5860 | 5881 | ]); |
| 5861 | 5882 | |
| 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 | 5902 | const onRevertToTurnCount = useCallback( |
| 5863 | 5903 | async (turnCount: number) => { |
| 5864 | 5904 | const localApi = readLocalApi(); |