Skip to content

Commit 93c7cae

Browse files
authored
feat: New pinned sidenav (#481)
1 parent e34eab9 commit 93c7cae

6 files changed

Lines changed: 190 additions & 7 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutionStore";
2+
import { PushPin } from "@phosphor-icons/react";
3+
import { Flex } from "@radix-ui/themes";
4+
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
5+
import type { PinnedData, TaskData } from "../hooks/useSidebarData";
6+
import { TaskItem } from "./items/TaskItem";
7+
8+
interface PinnedViewProps {
9+
pinnedData: PinnedData;
10+
activeTaskId: string | null;
11+
onTaskClick: (taskId: string) => void;
12+
onTaskContextMenu: (taskId: string, e: React.MouseEvent) => void;
13+
onTaskDelete: (taskId: string) => void;
14+
onTaskTogglePin: (taskId: string) => void;
15+
}
16+
17+
function PinnedTaskItem({
18+
task,
19+
isActive,
20+
onClick,
21+
onContextMenu,
22+
onDelete,
23+
onTogglePin,
24+
}: {
25+
task: TaskData;
26+
isActive: boolean;
27+
onClick: () => void;
28+
onContextMenu: (e: React.MouseEvent) => void;
29+
onDelete: () => void;
30+
onTogglePin: () => void;
31+
}) {
32+
const workspaces = useWorkspaceStore.use.workspaces();
33+
const taskStates = useTaskExecutionStore((state) => state.taskStates);
34+
35+
const workspace = workspaces[task.id];
36+
const taskState = taskStates[task.id];
37+
38+
return (
39+
<TaskItem
40+
id={task.id}
41+
label={task.title}
42+
isActive={isActive}
43+
worktreeName={workspace?.worktreeName ?? undefined}
44+
worktreePath={workspace?.worktreePath ?? workspace?.folderPath}
45+
workspaceMode={taskState?.workspaceMode}
46+
lastActivityAt={task.lastActivityAt}
47+
isGenerating={task.isGenerating}
48+
isUnread={task.isUnread}
49+
isPinned={task.isPinned}
50+
onClick={onClick}
51+
onContextMenu={onContextMenu}
52+
onDelete={onDelete}
53+
onTogglePin={onTogglePin}
54+
/>
55+
);
56+
}
57+
58+
export function PinnedView({
59+
pinnedData,
60+
activeTaskId,
61+
onTaskClick,
62+
onTaskContextMenu,
63+
onTaskDelete,
64+
onTaskTogglePin,
65+
}: PinnedViewProps) {
66+
const { tasks } = pinnedData;
67+
68+
if (tasks.length === 0) {
69+
return (
70+
<Flex
71+
direction="column"
72+
align="center"
73+
justify="center"
74+
gap="2"
75+
py="6"
76+
className="text-gray-10"
77+
>
78+
<PushPin size={24} />
79+
<span className="text-[12px]">No pinned tasks</span>
80+
<span className="px-4 text-center text-[11px] text-gray-9">
81+
Pin tasks from any view to quickly access them here
82+
</span>
83+
</Flex>
84+
);
85+
}
86+
87+
return (
88+
<Flex direction="column">
89+
<div className="px-2 py-1 font-medium font-mono text-[10px] text-gray-10 uppercase tracking-wide">
90+
Pinned ({tasks.length})
91+
</div>
92+
{tasks.map((task) => (
93+
<PinnedTaskItem
94+
key={task.id}
95+
task={task}
96+
isActive={activeTaskId === task.id}
97+
onClick={() => onTaskClick(task.id)}
98+
onContextMenu={(e) => onTaskContextMenu(task.id, e)}
99+
onDelete={() => onTaskDelete(task.id)}
100+
onTogglePin={() => onTaskTogglePin(task.id)}
101+
/>
102+
))}
103+
</Flex>
104+
);
105+
}

apps/array/src/renderer/features/sidebar/components/SidebarFooter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function SidebarFooter() {
2323
navigateToTaskInput();
2424
}, [navigateToTaskInput]);
2525

26-
const isHistoryView = viewMode === "history";
26+
const showNewTaskButton = viewMode !== "folders";
2727

