Preserve chat scroll position across workspace switches (#2838)
* fix(app): preserve chat scroll across workspace switches Hidden retained panels can emit geometry-driven scroll events while collapsed, which previously looked like user intent and disabled output following. Keep the retained DOM mounted, suspend its inactive scroll work, and only detach from the bottom after explicit input. * fix(app): classify chat scroll intent at input A held pointer is not evidence that a geometry-driven scroll was user initiated. Detach output following directly from upward wheel, key, touch, or mouse-drag input, and keep ordinary scroll events responsible only for metrics and bottom reattachment. * fix(app): keep editing keys out of scroll intent Question inputs render inside the chat viewport, so their caret-navigation keys bubble through the same keyboard boundary. Treat keys from editable descendants as editing intent while preserving viewport-level keyboard scrolling. * fix(app): exclude zoom gestures from scroll intent Ctrl-wheel and trackpad pinch events can carry a negative wheel delta without scrolling chat content. Keep those browser zoom gestures from disabling output following. * fix(app): require pointer-driven chat movement Mouse movement alone is not viewport scroll intent. Correlate actual upward scroll with short-lived content-drag evidence or a scrollbar-gutter gesture, while excluding embedded editors and retaining geometry-only following. * fix(app): recognize overlay chat scrollbars * fix(app): scope scroll intent to chat viewport * fix(app): rearm chat history at scroll start * fix(app): track chat autoscroll intent * fix(app): separate pointer scroll gestures
This commit is contained in:
@@ -269,6 +269,14 @@ const AGENT_CAPABILITY_FLAG_KEYS: (keyof AgentCapabilityFlags)[] = [
|
||||
];
|
||||
|
||||
const EMPTY_STREAM_HEAD: StreamItem[] = [];
|
||||
|
||||
function useRetainedValue<T>(value: T, active: boolean): T {
|
||||
const retainedRef = useRef(value);
|
||||
if (active) {
|
||||
retainedRef.current = value;
|
||||
}
|
||||
return active ? value : retainedRef.current;
|
||||
}
|
||||
const EMPTY_PENDING_MESSAGE_SUBMISSIONS: readonly PendingMessageSubmission[] = [];
|
||||
const GROUPED_TOOL_CALL_DETAIL_MAX_HEIGHT = 200;
|
||||
|
||||
@@ -465,20 +473,15 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
});
|
||||
});
|
||||
|
||||
// Freeze stream data while this tab slot is hidden to prevent offscreen FlatList
|
||||
// cell-window renders on every 48ms flush from background agents.
|
||||
// Freeze stream presentation while this tab slot is hidden to prevent offscreen
|
||||
// cell-window and turn-lifecycle renders from background agents.
|
||||
// When isActive flips back to true, the context change triggers a re-render and
|
||||
// the component reads the current (fresh) streamItems/streamHead from props.
|
||||
const isActive = useRetainedPanelActive();
|
||||
const frozenStreamItemsRef = useRef(streamItems);
|
||||
const frozenStreamHeadRef = useRef(streamHead);
|
||||
if (isActive) {
|
||||
frozenStreamItemsRef.current = streamItems;
|
||||
frozenStreamHeadRef.current = streamHead;
|
||||
}
|
||||
const effectiveStreamItems = isActive ? streamItems : frozenStreamItemsRef.current;
|
||||
const effectiveStreamHead = isActive ? streamHead : frozenStreamHeadRef.current;
|
||||
const isTurnActive = turnPresentation.isActive;
|
||||
const effectiveStreamItems = useRetainedValue(streamItems, isActive);
|
||||
const effectiveStreamHead = useRetainedValue(streamHead, isActive);
|
||||
const effectiveTurnPresentation = useRetainedValue(turnPresentation, isActive);
|
||||
const isTurnActive = effectiveTurnPresentation.isActive;
|
||||
// Keep retained history outside the 48ms live-head flush path.
|
||||
const preparedToolCallHistory = useMemo(
|
||||
() => prepareToolCallHistory(toolCallDetailLevel, effectiveStreamItems),
|
||||
@@ -505,7 +508,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
const baseRenderModel = useMemo(() => {
|
||||
return buildAgentStreamRenderModel({
|
||||
isTurnActive,
|
||||
activeTurnStartedAt: turnPresentation.startedAt,
|
||||
activeTurnStartedAt: effectiveTurnPresentation.startedAt,
|
||||
tail: projectedToolCalls.tail,
|
||||
head: projectedToolCalls.head,
|
||||
platform: isWeb ? "web" : "native",
|
||||
@@ -516,7 +519,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
isTurnActive,
|
||||
projectedToolCalls.head,
|
||||
projectedToolCalls.tail,
|
||||
turnPresentation.startedAt,
|
||||
effectiveTurnPresentation.startedAt,
|
||||
]);
|
||||
const streamLayout = useMemo(
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user