You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
On desktop, sign in and open a pad. Close the chat.
In DevTools, add a network throttling profile with 10,000 ms latency, and select it.
Select some pad text and use its Comment action.
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:
On desktop, sign in and open a pad. Open the chat of heading A, and wait for its composer.
Select text under heading B, and use its Comment action.
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.
insertChatComposerContentWithRetry and the insertContent field on the chat open event. They use the same retry, but no publisher sets insertContent. Remove dead chat composer code #274 removes them. Until it does, keep FOCUS_RETRY for the insert wrapper.
The soft keyboard on a phone. A focus call that runs after the tap may not open it. This issue is about focus only.
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.
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 underapps/webapp/src/.Two paths lose the focus.
A slow chat load.
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).isFeedReadyis false. A skeleton shows instead (components/chatroom/components/ChannelComposer/ChannelComposer.tsx:32-34).isFeedReadywaits 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, andjoinChannel(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_RETRYallows 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).retryWithBackoffresolvessuccess: false(:37-43).focusChatComposerWithRetrydrops 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.
components/chatroom/Chatroom.tsx:34-38). So a new heading mounts a new composer, and that composer waits for its own load.services/openHeadingChatroom.ts:133,:97) schedule a React render. They do not run it. The first attempt runs immediately (utils/retryWithBackoff.ts:35).editorInstance. It callsfocuson that editor and returnstrue, 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:
A heading switch, with no throttling:
Acceptance criteria
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.editorInstancethroughretryWithBackofffor about 2.3 to 4.6 s, then stops and reports nothing. The composer mounts only whenisFeedReadyis 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, callsfocuson 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 thefocusEditoroption both ask for focus.focusChatComposerWithRetry(),FOCUS_RETRY, andretryWithBackoff()with itsRetryResult.chatRoom.editorInstance,switchChatRoom(),setChatRoom(), anddestroyChatRoom().MessageComposer— its mount effect writeseditorInstanceto the chat room store.isFeedReadyinChatroomContext, and the skeleton gate inChannelComposer.useComposerDraft()— on desktop it focuses after a draft restore. It skips that in comment mode.Out of scope
insertChatComposerContentWithRetryand theinsertContentfield on the chat open event. They use the same retry, but no publisher setsinsertContent. Remove dead chat composer code #274 removes them. Until it does, keepFOCUS_RETRYfor the insert wrapper.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
insertContentis set (apps/webapp/src/services/openHeadingChatroom.ts:62-78,:163). None of the 12CHAT_OPENpublish sites inapps/webapp/srcsets it. The one spread, intocActions.openChatroom, adds onlyfocusEditor.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 asetSheetStateforce-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-js1.9.5 deliverspublishin asetTimeout. 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
editorInstanceand 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'sretryWithBackoff, 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.