| import React, { useState } from "react"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export default function SessionItem({ session, isActive, onSelect, onDelete }) { |
| const [hovering, setHovering] = useState(false); |
|
|
| const status = session.status || "active"; |
|
|
| const dotColor = { |
| active: "#F59E0B", |
| completed: "#10B981", |
| failed: "#EF4444", |
| waiting: "#3B82F6", |
| paused: "#6B7280", |
| }[status] || "#6B7280"; |
|
|
| const isPulsing = status === "active"; |
|
|
| const timeAgo = formatTimeAgo(session.updated_at); |
|
|
| |
| const title = |
| session.name || |
| (session.branch ? `${session.branch}` : `Session ${session.id?.slice(0, 8)}`); |
|
|
| return ( |
| <div |
| style={{ |
| ...styles.row, |
| backgroundColor: isActive |
| ? "rgba(59, 130, 246, 0.08)" |
| : hovering |
| ? "rgba(255,255,255,0.03)" |
| : "transparent", |
| borderLeft: isActive ? "2px solid #3B82F6" : "2px solid transparent", |
| }} |
| onClick={onSelect} |
| onMouseEnter={() => setHovering(true)} |
| onMouseLeave={() => setHovering(false)} |
| > |
| <style>{` |
| @keyframes session-pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.4; } |
| } |
| `}</style> |
| |
| {/* Status dot */} |
| <div |
| style={{ |
| ...styles.dot, |
| backgroundColor: dotColor, |
| animation: isPulsing ? "session-pulse 1.5s ease-in-out infinite" : "none", |
| }} |
| /> |
| |
| {/* Content */} |
| <div style={styles.content}> |
| <div style={styles.title}>{title}</div> |
| <div style={styles.meta}> |
| {timeAgo} |
| {session.mode && ( |
| <span style={{ |
| ...styles.badge, |
| background: session.mode === "github" ? "#1e3a5f" : "#2d2d1f", |
| color: session.mode === "github" ? "#60a5fa" : "#d4d48a", |
| }}> |
| {session.mode === "github" ? "GH" : session.mode === "local-git" ? "Git" : "Dir"} |
| </span> |
| )} |
| {session.message_count > 0 && ( |
| <span style={styles.badge}>{session.message_count} msgs</span> |
| )} |
| </div> |
| </div> |
| |
| {/* Delete button (on hover) */} |
| {hovering && ( |
| <button |
| type="button" |
| style={styles.deleteBtn} |
| onClick={(e) => { |
| e.stopPropagation(); |
| onDelete?.(); |
| }} |
| title="Delete session" |
| > |
| × |
| </button> |
| )} |
| </div> |
| ); |
| } |
|
|
| function formatTimeAgo(isoStr) { |
| if (!isoStr) return ""; |
| try { |
| const date = new Date(isoStr); |
| const now = new Date(); |
| const diffMs = now - date; |
| const diffMin = Math.floor(diffMs / 60000); |
| if (diffMin < 1) return "just now"; |
| if (diffMin < 60) return `${diffMin}m ago`; |
| const diffHr = Math.floor(diffMin / 60); |
| if (diffHr < 24) return `${diffHr}h ago`; |
| const diffDay = Math.floor(diffHr / 24); |
| return `${diffDay}d ago`; |
| } catch { |
| return ""; |
| } |
| } |
|
|
| const styles = { |
| row: { |
| display: "flex", |
| alignItems: "center", |
| gap: 8, |
| padding: "8px 10px", |
| borderRadius: 6, |
| cursor: "pointer", |
| transition: "background-color 0.15s", |
| position: "relative", |
| marginBottom: 2, |
| animation: "session-fade-in 0.25s ease-out", |
| }, |
| dot: { |
| width: 8, |
| height: 8, |
| borderRadius: "50%", |
| flexShrink: 0, |
| }, |
| content: { |
| flex: 1, |
| minWidth: 0, |
| overflow: "hidden", |
| }, |
| title: { |
| fontSize: 12, |
| fontWeight: 500, |
| color: "#E4E4E7", |
| whiteSpace: "nowrap", |
| overflow: "hidden", |
| textOverflow: "ellipsis", |
| }, |
| meta: { |
| fontSize: 10, |
| color: "#71717A", |
| marginTop: 2, |
| display: "flex", |
| alignItems: "center", |
| gap: 6, |
| }, |
| badge: { |
| fontSize: 9, |
| background: "#27272A", |
| padding: "1px 5px", |
| borderRadius: 8, |
| color: "#A1A1AA", |
| }, |
| deleteBtn: { |
| position: "absolute", |
| right: 6, |
| top: 6, |
| width: 18, |
| height: 18, |
| borderRadius: 3, |
| border: "none", |
| background: "rgba(239, 68, 68, 0.15)", |
| color: "#EF4444", |
| fontSize: 14, |
| cursor: "pointer", |
| display: "flex", |
| alignItems: "center", |
| justifyContent: "center", |
| lineHeight: 1, |
| }, |
| }; |
|
|