Skip to content

Commit b3c2180

Browse files
authored
feat(twig): add permission indicator to sidebar task items (#605)
1 parent f020b51 commit b3c2180

4 files changed

Lines changed: 56 additions & 28 deletions

File tree

apps/twig/src/renderer/features/sidebar/components/HistoryView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function HistoryTaskItem({
5858
isGenerating={task.isGenerating}
5959
isUnread={task.isUnread}
6060
isPinned={task.isPinned}
61+
needsPermission={task.needsPermission}
6162
onClick={onClick}
6263
onContextMenu={onContextMenu}
6364
onDelete={onDelete}
@@ -97,6 +98,7 @@ function PinnedTaskItem({
9798
isGenerating={task.isGenerating}
9899
isUnread={task.isUnread}
99100
isPinned={task.isPinned}
101+
needsPermission={task.needsPermission}
100102
onClick={onClick}
101103
onContextMenu={onContextMenu}
102104
onDelete={onDelete}

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useTaskContextMenu } from "@hooks/useTaskContextMenu";
66
import { Box, Flex } from "@radix-ui/themes";
77
import type { Task } from "@shared/types";
88
import { useNavigationStore } from "@stores/navigationStore";
9-
import { memo } from "react";
9+
import { memo, useEffect, useRef } from "react";
1010
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
1111
import { useSidebarData } from "../hooks/useSidebarData";
1212
import { usePinnedTasksStore } from "../stores/pinnedTasksStore";
@@ -35,6 +35,22 @@ function SidebarMenuComponent() {
3535
currentUser,
3636
});
3737

38+
const previousTaskIdRef = useRef<string | null>(null);
39+
40+
useEffect(() => {
41+
const currentTaskId =
42+
view.type === "task-detail" && view.data ? view.data.id : null;
43+
44+
if (
45+
previousTaskIdRef.current &&
46+
previousTaskIdRef.current !== currentTaskId
47+
) {
48+
markAsViewed(previousTaskIdRef.current);
49+
}
50+
51+
previousTaskIdRef.current = currentTaskId;
52+
}, [view, markAsViewed]);
53+
3854
const taskMap = new Map<string, Task>();
3955
for (const task of allTasks) {
4056
taskMap.set(task.id, task);
@@ -47,7 +63,6 @@ function SidebarMenuComponent() {
4763
const handleTaskClick = (taskId: string) => {
4864
const task = taskMap.get(taskId);
4965
if (task) {
50-
markAsViewed(taskId);
5166
navigateToTask(task);
5267
}
5368
};

apps/twig/src/renderer/features/sidebar/components/items/TaskItem.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DotsCircleSpinner } from "@components/DotsCircleSpinner";
22
import {
3+
BellRinging,
34
Cloud,
45
GitBranch as GitBranchIcon,
56
Laptop as LaptopIcon,
@@ -28,6 +29,7 @@ interface TaskItemProps {
2829
isGenerating?: boolean;
2930
isUnread?: boolean;
3031
isPinned?: boolean;
32+
needsPermission?: boolean;
3133
onClick: () => void;
3234
onContextMenu: (e: React.MouseEvent) => void;
3335
onDelete?: () => void;
@@ -152,6 +154,7 @@ export function TaskItem({
152154
isGenerating,
153155
isUnread,
154156
isPinned = false,
157+
needsPermission = false,
155158
onClick,
156159
onContextMenu,
157160
onDelete,
@@ -163,18 +166,24 @@ export function TaskItem({
163166

164167
const isCloudTask = workspaceMode === "cloud";
165168

166-
const activityText = isGenerating
167-
? "Generating..."
168-
: lastActivityAt
169-
? formatRelativeTime(lastActivityAt)
170-
: undefined;
169+
const activityText = needsPermission
170+
? "Needs permission"
171+
: isGenerating
172+
? "Generating..."
173+
: lastActivityAt
174+
? formatRelativeTime(lastActivityAt)
175+
: undefined;
171176

172177
const repoName = mainRepoPath?.split("/").pop();
173178
const subtitle = (
174179
<span className="flex items-center gap-1">
175180
{repoName && <span>{repoName}</span>}
176181
{repoName && activityText && <span>·</span>}
177-
{activityText && <span>{activityText}</span>}
182+
{activityText && (
183+
<span className={needsPermission ? "text-blue-11" : ""}>
184+
{activityText}
185+
</span>
186+
)}
178187
{!isCloudTask && <DiffStatsDisplay taskId={id} />}
179188
</span>
180189
);
@@ -187,7 +196,9 @@ export function TaskItem({
187196
? "Workspace"
188197
: "Local";
189198

190-
const icon = isGenerating ? (
199+
const icon = needsPermission ? (
200+
<BellRinging size={16} className="text-blue-11" />
201+
) : isGenerating ? (
191202
<DotsCircleSpinner size={16} className="text-accent-11" />
192203
) : isUnread ? (
193204
<span className="flex h-4 w-4 items-center justify-center text-[8px] text-green-11">

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface TaskData {
4444
isGenerating?: boolean;
4545
isUnread?: boolean;
4646
isPinned?: boolean;
47+
needsPermission?: boolean;
4748
}
4849

4950
export interface HistoryTaskData extends TaskData {
@@ -196,7 +197,7 @@ function buildHistoryData(
196197
lastViewedAt: Record<string, number>,
197198
localActivityAt: Record<string, number>,
198199
pinnedTaskIds: Set<string>,
199-
activeTaskId: string | null,
200+
_activeTaskId: string | null,
200201
visibleCount: number,
201202
): HistoryData {
202203
const getSessionForTask = (taskId: string): AgentSession | undefined => {
@@ -218,11 +219,8 @@ function buildHistoryData(
218219
: apiUpdatedAt;
219220

220221
const taskLastViewedAt = lastViewedAt[task.id];
221-
const isCurrentlyViewing = activeTaskId === task.id;
222222
const isUnread =
223-
!isCurrentlyViewing &&
224-
taskLastViewedAt !== undefined &&
225-
lastActivityAt > taskLastViewedAt;
223+
taskLastViewedAt !== undefined && lastActivityAt > taskLastViewedAt;
226224

227225
return {
228226
id: task.id,
@@ -232,20 +230,21 @@ function buildHistoryData(
232230
isGenerating: session?.isPromptPending ?? false,
233231
isUnread,
234232
isPinned: pinnedTaskIds.has(task.id),
233+
needsPermission: (session?.pendingPermissions?.size ?? 0) > 0,
235234
folderName: folder?.name,
236235
};
237236
});
238237

239238
// Filter out pinned tasks - they will be shown in their own section
240239
const unpinnedTasks = historyTasks.filter((t) => !pinnedTaskIds.has(t.id));
241240

242-
// Partition into active (unread) and inactive tasks
241+
// Partition into active (unread or needs permission) and inactive tasks
243242
const activeTasks = unpinnedTasks
244-
.filter((t) => t.isUnread)
243+
.filter((t) => t.isUnread || t.needsPermission)
245244
.sort((a, b) => (b.lastActivityAt ?? 0) - (a.lastActivityAt ?? 0));
246245

247246
const inactiveTasks = unpinnedTasks
248-
.filter((t) => !t.isUnread)
247+
.filter((t) => !t.isUnread && !t.needsPermission)
249248
.sort((a, b) => b.createdAt - a.createdAt);
250249

251250
// Apply pagination to inactive tasks only (active always shown)
@@ -267,7 +266,7 @@ function buildPinnedData(
267266
lastViewedAt: Record<string, number>,
268267
localActivityAt: Record<string, number>,
269268
pinnedTaskIds: Set<string>,
270-
activeTaskId: string | null,
269+
_activeTaskId: string | null,
271270
): PinnedData {
272271
const getSessionForTask = (taskId: string): AgentSession | undefined => {
273272
return Object.values(sessions).find((s) => s.taskId === taskId);
@@ -287,11 +286,8 @@ function buildPinnedData(
287286
: apiUpdatedAt;
288287

289288
const taskLastViewedAt = lastViewedAt[task.id];
290-
const isCurrentlyViewing = activeTaskId === task.id;
291289
const isUnread =
292-
!isCurrentlyViewing &&
293-
taskLastViewedAt !== undefined &&
294-
lastActivityAt > taskLastViewedAt;
290+
taskLastViewedAt !== undefined && lastActivityAt > taskLastViewedAt;
295291

296292
return {
297293
id: task.id,
@@ -300,6 +296,7 @@ function buildPinnedData(
300296
isGenerating: session?.isPromptPending ?? false,
301297
isUnread,
302298
isPinned: true,
299+
needsPermission: (session?.pendingPermissions?.size ?? 0) > 0,
303300
};
304301
});
305302

@@ -370,7 +367,6 @@ export function useSidebarData({
370367

371368
const tasksWithActivity = folderTasks.map((task) => {
372369
const session = getSessionForTask(task.id);
373-
// Use max of task.updated_at and local activity timestamp for accurate ordering
374370
const apiUpdatedAt = new Date(task.updated_at).getTime();
375371
const localActivity = localActivityAt[task.id];
376372
const lastActivityAt = localActivity
@@ -382,6 +378,7 @@ export function useSidebarData({
382378
lastActivityAt,
383379
isGenerating: session?.isPromptPending ?? false,
384380
isPinned,
381+
needsPermission: (session?.pendingPermissions?.size ?? 0) > 0,
385382
};
386383
});
387384

@@ -399,12 +396,15 @@ export function useSidebarData({
399396
name: folder.name,
400397
path: folder.path,
401398
tasks: tasksWithActivity.map(
402-
({ task, lastActivityAt, isGenerating, isPinned }) => {
399+
({
400+
task,
401+
lastActivityAt,
402+
isGenerating,
403+
isPinned,
404+
needsPermission,
405+
}) => {
403406
const taskLastViewedAt = lastViewedAt[task.id];
404-
const isCurrentlyViewing = activeTaskId === task.id;
405-
// Only show unread if: user has viewed it before AND there's new activity since
406407
const isUnread =
407-
!isCurrentlyViewing &&
408408
taskLastViewedAt !== undefined &&
409409
lastActivityAt > taskLastViewedAt;
410410

@@ -415,6 +415,7 @@ export function useSidebarData({
415415
isGenerating,
416416
isUnread,
417417
isPinned,
418+
needsPermission,
418419
};
419420
},
420421
),
@@ -427,7 +428,6 @@ export function useSidebarData({
427428
localActivityAt,
428429
pinnedTaskIds,
429430
lastViewedAt,
430-
activeTaskId,
431431
]);
432432

433433
const historyData = useMemo(

0 commit comments

Comments
 (0)