fix(chat): keep the soft keyboard closed when a session becomes active on touch - #865
Merged
xintaofei merged 3 commits intoSep 30, 2026
Conversation
…e on touch Switching to another session pops the soft keyboard over the transcript on a phone. `MessageInput` auto-focuses the composer whenever its tab becomes active, and focusing a contenteditable raises the keyboard — so a switch that was meant to read another session's progress hides it instead, and the keyboard has to be dismissed by hand. Skip that auto-focus when the primary pointer is coarse, using the same `useIsCoarsePointer` signal the tab bar already uses for touch-only behaviour. Tapping the composer still focuses it exactly as before — natively inside the text, through the chrome-press handler in the padding — so the keyboard comes up on demand rather than on tab activation. A fine pointer keeps the current behaviour: opening a tab still lands the caret in the composer. Covered by two cases in message-input.test.tsx: a coarse pointer leaves the composer unfocused when the tab becomes active, a fine pointer still focuses it. Validation: eslint clean, tsc --noEmit clean, full vitest suite green (538 files / 8019 tests).
Author
|
Ready for review. Local validation on this branch: Behaviour verified against the built app as well (touch-emulated browser on the LAN web service): with the gate in place a real session switch leaves the composer unfocused 鈥?the transcript stays visible, no keyboard 鈥?while tapping the composer focuses it and typing works; a desktop viewport keeps the previous auto-focus behaviour. |
…ests The coarse-pointer case asserted one frame after the editor mounted, before the composer was even ready, so it passed with the gate removed. Both pointer kinds now seed a draft, wait for it to be restored (the restore and the auto-focus are scheduled by the same render, in that order), then wait the frame Tiptap's focus command defers the DOM focus to, and assert. The fine-pointer side proves those frames are enough, so the coarse side cannot pass by looking too early. Also covers switching to a session already open in the background, the isActive false -> true path a switch to an open tab takes. That case first checks the background composer holds no focus, so the focus seen after the switch is the switch's doing.
…focus The auto-focus gate subscribed every mounted composer to the coarse-pointer media query. Open tabs keep their composers mounted, so each one held a listener, and a pointer change re-rendered all of them and re-ran the effect, which moved focus into the active composer whenever the pointer turned fine (a 2-in-1 docking to its mouse). The effect now asks `isPrimaryPointerCoarse()` at the moment it would focus. The composer holds no listener, a pointer change renders nothing, and the effect is back to its original dependencies. Its pending frame is also cancelled when the tab goes inactive or a turn starts before it runs, as the draft restore above already does, so a tiled group's deactivated composer cannot take the caret. The tests stub the browser's media query rather than mocking the hook, so they hold whichever way the pointer is read, and cover both changes.
Owner
|
codeg work task |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On a phone, switching to another session pops the soft keyboard over the transcript.
MessageInputfocuses the composer whenever its tab becomes active:On desktop that is exactly what you want. On a touch device focusing a contenteditable raises the soft keyboard, so a switch that was meant to read another session's progress hides it instead — and the keyboard has to be dismissed by hand every time.
Reproduced on the 0.32.0 / 0.32.4 web build (LAN web service) in a touch-emulated browser: after choosing another session from the mobile session list,
document.activeElementbecomes the composer's contenteditable within ~400ms, i.e. the keyboard comes up.Fix
Skip that auto-focus when the primary pointer is coarse, using the same
useIsCoarsePointersignal the tab bar (tab-bar.tsx,file-workspace-tab-bar.tsx) already uses for touch-only behaviour.Tapping the composer still focuses it exactly as before — natively inside the text, and through the chrome-press handler in the padding — so the keyboard comes up on demand instead of on tab activation. A fine pointer keeps today's behaviour: opening a tab still lands the caret in the composer.
Tests
Two cases added to
src/components/chat/message-input.test.tsx:isActive→ the composer is not focusedisActive→ the composer is focused (guards the desktop behaviour)Validation
pnpm lint(eslint + prettier) — clean on the touched filespnpm test— full suite green: 538 files, 8019 teststsc --noEmit— cleanNotes
Verified against the built app as well: with the change applied the composer stays unfocused through a real session switch (the transcript is left visible), while tapping the composer focuses it and typing works — on both a phone-sized touch viewport and a desktop viewport.