Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 118 additions & 74 deletions apps/array/src/renderer/features/sidebar/hooks/useSidebarData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getUserDisplayName } from "@hooks/useUsers";
import { filtersMatch } from "@lib/filters";
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
import type { RegisteredFolder, Task, Workspace } from "@shared/types";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
import {
getTaskRepository,
Expand Down Expand Up @@ -339,94 +339,138 @@ export function useSidebarData({
syncFolderOrder(folderIds);
}, [syncFolderOrder, folderIds]);

// Sort folders by persisted order
const sortedFolders = sortFoldersByOrder(folders, folderOrder);
const tasksByFolder = groupTasksByFolder(allTasks, folders, workspaces);

const activeTaskId =
activeView.type === "task-detail" && activeView.data
? activeView.data.id
: null;

const getSessionForTask = (taskId: string): AgentSession | undefined => {
return Object.values(sessions).find((s) => s.taskId === taskId);
};

const folderData: FolderData[] = sortedFolders.map((folder) => {
const folderTasks = tasksByFolder.get(folder.id) || [];

const tasksWithActivity = folderTasks.map((task) => {
const session = getSessionForTask(task.id);
// Use max of task.updated_at and local activity timestamp for accurate ordering
const apiUpdatedAt = new Date(task.updated_at).getTime();
const localActivity = localActivityAt[task.id];
const lastActivityAt = localActivity
? Math.max(apiUpdatedAt, localActivity)
: apiUpdatedAt;
const isPinned = pinnedTaskIds.has(task.id);
return {
task,
lastActivityAt,
isGenerating: session?.isPromptPending ?? false,
isPinned,
};
});
// Memoize sorted folders to maintain stable reference
const sortedFolders = useMemo(
() => sortFoldersByOrder(folders, folderOrder),
[folders, folderOrder],
);

// Sort by pinned first, then by most recent activity
tasksWithActivity.sort((a, b) => {
// Pinned tasks come first
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
// Then sort by most recent activity
return b.lastActivityAt - a.lastActivityAt;
});
// Memoize tasks grouped by folder to maintain stable reference
const tasksByFolder = useMemo(
() => groupTasksByFolder(allTasks, folders, workspaces),
[allTasks, folders, workspaces],
);

return {
id: folder.id,
name: folder.name,
path: folder.path,
tasks: tasksWithActivity.map(
({ task, lastActivityAt, isGenerating, isPinned }) => {
const taskLastViewedAt = lastViewedAt[task.id];
const isCurrentlyViewing = activeTaskId === task.id;
// Only show unread if: user has viewed it before AND there's new activity since
const isUnread =
!isCurrentlyViewing &&
taskLastViewedAt !== undefined &&
lastActivityAt > taskLastViewedAt;

return {
id: task.id,
title: task.title,
lastActivityAt,
isGenerating,
isUnread,
isPinned,
};
},
),
// Memoize folder data to prevent unnecessary re-renders in consumers
const folderData: FolderData[] = useMemo(() => {
const getSessionForTask = (taskId: string): AgentSession | undefined => {
return Object.values(sessions).find((s) => s.taskId === taskId);
};
});

const historyData = buildHistoryData(
allTasks,
workspaces,
folders,
return sortedFolders.map((folder) => {
const folderTasks = tasksByFolder.get(folder.id) || [];

const tasksWithActivity = folderTasks.map((task) => {
const session = getSessionForTask(task.id);
// Use max of task.updated_at and local activity timestamp for accurate ordering
const apiUpdatedAt = new Date(task.updated_at).getTime();
const localActivity = localActivityAt[task.id];
const lastActivityAt = localActivity
? Math.max(apiUpdatedAt, localActivity)
: apiUpdatedAt;
const isPinned = pinnedTaskIds.has(task.id);
return {
task,
lastActivityAt,
isGenerating: session?.isPromptPending ?? false,
isPinned,
};
});

// Sort by pinned first, then by most recent activity
tasksWithActivity.sort((a, b) => {
// Pinned tasks come first
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
// Then sort by most recent activity
return b.lastActivityAt - a.lastActivityAt;
});

return {
id: folder.id,
name: folder.name,
path: folder.path,
tasks: tasksWithActivity.map(
({ task, lastActivityAt, isGenerating, isPinned }) => {
const taskLastViewedAt = lastViewedAt[task.id];
const isCurrentlyViewing = activeTaskId === task.id;
// Only show unread if: user has viewed it before AND there's new activity since
const isUnread =
!isCurrentlyViewing &&
taskLastViewedAt !== undefined &&
lastActivityAt > taskLastViewedAt;

return {
id: task.id,
title: task.title,
lastActivityAt,
isGenerating,
isUnread,
isPinned,
};
},
),
};
});
}, [
sortedFolders,
tasksByFolder,
sessions,
lastViewedAt,
localActivityAt,
pinnedTaskIds,
lastViewedAt,
activeTaskId,
historyVisibleCount,
]);

const historyData = useMemo(
() =>
buildHistoryData(
allTasks,
workspaces,
folders,
sessions,
lastViewedAt,
localActivityAt,
pinnedTaskIds,
activeTaskId,
historyVisibleCount,
),
[
allTasks,
workspaces,
folders,
sessions,
lastViewedAt,
localActivityAt,
pinnedTaskIds,
activeTaskId,
historyVisibleCount,
],
);

const pinnedData = buildPinnedData(
allTasks,
sessions,
lastViewedAt,
localActivityAt,
pinnedTaskIds,
activeTaskId,
const pinnedData = useMemo(
() =>
buildPinnedData(
allTasks,
sessions,
lastViewedAt,
localActivityAt,
pinnedTaskIds,
activeTaskId,
),
[
allTasks,
sessions,
lastViewedAt,
localActivityAt,
pinnedTaskIds,
activeTaskId,
],
);

return {
Expand Down