diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index ddacc605..edf370ad 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -43,6 +43,11 @@ import { SidebarRail, useSidebar, } from "@/components/ui/sidebar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" import { Tooltip, TooltipContent, @@ -110,9 +115,18 @@ type BackgroundTaskItem = { type ServiceEventType = z.infer -const MAX_SYNC_EVENTS = 30 +const MAX_SYNC_EVENTS = 1000 const RUN_STALE_MS = 2 * 60 * 60 * 1000 +const SERVICE_LABELS: Record = { + gmail: "Syncing Gmail", + calendar: "Syncing Calendar", + fireflies: "Syncing Fireflies", + granola: "Syncing Granola", + graph: "Updating knowledge", + voice_memo: "Processing voice memo", +} + type TasksActions = { onNewChat: () => void onSelectRun: (runId: string) => void @@ -146,29 +160,28 @@ function formatEventTime(ts: string): string { function SyncStatusBar() { const { state, isMobile } = useSidebar() - const [events, setEvents] = useState([]) - const [activeRuns, setActiveRuns] = useState>(new Set()) - const [isExpanded, setIsExpanded] = useState(false) + const [activeServices, setActiveServices] = useState>(new Map()) + const [popoverOpen, setPopoverOpen] = useState(false) + const [logEvents, setLogEvents] = useState([]) + const [logLoading, setLogLoading] = useState(false) const runTimeoutsRef = useRef>>(new Map()) + // Track active runs from real-time events useEffect(() => { const cleanup = window.ipc.on('services:events', (event) => { const nextEvent = event as ServiceEventType - setEvents((prev) => [nextEvent, ...prev].slice(0, MAX_SYNC_EVENTS)) if (nextEvent.type === 'run_start') { - setActiveRuns((prev) => { - const next = new Set(prev) - next.add(nextEvent.runId) + setActiveServices((prev) => { + const next = new Map(prev) + next.set(nextEvent.runId, nextEvent.service) return next }) const existingTimeout = runTimeoutsRef.current.get(nextEvent.runId) - if (existingTimeout) { - clearTimeout(existingTimeout) - } + if (existingTimeout) clearTimeout(existingTimeout) const timeout = setTimeout(() => { - setActiveRuns((prev) => { + setActiveServices((prev) => { if (!prev.has(nextEvent.runId)) return prev - const next = new Set(prev) + const next = new Map(prev) next.delete(nextEvent.runId) return next }) @@ -176,8 +189,8 @@ function SyncStatusBar() { }, RUN_STALE_MS) runTimeoutsRef.current.set(nextEvent.runId, timeout) } else if (nextEvent.type === 'run_complete') { - setActiveRuns((prev) => { - const next = new Set(prev) + setActiveServices((prev) => { + const next = new Map(prev) next.delete(nextEvent.runId) return next }) @@ -198,9 +211,48 @@ function SyncStatusBar() { } }, []) - const isSyncing = activeRuns.size > 0 + // Load logs from JSONL file when popover opens + useEffect(() => { + if (!popoverOpen) return + let cancelled = false + async function loadLogs() { + setLogLoading(true) + try { + const result = await window.ipc.invoke('workspace:readFile', { + path: 'logs/services.jsonl', + encoding: 'utf8', + }) + if (cancelled) return + const lines = result.data.trim().split('\n').filter(Boolean) + const parsed: ServiceEventType[] = [] + for (const line of lines) { + try { + parsed.push(JSON.parse(line)) + } catch { + // skip malformed lines + } + } + // Newest first, limit to 1000 + setLogEvents(parsed.reverse().slice(0, MAX_SYNC_EVENTS)) + } catch { + if (!cancelled) setLogEvents([]) + } finally { + if (!cancelled) setLogLoading(false) + } + } + loadLogs() + return () => { cancelled = true } + }, [popoverOpen]) + + const isSyncing = activeServices.size > 0 const isCollapsed = state === "collapsed" + // Build status label from active services + const activeServiceNames = [...new Set(activeServices.values())] + const statusLabel = isSyncing + ? activeServiceNames.map((s) => SERVICE_LABELS[s] || s).join(", ") + : "All caught up" + return ( <> {!isMobile && isCollapsed && isSyncing && ( @@ -213,37 +265,72 @@ function SyncStatusBar() { )} - - {isExpanded && ( -
- {events.length === 0 ? ( -
No recent activity.
- ) : ( - events.map((event, idx) => ( -
- - {formatEventTime(event.ts)} - - {event.message} + + + + + +
+

Sync Activity

+

+ {isSyncing ? statusLabel : "All services up to date"} +

+
+
+ {logLoading ? ( +
+
- )) - )} -
- )} + ) : logEvents.length === 0 ? ( +
+ No recent activity. +
+ ) : ( +
+ {logEvents.map((event, idx) => ( +
+ + {formatEventTime(event.ts)} + + + + {SERVICE_LABELS[event.service]?.split(" ").slice(-1)[0] || event.service} + + + {event.message} +
+ ))} +
+ )} +
+ + )