Skip to content

feat: setting to anchor sent messages at the top of the timeline - #42

Merged
connorch merged 4 commits into
mainfrom
connorch/add-thread-scroll-anchor-setting
Oct 2, 2026
Merged

connorch merged 4 commits into
mainfrom
connorch/add-thread-scroll-anchor-setting

Conversation

@connorch

@connorch connorch commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

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:

  • Queued follow-ups and steers stay with the live stream. A steer's reply keeps streaming above it, so there is nothing to read downward from.
  • The anchor still releases on tool activity, exactly as the first-message anchor does today (fix(web): stop tool calls from leaving a blank page in threads pingdotgg/t3code#7971 upstream), so this is "first-message behavior for every new turn".
  • Scrolling away and back to the live edge (by gesture or the Scroll to end pill) resumes the anchored framing instead of releasing it. Releasing collapsed the end space and dropped the anchored message to the bottom of the viewport, which read as a jump.
  • resolveChatListAnchoredEndSpace now matches the anchor at any index; it was the only place encoding the first-message rule.
  • While the anchored reply streams, the reveal scroll now lands with the reply's end 24px above 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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T19:25:43.812849Z e417549 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +27 to +30
const anchorIndex = items.findIndex((item) => getAnchorId(item) === anchorId);
return anchorIndex === -1
? undefined
: { anchorIndex, anchorOffset: options.anchorOffset ?? CHAT_LIST_ANCHOR_OFFSET };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +66 to +68
if (input.turnAnchor === "top") {
return input.queuedMessageCount > 0 ? input.currentAnchorMessageId : input.submittedMessageId;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment on lines +8257 to +8260
if (shouldAnchorFirstMessage || settings.chatTurnAnchor === "top") {
anchorSentMessage(
scopeThreadRef(activeThread.environmentId, threadIdForSend),
messageIdForSend,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +5421 to +5424
// 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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +2805 to +2807
<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."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread docs/user/composer.md
Comment on lines +55 to +56
**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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +5438 to 5439
const anchorSentMessage = useCallback((threadRef: ScopedThreadRef, messageId: MessageId) => {
isAtEndRef.current = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +5708 to +5709
const resumeAnchor = activeTimelineAnchorIndexRef.current !== null;
timelineScrollModeRef.current = resumeAnchor ? "anchoring-new-turn" : "following-end";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@connorch
connorch merged commit 60a5522 into main Oct 2, 2026
7 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant