- Notifications
+ Inbox
{unreadCount > 0 && (
- {unreadCount} new
+ {unreadCount}
)}
@@ -82,33 +130,86 @@ export const NotificationCenter: React.FC
= ({
data-testid="mark-all-read-btn"
>
- Mark all as read
)}
+ {/* Filters */}
+
+ {(['all', 'task', 'payment', 'agent', 'system'] as NotificationFilter[]).map(f => (
+
+ ))}
+
+
{/* Body */}
- {notifications.length === 0 ? (
+ {filteredNotifications.length === 0 ? (
-
No notifications yet
-
We'll alert you when tasks update or payments settle.
+
No notifications
+
Stay tuned for updates on your tasks and payments.
) : (
-
-
- {notifications.map(notification => (
-
- ))}
-
+
+ {Object.entries(groupedNotifications).map(([groupKey, group]) => (
+ group.length > 0 && (
+
+
+
+ {expandedGroups.has(groupKey) && (
+
+ {group.map(notification => (
+
+ ))}
+
+ )}
+
+
+ )
+ ))}
)}
diff --git a/frontend/src/components/tasks/TaskTimeline.module.css b/frontend/src/components/tasks/TaskTimeline.module.css
index d2037ea5..4b073d6e 100644
--- a/frontend/src/components/tasks/TaskTimeline.module.css
+++ b/frontend/src/components/tasks/TaskTimeline.module.css
@@ -307,6 +307,35 @@
box-shadow: none;
}
+/* ─── Row actions (on hover) ────────────────────────────────────── */
+
+.rowActions {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+}
+
+.actionBtn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ background: transparent;
+ border: 1px solid var(--panel-border);
+ border-radius: 6px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ padding: 0;
+}
+
+.actionBtn:hover {
+ background: rgba(56, 189, 248, 0.1);
+ color: var(--accent);
+ border-color: rgba(56, 189, 248, 0.3);
+}
+
/* ─── Execution bar ─────────────────────────────────────────────── */
.execBar {
diff --git a/frontend/src/components/tasks/TaskTimeline.tsx b/frontend/src/components/tasks/TaskTimeline.tsx
index 584f6be1..68a2d3b6 100644
--- a/frontend/src/components/tasks/TaskTimeline.tsx
+++ b/frontend/src/components/tasks/TaskTimeline.tsx
@@ -9,6 +9,9 @@ import {
ChevronRight,
ExternalLink,
AlertCircle,
+ Copy,
+ Download,
+ Play,
} from 'lucide-react';
import type { TaskResponse } from '../../types/api';
import {
@@ -147,6 +150,7 @@ const TimelineEntry: React.FC
= ({
}) => {
const navigate = useNavigate();
const [errorExpanded, setErrorExpanded] = useState(false);
+ const [showActions, setShowActions] = useState(false);
const taskId = task.taskId || task.id || '';
const meta = getStatusMeta(task.status);
@@ -171,12 +175,36 @@ const TimelineEntry: React.FC = ({
navigate(`/tasks/${taskId}`);
};
+ const handleResume = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigate('/tasks/new', { state: { previousTask: task } });
+ };
+
+ const handleDuplicate = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigate('/tasks/new', { state: { duplicateFrom: task } });
+ };
+
+ const handleExport = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const json = JSON.stringify(task, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `task-${taskId}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
return (
setShowActions(true)}
+ onMouseLeave={() => setShowActions(false)}
>
{/* Timeline connector */}
@@ -245,6 +273,39 @@ const TimelineEntry: React.FC
= ({
>
+
+ {/* Row actions (visible on hover) */}
+ {showActions && (
+
+ )}