fix(web): summarize session titles, sort by activity, and paginate history (BUG-553)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 14:34:30 +08:00
co-authored by Cursor
parent d04990fc4a
commit a1956deb63
36 changed files with 1089 additions and 64 deletions
+24 -1
View File
@@ -40,6 +40,7 @@ import {
import { ThemePreferenceMenu } from "@/components/theme-preference-menu";
import { UserAvatar } from "@/components/user-avatar";
import type { BeamAvatar } from "@/lib/beam-avatar";
import { groupSessionsByRecency } from "@/lib/session-groups";
export type SidebarAccount = {
name: string;
@@ -106,12 +107,25 @@ export function AppSidebar({
const { isMobile, setOpen, setOpenMobile, state, viewport } = useSidebar();
const firstSessionRef = useRef<HTMLButtonElement>(null);
const historyHeadingRef = useRef<HTMLElement>(null);
const loadMoreRef = useRef<HTMLDivElement>(null);
const isCollapsedDesktop = state === "collapsed" && !isMobile;
const showExpandedContent = !isCollapsedDesktop;
const menuPlacement = `${viewport}:${state}`;
const previousMenuPlacement = useRef(menuPlacement);
const favoriteSessions = sessions.filter((session) => session.pinned);
const historySessions = sessions.filter((session) => !session.pinned);
const historyGroups = groupSessionsByRecency(historySessions);
useEffect(() => {
if (!sessionControls.hasMore || !sessionControls.onLoadMore) return;
const node = loadMoreRef.current;
if (!node) return;
const observer = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) sessionControls.onLoadMore?.();
});
observer.observe(node);
return () => observer.disconnect();
}, [historySessions.length, sessionControls.hasMore, sessionControls.onLoadMore]);
useEffect(() => {
if (previousMenuPlacement.current !== menuPlacement && accountMenuOpen) {
@@ -259,7 +273,16 @@ export function AppSidebar({
<SidebarGroupContent className="sidebar-nested">
{historySessions.length === 0 ? <p className="sidebar-empty"></p> : (
<SidebarMenu className="session-list">
{historySessions.map((session, index) => renderSession(session, favoriteSessions.length + index))}
{historyGroups.map((group) => (
<div key={group.key}>
<p className="sidebar-group-label">{group.label}</p>
{group.sessions.map((session) => renderSession(
session,
favoriteSessions.length + historySessions.findIndex((item) => item.id === session.id),
))}
</div>
))}
{sessionControls.hasMore ? <div ref={loadMoreRef} aria-hidden className="session-list-sentinel" /> : null}
</SidebarMenu>
)}
</SidebarGroupContent>
@@ -19,13 +19,17 @@ import { sessionMutationMenuVisible } from "@/lib/chat-session-persistence";
export type SidebarSession = {
readonly id: string;
readonly title: string;
readonly subtitle?: string | null;
readonly pinned: boolean;
readonly archived: boolean;
readonly updatedAt: number;
};
export type SidebarSessionControls = {
readonly archivedCount: number;
readonly showingArchived: boolean;
readonly hasMore?: boolean;
readonly onLoadMore?: () => void;
readonly menuSessionId: string | null;
readonly disabled: boolean;
readonly onToggleArchivedView: () => void;
@@ -96,6 +100,7 @@ export const SidebarSessionRow = forwardRef<HTMLButtonElement, SidebarSessionRow
<span className="truncate">{session.title}</span>
{opening ? <span className="session-opening-note">{RECTIFICATION_SIDEBAR_OPENING_NOTE}</span> : null}
</span>
{session.subtitle ? <small className="session-subtitle">{session.subtitle}</small> : null}
</SidebarMenuButton>
<Menu.Trigger
className="session-menu-trigger"