Skip to content

Commit 7ab18ad

Browse files
committed
Finalize sidebar key typing
1 parent 96b3810 commit 7ab18ad

4 files changed

Lines changed: 64 additions & 139 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import {
119119
ThreadStatusPill,
120120
} from "./Sidebar.logic";
121121
import {
122+
createSidebarSortedProjectKeysSelector,
122123
createSidebarProjectRenderStateSelector,
123124
createSidebarProjectThreadStatusInputsSelector,
124125
createSidebarThreadMetaSnapshotSelectorByRef,
@@ -129,7 +130,6 @@ import {
129130
import { THREAD_PREVIEW_LIMIT } from "./sidebar/sidebarConstants";
130131
import {
131132
SidebarKeyboardController,
132-
SidebarProjectOrderingController,
133133
SidebarSelectionController,
134134
} from "./sidebar/sidebarControllers";
135135
import {
@@ -138,7 +138,6 @@ import {
138138
resetSidebarViewState,
139139
useSidebarIsActiveThread,
140140
useSidebarProjectActiveRouteThreadKey,
141-
useSidebarProjectKeys,
142141
useSidebarProjectThreadListExpanded,
143142
useSidebarThreadJumpLabel,
144143
} from "./sidebar/sidebarViewStore";
@@ -156,6 +155,7 @@ import {
156155
useSavedEnvironmentRegistryStore,
157156
useSavedEnvironmentRuntimeStore,
158157
} from "../environments/runtime";
158+
import type { LogicalProjectKey } from "../logicalProject";
159159
const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = {
160160
updated_at: "Last user message",
161161
created_at: "Created at",
@@ -1963,7 +1963,8 @@ const SidebarChromeFooter = memo(function SidebarChromeFooter() {
19631963
});
19641964

19651965
interface SidebarProjectsContentProps {
1966-
sidebarProjectByKey: ReadonlyMap<string, SidebarProjectSnapshot>;
1966+
sortedProjectKeys: readonly LogicalProjectKey[];
1967+
sidebarProjectByKey: ReadonlyMap<LogicalProjectKey, SidebarProjectSnapshot>;
19671968
showArm64IntelBuildWarning: boolean;
19681969
arm64IntelBuildWarningDescription: string | null;
19691970
desktopUpdateButtonAction: "download" | "install" | "none";
@@ -2004,6 +2005,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
20042005
props: SidebarProjectsContentProps,
20052006
) {
20062007
const {
2008+
sortedProjectKeys,
20072009
sidebarProjectByKey,
20082010
showArm64IntelBuildWarning,
20092011
arm64IntelBuildWarningDescription,
@@ -2040,8 +2042,6 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
20402042
attachProjectListAutoAnimateRef,
20412043
projectsLength,
20422044
} = props;
2043-
const sortedProjectKeys = useSidebarProjectKeys();
2044-
20452045
const handleProjectSortOrderChange = useCallback(
20462046
(sortOrder: SidebarProjectSortOrder) => {
20472047
updateSettings({ sidebarProjectSortOrder: sortOrder });
@@ -2304,8 +2304,8 @@ export default function Sidebar() {
23042304
);
23052305

23062306
const previousSidebarProjectSnapshotByKeyRef = useRef<
2307-
ReadonlyMap<string, SidebarProjectSnapshot>
2308-
>(new Map<string, SidebarProjectSnapshot>());
2307+
ReadonlyMap<LogicalProjectKey, SidebarProjectSnapshot>
2308+
>(new Map<LogicalProjectKey, SidebarProjectSnapshot>());
23092309
const sidebarProjects = useMemo<SidebarProjectSnapshot[]>(() => {
23102310
const { projectSnapshotByKey, sidebarProjects: nextSidebarProjects } =
23112311
buildSidebarProjectSnapshots({
@@ -2325,9 +2325,23 @@ export default function Sidebar() {
23252325
]);
23262326

23272327
const sidebarProjectByKey = useMemo(
2328-
() => new Map(sidebarProjects.map((project) => [project.projectKey, project] as const)),
2328+
() =>
2329+
new Map<LogicalProjectKey, SidebarProjectSnapshot>(
2330+
sidebarProjects.map((project) => [project.projectKey, project] as const),
2331+
),
23292332
[sidebarProjects],
23302333
);
2334+
const sortedProjectKeys = useStore(
2335+
useMemo(
2336+
() =>
2337+
createSidebarSortedProjectKeysSelector({
2338+
physicalToLogicalKey,
2339+
projects: sidebarProjects,
2340+
sortOrder: sidebarProjectSortOrder,
2341+
}),
2342+
[physicalToLogicalKey, sidebarProjectSortOrder, sidebarProjects],
2343+
),
2344+
);
23312345
const focusMostRecentThreadForProject = useCallback(
23322346
(projectRef: { environmentId: EnvironmentId; projectId: ProjectId }) => {
23332347
const physicalKey = scopedProjectKey(
@@ -2654,15 +2668,11 @@ export default function Sidebar() {
26542668
return (
26552669
<>
26562670
<SidebarChromeHeader isElectron={isElectron} />
2657-
<SidebarProjectOrderingController
2658-
sidebarProjects={sidebarProjects}
2659-
physicalToLogicalKey={physicalToLogicalKey}
2660-
sidebarProjectSortOrder={sidebarProjectSortOrder}
2661-
/>
26622671
<SidebarSelectionController />
26632672
<SidebarKeyboardController
26642673
navigateToThread={navigateToThread}
26652674
physicalToLogicalKey={physicalToLogicalKey}
2675+
sortedProjectKeys={sortedProjectKeys}
26662676
sidebarThreadSortOrder={sidebarThreadSortOrder}
26672677
/>
26682678

@@ -2671,6 +2681,7 @@ export default function Sidebar() {
26712681
) : (
26722682
<>
26732683
<SidebarProjectsContent
2684+
sortedProjectKeys={sortedProjectKeys}
26742685
sidebarProjectByKey={sidebarProjectByKey}
26752686
showArm64IntelBuildWarning={showArm64IntelBuildWarning}
26762687
arm64IntelBuildWarningDescription={arm64IntelBuildWarningDescription}

apps/web/src/components/sidebar/sidebarControllers.tsx

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react";
22
import { parseScopedThreadKey, scopedThreadKey } from "@t3tools/client-runtime";
33
import { useParams } from "@tanstack/react-router";
44
import type { ScopedThreadRef } from "@t3tools/contracts";
5-
import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
5+
import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
66
import { useShallow } from "zustand/react/shallow";
77
import { isTerminalFocused } from "../../lib/terminalFocus";
88
import { resolveThreadRouteRef } from "../../threadRoutes";
@@ -23,20 +23,17 @@ import {
2323
} from "../Sidebar.logic";
2424
import {
2525
createSidebarActiveRouteProjectKeySelectorByRef,
26-
createSidebarSortedProjectKeysSelector,
27-
createSidebarVisibleThreadKeysSelector,
26+
createSidebarSortedThreadKeysByLogicalProjectSelector,
2827
} from "./sidebarSelectors";
2928
import type { LogicalProjectKey } from "../../logicalProject";
3029
import {
3130
setSidebarKeyboardState,
32-
setSidebarProjectOrdering,
3331
useSidebarExpandedThreadListsByProject,
34-
useSidebarProjectKeys,
3532
} from "./sidebarViewStore";
36-
import type { SidebarProjectSnapshot } from "./sidebarProjectSnapshots";
3733
import { useServerKeybindings } from "../../rpc/serverState";
3834
import { useStore } from "../../store";
3935
import { useThreadSelectionStore } from "../../threadSelectionStore";
36+
import { THREAD_PREVIEW_LIMIT } from "./sidebarConstants";
4037

4138
const EMPTY_THREAD_JUMP_LABELS = new Map<string, string>();
4239

@@ -98,27 +95,47 @@ function useSidebarKeyboardController(input: {
9895
),
9996
);
10097
const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility();
101-
const visibleSidebarThreadKeys = useStore(
98+
const sortedThreadKeysByLogicalProject = useStore(
10299
useMemo(
103100
() =>
104-
createSidebarVisibleThreadKeysSelector({
105-
expandedThreadListsByProject,
101+
createSidebarSortedThreadKeysByLogicalProjectSelector({
106102
physicalToLogicalKey,
107-
projectExpandedStates,
108-
routeThreadKey,
109-
sortedProjectKeys,
110103
threadSortOrder,
111104
}),
112-
[
113-
expandedThreadListsByProject,
114-
physicalToLogicalKey,
115-
projectExpandedStates,
116-
routeThreadKey,
117-
sortedProjectKeys,
118-
threadSortOrder,
119-
],
105+
[physicalToLogicalKey, threadSortOrder],
120106
),
121107
);
108+
const visibleSidebarThreadKeys = useMemo(
109+
() =>
110+
sortedProjectKeys.flatMap((projectKey, index) => {
111+
const projectThreadKeys = sortedThreadKeysByLogicalProject.get(projectKey) ?? [];
112+
const projectExpanded = projectExpandedStates[index] ?? true;
113+
const activeThreadKey = routeThreadKey ?? undefined;
114+
const pinnedCollapsedThread =
115+
!projectExpanded && activeThreadKey
116+
? (projectThreadKeys.find((threadKey) => threadKey === activeThreadKey) ?? null)
117+
: null;
118+
const shouldShowThreadPanel = projectExpanded || pinnedCollapsedThread !== null;
119+
if (!shouldShowThreadPanel) {
120+
return [];
121+
}
122+
123+
const isThreadListExpanded = expandedThreadListsByProject.has(projectKey);
124+
const hasOverflowingThreads = projectThreadKeys.length > THREAD_PREVIEW_LIMIT;
125+
const previewThreadKeys =
126+
isThreadListExpanded || !hasOverflowingThreads
127+
? projectThreadKeys
128+
: projectThreadKeys.slice(0, THREAD_PREVIEW_LIMIT);
129+
return pinnedCollapsedThread ? [pinnedCollapsedThread] : previewThreadKeys;
130+
}),
131+
[
132+
expandedThreadListsByProject,
133+
projectExpandedStates,
134+
routeThreadKey,
135+
sortedProjectKeys,
136+
sortedThreadKeysByLogicalProject,
137+
],
138+
);
122139
const threadJumpCommandByKey = useMemo(() => {
123140
const mapping = new Map<string, NonNullable<ReturnType<typeof threadJumpCommandForIndex>>>();
124141
for (const [visibleThreadIndex, threadKey] of visibleSidebarThreadKeys.entries()) {
@@ -360,40 +377,14 @@ export const SidebarSelectionController = memo(function SidebarSelectionControll
360377
return null;
361378
});
362379

363-
export const SidebarProjectOrderingController = memo(
364-
function SidebarProjectOrderingController(props: {
365-
sidebarProjects: readonly SidebarProjectSnapshot[];
366-
physicalToLogicalKey: ReadonlyMap<string, LogicalProjectKey>;
367-
sidebarProjectSortOrder: SidebarProjectSortOrder;
368-
}) {
369-
const { sidebarProjects, physicalToLogicalKey, sidebarProjectSortOrder } = props;
370-
const sortedProjectKeys = useStore(
371-
useMemo(
372-
() =>
373-
createSidebarSortedProjectKeysSelector({
374-
physicalToLogicalKey,
375-
projects: sidebarProjects,
376-
sortOrder: sidebarProjectSortOrder,
377-
}),
378-
[physicalToLogicalKey, sidebarProjectSortOrder, sidebarProjects],
379-
),
380-
);
381-
382-
useEffect(() => {
383-
setSidebarProjectOrdering(sortedProjectKeys);
384-
}, [sortedProjectKeys]);
385-
386-
return null;
387-
},
388-
);
389-
390380
export const SidebarKeyboardController = memo(function SidebarKeyboardController(props: {
391381
navigateToThread: (threadRef: ScopedThreadRef) => void;
392382
physicalToLogicalKey: ReadonlyMap<string, LogicalProjectKey>;
383+
sortedProjectKeys: readonly LogicalProjectKey[];
393384
sidebarThreadSortOrder: SidebarThreadSortOrder;
394385
}) {
395-
const { navigateToThread, physicalToLogicalKey, sidebarThreadSortOrder } = props;
396-
const sortedProjectKeys = useSidebarProjectKeys();
386+
const { navigateToThread, physicalToLogicalKey, sortedProjectKeys, sidebarThreadSortOrder } =
387+
props;
397388
const expandedThreadListsByProject = useSidebarExpandedThreadListsByProject();
398389
const routeThreadRef = useParams({
399390
strict: false,

apps/web/src/components/sidebar/sidebarSelectors.ts

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
type ThreadStatusLatestTurnSnapshot,
1414
type ThreadStatusSessionSnapshot,
1515
} from "../Sidebar.logic";
16-
import { THREAD_PREVIEW_LIMIT } from "./sidebarConstants";
1716
import type { AppState, EnvironmentState } from "../../store";
1817
import type { SidebarThreadSummary } from "../../types";
1918
import type { LogicalProjectKey } from "../../logicalProject";
@@ -404,59 +403,6 @@ export function createSidebarSortedThreadKeysByLogicalProjectSelector(input: {
404403
};
405404
}
406405

407-
export function createSidebarVisibleThreadKeysSelector(input: {
408-
expandedThreadListsByProject: ReadonlySet<LogicalProjectKey>;
409-
physicalToLogicalKey: ReadonlyMap<string, LogicalProjectKey>;
410-
projectExpandedStates: readonly boolean[];
411-
sortedProjectKeys: readonly LogicalProjectKey[];
412-
threadSortOrder: SidebarThreadSortOrder;
413-
routeThreadKey: string | null;
414-
}): (state: AppState) => readonly string[] {
415-
let previousResult: readonly string[] = EMPTY_PROJECT_THREAD_KEYS;
416-
const sortedThreadKeysByLogicalProjectSelector =
417-
createSidebarSortedThreadKeysByLogicalProjectSelector({
418-
physicalToLogicalKey: input.physicalToLogicalKey,
419-
threadSortOrder: input.threadSortOrder,
420-
});
421-
422-
return (state) => {
423-
const sortedThreadKeysByLogicalProject = sortedThreadKeysByLogicalProjectSelector(state);
424-
const nextVisibleThreadKeys: string[] = [];
425-
426-
input.sortedProjectKeys.forEach((projectKey, index) => {
427-
const projectThreadKeys = sortedThreadKeysByLogicalProject.get(projectKey) ?? [];
428-
const projectExpanded = input.projectExpandedStates[index] ?? true;
429-
const activeThreadKey = input.routeThreadKey ?? undefined;
430-
const pinnedCollapsedThread =
431-
!projectExpanded && activeThreadKey
432-
? (projectThreadKeys.find((threadKey) => threadKey === activeThreadKey) ?? null)
433-
: null;
434-
const shouldShowThreadPanel = projectExpanded || pinnedCollapsedThread !== null;
435-
if (!shouldShowThreadPanel) {
436-
return;
437-
}
438-
439-
const isThreadListExpanded = input.expandedThreadListsByProject.has(projectKey);
440-
const hasOverflowingThreads = projectThreadKeys.length > THREAD_PREVIEW_LIMIT;
441-
const visibleProjectThreadKeys =
442-
isThreadListExpanded || !hasOverflowingThreads
443-
? projectThreadKeys
444-
: projectThreadKeys.slice(0, THREAD_PREVIEW_LIMIT);
445-
nextVisibleThreadKeys.push(
446-
...(pinnedCollapsedThread ? [pinnedCollapsedThread] : visibleProjectThreadKeys),
447-
);
448-
});
449-
450-
if (stringArraysEqual(previousResult, nextVisibleThreadKeys)) {
451-
return previousResult;
452-
}
453-
454-
previousResult =
455-
nextVisibleThreadKeys.length === 0 ? EMPTY_PROJECT_THREAD_KEYS : nextVisibleThreadKeys;
456-
return previousResult;
457-
};
458-
}
459-
460406
export function createSidebarThreadRowSnapshotSelectorByRef(
461407
ref: ScopedThreadRef | null | undefined,
462408
): (state: AppState) => SidebarThreadRowSnapshot | undefined {

apps/web/src/components/sidebar/sidebarViewStore.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@ import { createStore } from "zustand/vanilla";
44
import type { LogicalProjectKey } from "../../logicalProject";
55

66
interface SidebarTransientState {
7-
sortedProjectKeys: readonly LogicalProjectKey[];
87
activeRouteThreadKey: string | null;
98
activeRouteProjectKey: LogicalProjectKey | null;
109
threadJumpLabelByKey: ReadonlyMap<string, string>;
1110
expandedThreadListsByProject: ReadonlySet<LogicalProjectKey>;
1211
}
1312

1413
const EMPTY_THREAD_JUMP_LABELS = new Map<string, string>();
15-
const EMPTY_SIDEBAR_PROJECT_KEYS: LogicalProjectKey[] = [];
1614
const EMPTY_EXPANDED_THREAD_LISTS_BY_PROJECT = new Set<LogicalProjectKey>();
1715

1816
const sidebarViewStore = createStore<SidebarTransientState>(() => ({
19-
sortedProjectKeys: EMPTY_SIDEBAR_PROJECT_KEYS,
2017
activeRouteThreadKey: null,
2118
activeRouteProjectKey: null,
2219
threadJumpLabelByKey: EMPTY_THREAD_JUMP_LABELS,
@@ -25,22 +22,13 @@ const sidebarViewStore = createStore<SidebarTransientState>(() => ({
2522

2623
export function resetSidebarViewState(): void {
2724
sidebarViewStore.setState({
28-
sortedProjectKeys: EMPTY_SIDEBAR_PROJECT_KEYS,
2925
activeRouteThreadKey: null,
3026
activeRouteProjectKey: null,
3127
threadJumpLabelByKey: EMPTY_THREAD_JUMP_LABELS,
3228
expandedThreadListsByProject: EMPTY_EXPANDED_THREAD_LISTS_BY_PROJECT,
3329
});
3430
}
3531

36-
export function stringArraysEqual(left: readonly string[], right: readonly string[]): boolean {
37-
return left.length === right.length && left.every((value, index) => value === right[index]);
38-
}
39-
40-
export function useSidebarProjectKeys(): readonly LogicalProjectKey[] {
41-
return useZustandStore(sidebarViewStore, (state) => state.sortedProjectKeys);
42-
}
43-
4432
export function useSidebarProjectThreadListExpanded(projectKey: LogicalProjectKey): boolean {
4533
return useZustandStore(
4634
sidebarViewStore,
@@ -116,17 +104,6 @@ export function useSidebarProjectActiveRouteThreadKey(
116104
);
117105
}
118106

119-
export function setSidebarProjectOrdering(sortedProjectKeys: readonly LogicalProjectKey[]): void {
120-
const currentState = sidebarViewStore.getState();
121-
if (stringArraysEqual(currentState.sortedProjectKeys, sortedProjectKeys)) {
122-
return;
123-
}
124-
125-
sidebarViewStore.setState({
126-
sortedProjectKeys,
127-
});
128-
}
129-
130107
export function setSidebarKeyboardState(input: {
131108
activeRouteProjectKey: LogicalProjectKey | null;
132109
activeRouteThreadKey: string | null;

0 commit comments

Comments
 (0)