Skip to content

[Bug]: Typing with the composer unfocused no longer goes to the composer #13403

Description

@deathemperor

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

  1. Open any thread on web or desktop.
  2. Click an empty spot in the timeline so nothing editable has focus.
  3. Type hi.

Expected behavior

The composer takes focus and contains hi (type-to-focus).

Actual behavior

Nothing happens. Keys are dropped. The right panel launcher's letter shortcuts (T, B, F, M…) are dead for the same reason.

There are two causes:

  1. fix(web): keep narrow chat headers readable and aligned #12453 made the chat header's actions MenuPopup keepMounted, so a closed [data-slot="menu-popup"] now stays in the DOM on every thread. shouldRedirectInputToComposer in ChatView.tsx and the launcher's capture handler in RightPanelTabs.tsx both bail on document.querySelector(<popup slots>) without checking whether the popup is open, so they always bail. The dialog entries in the same selector already use :is([data-open],[data-ending-style]); the menu/select/popover/combobox/autocomplete entries don't.
  2. With (1) fixed, hi becomes ih. The first key goes through insertTextAtEnd. The controlled layout effect in ComposerPromptEditorTiptap stores the ProseMirror selection after it while the editor is unfocused. Then focusAt calls editor.view.dom.focus(), which leaves the browser caret at the start. setTextSelection to the already-stored position does nothing, so the DOM caret never moves and the next key lands before the first. This has been there since feat(web): enable rich text composer by default #12160.

Impact

Major degradation or frequent failure

Version or commit

main at b2b43be (desktop 0.0.42 nightly shows it too)

Environment

macOS 26, desktop app and web (Chromium)

Workaround

Click the composer before typing.

Activity

  1. juliusmarminge commented on Sep 24, 2026

    @juliusmarminge
    Member

    Confirmed on main at b2b43be. Both causes match the current web/desktop chat UI. Mobile uses a separate composer and is not on this path.

    Closed header menu always blocks type-to-focus and launcher letters. #12453 renders the chat header actions MenuPopup with keepMounted (apps/web/src/components/chat/ChatHeader.tsx). Base UI’s menu portal stays in the DOM when keepMounted is set, and a closed popup carries data-closed rather than data-open. That popup is mounted for every thread, including wide layouts where the trigger is hidden.

    shouldRedirectInputToComposer in apps/web/src/components/ChatView.tsx bails when document.querySelector finds any [data-slot="menu-popup"]. The same unguarded list is LAUNCHER_SHORTCUT_BLOCKING_LAYERS in apps/web/src/components/RightPanelTabs.tsx, which is why the launcher letters (T, B, F, M, …) never fire. Dialog, alert-dialog, command-dialog, sheet, and mobile sidebar entries in the ChatView selector already require :is([data-open],[data-ending-style]). The menu, select, popover, combobox, and autocomplete entries do not. The timeline keyboard-scroll handler in ChatView uses that same selector, so it is gated by the closed menu as well.

    Other menus unmount when closed. The header menu is the one that makes the check succeed on every thread. An actually open popup, including one animating out (data-ending-style), should keep blocking.

    After that gate is fixed, the first typed character and the caret disagree. This path has been in ComposerPromptEditorTiptap since #12160. The first key goes through insertTextAtEnd → applyPromptReplacement, which schedules focusAt on the next frame. The controlled layout effect runs first and calls setTextSelection while the editor is unfocused. In prosemirror-view 1.42.3, selectionToDOM returns immediately unless the view has focus, so the ProseMirror selection is stored at the end and the DOM caret is not moved. focusAt then calls editor.view.dom.focus() rather than view.focus() (which would sync the DOM selection after focusing). The following setTextSelection targets the selection already stored, and updateStateInner skips selectionToDOM when the document and selection are unchanged. The browser caret stays at the start, so the next key lands in front of the first (hi → ih). A single character still looks right, because only the second key is a native insert.

    Workaround is unchanged: click the composer before typing.

  2. added
    acceptedfeature request accepted
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions