Skip to content

Commit b7cd0c2

Browse files
committed
fix: focus the chat textarea when clicking the composer action row
1 parent 3b3d06d commit b7cd0c2

1 file changed

Lines changed: 38 additions & 2 deletions

File tree

copilot/src/components/chat/chat_pane.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ import type { EmbedTools, SimplePDFToolName } from '@simplepdf/react-embed-pdf/a
66
import { getRouteApi } from '@tanstack/react-router'
77
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, type UIMessage } from 'ai'
88
import { ArrowUp, Mic, X } from 'lucide-react'
9-
import { type ReactElement, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
9+
import {
10+
type ReactElement,
11+
type MouseEvent as ReactMouseEvent,
12+
useCallback,
13+
useEffect,
14+
useLayoutEffect,
15+
useMemo,
16+
useRef,
17+
useState,
18+
} from 'react'
1019
import { useTranslation } from 'react-i18next'
1120
import { useStickToBottom } from 'use-stick-to-bottom'
1221
import {
@@ -999,6 +1008,11 @@ export const ChatPane = ({
9991008
return t('chat.subtitleWaiting')
10001009
}, [hasActiveModel, requiresUserUpload, t])
10011010
const inputPlaceholder = canSend ? t('chat.inputPlaceholderReady') : chatStatusMessage
1011+
// The composer only behaves as a text target when the textarea is mounted
1012+
// (idle, not recording) and actually editable. Gates both the text cursor
1013+
// and the click-to-focus redirect so neither signals "type here" while the
1014+
// input is disabled or swapped out for the voice bar.
1015+
const isComposerInteractive = canSend && voice.status === 'idle'
10021016

10031017
useEffect(() => {
10041018
if (canSend) {
@@ -1063,6 +1077,27 @@ export const ChatPane = ({
10631077
[sendMessage, voice.dismissError],
10641078
)
10651079

1080+
// Make the whole composer act as one big input target: a mousedown landing
1081+
// on padding / empty space (anything that isn't the textarea or a control)
1082+
// redirects focus to the textarea. preventDefault stops the textarea from
1083+
// blurring first, so there is no focus flicker.
1084+
const handleComposerMouseDown = useCallback(
1085+
(event: ReactMouseEvent<HTMLFormElement>): void => {
1086+
if (!isComposerInteractive) {
1087+
return
1088+
}
1089+
if (!(event.target instanceof HTMLElement)) {
1090+
return
1091+
}
1092+
if (event.target.closest('button, textarea') !== null) {
1093+
return
1094+
}
1095+
event.preventDefault()
1096+
inputRef.current?.focus()
1097+
},
1098+
[isComposerInteractive],
1099+
)
1100+
10661101
return (
10671102
<div className="flex h-full flex-col">
10681103
<ChatPaneHeader
@@ -1190,7 +1225,8 @@ export const ChatPane = ({
11901225
event.preventDefault()
11911226
handleSend(draft)
11921227
}}
1193-
className="flex items-end gap-2"
1228+
onMouseDown={handleComposerMouseDown}
1229+
className={`flex items-end gap-2 ${isComposerInteractive ? 'cursor-text' : ''}`}
11941230
>
11951231
{/* One composer box (P070-03) shared by every voice state, so
11961232
clicking the mic causes NO layout shift: the textarea ↔

0 commit comments

Comments
 (0)