2828
return (
2929
<Box
@@ -38,7 +38,7 @@ export function SidebarFooter() {
3838
}}
3939
>
4040
<Flex align="center" gap="2" justify="between">
41-
{isHistoryView ? (
41+
{showNewTaskButton ? (
4242
<Button size="1" variant="ghost" color="gray" onClick={handleNewTask}>
4343
<Plus size={14} weight="bold" />
4444
New task

apps/array/src/renderer/features/sidebar/components/SidebarMenu.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { HistoryView } from "./HistoryView";
2222
import { HomeItem } from "./items/HomeItem";
2323
import { NewTaskItem } from "./items/NewTaskItem";
2424
import { TaskItem } from "./items/TaskItem";
25+
import { PinnedView } from "./PinnedView";
2526
import { SidebarFooter } from "./SidebarFooter";
2627
import { SortableFolderSection } from "./SortableFolderSection";
2728
import { ViewModeSelector } from "./ViewModeSelector";
@@ -205,7 +206,7 @@ function SidebarMenuComponent() {
205206

206207
<div className="mx-2 my-2 border-gray-6 border-t" />
207208

208-
{viewMode === "history" ? (
209+
{viewMode === "history" && (
209210
<HistoryView
210211
historyData={sidebarData.historyData}
211212
activeTaskId={sidebarData.activeTaskId}
@@ -214,7 +215,20 @@ function SidebarMenuComponent() {
214215
onTaskDelete={handleTaskDelete}
215216
onTaskTogglePin={handleTaskTogglePin}
216217
/>
217-
) : (
218+
)}
219+
220+
{viewMode === "pinned" && (
221+
<PinnedView
222+
pinnedData={sidebarData.pinnedData}
223+
activeTaskId={sidebarData.activeTaskId}
224+
onTaskClick={handleTaskClick}
225+
onTaskContextMenu={handleTaskContextMenu}
226+
onTaskDelete={handleTaskDelete}
227+
onTaskTogglePin={handleTaskTogglePin}
228+
/>
229+
)}
230+
231+
{viewMode === "folders" && (
218232
<DragDropProvider
219233
onDragOver={handleDragOver}
220234
sensors={[

apps/array/src/renderer/features/sidebar/components/ViewModeSelector.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { ClockCounterClockwise, Folder } from "@phosphor-icons/react";
1+
import { ClockCounterClockwise, Folder, PushPin } from "@phosphor-icons/react";
22
import { Select, Text } from "@radix-ui/themes";
33
import { type SidebarViewMode, useSidebarStore } from "../stores/sidebarStore";
44

55
const VIEW_OPTIONS = [
6-
{ value: "folders" as const, label: "Repositories", Icon: Folder },
76
{ value: "history" as const, label: "History", Icon: ClockCounterClockwise },
7+
{ value: "pinned" as const, label: "Pinned", Icon: PushPin },
8+
{ value: "folders" as const, label: "Repositories", Icon: Folder },
89
];
910

1011
export function ViewModeSelector() {

apps/array/src/renderer/features/sidebar/hooks/useSidebarData.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export interface HistoryData {
5858
hasMore: boolean;
5959
}
6060

61+
export interface PinnedData {
62+
tasks: TaskData[];
63+
}
64+
6165
export interface SidebarData {
6266
userName: string;
6367
isHomeActive: boolean;
@@ -69,6 +73,7 @@ export interface SidebarData {
6973
folders: FolderData[];
7074
activeTaskId: string | null;
7175
historyData: HistoryData;
76+
pinnedData: PinnedData;
7277
}
7378

7479
interface ViewState {
@@ -253,6 +258,54 @@ function buildHistoryData(
253258
};
254259
}
255260

261+
function buildPinnedData(
262+
allTasks: Task[],
263+
sessions: Record<string, AgentSession>,
264+
lastViewedAt: Record<string, number>,
265+
localActivityAt: Record<string, number>,
266+
pinnedTaskIds: Set<string>,
267+
activeTaskId: string | null,
268+
): PinnedData {
269+
const getSessionForTask = (taskId: string): AgentSession | undefined => {
270+
return Object.values(sessions).find((s) => s.taskId === taskId);
271+
};
272+
273+
// Filter to only pinned tasks
274+
const pinnedTasks = allTasks.filter((task) => pinnedTaskIds.has(task.id));
275+
276+
// Transform to TaskData
277+
const tasks: TaskData[] = pinnedTasks.map((task) => {
278+
const session = getSessionForTask(task.id);
279+
280+
const apiUpdatedAt = new Date(task.updated_at).getTime();
281+
const localActivity = localActivityAt[task.id];
282+
const lastActivityAt = localActivity
283+
? Math.max(apiUpdatedAt, localActivity)
284+
: apiUpdatedAt;
285+
286+
const taskLastViewedAt = lastViewedAt[task.id];
287+
const isCurrentlyViewing = activeTaskId === task.id;
288+
const isUnread =
289+
!isCurrentlyViewing &&
290+
taskLastViewedAt !== undefined &&
291+
lastActivityAt > taskLastViewedAt;
292+
293+
return {
294+
id: task.id,
295+
title: task.title,
296+
lastActivityAt,
297+
isGenerating: session?.isPromptPending ?? false,
298+
isUnread,
299+
isPinned: true,
300+
};
301+
});
302+
303+
// Sort by activity
304+
tasks.sort((a, b) => (b.lastActivityAt ?? 0) - (a.lastActivityAt ?? 0));
305+
306+
return { tasks };
307+
}
308+
256309
export function useSidebarData({
257310
activeView,
258311
activeFilters,
@@ -367,6 +420,15 @@ export function useSidebarData({
367420
historyVisibleCount,
368421
);
369422

423+
const pinnedData = buildPinnedData(
424+
allTasks,
425+
sessions,
426+
lastViewedAt,
427+
localActivityAt,
428+
pinnedTaskIds,
429+
activeTaskId,
430+
);
431+
370432
return {
371433
userName,
372434
isHomeActive,
@@ -378,5 +440,6 @@ export function useSidebarData({
378440
folders: folderData,
379441
activeTaskId,
380442
historyData,
443+
pinnedData,
381444
};
382445
}

apps/array/src/renderer/features/sidebar/stores/sidebarStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { create } from "zustand";
22
import { persist } from "zustand/middleware";
33

4-
export type SidebarViewMode = "folders" | "history";
4+
export type SidebarViewMode = "folders" | "history" | "pinned";
55

66
interface SidebarStoreState {
77
open: boolean;

0 commit comments

Comments
 (0)