Repository navigation
feat: setting to anchor sent messages at the top of the timeline - #42
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ceb6b56d12
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const anchorIndex = items.findIndex((item) => getAnchorId(item) === anchorId); | ||
| return anchorIndex === -1 | ||
| ? undefined | ||
| : { anchorIndex, anchorOffset: options.anchorOffset ?? CHAT_LIST_ANCHOR_OFFSET }; |
There was a problem hiding this comment.
Update the stale follow-up anchoring test
Searching every row makes the existing MessagesTimeline.test.tsx case “does not reserve end space for a follow-up user message” deterministically receive anchorIndex: 1, but that test still asserts that no anchor is rendered, end maintenance remains enabled, and onAnchorReady is never called. Any web test run containing that file will therefore fail; update the test expectations to cover the newly supported later-message anchor.
Useful? React with 👍 / 👎.
| if (input.turnAnchor === "top") { | ||
| return input.queuedMessageCount > 0 ? input.currentAnchorMessageId : input.submittedMessageId; | ||
| } |
There was a problem hiding this comment.
Keep the viewport still for the first queued mobile follow-up
When the agent is already running and the mobile outbox is initially empty, selectedThreadQueueCount is captured as 0 before onSendMessage synchronously enqueues the new message. The top branch consequently returns the new message ID and scrolls that queued row to the top even though it is waiting behind the active turn, contradicting the intended behavior that queued messages retain the current framing. The decision needs to account for an active turn, not only messages already present in the queue.
Useful? React with 👍 / 👎.
| if (shouldAnchorFirstMessage || settings.chatTurnAnchor === "top") { | ||
| anchorSentMessage( | ||
| scopeThreadRef(activeThread.environmentId, threadIdForSend), | ||
| messageIdForSend, |
There was a problem hiding this comment.
Ignore earlier tool activity when anchoring a steer
When a user sends a direct steer while the current turn is already running, this activates the new anchor against that same activeRunningTurnId. The subsequent layout effect scans all existing timeline entries for that turn, so any tool call that occurred before the steer satisfies shouldReleaseTimelineAnchorForToolActivity and immediately calls scrollToEnd(). Thus Top silently fails for steers sent after tool activity; anchor release must distinguish activity added after the newly anchored message.
Useful? React with 👍 / 👎.
| // Holds a message the user just sent near the top of the viewport so its turn | ||
| // reads downward from it. ChatView drives the streaming scrolls itself while | ||
| // this "anchoring-new-turn" mode lasts; see releaseChatTimelineAnchor. | ||
| const anchorSentMessage = useCallback((threadRef: ScopedThreadRef, messageId: MessageId) => { |
There was a problem hiding this comment.
Apply top anchoring to compact messages
The new anchoring helper is wired into ordinary and plan-follow-up sends, but the separate onCompactContext send path still unconditionally calls scrollToEnd() after adding its /compact user message. Consequently, choosing Top has no effect when the user sends Compact even though that action produces a sent-message row; route this send path through the same setting-aware anchor decision.
AGENTS.md reference: AGENTS.md:L67-L70
Useful? React with 👍 / 👎.
| <SettingsRow | ||
| {...searchableSetting("sent-message-position")} | ||
| description="Top holds each message you send at the top of the view so the reply reads downward. Bottom keeps the conversation pinned to the latest message." |
There was a problem hiding this comment.
Describe Bottom's first-message exception
The setting claims that Bottom keeps the conversation pinned to the latest message, and the new user guide repeats that default behavior, but shouldAnchorFirstMessage deliberately overrides Bottom and holds the first prompt at the top. Since preserving first-turn anchoring is explicit in this change, the setting and documentation should state that Bottom applies only to later sends; otherwise users cannot infer what either option will do on a new thread.
AGENTS.md reference: AGENTS.md:L133-L134
Useful? React with 👍 / 👎.
Scrolling up and back down after a top-anchored send released the anchor, collapsing its end space and dropping the message to the bottom of the viewport. Returning to the live edge (by gesture or the scroll-to-end pill) now resumes the anchored framing while the send-time anchor is installed.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8dd7dad312
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| **Settings → General → Sent message position**, choose **Top** to hold each | ||
| message you send at the top of the view while the reply streams in below it. |
There was a problem hiding this comment.
Clarify that tool activity releases Top anchoring
On web and desktop, any turn that emits tool activity satisfies shouldReleaseTimelineAnchorForToolActivity and immediately calls scrollToEnd(), so the sent message stops being held at the top even though the reply is still streaming. Since tool-using turns are common and this release is intentional, the guide's unconditional description gives users the wrong expectation; document that Top lasts only until tool activity begins.
AGENTS.md reference: AGENTS.md:L133-L134
Useful? React with 👍 / 👎.
| const anchorSentMessage = useCallback((threadRef: ScopedThreadRef, messageId: MessageId) => { | ||
| isAtEndRef.current = true; |
There was a problem hiding this comment.
Cancel position restoration before anchoring a send
When a user opens a thread with a remembered non-end position and sends before MessagesTimeline finishes stabilizing that restoration, this path installs the new send anchor without invoking cancelPositionRestoreRef, unlike scrollToEnd. Appending the optimistic row reruns the restoration effect, which can scroll back to the saved history row and override the requested Top framing; cancel the pending restoration before switching into anchoring-new-turn mode.
Useful? React with 👍 / 👎.
…he composer LegendList reports row positions relative to its rows layer, which starts one list header below the content start, while the scroll offset is a content offset. The anchored-turn reveal mixed the two, so every streaming scroll landed a header short and the end of the reply sat under the composer overlay. The list now reports its header size and the anchored metrics convert rows into scroll offsets with it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e4175496f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const resumeAnchor = activeTimelineAnchorIndexRef.current !== null; | ||
| timelineScrollModeRef.current = resumeAnchor ? "anchoring-new-turn" : "following-end"; |
There was a problem hiding this comment.
Release anchors after backgrounded tool activity
If the user scrolls away while an anchored turn is running and that turn emits tool activity and finishes before they return, the tool-release effect is skipped while live follow is disabled and can no longer detect the activity once activeRunningTurnId becomes null. This branch then resumes the preserved anchor solely because its index exists, leaving the completed tool-using turn pinned even though tool activity is supposed to release the anchor; track that the anchored turn saw tool activity or clear the anchor before resuming it.
Useful? React with 👍 / 👎.
When you send a message, the timeline pins to the bottom and the reply streams in right above the composer. Reading a long answer that way means watching the bottom edge. T3 Code already holds the first message of a thread near the top so its turn reads downward; there was no way to get that for every message.
This adds a client setting, Settings → General → Sent message position (Bottom / Top, default Bottom). With Top, every send that starts a turn reuses the existing first-message anchoring: the message holds at the top of the viewport and the reply streams below it. Mobile gets the same choice as a device-local switch under Settings → Thread behavior → Sent messages at top.
Behavior notes:
resolveChatListAnchoredEndSpacenow matches the anchor at any index; it was the only place encoding the first-message rule.24pxabove the composer. It used to land one list header short (row positions exclude the header, scroll offsets include it), leaving the tail of the reply and the working indicator under the composer overlay.QA in the Browser panel against a snapshot of real data (web): settings row, reset, and search; Top on a long thread (anchors, then follows once the reply overflows); wheel-scroll away mid-stream holds position and shows the Scroll to end pill, pill returns and re-follows; scroll up then back down while idle keeps the message at the top, and mid-stream resumes follow with the pill hidden; Bottom mode unchanged; Top on a short thread; queued dispatch follows the end; steer follows the stream; thread switch and back restores position.
Made by Fable 5.1 using Claude Code.
🤖 Generated with Claude Code