From aa96bfddb34e0c4a63d7aad93bd2bdd635b9bec7 Mon Sep 17 00:00:00 2001 From: Robrusi Date: Tue, 7 Apr 2026 22:16:40 +0200 Subject: [PATCH 1/3] Add support for pinned threads in the sidebar --- apps/web/src/components/Sidebar.logic.ts | 7 + apps/web/src/components/Sidebar.tsx | 208 ++++++++++++++++++++--- apps/web/src/hooks/useThreadActions.ts | 9 +- apps/web/src/uiStateStore.test.ts | 18 ++ apps/web/src/uiStateStore.ts | 59 ++++++- 5 files changed, 277 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index ed151c6da8..850bd00861 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -14,6 +14,7 @@ type SidebarProject = { updatedAt?: string | undefined; }; type SidebarThreadSortInput = Pick & { + isPinned?: boolean; latestUserMessageAt?: string | null; messages?: Pick[]; }; @@ -485,6 +486,12 @@ export function sortThreadsForSidebar< T extends Pick & SidebarThreadSortInput, >(threads: readonly T[], sortOrder: SidebarThreadSortOrder): T[] { return threads.toSorted((left, right) => { + const leftPinned = left.isPinned === true; + const rightPinned = right.isPinned === true; + if (leftPinned !== rightPinned) { + return rightPinned ? 1 : -1; + } + const rightTimestamp = getThreadSortTimestamp(right, sortOrder); const leftTimestamp = getThreadSortTimestamp(left, sortOrder); const byTimestamp = diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 5b4da4655c..a7a268743f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -4,6 +4,7 @@ import { ChevronRightIcon, FolderIcon, GitPullRequestIcon, + PinIcon, PlusIcon, SettingsIcon, SquarePenIcon, @@ -258,7 +259,9 @@ function resolveThreadPr( interface SidebarThreadRowProps { threadId: ThreadId; projectCwd: string | null; - orderedProjectThreadIds: readonly ThreadId[]; + isPinned: boolean; + folderLabel?: string | null; + orderedSidebarThreadIds: readonly ThreadId[]; routeThreadId: ThreadId | null; selectedThreadIds: ReadonlySet; showThreadJumpHints: boolean; @@ -276,7 +279,7 @@ interface SidebarThreadRowProps { handleThreadClick: ( event: MouseEvent, threadId: ThreadId, - orderedProjectThreadIds: readonly ThreadId[], + orderedSidebarThreadIds: readonly ThreadId[], ) => void; navigateToThread: (threadId: ThreadId) => void; handleMultiSelectContextMenu: (position: { x: number; y: number }) => Promise; @@ -353,7 +356,7 @@ function SidebarThreadRow(props: SidebarThreadRowProps) { isSelected, })} relative isolate`} onClick={(event) => { - props.handleThreadClick(event, thread.id, props.orderedProjectThreadIds); + props.handleThreadClick(event, thread.id, props.orderedSidebarThreadIds); }} onKeyDown={(event) => { if (event.key !== "Enter" && event.key !== " ") return; @@ -399,6 +402,21 @@ function SidebarThreadRow(props: SidebarThreadRowProps) { )} {threadStatus && } + {props.isPinned ? ( + + + + + } + /> + Pinned + + ) : null} {props.renamingThreadId === thread.id ? ( event.stopPropagation()} /> ) : ( - {thread.title} +
+ {thread.title} + {props.folderLabel ? ( + + /{props.folderLabel} + + ) : null} +
)}
@@ -677,14 +702,17 @@ export default function Sidebar() { const projects = useStore((store) => store.projects); const sidebarThreadsById = useStore((store) => store.sidebarThreadsById); const threadIdsByProjectId = useStore((store) => store.threadIdsByProjectId); - const { projectExpandedById, projectOrder, threadLastVisitedAtById } = useUiStateStore( - useShallow((store) => ({ - projectExpandedById: store.projectExpandedById, - projectOrder: store.projectOrder, - threadLastVisitedAtById: store.threadLastVisitedAtById, - })), - ); + const { pinnedThreadIds, projectExpandedById, projectOrder, threadLastVisitedAtById } = + useUiStateStore( + useShallow((store) => ({ + pinnedThreadIds: store.pinnedThreadIds, + projectExpandedById: store.projectExpandedById, + projectOrder: store.projectOrder, + threadLastVisitedAtById: store.threadLastVisitedAtById, + })), + ); const markThreadUnread = useUiStateStore((store) => store.markThreadUnread); + const setThreadPinned = useUiStateStore((store) => store.setThreadPinned); const toggleProject = useUiStateStore((store) => store.toggleProject); const reorderProjects = useUiStateStore((store) => store.reorderProjects); const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearDraftThread); @@ -757,10 +785,21 @@ export default function Sidebar() { [orderedProjects, projectExpandedById], ); const sidebarThreads = useMemo(() => Object.values(sidebarThreadsById), [sidebarThreadsById]); + const pinnedThreadIdSet = useMemo(() => new Set(pinnedThreadIds), [pinnedThreadIds]); const projectCwdById = useMemo( () => new Map(projects.map((project) => [project.id, project.cwd] as const)), [projects], ); + const projectFolderLabelById = useMemo( + () => + new Map( + projects.map((project) => [ + project.id, + project.cwd.split(/[/\\]/).findLast(isNonEmptyString) ?? project.name, + ]), + ), + [projects], + ); const routeTerminalOpen = routeThreadId ? selectThreadTerminalState(terminalStateByThreadId, routeThreadId).terminalOpen : false; @@ -815,7 +854,14 @@ export default function Sidebar() { (projectId: ProjectId) => { const latestThread = sortThreadsForSidebar( (threadIdsByProjectId[projectId] ?? []) - .map((threadId) => sidebarThreadsById[threadId]) + .map((threadId) => { + const thread = sidebarThreadsById[threadId]; + return thread + ? Object.assign({}, thread, { + isPinned: pinnedThreadIdSet.has(thread.id), + }) + : undefined; + }) .filter((thread): thread is NonNullable => thread !== undefined) .filter((thread) => thread.archivedAt === null), appSettings.sidebarThreadSortOrder, @@ -827,7 +873,13 @@ export default function Sidebar() { params: { threadId: latestThread.id }, }); }, - [appSettings.sidebarThreadSortOrder, navigate, sidebarThreadsById, threadIdsByProjectId], + [ + appSettings.sidebarThreadSortOrder, + navigate, + pinnedThreadIdSet, + sidebarThreadsById, + threadIdsByProjectId, + ], ); const addProjectFromPath = useCallback( @@ -1099,6 +1151,10 @@ export default function Sidebar() { const clicked = await api.contextMenu.show( [ { id: "rename", label: "Rename thread" }, + { + id: "toggle-pin", + label: pinnedThreadIdSet.has(threadId) ? "Unpin thread" : "Pin thread", + }, { id: "mark-unread", label: "Mark unread" }, { id: "copy-path", label: "Copy Path" }, { id: "copy-thread-id", label: "Copy Thread ID" }, @@ -1116,6 +1172,11 @@ export default function Sidebar() { return; } + if (clicked === "toggle-pin") { + setThreadPinned(threadId, !pinnedThreadIdSet.has(threadId)); + return; + } + if (clicked === "mark-unread") { markThreadUnread(threadId, thread.latestTurn?.completedAt); return; @@ -1156,7 +1217,9 @@ export default function Sidebar() { copyThreadIdToClipboard, deleteThread, markThreadUnread, + pinnedThreadIdSet, projectCwdById, + setThreadPinned, sidebarThreadsById, ], ); @@ -1171,12 +1234,26 @@ export default function Sidebar() { const clicked = await api.contextMenu.show( [ + { + id: "toggle-pin", + label: ids.every((id) => pinnedThreadIdSet.has(id)) + ? `Unpin (${count})` + : `Pin (${count})`, + }, { id: "mark-unread", label: `Mark unread (${count})` }, { id: "delete", label: `Delete (${count})`, destructive: true }, ], position, ); + if (clicked === "toggle-pin") { + const shouldPin = !ids.every((id) => pinnedThreadIdSet.has(id)); + for (const id of ids) { + setThreadPinned(id, shouldPin); + } + return; + } + if (clicked === "mark-unread") { for (const id of ids) { const thread = sidebarThreadsById[id]; @@ -1209,8 +1286,10 @@ export default function Sidebar() { clearSelection, deleteThread, markThreadUnread, + pinnedThreadIdSet, removeFromSelection, selectedThreadIds, + setThreadPinned, sidebarThreadsById, ], ); @@ -1427,6 +1506,20 @@ export default function Sidebar() { () => sidebarThreads.filter((thread) => thread.archivedAt === null), [sidebarThreads], ); + const pinnedThreads = useMemo( + () => + sortThreadsForSidebar( + visibleThreads + .filter((thread) => pinnedThreadIdSet.has(thread.id)) + .map((thread) => + Object.assign({}, thread, { + isPinned: true, + }), + ), + appSettings.sidebarThreadSortOrder, + ), + [appSettings.sidebarThreadSortOrder, pinnedThreadIdSet, visibleThreads], + ); const sortedProjects = useMemo( () => sortProjectsForSidebar(sidebarProjects, visibleThreads, appSettings.sidebarProjectSortOrder), @@ -1445,7 +1538,14 @@ export default function Sidebar() { }); const projectThreads = sortThreadsForSidebar( (threadIdsByProjectId[project.id] ?? []) - .map((threadId) => sidebarThreadsById[threadId]) + .map((threadId) => { + const thread = sidebarThreadsById[threadId]; + return thread + ? Object.assign({}, thread, { + isPinned: pinnedThreadIdSet.has(thread.id), + }) + : undefined; + }) .filter((thread): thread is NonNullable => thread !== undefined) .filter((thread) => thread.archivedAt === null), appSettings.sidebarThreadSortOrder, @@ -1496,15 +1596,33 @@ export default function Sidebar() { expandedThreadListsByProject, routeThreadId, sortedProjects, + pinnedThreadIdSet, sidebarThreadsById, threadIdsByProjectId, threadLastVisitedAtById, ], ); - const visibleSidebarThreadIds = useMemo( - () => getVisibleSidebarThreadIds(renderedProjects), - [renderedProjects], - ); + const visibleSidebarThreadIds = useMemo(() => { + const renderedThreadIds = getVisibleSidebarThreadIds([ + { + shouldShowThreadPanel: pinnedThreads.length > 0, + renderedThreadIds: pinnedThreads.map((thread) => thread.id), + }, + ...renderedProjects, + ]); + + const uniqueThreadIds: ThreadId[] = []; + const seenThreadIds = new Set(); + for (const threadId of renderedThreadIds) { + if (seenThreadIds.has(threadId)) { + continue; + } + seenThreadIds.add(threadId); + uniqueThreadIds.push(threadId); + } + + return uniqueThreadIds; + }, [pinnedThreads, renderedProjects]); const threadJumpCommandById = useMemo(() => { const mapping = new Map>>(); for (const [visibleThreadIndex, threadId] of visibleSidebarThreadIds.entries()) { @@ -1797,7 +1915,8 @@ export default function Sidebar() { key={threadId} threadId={threadId} projectCwd={project.cwd} - orderedProjectThreadIds={orderedProjectThreadIds} + isPinned={pinnedThreadIdSet.has(threadId)} + orderedSidebarThreadIds={orderedProjectThreadIds} routeThreadId={routeThreadId} selectedThreadIds={selectedThreadIds} showThreadJumpHints={showThreadJumpHints} @@ -2111,6 +2230,57 @@ export default function Sidebar() { ) : null} + {pinnedThreads.length > 0 ? ( + +
+ + Pinned + +
+ + + + {pinnedThreads.map((thread) => ( + + ))} + + + +
+ ) : null}
diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index d5557b4a96..2512d79fdf 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -2,6 +2,7 @@ import { ThreadId } from "@t3tools/contracts"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useParams } from "@tanstack/react-router"; import { useCallback } from "react"; +import { useUiStateStore } from "../uiStateStore"; import { getFallbackThreadIdAfterDelete } from "../components/Sidebar.logic"; import { useComposerDraftStore } from "../composerDraftStore"; @@ -22,6 +23,7 @@ export function useThreadActions() { (store) => store.clearProjectDraftThreadById, ); const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState); + const pinnedThreadIds = useUiStateStore((state) => state.pinnedThreadIds); const routeThreadId = useParams({ strict: false, select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null), @@ -113,7 +115,11 @@ export function useThreadActions() { const deletedThreadIds = opts.deletedThreadIds ?? new Set(); const shouldNavigateToFallback = routeThreadId === threadId; const fallbackThreadId = getFallbackThreadIdAfterDelete({ - threads, + threads: threads.map((entry) => + Object.assign({}, entry, { + isPinned: pinnedThreadIds.includes(entry.id), + }), + ), deletedThreadId: threadId, deletedThreadIds, sortOrder: appSettings.sidebarThreadSortOrder, @@ -170,6 +176,7 @@ export function useThreadActions() { clearTerminalState, appSettings.sidebarThreadSortOrder, navigate, + pinnedThreadIds, removeWorktreeMutation, routeThreadId, ], diff --git a/apps/web/src/uiStateStore.test.ts b/apps/web/src/uiStateStore.test.ts index b0b19f763a..0a1267a406 100644 --- a/apps/web/src/uiStateStore.test.ts +++ b/apps/web/src/uiStateStore.test.ts @@ -5,6 +5,7 @@ import { clearThreadUi, markThreadUnread, reorderProjects, + setThreadPinned, setProjectExpanded, syncProjects, syncThreads, @@ -16,6 +17,7 @@ function makeUiState(overrides: Partial = {}): UiState { projectExpandedById: {}, projectOrder: [], threadLastVisitedAtById: {}, + pinnedThreadIds: [], ...overrides, }; } @@ -137,6 +139,7 @@ describe("uiStateStore pure functions", () => { [thread1]: "2026-02-25T12:35:00.000Z", [thread2]: "2026-02-25T12:36:00.000Z", }, + pinnedThreadIds: [thread1, thread2], }); const next = syncThreads(initialState, [{ id: thread1 }]); @@ -144,6 +147,7 @@ describe("uiStateStore pure functions", () => { expect(next.threadLastVisitedAtById).toEqual({ [thread1]: "2026-02-25T12:35:00.000Z", }); + expect(next.pinnedThreadIds).toEqual([thread1]); }); it("syncThreads seeds visit state for unseen snapshot threads", () => { @@ -183,10 +187,24 @@ describe("uiStateStore pure functions", () => { threadLastVisitedAtById: { [thread1]: "2026-02-25T12:35:00.000Z", }, + pinnedThreadIds: [thread1], }); const next = clearThreadUi(initialState, thread1); expect(next.threadLastVisitedAtById).toEqual({}); + expect(next.pinnedThreadIds).toEqual([]); + }); + + it("setThreadPinned adds and removes pinned threads", () => { + const thread1 = ThreadId.makeUnsafe("thread-1"); + const thread2 = ThreadId.makeUnsafe("thread-2"); + const pinnedState = setThreadPinned(makeUiState({ pinnedThreadIds: [thread2] }), thread1, true); + + expect(pinnedState.pinnedThreadIds).toEqual([thread1, thread2]); + + const unpinnedState = setThreadPinned(pinnedState, thread2, false); + + expect(unpinnedState.pinnedThreadIds).toEqual([thread1]); }); }); diff --git a/apps/web/src/uiStateStore.ts b/apps/web/src/uiStateStore.ts index 342f2db18f..bc4794d5e9 100644 --- a/apps/web/src/uiStateStore.ts +++ b/apps/web/src/uiStateStore.ts @@ -19,6 +19,7 @@ const LEGACY_PERSISTED_STATE_KEYS = [ interface PersistedUiState { expandedProjectCwds?: string[]; projectOrderCwds?: string[]; + pinnedThreadIds?: string[]; } export interface UiProjectState { @@ -28,6 +29,7 @@ export interface UiProjectState { export interface UiThreadState { threadLastVisitedAtById: Record; + pinnedThreadIds: ThreadId[]; } export interface UiState extends UiProjectState, UiThreadState {} @@ -46,10 +48,12 @@ const initialState: UiState = { projectExpandedById: {}, projectOrder: [], threadLastVisitedAtById: {}, + pinnedThreadIds: [], }; const persistedExpandedProjectCwds = new Set(); const persistedProjectOrderCwds: string[] = []; +const persistedPinnedThreadIds: ThreadId[] = []; const currentProjectCwdById = new Map(); let legacyKeysCleanedUp = false; @@ -66,12 +70,18 @@ function readPersistedState(): UiState { continue; } hydratePersistedProjectState(JSON.parse(legacyRaw) as PersistedUiState); - return initialState; + return { + ...initialState, + pinnedThreadIds: [...persistedPinnedThreadIds], + }; } return initialState; } hydratePersistedProjectState(JSON.parse(raw) as PersistedUiState); - return initialState; + return { + ...initialState, + pinnedThreadIds: [...persistedPinnedThreadIds], + }; } catch { return initialState; } @@ -80,6 +90,7 @@ function readPersistedState(): UiState { function hydratePersistedProjectState(parsed: PersistedUiState): void { persistedExpandedProjectCwds.clear(); persistedProjectOrderCwds.length = 0; + persistedPinnedThreadIds.length = 0; for (const cwd of parsed.expandedProjectCwds ?? []) { if (typeof cwd === "string" && cwd.length > 0) { persistedExpandedProjectCwds.add(cwd); @@ -90,6 +101,15 @@ function hydratePersistedProjectState(parsed: PersistedUiState): void { persistedProjectOrderCwds.push(cwd); } } + for (const threadId of parsed.pinnedThreadIds ?? []) { + if ( + typeof threadId === "string" && + threadId.length > 0 && + !persistedPinnedThreadIds.includes(threadId as ThreadId) + ) { + persistedPinnedThreadIds.push(threadId as ThreadId); + } + } } function persistState(state: UiState): void { @@ -112,6 +132,7 @@ function persistState(state: UiState): void { JSON.stringify({ expandedProjectCwds, projectOrderCwds, + pinnedThreadIds: state.pinnedThreadIds, } satisfies PersistedUiState), ); if (!legacyKeysCleanedUp) { @@ -147,6 +168,10 @@ function projectOrdersEqual(left: readonly ProjectId[], right: readonly ProjectI ); } +function threadIdListsEqual(left: readonly ThreadId[], right: readonly ThreadId[]): boolean { + return left.length === right.length && left.every((threadId, index) => threadId === right[index]); +} + export function syncProjects(state: UiState, projects: readonly SyncProjectInput[]): UiState { const previousProjectCwdById = new Map(currentProjectCwdById); const previousProjectIdByCwd = new Map( @@ -261,12 +286,19 @@ export function syncThreads(state: UiState, threads: readonly SyncThreadInput[]) nextThreadLastVisitedAtById[thread.id] = thread.seedVisitedAt; } } - if (recordsEqual(state.threadLastVisitedAtById, nextThreadLastVisitedAtById)) { + const nextPinnedThreadIds = state.pinnedThreadIds.filter((threadId) => + retainedThreadIds.has(threadId), + ); + if ( + recordsEqual(state.threadLastVisitedAtById, nextThreadLastVisitedAtById) && + threadIdListsEqual(state.pinnedThreadIds, nextPinnedThreadIds) + ) { return state; } return { ...state, threadLastVisitedAtById: nextThreadLastVisitedAtById, + pinnedThreadIds: nextPinnedThreadIds, }; } @@ -317,7 +349,9 @@ export function markThreadUnread( } export function clearThreadUi(state: UiState, threadId: ThreadId): UiState { - if (!(threadId in state.threadLastVisitedAtById)) { + const hasVisitState = threadId in state.threadLastVisitedAtById; + const isPinned = state.pinnedThreadIds.includes(threadId); + if (!hasVisitState && !isPinned) { return state; } const nextThreadLastVisitedAtById = { ...state.threadLastVisitedAtById }; @@ -325,6 +359,21 @@ export function clearThreadUi(state: UiState, threadId: ThreadId): UiState { return { ...state, threadLastVisitedAtById: nextThreadLastVisitedAtById, + pinnedThreadIds: state.pinnedThreadIds.filter((pinnedThreadId) => pinnedThreadId !== threadId), + }; +} + +export function setThreadPinned(state: UiState, threadId: ThreadId, pinned: boolean): UiState { + const alreadyPinned = state.pinnedThreadIds.includes(threadId); + if (alreadyPinned === pinned) { + return state; + } + + return { + ...state, + pinnedThreadIds: pinned + ? [threadId, ...state.pinnedThreadIds] + : state.pinnedThreadIds.filter((pinnedThreadId) => pinnedThreadId !== threadId), }; } @@ -387,6 +436,7 @@ interface UiStateStore extends UiState { markThreadVisited: (threadId: ThreadId, visitedAt?: string) => void; markThreadUnread: (threadId: ThreadId, latestTurnCompletedAt: string | null | undefined) => void; clearThreadUi: (threadId: ThreadId) => void; + setThreadPinned: (threadId: ThreadId, pinned: boolean) => void; toggleProject: (projectId: ProjectId) => void; setProjectExpanded: (projectId: ProjectId, expanded: boolean) => void; reorderProjects: (draggedProjectId: ProjectId, targetProjectId: ProjectId) => void; @@ -401,6 +451,7 @@ export const useUiStateStore = create((set) => ({ markThreadUnread: (threadId, latestTurnCompletedAt) => set((state) => markThreadUnread(state, threadId, latestTurnCompletedAt)), clearThreadUi: (threadId) => set((state) => clearThreadUi(state, threadId)), + setThreadPinned: (threadId, pinned) => set((state) => setThreadPinned(state, threadId, pinned)), toggleProject: (projectId) => set((state) => toggleProject(state, projectId)), setProjectExpanded: (projectId, expanded) => set((state) => setProjectExpanded(state, projectId, expanded)), From b164956ea70d03d8383360237c9c68ea921b6a48 Mon Sep 17 00:00:00 2001 From: Robrusi Date: Tue, 7 Apr 2026 23:12:13 +0200 Subject: [PATCH 2/3] Read pinned threads from store at delete time - Avoid capturing stale pinned thread IDs in thread delete actions - Read the latest pin state when computing fallback navigation --- apps/web/src/hooks/useThreadActions.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index 2512d79fdf..f920019f2f 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -23,7 +23,6 @@ export function useThreadActions() { (store) => store.clearProjectDraftThreadById, ); const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState); - const pinnedThreadIds = useUiStateStore((state) => state.pinnedThreadIds); const routeThreadId = useParams({ strict: false, select: (params) => (params.threadId ? ThreadId.makeUnsafe(params.threadId) : null), @@ -113,6 +112,7 @@ export function useThreadActions() { } const deletedThreadIds = opts.deletedThreadIds ?? new Set(); + const pinnedThreadIds = useUiStateStore.getState().pinnedThreadIds; const shouldNavigateToFallback = routeThreadId === threadId; const fallbackThreadId = getFallbackThreadIdAfterDelete({ threads: threads.map((entry) => @@ -176,7 +176,6 @@ export function useThreadActions() { clearTerminalState, appSettings.sidebarThreadSortOrder, navigate, - pinnedThreadIds, removeWorktreeMutation, routeThreadId, ], From 58ae6f9386caf2bc5d6edbe329e898c118941161 Mon Sep 17 00:00:00 2001 From: Robrusi Date: Tue, 7 Apr 2026 23:24:42 +0200 Subject: [PATCH 3/3] Deduplicate UI state array equality helper --- apps/web/src/uiStateStore.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/web/src/uiStateStore.ts b/apps/web/src/uiStateStore.ts index bc4794d5e9..fff564b994 100644 --- a/apps/web/src/uiStateStore.ts +++ b/apps/web/src/uiStateStore.ts @@ -162,14 +162,8 @@ function recordsEqual(left: Record, right: Record): boo return true; } -function projectOrdersEqual(left: readonly ProjectId[], right: readonly ProjectId[]): boolean { - return ( - left.length === right.length && left.every((projectId, index) => projectId === right[index]) - ); -} - -function threadIdListsEqual(left: readonly ThreadId[], right: readonly ThreadId[]): boolean { - return left.length === right.length && left.every((threadId, index) => threadId === right[index]); +function arraysEqual(left: readonly T[], right: readonly T[]): boolean { + return left.length === right.length && left.every((value, index) => value === right[index]); } export function syncProjects(state: UiState, projects: readonly SyncProjectInput[]): UiState { @@ -257,7 +251,7 @@ export function syncProjects(state: UiState, projects: readonly SyncProjectInput if ( recordsEqual(state.projectExpandedById, nextExpandedById) && - projectOrdersEqual(state.projectOrder, nextProjectOrder) && + arraysEqual(state.projectOrder, nextProjectOrder) && !cwdMappingChanged ) { return state; @@ -291,7 +285,7 @@ export function syncThreads(state: UiState, threads: readonly SyncThreadInput[]) ); if ( recordsEqual(state.threadLastVisitedAtById, nextThreadLastVisitedAtById) && - threadIdListsEqual(state.pinnedThreadIds, nextPinnedThreadIds) + arraysEqual(state.pinnedThreadIds, nextPinnedThreadIds) ) { return state; }