feat(ui): implement true sticky topic header in app layouts

- Relocated TopicStickyHeader from chat history to global AppLayouts (Default and ScreenReader) for permanent viewport stickiness.
- Fixed duplication issues by removing complex history injection logic in MainContent.tsx.
- Restored natural rendering of topic updates in chat history to ensure consistent scrollback context.
- Added persistent styling (background and separator) to TopicStickyHeader for better visual hierarchy.
- Simplified MainContent.tsx logic by removing redundant overlay and virtualization hacks.
- Updated unit tests and snapshots to align with the new stable layout architecture.
This commit is contained in:
Abhijit Balaji
2026-04-02 14:32:43 -04:00
parent ba207ad00b
commit f790f7f40b
22 changed files with 351 additions and 578 deletions
+14 -38
View File
@@ -15,7 +15,7 @@ import {
type VirtualizedListRef,
} from './shared/VirtualizedList.js';
import { ScrollableList } from './shared/ScrollableList.js';
import { useMemo, memo, useCallback, useEffect, useRef } from 'react';
import { useMemo, memo, useEffect, useRef } from 'react';
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
@@ -147,16 +147,6 @@ export const MainContent = () => {
],
);
const staticHistoryItems = useMemo(
() => historyItems.slice(0, lastUserPromptIndex + 1),
[historyItems, lastUserPromptIndex],
);
const lastResponseHistoryItems = useMemo(
() => historyItems.slice(lastUserPromptIndex + 1),
[historyItems, lastUserPromptIndex],
);
const pendingItems = useMemo(
() => (
<Box flexDirection="column" key="pending-items-group">
@@ -209,31 +199,17 @@ export const MainContent = () => {
],
);
const virtualizedData = useMemo(
() => [
...augmentedHistory.map(
({
item,
isExpandable,
isFirstThinking,
isFirstAfterThinking,
suppressNarration,
}) => ({
type: 'history' as const,
item,
isExpandable,
isFirstThinking,
isFirstAfterThinking,
suppressNarration,
}),
),
if (isAlternateBuffer) {
const virtualizedData = [
...augmentedHistory.map((ah) => ({ type: 'history' as const, ...ah })),
{ type: 'pending' as const },
],
[augmentedHistory],
);
];
const renderItem = useCallback(
({ item }: { item: (typeof virtualizedData)[number] }) => {
const renderItem = ({
item,
}: {
item: (typeof virtualizedData)[number];
}) => {
if (item.type === 'history') {
return (
<MemoizedHistoryItemDisplay
@@ -257,11 +233,8 @@ export const MainContent = () => {
} else {
return pendingItems;
}
},
[mainAreaWidth, uiState.slashCommands, pendingItems, uiState.constrainHeight, staticAreaMaxItemHeight]
);
};
if (isAlternateBuffer) {
return (
<ScrollableList
ref={scrollableListRef}
@@ -280,6 +253,9 @@ export const MainContent = () => {
);
}
const staticHistoryItems = historyItems.slice(0, lastUserPromptIndex + 1);
const lastResponseHistoryItems = historyItems.slice(lastUserPromptIndex + 1);
return (
<>
<Static