Skip to content

Commit e34eab9

Browse files
authored
feat: New history based side nav (#480)
1 parent 3cb7f3a commit e34eab9

6 files changed

Lines changed: 441 additions & 87 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutionStore";
2+
import { Button, Flex } from "@radix-ui/themes";
3+
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
4+
import type { HistoryData, HistoryTaskData } from "../hooks/useSidebarData";
5+
import { useSidebarStore } from "../stores/sidebarStore";
6+
import { TaskItem } from "./items/TaskItem";
7+
8+
interface HistoryViewProps {
9+
historyData: HistoryData;
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 HistorySectionLabel({ label }: { label: string }) {
18+
return (
19+
<div className="px-2 py-1 font-medium font-mono text-[10px] text-gray-10 uppercase tracking-wide">
20+
{label}
21+
</div>
22+
);
23+
}
24+
25+
interface HistoryTaskItemProps {
26+
task: HistoryTaskData;
27+
isActive: boolean;
28+
onClick: () => void;
29+
onContextMenu: (e: React.MouseEvent) => void;
30+
onDelete: () => void;
31+
onTogglePin: () => void;
32+
}
33+
34+
function HistoryTaskItem({
35+
task,
36+
isActive,
37+
onClick,
38+
onContextMenu,
39+
onDelete,
40+
onTogglePin,
41+
}: HistoryTaskItemProps) {
42+
const workspaces = useWorkspaceStore.use.workspaces();
43+
const taskStates = useTaskExecutionStore((state) => state.taskStates);
44+
45+
const workspace = workspaces[task.id];
46+
const taskState = taskStates[task.id];
47+
48+
return (
49+
<TaskItem
50+
id={task.id}
51+
label={task.title}
52+
isActive={isActive}
53+
worktreeName={workspace?.worktreeName ?? undefined}
54+
worktreePath={workspace?.worktreePath ?? workspace?.folderPath}
55+
workspaceMode={taskState?.workspaceMode}
56+
lastActivityAt={task.lastActivityAt}
57+
isGenerating={task.isGenerating}
58+
isUnread={task.isUnread}
59+
isPinned={task.isPinned}
60+
onClick={onClick}
61+
onContextMenu={onContextMenu}
62+
onDelete={onDelete}
63+
onTogglePin={onTogglePin}
64+
/>
65+
);
66+
}
67+
68+
export function HistoryView({
69+
historyData,
70+
activeTaskId,
71+
onTaskClick,
72+
onTaskContextMenu,
73+
onTaskDelete,
74+
onTaskTogglePin,
75+
}: HistoryViewProps) {
76+
const loadMoreHistory = useSidebarStore((state) => state.loadMoreHistory);
77+
const { activeTasks, recentTasks, hasMore } = historyData;
78+
79+
const hasActiveTasks = activeTasks.length > 0;
80+
const hasRecentTasks = recentTasks.length > 0;
81+
82+
return (
83+
<Flex direction="column">
84+
{hasActiveTasks && (
85+
<>
86+
<HistorySectionLabel label="Active" />
87+
{activeTasks.map((task) => (
88+
<HistoryTaskItem
89+
key={task.id}
90+
task={task}
91+
isActive={activeTaskId === task.id}
92+
onClick={() => onTaskClick(task.id)}
93+
onContextMenu={(e) => onTaskContextMenu(task.id, e)}
94+
onDelete={() => onTaskDelete(task.id)}
95+
onTogglePin={() => onTaskTogglePin(task.id)}
96+
/>
97+
))}
98+
{hasRecentTasks && (
99+
<div className="mx-2 my-2 border-gray-6 border-t" />
100+
)}
101+
</>
102+
)}
103+
104+
{hasRecentTasks && (
105+
<>
106+
<HistorySectionLabel label="Recent" />
107+
{recentTasks.map((task) => (
108+
<HistoryTaskItem
109+
key={task.id}
110+
task={task}
111+
isActive={activeTaskId === task.id}
112+
onClick={() => onTaskClick(task.id)}
113+
onContextMenu={(e) => onTaskContextMenu(task.id, e)}
114+
onDelete={() => onTaskDelete(task.id)}
115+
onTogglePin={() => onTaskTogglePin(task.id)}
116+
/>
117+
))}
118+
</>
119+
)}
120+
121+
{hasMore && (
122+
<div className="px-2 py-2">
123+
<Button
124+
size="1"
125+
variant="ghost"
126+
color="gray"
127+
onClick={loadMoreHistory}
128+
style={{ width: "100%" }}
129+
>
130+
Show more
131+
</Button>
132+
</div>
133+
)}
134+
</Flex>
135+
);
136+
}

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersSto
55
import { trpcVanilla } from "@renderer/trpc";
66
import { useNavigationStore } from "@stores/navigationStore";
77
import { useCallback } from "react";
8+
import { useSidebarStore } from "../stores/sidebarStore";
89

910
export function SidebarFooter() {
1011
const addFolder = useRegisteredFoldersStore((state) => state.addFolder);
11-
const { toggleSettings } = useNavigationStore();
12+
const { toggleSettings, navigateToTaskInput } = useNavigationStore();
13+
const viewMode = useSidebarStore((state) => state.viewMode);
1214

1315
const handleAddRepository = useCallback(async () => {
1416
const selectedPath = await trpcVanilla.os.selectDirectory.query();
@@ -17,6 +19,12 @@ export function SidebarFooter() {
1719
}
1820
}, [addFolder]);
1921

22+
const handleNewTask = useCallback(() => {
23+
navigateToTaskInput();
24+
}, [navigateToTaskInput]);
25+
26+
const isHistoryView = viewMode === "history";
27+
2028
return (
2129
<Box
2230
style={{
@@ -30,15 +38,22 @@ export function SidebarFooter() {
3038
}}
3139
>
3240
<Flex align="center" gap="2" justify="between">
33-
<Button
34-
size="1"
35-
variant="ghost"
36-
color="gray"
37-
onClick={handleAddRepository}
38-
>
39-
<Plus size={14} weight="bold" />
40-
Add repository
41-
</Button>
41+
{isHistoryView ? (
42+
<Button size="1" variant="ghost" color="gray" onClick={handleNewTask}>
43+
<Plus size={14} weight="bold" />
44+
New task
45+
</Button>
46+
) : (
47+
<Button
48+
size="1"
49+
variant="ghost"
50+
color="gray"
51+
onClick={handleAddRepository}
52+
>
53+
<Plus size={14} weight="bold" />
54+
Add repository
55+
</Button>
56+
)}
4257

4358
<IconButton
4459
size="1"

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

Lines changed: 97 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import { useSidebarData } from "../hooks/useSidebarData";
1818
import { usePinnedTasksStore } from "../stores/pinnedTasksStore";
1919
import { useSidebarStore } from "../stores/sidebarStore";
2020
import { useTaskViewedStore } from "../stores/taskViewedStore";
21+
import { HistoryView } from "./HistoryView";
2122
import { HomeItem } from "./items/HomeItem";
2223
import { NewTaskItem } from "./items/NewTaskItem";
2324
import { TaskItem } from "./items/TaskItem";
2425
import { SidebarFooter } from "./SidebarFooter";
2526
import { SortableFolderSection } from "./SortableFolderSection";
27+
import { ViewModeSelector } from "./ViewModeSelector";
2628

2729
function SidebarMenuComponent() {
2830
const {
@@ -41,6 +43,7 @@ function SidebarMenuComponent() {
4143
const toggleSection = useSidebarStore((state) => state.toggleSection);
4244
const folderOrder = useSidebarStore((state) => state.folderOrder);
4345
const reorderFolders = useSidebarStore((state) => state.reorderFolders);
46+
const viewMode = useSidebarStore((state) => state.viewMode);
4447
const workspaces = useWorkspaceStore.use.workspaces();
4548
const taskStates = useTaskExecutionStore((state) => state.taskStates);
4649
const markAsViewed = useTaskViewedStore((state) => state.markAsViewed);
@@ -196,87 +199,104 @@ function SidebarMenuComponent() {
196199
onClick={handleHomeClick}
197200
/>
198201

202+
<div className="px-2 py-1">
203+
<ViewModeSelector />
204+
</div>
205+
199206
<div className="mx-2 my-2 border-gray-6 border-t" />
200207

201-
<DragDropProvider
202-
onDragOver={handleDragOver}
203-
sensors={[
204-
PointerSensor.configure({
205-
activationConstraints: {
206-
distance: { value: 5 },
207-
},
208-
}),
209-
]}
210-
>
211-
{sidebarData.folders.map((folder, index) => {
212-
const isExpanded = !collapsedSections.has(folder.id);
213-
return (
214-
<div key={folder.id}>
215-
{index > 0 && (
216-
<div className="mx-2 my-2 border-gray-6 border-t" />
217-
)}
218-
<SortableFolderSection
219-
id={folder.id}
220-
index={index}
221-
label={folder.name}
222-
icon={
223-
isExpanded ? (
224-
<FolderOpenIcon size={14} weight="regular" />
225-
) : (
226-
<FolderIcon size={14} weight="regular" />
227-
)
228-
}
229-
isExpanded={isExpanded}
230-
onToggle={() => toggleSection(folder.id)}
231-
onSettingsClick={() => handleFolderSettings(folder.id)}
232-
onContextMenu={(e) =>
233-
handleFolderContextMenu(folder.id, e)
234-
}
235-
>
236-
<NewTaskItem
237-
onClick={() => handleFolderNewTask(folder.id)}
238-
/>
239-
{folder.tasks.map((task) => (
240-
<TaskItem
241-
key={task.id}
242-
id={task.id}
243-
label={task.title}
244-
isActive={sidebarData.activeTaskId === task.id}
245-
worktreeName={
246-
workspaces[task.id]?.worktreeName ?? undefined
247-
}
248-
worktreePath={
249-
workspaces[task.id]?.worktreePath ??
250-
workspaces[task.id]?.folderPath
251-
}
252-
workspaceMode={taskStates[task.id]?.workspaceMode}
253-
lastActivityAt={task.lastActivityAt}
254-
isGenerating={task.isGenerating}
255-
isUnread={task.isUnread}
256-
isPinned={task.isPinned}
257-
onClick={() => handleTaskClick(task.id)}
258-
onContextMenu={(e) =>
259-
handleTaskContextMenu(task.id, e)
260-
}
261-
onDelete={() => handleTaskDelete(task.id)}
262-
onTogglePin={() => handleTaskTogglePin(task.id)}
208+
{viewMode === "history" ? (
209+
<HistoryView
210+
historyData={sidebarData.historyData}
211+
activeTaskId={sidebarData.activeTaskId}
212+
onTaskClick={handleTaskClick}
213+
onTaskContextMenu={handleTaskContextMenu}
214+
onTaskDelete={handleTaskDelete}
215+
onTaskTogglePin={handleTaskTogglePin}
216+
/>
217+
) : (
218+
<DragDropProvider
219+
onDragOver={handleDragOver}
220+
sensors={[
221+
PointerSensor.configure({
222+
activationConstraints: {
223+
distance: { value: 5 },
224+
},
225+
}),
226+
]}
227+
>
228+
{sidebarData.folders.map((folder, index) => {
229+
const isExpanded = !collapsedSections.has(folder.id);
230+
return (
231+
<div key={folder.id}>
232+
{index > 0 && (
233+
<div className="mx-2 my-2 border-gray-6 border-t" />
234+
)}
235+
<SortableFolderSection
236+
id={folder.id}
237+
index={index}
238+
label={folder.name}
239+
icon={
240+
isExpanded ? (
241+
<FolderOpenIcon size={14} weight="regular" />
242+
) : (
243+
<FolderIcon size={14} weight="regular" />
244+
)
245+
}
246+
isExpanded={isExpanded}
247+
onToggle={() => toggleSection(folder.id)}
248+
onSettingsClick={() => handleFolderSettings(folder.id)}
249+
onContextMenu={(e) =>
250+
handleFolderContextMenu(folder.id, e)
251+
}
252+
>
253+
<NewTaskItem
254+
onClick={() => handleFolderNewTask(folder.id)}
263255
/>
264-
))}
265-
</SortableFolderSection>
266-
</div>
267-
);
268-
})}
269-
<DragOverlay>
270-
{(source) =>
271-
source?.type === "folder" ? (
272-
<div className="flex w-full items-center gap-1 rounded bg-gray-2 px-2 py-1 font-mono text-[12px] text-gray-11 shadow-lg">
273-
<FolderIcon size={14} weight="regular" />
274-
<span className="font-medium">{source.data?.label}</span>
256+
{folder.tasks.map((task) => (
257+
<TaskItem
258+
key={task.id}
259+
id={task.id}
260+
label={task.title}
261+
isActive={sidebarData.activeTaskId === task.id}
262+
worktreeName={
263+
workspaces[task.id]?.worktreeName ?? undefined
264+
}
265+
worktreePath={
266+
workspaces[task.id]?.worktreePath ??
267+
workspaces[task.id]?.folderPath
268+
}
269+
workspaceMode={taskStates[task.id]?.workspaceMode}
270+
lastActivityAt={task.lastActivityAt}
271+
isGenerating={task.isGenerating}
272+
isUnread={task.isUnread}
273+
isPinned={task.isPinned}
274+
onClick={() => handleTaskClick(task.id)}
275+
onContextMenu={(e) =>
276+
handleTaskContextMenu(task.id, e)
277+
}
278+
onDelete={() => handleTaskDelete(task.id)}
279+
onTogglePin={() => handleTaskTogglePin(task.id)}
280+
/>
281+
))}
282+
</SortableFolderSection>
275283
</div>
276-
) : null
277-
}
278-
</DragOverlay>
279-
</DragDropProvider>
284+
);
285+
})}
286+
<DragOverlay>
287+
{(source) =>
288+
source?.type === "folder" ? (
289+
<div className="flex w-full items-center gap-1 rounded bg-gray-2 px-2 py-1 font-mono text-[12px] text-gray-11 shadow-lg">
290+
<FolderIcon size={14} weight="regular" />
291+
<span className="font-medium">
292+
{source.data?.label}
293+
</span>
294+
</div>
295+
) : null
296+
}
297+
</DragOverlay>
298+
</DragDropProvider>
299+
)}
280300
</Flex>
281301
</Box>
282302
<SidebarFooter />

0 commit comments

Comments
 (0)