feat: add active turn anchor management for improved scroll position handling

This commit is contained in:
Bohdan Triapitsyn
2025-12-29 12:12:52 +02:00
parent 66428cbded
commit 9b4283d618
7 changed files with 127 additions and 16 deletions
+31
View File
@@ -361,6 +361,8 @@ interface MessageActions {
updateMessageInfo: (sessionId: string, messageId: string, messageInfo: any) => void;
syncMessages: (sessionId: string, messages: { info: Message; parts: Part[] }[]) => void;
updateViewportAnchor: (sessionId: string, anchor: number) => void;
updateActiveTurnAnchor: (sessionId: string, anchorId: string | null, spacerHeight: number) => void;
getActiveTurnAnchor: (sessionId: string) => { anchorId: string | null; spacerHeight: number } | null;
trimToViewportWindow: (sessionId: string, targetSize?: number, currentSessionId?: string) => void;
evictLeastRecentlyUsed: (currentSessionId?: string) => void;
loadMoreMessages: (sessionId: string, direction: "up" | "down") => Promise<void>;
@@ -479,6 +481,7 @@ export const useMessageStore = create<MessageStore>()(
const newMemoryState = new Map(state.sessionMemoryState);
const previousMemoryState = state.sessionMemoryState.get(sessionId);
newMemoryState.set(sessionId, {
...previousMemoryState,
viewportAnchor: mergedMessages.length - 1,
isStreaming: false,
lastAccessedAt: Date.now(),
@@ -2202,6 +2205,34 @@ export const useMessageStore = create<MessageStore>()(
});
},
updateActiveTurnAnchor: (sessionId: string, anchorId: string | null, spacerHeight: number) => {
set((state) => {
const memoryState = state.sessionMemoryState.get(sessionId) || {
viewportAnchor: 0,
isStreaming: false,
lastAccessedAt: Date.now(),
backgroundMessageCount: 0,
};
const newMemoryState = new Map(state.sessionMemoryState);
newMemoryState.set(sessionId, {
...memoryState,
activeTurnAnchorId: anchorId ?? undefined,
activeTurnSpacerHeight: spacerHeight,
});
return { sessionMemoryState: newMemoryState };
});
},
getActiveTurnAnchor: (sessionId: string) => {
const memoryState = get().sessionMemoryState.get(sessionId);
if (!memoryState) return null;
return {
anchorId: memoryState.activeTurnAnchorId ?? null,
spacerHeight: memoryState.activeTurnSpacerHeight ?? 0,
};
},
trimToViewportWindow: (sessionId: string, targetSize: number = MEMORY_LIMITS.VIEWPORT_MESSAGES, currentSessionId?: string) => {
const state = get();
const sessionMessages = state.messages.get(sessionId);
@@ -34,6 +34,10 @@ export interface SessionMemoryState {
hasMoreAbove?: boolean;
trimmedHeadMaxId?: string;
streamingCooldownUntil?: number;
/** Message ID of the user's active turn anchor (for scroll position preservation) */
activeTurnAnchorId?: string;
/** Height of the spacer below messages for active turn positioning */
activeTurnSpacerHeight?: number;
}
export interface SessionContextUsage {
@@ -151,6 +155,8 @@ export interface SessionStore {
clearAttachedFiles: () => void;
updateViewportAnchor: (sessionId: string, anchor: number) => void;
updateActiveTurnAnchor: (sessionId: string, anchorId: string | null, spacerHeight: number) => void;
getActiveTurnAnchor: (sessionId: string) => { anchorId: string | null; spacerHeight: number } | null;
trimToViewportWindow: (sessionId: string, targetSize?: number) => void;
evictLeastRecentlyUsed: () => void;
loadMoreMessages: (sessionId: string, direction: "up" | "down") => Promise<void>;
@@ -442,6 +442,8 @@ export const useSessionStore = create<SessionStore>()(
clearAttachedFiles: () => useFileStore.getState().clearAttachedFiles(),
updateViewportAnchor: (sessionId: string, anchor: number) => useMessageStore.getState().updateViewportAnchor(sessionId, anchor),
updateActiveTurnAnchor: (sessionId: string, anchorId: string | null, spacerHeight: number) => useMessageStore.getState().updateActiveTurnAnchor(sessionId, anchorId, spacerHeight),
getActiveTurnAnchor: (sessionId: string) => useMessageStore.getState().getActiveTurnAnchor(sessionId),
trimToViewportWindow: (sessionId: string, targetSize?: number) => {
const currentSessionId = useSessionManagementStore.getState().currentSessionId;
return useMessageStore.getState().trimToViewportWindow(sessionId, targetSize, currentSessionId || undefined);