feat(settings): add streaming auto-follow toggle

New Streaming section on the Chat settings page with a checkbox that
controls whether the viewport follows new content while a response
streams. Default stays on. With it off, the anchored user message still
parks at the top on send, but no glide or end-follow correction runs and
the list's maintain-scroll-at-end stays disabled; the scroll-to-bottom
pill and session open keep scrolling explicitly.

Persisted through desktop settings like the other chat toggles (auto-save
diff, authoritative apply, sanitize), registered in settings search, and
localized in every locale.
This commit is contained in:
Bohdan Triapitsyn
2026-08-25 14:53:43 +03:00
parent 87c1ae809d
commit dac31ee026
19 changed files with 109 additions and 1 deletions
@@ -1021,6 +1021,10 @@ const TimelineList = React.memo(({
rowContext,
}: TimelineListProps) => {
const listRef = React.useRef<LegendListRef | null>(null);
// With streaming auto-follow off, content growth must never move the
// viewport; explicit commands (the scroll-to-bottom pill, session open)
// still scroll through the imperative handle.
const streamingAutoFollowEnabled = useUIStore((state) => state.streamingAutoFollowEnabled);
const isAtEndRef = React.useRef(true);
const setListRef = React.useCallback((list: LegendListRef | null) => {
@@ -1067,7 +1071,7 @@ const TimelineList = React.memo(({
contentInsetEndAdjustment={composerOverlayHeight}
// While a turn is anchored, the reserved end space — not the
// live edge — defines where the viewport rests.
maintainScrollAtEnd={anchoredEndSpace
maintainScrollAtEnd={anchoredEndSpace || !streamingAutoFollowEnabled
? false
: { animated: false, on: { dataChange: true, itemLayout: true, layout: true } }}
// Prepending older history must not move what the user is
@@ -302,6 +302,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const sessionGoalDefaultBudget = useUIStore(state => state.sessionGoalDefaultBudget);
const setSessionGoalDefaultBudget = useUIStore(state => state.setSessionGoalDefaultBudget);
const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces);
const streamingAutoFollowEnabled = useUIStore(state => state.streamingAutoFollowEnabled);
const setStreamingAutoFollowEnabled = useUIStore(state => state.setStreamingAutoFollowEnabled);
const collapsibleThinkingBlocks = useUIStore(state => state.collapsibleThinkingBlocks);
const setCollapsibleThinkingBlocks = useUIStore(state => state.setCollapsibleThinkingBlocks);
@@ -1839,6 +1841,20 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
)}
</SettingsSection>
)}
<SettingsSection
title={t('settings.openchamber.visual.section.streaming')}
settingsItem="chat.streaming"
contentClassName={SETTINGS_OPTION_STACK_CLASS}
>
<SettingsCheckboxRow
checked={streamingAutoFollowEnabled}
onChange={setStreamingAutoFollowEnabled}
label={t('settings.openchamber.visual.field.streamingAutoFollow')}
ariaLabel={t('settings.openchamber.visual.field.streamingAutoFollowAria')}
info={t('settings.openchamber.visual.field.streamingAutoFollowInfo')}
settingsItem="chat.streaming-auto-follow"
/>
</SettingsSection>
{(shouldShow('collapsibleUserMessages') || shouldShow('stickyUserHeader') || (shouldShow('promptNavigatorEnabled') && !isVSCode) || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('codeBlockLineWrap')) && (
<SettingsSection