Keep large sidebars responsive (#1966)

* perf(app): keep large sidebars responsive

Sidebar rows rescanned every agent for each session update, multiplying work with both workspace and agent count. Build workspace activity once and share the derived sidebar model so updates scale with the underlying collections.

* test(app): use valid agent fixture for workspace restore

* refactor(app): keep sidebar model surface focused
This commit is contained in:
Mohamed Boudra
2026-07-10 06:12:56 +08:00
committed by GitHub
parent b2c6476817
commit a743a22496
18 changed files with 445 additions and 262 deletions
@@ -0,0 +1,76 @@
import React, { createContext, useContext, useMemo, type ReactNode } from "react";
import {
useSidebarWorkspacesList,
type SidebarWorkspacesListResult,
} from "@/hooks/use-sidebar-workspaces-list";
import { useStatusModeWorkspacePlacements } from "@/hooks/use-status-mode-workspaces";
import { buildStatusGroups, type StatusGroup } from "@/hooks/sidebar-status-view-model";
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
import { useSidebarViewStore, type SidebarGroupMode } from "@/stores/sidebar-view-store";
import {
buildSidebarShortcutModel,
buildStatusSidebarShortcutModel,
type SidebarShortcutModel,
} from "@/utils/sidebar-shortcuts";
interface SidebarModel extends SidebarWorkspacesListResult {
groupMode: SidebarGroupMode;
statusGroups: StatusGroup[];
collapsedProjectKeys: ReadonlySet<string>;
toggleProjectCollapsed: (projectKey: string) => void;
shortcutModel: SidebarShortcutModel;
}
const SidebarModelContext = createContext<SidebarModel | null>(null);
export function SidebarModelProvider({ children }: { children: ReactNode }) {
const list = useSidebarWorkspacesList();
const groupMode = useSidebarViewStore((state) => state.groupMode);
const collapsedProjectKeys = useSidebarCollapsedSectionsStore(
(state) => state.collapsedProjectKeys,
);
const collapsedStatusGroupKeys = useSidebarCollapsedSectionsStore(
(state) => state.collapsedStatusGroupKeys,
);
const toggleProjectCollapsed = useSidebarCollapsedSectionsStore(
(state) => state.toggleProjectCollapsed,
);
const isStatusMode = groupMode === "status";
const statusWorkspacePlacements = useStatusModeWorkspacePlacements({
placements: list.workspacePlacements,
enabled: isStatusMode,
});
const statusGroups = useMemo(
() =>
isStatusMode ? buildStatusGroups(statusWorkspacePlacements, list.projectNamesByKey) : [],
[isStatusMode, list.projectNamesByKey, statusWorkspacePlacements],
);
const shortcutModel = useMemo(() => {
if (isStatusMode) {
return buildStatusSidebarShortcutModel({
groups: statusGroups,
collapsedStatusGroupKeys,
});
}
return buildSidebarShortcutModel({ projects: list.projects, collapsedProjectKeys });
}, [collapsedProjectKeys, collapsedStatusGroupKeys, isStatusMode, list.projects, statusGroups]);
const value = useMemo(
() => ({
...list,
groupMode,
statusGroups,
collapsedProjectKeys,
toggleProjectCollapsed,
shortcutModel,
}),
[collapsedProjectKeys, groupMode, list, shortcutModel, statusGroups, toggleProjectCollapsed],
);
return <SidebarModelContext.Provider value={value}>{children}</SidebarModelContext.Provider>;
}
export function useSidebarModel(): SidebarModel {
const model = useContext(SidebarModelContext);
if (!model) throw new Error("SidebarModelProvider is required");
return model;
}
@@ -9,11 +9,7 @@ import {
type SidebarStatusWorkspacePlacement,
type SidebarWorkspaceEntry,
} from "@/hooks/use-sidebar-workspaces-list";
import {
buildStatusGroups,
buildStatusShortcutIndex,
type StatusGroup,
} from "@/hooks/sidebar-status-view-model";
import type { StatusGroup } from "@/hooks/sidebar-status-view-model";
import { isWeb as platformIsWeb, isNative as platformIsNative } from "@/constants/platform";
import { StyleSheet } from "react-native-unistyles";
import type { Theme } from "@/styles/theme";
@@ -90,7 +86,7 @@ const archiveLeadingIcon = <ThemedArchive size={14} uniProps={foregroundMutedCol
const renameLeadingIcon = <ThemedPencil size={14} uniProps={foregroundMutedColorMapping} />;
interface StatusWorkspaceListProps {
workspaces: SidebarStatusWorkspacePlacement[];
groups: StatusGroup[];
projectNamesByKey: Map<string, string>;
shortcutIndexByWorkspaceKey: Map<string, number>;
showShortcutBadges: boolean;
@@ -100,31 +96,19 @@ interface StatusWorkspaceListProps {
}
export function SidebarStatusWorkspaceList({
workspaces,
groups,
projectNamesByKey,
shortcutIndexByWorkspaceKey: _projectShortcutIndex,
shortcutIndexByWorkspaceKey,
showShortcutBadges,
onWorkspacePress,
hostLabelByServerId,
showHostLabels,
}: StatusWorkspaceListProps) {
const groups = useMemo(
() => buildStatusGroups(workspaces, projectNamesByKey),
[workspaces, projectNamesByKey],
);
const collapsedStatusGroupKeys = useSidebarCollapsedSectionsStore(
(state) => state.collapsedStatusGroupKeys,
);
const statusShortcutIndex = useMemo(
() =>
showShortcutBadges
? buildStatusShortcutIndex(
groups.filter((group) => !collapsedStatusGroupKeys.has(group.bucket)),
)
: new Map<string, number>(),
[collapsedStatusGroupKeys, groups, showShortcutBadges],
);
const statusShortcutIndex = showShortcutBadges ? shortcutIndexByWorkspaceKey : new Map();
return (
<View style={styles.container}>