Skip to content

Focus the requested chat composer when it appears #294

Description

@HMarzban

Problem

Starting a comment asks the chat composer to take focus. So does the desktop TOC chat button. The request can fail, and nothing reports it. The composer then shows with no caret, and the user must click it before typing.

On 14ab7f9c1. Paths are under apps/webapp/src/.

Two paths lose the focus.

A slow chat load.

  • The focus request polls chatRoom.editorInstance (services/openHeadingChatroom.ts:50-56, :58-60). The composer sets that field when its editor mounts (components/chatroom/components/MessageComposer/MessageComposer.tsx:215-223).
  • The composer does not mount while isFeedReady is false. A skeleton shows instead (components/chatroom/components/ChannelComposer/ChannelComposer.tsx:32-34).
  • isFeedReady waits for the channel data and the message window (components/chatroom/ChatroomContext.tsx:288). The channel data waits on up to three requests in series: upsertChannel, fetchChannelInitialData, and joinChannel (components/chatroom/hooks/useChannelMetadata.ts:34, :50, :60). The message window is one more request (components/chatroom/hooks/useChannelMessages.ts:73). None of them has a timeout.
  • FOCUS_RETRY allows six attempts (services/openHeadingChatroom.ts:44). The retry uses a factor of 2 and jitter by default (utils/retryWithBackoff.ts:26-27). So the last attempt runs 2.3 to 4.6 s after the open (:63-72).
  • After the last attempt, retryWithBackoff resolves success: false (:37-43). focusChatComposerWithRetry drops that promise, so nothing tries again and nothing is logged.

When the load takes longer than the last attempt, the composer appears without focus.

A heading switch while the chat is open.

  • The chat subtree is keyed by heading (components/chatroom/Chatroom.tsx:34-38). So a new heading mounts a new composer, and that composer waits for its own load.
  • The store writes on the open path (services/openHeadingChatroom.ts:133, :97) schedule a React render. They do not run it. The first attempt runs immediately (utils/retryWithBackoff.ts:35).
  • So the first attempt still finds the previous heading's editor in editorInstance. It calls focus on that editor and returns true, so the retry stops. That composer then unmounts, and the new composer mounts with no focus.

This path does not depend on network speed.

Nothing is lost except focus. The comment anchor is stored before the request (services/openHeadingChatroom.ts:140-145), so the comment bar still shows. In comment mode, the composer does not focus itself on mount (components/chatroom/components/MessageComposer/hooks/useComposerDraft.ts:36-38). On desktop, the composer focuses itself when it restores a saved draft (:50-56). So the TOC path loses focus only when the composer does not restore a saved draft.

Steps to reproduce

A slow load:

  1. On desktop, sign in and open a pad. Close the chat.
  2. In DevTools, add a network throttling profile with 10,000 ms latency, and select it.
  3. Select some pad text and use its Comment action.
  4. Wait for the composer skeleton to turn into the composer. See the comment bar, but no caret in the composer. Typed keys do not reach it.

A heading switch, with no throttling:

  1. On desktop, sign in and open a pad. Open the chat of heading A, and wait for its composer.
  2. Select text under heading B, and use its Comment action.
  3. When the composer of heading B appears, see that it has no focus.

Acceptance criteria

  • With the network throttled to 10,000 ms latency after the pad loads, a comment start ends with the new composer focused. It is in comment mode, on the same anchor.
  • With the chat of heading A open, a comment start under heading B ends with the composer of heading B focused, in comment mode.
  • On desktop, a click on a TOC row's chat button ends with that composer focused. The heading has no saved draft, and its chat is not the open one. This holds with a fast load and with the throttled load above.
  • A focus request ends when the user closes the chat or opens another heading before the composer appears. No composer takes focus from that request later.
  • If the user focuses the pad or another field while the load is pending, the later mount does not take focus.
  • For a heading with no saved draft, opens that do not ask for focus still leave the composer unfocused. This covers the heading hover chat button, the breadcrumb, a bookmark, and a notification. It also covers a chat link in the pad, a URL deep link, and the TOC on a phone.
  • A comment start on the heading whose composer already shows still focuses it immediately.
  • The readiness rule for the chat open in the webapp agent docs matches the result.

Agent Brief

Category: bug
Summary: A chat open that asks for focus must focus the requested heading's composer when it mounts, unless the user moved focus first.

Current behavior:
The open path polls chatRoom.editorInstance through retryWithBackoff for about 2.3 to 4.6 s, then stops and reports nothing. The composer mounts only when isFeedReady is true, and that load has no upper bound. On a heading switch with the chat open, the first attempt finds the previous heading's editor, calls focus on it, and stops.

Desired behavior:
The open stores one focus request for the target heading. The composer of that heading takes focus when it mounts. If it already shows when the request arrives, it takes focus at once. It clears the request either way. The previous heading's composer never takes the request. Closing the chat or opening another heading also clears it. Focus does not depend on a timed retry.

Key interfaces:

  • openHeadingChatroom() — the comment intent and the focusEditor option both ask for focus.
  • focusChatComposerWithRetry(), FOCUS_RETRY, and retryWithBackoff() with its RetryResult.
  • The chat room store: chatRoom.editorInstance, switchChatRoom(), setChatRoom(), and destroyChatRoom().
  • MessageComposer — its mount effect writes editorInstance to the chat room store.
  • isFeedReady in ChatroomContext, and the skeleton gate in ChannelComposer.
  • useComposerDraft() — on desktop it focuses after a draft restore. It skips that in comment mode.

Out of scope

Notes

The insert wrapper drops its result the same way, but no user can reach it. It logs each retry, not the final drop. It runs only when insertContent is set (apps/webapp/src/services/openHeadingChatroom.ts:62-78, :163). None of the 12 CHAT_OPEN publish sites in apps/webapp/src sets it. The one spread, in tocActions.openChatroom, adds only focusEditor.

After this change and #274, nothing calls retryWithBackoff. The change that lands last deletes it.

The webapp agent docs tie the first retry delay to the pane's 200 ms height transition, not to the chat load (apps/webapp/CLAUDE.md:107). The same line forbids a four-state open lifecycle and a setSheetState force-write. One pending focus request for one heading is neither. Update that line in the same change.

The heading-switch path depends on React scheduling. pubsub-js 1.9.5 delivers publish in a setTimeout. A store write in that callback schedules the React render in a microtask. So the first attempt runs before the old composer unmounts.

On a phone, the pane opens from height 0 over 200 ms (apps/webapp/src/components/pages/document/components/chat/ChatPane.tsx:103-109). A fast load can mount the composer inside that window. Check that focus then does not scroll the pane.

Coordinate with #272. It types editorInstance and the other chat room store fields. Give a new request field a real type from the start.

Coordinate with #268. It cites the overlay dismissal in the same comment-open function (apps/webapp/src/services/openHeadingChatroom.ts:139).

#150 asks for a real readiness signal in place of the 800 ms timer for URL deep links. Neither issue closes the other.

The evidence is a code trace on 14ab7f9c1. Two verifiers confirmed the retry result and the unbounded skeleton phase. The heading-switch path also ran in a scratch harness with real React, zustand, pubsub-js, the repo's retryWithBackoff, and a stand-in composer. The first attempt focused the previous heading's editor, and the retry stopped. Neither path was reproduced in a browser.

Activity

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

    ChatRelated to chat featuresUIbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions