Skip to content

Commit 6aea4a2

Browse files
committed
Harden sidebar controllers and view state
1 parent 5eff638 commit 6aea4a2

5 files changed

Lines changed: 349 additions & 277 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 84 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,18 @@ import {
135135
import {
136136
collapseSidebarProjectThreadList,
137137
expandSidebarProjectThreadList,
138-
syncSidebarProjectMappings,
138+
resetSidebarViewState,
139139
useSidebarIsActiveThread,
140140
useSidebarProjectActiveRouteThreadKey,
141141
useSidebarProjectKeys,
142-
useSidebarProjectSnapshot,
143142
useSidebarProjectThreadListExpanded,
144143
useSidebarThreadJumpLabel,
145-
type SidebarProjectSnapshot,
146144
} from "./sidebar/sidebarViewStore";
147145
import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill";
148146
import {
149147
buildSidebarPhysicalToLogicalKeyMap,
150148
buildSidebarProjectSnapshots,
149+
type SidebarProjectSnapshot,
151150
} from "./sidebar/sidebarProjectSnapshots";
152151
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
153152
import { readEnvironmentApi } from "../environmentApi";
@@ -504,13 +503,12 @@ const SidebarThreadTerminalStatusIndicator = memo(
504503

505504
interface SidebarThreadRowProps {
506505
threadKey: string;
507-
projectKey: string;
506+
project: SidebarProjectSnapshot;
508507
}
509508

510509
const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowProps) {
511-
const { threadKey, projectKey } = props;
510+
const { threadKey, project } = props;
512511
const threadRef = useMemo(() => parseScopedThreadKey(threadKey), [threadKey]);
513-
const project = useSidebarProjectSnapshot(projectKey);
514512
const threadSortOrder = useSettings<SidebarThreadSortOrder>(
515513
(settings) => settings.sidebarThreadSortOrder,
516514
);
@@ -1092,7 +1090,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
10921090
});
10931091

10941092
interface SidebarProjectThreadListProps {
1095-
projectKey: string;
1093+
project: SidebarProjectSnapshot;
10961094
projectExpanded: boolean;
10971095
hasOverflowingThreads: boolean;
10981096
hiddenThreadKeys: readonly string[];
@@ -1107,7 +1105,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
11071105
props: SidebarProjectThreadListProps,
11081106
) {
11091107
const {
1110-
projectKey,
1108+
project,
11111109
projectExpanded,
11121110
hasOverflowingThreads,
11131111
hiddenThreadKeys,
@@ -1137,7 +1135,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
11371135
) : null}
11381136
{shouldShowThreadPanel &&
11391137
renderedThreadKeys.map((threadKey) => {
1140-
return <SidebarThreadRow key={threadKey} threadKey={threadKey} projectKey={projectKey} />;
1138+
return <SidebarThreadRow key={threadKey} threadKey={threadKey} project={project} />;
11411139
})}
11421140

11431141
{projectExpanded && hasOverflowingThreads && !isThreadListExpanded && (
@@ -1148,12 +1146,12 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
11481146
size="sm"
11491147
className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"
11501148
onClick={() => {
1151-
expandSidebarProjectThreadList(projectKey);
1149+
expandSidebarProjectThreadList(project.projectKey);
11521150
}}
11531151
>
11541152
<span className="flex min-w-0 flex-1 items-center gap-2">
11551153
<SidebarProjectOverflowStatusLabel
1156-
projectKey={projectKey}
1154+
project={project}
11571155
hiddenThreadKeys={hiddenThreadKeys}
11581156
/>
11591157
<span>Show more</span>
@@ -1169,7 +1167,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
11691167
size="sm"
11701168
className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"
11711169
onClick={() => {
1172-
collapseSidebarProjectThreadList(projectKey);
1170+
collapseSidebarProjectThreadList(project.projectKey);
11731171
}}
11741172
>
11751173
<span>Show less</span>
@@ -1322,11 +1320,10 @@ const SidebarProjectHeaderStatusIndicator = memo(
13221320

13231321
const SidebarProjectOverflowStatusLabel = memo(function SidebarProjectOverflowStatusLabel(props: {
13241322
hiddenThreadKeys: readonly string[];
1325-
projectKey: string;
1323+
project: SidebarProjectSnapshot;
13261324
}) {
1327-
const { hiddenThreadKeys, projectKey } = props;
1328-
const project = useSidebarProjectSnapshot(projectKey);
1329-
if (!project || hiddenThreadKeys.length === 0) {
1325+
const { hiddenThreadKeys, project } = props;
1326+
if (hiddenThreadKeys.length === 0) {
13301327
return null;
13311328
}
13321329
const statusInputs = useSidebarProjectStatusInputs(project);
@@ -1345,23 +1342,19 @@ const SidebarProjectOverflowStatusLabel = memo(function SidebarProjectOverflowSt
13451342
});
13461343

13471344
interface SidebarProjectThreadSectionProps {
1348-
projectKey: string;
1345+
project: SidebarProjectSnapshot;
13491346
attachThreadListAutoAnimateRef: (node: HTMLElement | null) => void;
13501347
}
13511348

13521349
const SidebarProjectThreadSection = memo(function SidebarProjectThreadSection(
13531350
props: SidebarProjectThreadSectionProps,
13541351
) {
1355-
const { projectKey, attachThreadListAutoAnimateRef } = props;
1356-
const project = useSidebarProjectSnapshot(projectKey);
1357-
const isThreadListExpanded = useSidebarProjectThreadListExpanded(projectKey);
1352+
const { project, attachThreadListAutoAnimateRef } = props;
1353+
const isThreadListExpanded = useSidebarProjectThreadListExpanded(project.projectKey);
13581354
const threadSortOrder = useSettings<SidebarThreadSortOrder>(
13591355
(settings) => settings.sidebarThreadSortOrder,
13601356
);
1361-
if (!project) {
1362-
return null;
1363-
}
1364-
const activeRouteThreadKey = useSidebarProjectActiveRouteThreadKey(projectKey);
1357+
const activeRouteThreadKey = useSidebarProjectActiveRouteThreadKey(project.projectKey);
13651358
const projectExpanded = useUiStateStore(
13661359
(state) => state.projectExpandedById[project.projectKey] ?? true,
13671360
);
@@ -1381,7 +1374,7 @@ const SidebarProjectThreadSection = memo(function SidebarProjectThreadSection(
13811374

13821375
return (
13831376
<SidebarProjectThreadList
1384-
projectKey={project.projectKey}
1377+
project={project}
13851378
projectExpanded={projectExpanded}
13861379
hasOverflowingThreads={hasOverflowingThreads}
13871380
hiddenThreadKeys={hiddenThreadKeys}
@@ -1395,7 +1388,7 @@ const SidebarProjectThreadSection = memo(function SidebarProjectThreadSection(
13951388
});
13961389

13971390
interface SidebarProjectHeaderProps {
1398-
projectKey: string;
1391+
project: SidebarProjectSnapshot;
13991392
dragInProgressRef: React.RefObject<boolean>;
14001393
suppressProjectClickAfterDragRef: React.RefObject<boolean>;
14011394
suppressProjectClickForContextMenuRef: React.RefObject<boolean>;
@@ -1405,14 +1398,13 @@ interface SidebarProjectHeaderProps {
14051398

14061399
const SidebarProjectHeader = memo(function SidebarProjectHeader(props: SidebarProjectHeaderProps) {
14071400
const {
1408-
projectKey,
1401+
project,
14091402
dragInProgressRef,
14101403
suppressProjectClickAfterDragRef,
14111404
suppressProjectClickForContextMenuRef,
14121405
isManualProjectSorting,
14131406
dragHandleProps,
14141407
} = props;
1415-
const project = useSidebarProjectSnapshot(projectKey);
14161408
const defaultThreadEnvMode = useSettings<ThreadEnvMode>(
14171409
(settings) => settings.defaultThreadEnvMode,
14181410
);
@@ -1448,7 +1440,9 @@ const SidebarProjectHeader = memo(function SidebarProjectHeader(props: SidebarPr
14481440
});
14491441
},
14501442
});
1451-
const projectExpanded = useUiStateStore((state) => state.projectExpandedById[projectKey] ?? true);
1443+
const projectExpanded = useUiStateStore(
1444+
(state) => state.projectExpandedById[project.projectKey] ?? true,
1445+
);
14521446
const projectThreadCount = useSidebarProjectThreadCount(project);
14531447
const newThreadShortcutLabelOptions = useMemo(
14541448
() => ({
@@ -1463,10 +1457,6 @@ const SidebarProjectHeader = memo(function SidebarProjectHeader(props: SidebarPr
14631457
const newThreadShortcutLabel =
14641458
shortcutLabelForCommand(keybindings, "chat.newLocal", newThreadShortcutLabelOptions) ??
14651459
shortcutLabelForCommand(keybindings, "chat.new", newThreadShortcutLabelOptions);
1466-
if (!project) {
1467-
return null;
1468-
}
1469-
14701460
const handleProjectButtonClick = useCallback(
14711461
(event: React.MouseEvent<HTMLButtonElement>) => {
14721462
if (suppressProjectClickForContextMenuRef.current) {
@@ -1489,12 +1479,12 @@ const SidebarProjectHeader = memo(function SidebarProjectHeader(props: SidebarPr
14891479
if (selectedThreadCount > 0) {
14901480
clearSelection();
14911481
}
1492-
toggleProject(projectKey);
1482+
toggleProject(project.projectKey);
14931483
},
14941484
[
14951485
clearSelection,
14961486
dragInProgressRef,
1497-
projectKey,
1487+
project.projectKey,
14981488
selectedThreadCount,
14991489
suppressProjectClickAfterDragRef,
15001490
suppressProjectClickForContextMenuRef,
@@ -1509,9 +1499,9 @@ const SidebarProjectHeader = memo(function SidebarProjectHeader(props: SidebarPr
15091499
if (dragInProgressRef.current) {
15101500
return;
15111501
}
1512-
toggleProject(projectKey);
1502+
toggleProject(project.projectKey);
15131503
},
1514-
[dragInProgressRef, projectKey, toggleProject],
1504+
[dragInProgressRef, project.projectKey, toggleProject],
15151505
);
15161506

15171507
const handleProjectButtonPointerDownCapture = useCallback(
@@ -1733,7 +1723,7 @@ const SidebarProjectHeader = memo(function SidebarProjectHeader(props: SidebarPr
17331723
});
17341724

17351725
interface SidebarProjectItemProps {
1736-
projectKey: string;
1726+
project: SidebarProjectSnapshot;
17371727
attachThreadListAutoAnimateRef: (node: HTMLElement | null) => void;
17381728
dragInProgressRef: React.RefObject<boolean>;
17391729
suppressProjectClickAfterDragRef: React.RefObject<boolean>;
@@ -1744,7 +1734,7 @@ interface SidebarProjectItemProps {
17441734

17451735
const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjectItemProps) {
17461736
const {
1747-
projectKey,
1737+
project,
17481738
attachThreadListAutoAnimateRef,
17491739
dragInProgressRef,
17501740
suppressProjectClickAfterDragRef,
@@ -1756,7 +1746,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
17561746
return (
17571747
<>
17581748
<SidebarProjectHeader
1759-
projectKey={projectKey}
1749+
project={project}
17601750
dragInProgressRef={dragInProgressRef}
17611751
suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef}
17621752
suppressProjectClickForContextMenuRef={suppressProjectClickForContextMenuRef}
@@ -1765,7 +1755,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
17651755
/>
17661756

17671757
<SidebarProjectThreadSection
1768-
projectKey={projectKey}
1758+
project={project}
17691759
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
17701760
/>
17711761
</>
@@ -1973,6 +1963,7 @@ const SidebarChromeFooter = memo(function SidebarChromeFooter() {
19731963
});
19741964

19751965
interface SidebarProjectsContentProps {
1966+
sidebarProjectByKey: ReadonlyMap<string, SidebarProjectSnapshot>;
19761967
showArm64IntelBuildWarning: boolean;
19771968
arm64IntelBuildWarningDescription: string | null;
19781969
desktopUpdateButtonAction: "download" | "install" | "none";
@@ -2013,6 +2004,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
20132004
props: SidebarProjectsContentProps,
20142005
) {
20152006
const {
2007+
sidebarProjectByKey,
20162008
showArm64IntelBuildWarning,
20172009
arm64IntelBuildWarningDescription,
20182010
desktopUpdateButtonAction,
@@ -2202,40 +2194,56 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
22022194
items={[...sortedProjectKeys]}
22032195
strategy={verticalListSortingStrategy}
22042196
>
2205-
{sortedProjectKeys.map((projectKey) => (
2206-
<SortableProjectItem key={projectKey} projectId={projectKey}>
2207-
{(dragHandleProps) => (
2208-
<SidebarProjectItem
2209-
projectKey={projectKey}
2210-
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
2211-
dragInProgressRef={dragInProgressRef}
2212-
suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef}
2213-
suppressProjectClickForContextMenuRef={
2214-
suppressProjectClickForContextMenuRef
2215-
}
2216-
isManualProjectSorting={isManualProjectSorting}
2217-
dragHandleProps={dragHandleProps}
2218-
/>
2219-
)}
2220-
</SortableProjectItem>
2221-
))}
2197+
{sortedProjectKeys.map((projectKey) =>
2198+
(() => {
2199+
const project = sidebarProjectByKey.get(projectKey);
2200+
if (!project) {
2201+
return null;
2202+
}
2203+
return (
2204+
<SortableProjectItem key={projectKey} projectId={projectKey}>
2205+
{(dragHandleProps) => (
2206+
<SidebarProjectItem
2207+
project={project}
2208+
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
2209+
dragInProgressRef={dragInProgressRef}
2210+
suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef}
2211+
suppressProjectClickForContextMenuRef={
2212+
suppressProjectClickForContextMenuRef
2213+
}
2214+
isManualProjectSorting={isManualProjectSorting}
2215+
dragHandleProps={dragHandleProps}
2216+
/>
2217+
)}
2218+
</SortableProjectItem>
2219+
);
2220+
})(),
2221+
)}
22222222
</SortableContext>
22232223
</SidebarMenu>
22242224
</DndContext>
22252225
) : (
22262226
<SidebarMenu ref={attachProjectListAutoAnimateRef}>
2227-
{sortedProjectKeys.map((projectKey) => (
2228-
<SidebarProjectListRow
2229-
key={projectKey}
2230-
projectKey={projectKey}
2231-
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
2232-
dragInProgressRef={dragInProgressRef}
2233-
suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef}
2234-
suppressProjectClickForContextMenuRef={suppressProjectClickForContextMenuRef}
2235-
isManualProjectSorting={isManualProjectSorting}
2236-
dragHandleProps={null}
2237-
/>
2238-
))}
2227+
{sortedProjectKeys.map((projectKey) =>
2228+
(() => {
2229+
const project = sidebarProjectByKey.get(projectKey);
2230+
if (!project) {
2231+
return null;
2232+
}
2233+
return (
2234+
<SidebarProjectListRow
2235+
key={projectKey}
2236+
project={project}
2237+
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
2238+
dragInProgressRef={dragInProgressRef}
2239+
suppressProjectClickAfterDragRef={suppressProjectClickAfterDragRef}
2240+
suppressProjectClickForContextMenuRef={suppressProjectClickForContextMenuRef}
2241+
isManualProjectSorting={isManualProjectSorting}
2242+
dragHandleProps={null}
2243+
/>
2244+
);
2245+
})(),
2246+
)}
22392247
</SidebarMenu>
22402248
)}
22412249

@@ -2534,11 +2542,10 @@ export default function Sidebar() {
25342542
const isManualProjectSorting = sidebarProjectSortOrder === "manual";
25352543

25362544
useEffect(() => {
2537-
syncSidebarProjectMappings({
2538-
projectSnapshotByKey: sidebarProjectByKey,
2539-
physicalToLogicalKey,
2540-
});
2541-
}, [physicalToLogicalKey, sidebarProjectByKey]);
2545+
return () => {
2546+
resetSidebarViewState();
2547+
};
2548+
}, []);
25422549

25432550
useEffect(() => {
25442551
const onMouseDown = (event: globalThis.MouseEvent) => {
@@ -2669,6 +2676,7 @@ export default function Sidebar() {
26692676
/>
26702677
<SidebarKeyboardController
26712678
navigateToThread={navigateToThread}
2679+
physicalToLogicalKey={physicalToLogicalKey}
26722680
sidebarThreadSortOrder={sidebarThreadSortOrder}
26732681
/>
26742682

@@ -2677,6 +2685,7 @@ export default function Sidebar() {
26772685
) : (
26782686
<>
26792687
<SidebarProjectsContent
2688+
sidebarProjectByKey={sidebarProjectByKey}
26802689
showArm64IntelBuildWarning={showArm64IntelBuildWarning}
26812690
arm64IntelBuildWarningDescription={arm64IntelBuildWarningDescription}
26822691
desktopUpdateButtonAction={desktopUpdateButtonAction}

0 commit comments

Comments
 (0)