feat: enhance SyncStatusBar with popover for service logs and increase event limit

- Introduced a Popover component to display sync activity and logs.
- Increased the maximum sync events from 30 to 1000.
- Added a mapping for service labels to improve user feedback on active services.
- Refactored state management to track active services and log events more effectively.
This commit is contained in:
tusharmagar 2026-02-09 14:20:55 +05:30
parent 950c862e02
commit 2bb27e477f

View file

@ -43,6 +43,11 @@ import {
SidebarRail, SidebarRail,
useSidebar, useSidebar,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -110,9 +115,18 @@ type BackgroundTaskItem = {
type ServiceEventType = z.infer<typeof ServiceEvent> type ServiceEventType = z.infer<typeof ServiceEvent>
const MAX_SYNC_EVENTS = 30 const MAX_SYNC_EVENTS = 1000
const RUN_STALE_MS = 2 * 60 * 60 * 1000 const RUN_STALE_MS = 2 * 60 * 60 * 1000
const SERVICE_LABELS: Record<string, string> = {
gmail: "Syncing Gmail",
calendar: "Syncing Calendar",
fireflies: "Syncing Fireflies",
granola: "Syncing Granola",
graph: "Updating knowledge",
voice_memo: "Processing voice memo",
}
type TasksActions = { type TasksActions = {
onNewChat: () => void onNewChat: () => void
onSelectRun: (runId: string) => void onSelectRun: (runId: string) => void
@ -146,29 +160,28 @@ function formatEventTime(ts: string): string {
function SyncStatusBar() { function SyncStatusBar() {
const { state, isMobile } = useSidebar() const { state, isMobile } = useSidebar()
const [events, setEvents] = useState<ServiceEventType[]>([]) const [activeServices, setActiveServices] = useState<Map<string, string>>(new Map())
const [activeRuns, setActiveRuns] = useState<Set<string>>(new Set()) const [popoverOpen, setPopoverOpen] = useState(false)
const [isExpanded, setIsExpanded] = useState(false) const [logEvents, setLogEvents] = useState<ServiceEventType[]>([])
const [logLoading, setLogLoading] = useState(false)
const runTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map()) const runTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
// Track active runs from real-time events
useEffect(() => { useEffect(() => {
const cleanup = window.ipc.on('services:events', (event) => { const cleanup = window.ipc.on('services:events', (event) => {
const nextEvent = event as ServiceEventType const nextEvent = event as ServiceEventType
setEvents((prev) => [nextEvent, ...prev].slice(0, MAX_SYNC_EVENTS))
if (nextEvent.type === 'run_start') { if (nextEvent.type === 'run_start') {
setActiveRuns((prev) => { setActiveServices((prev) => {
const next = new Set(prev) const next = new Map(prev)
next.add(nextEvent.runId) next.set(nextEvent.runId, nextEvent.service)
return next return next
}) })
const existingTimeout = runTimeoutsRef.current.get(nextEvent.runId) const existingTimeout = runTimeoutsRef.current.get(nextEvent.runId)
if (existingTimeout) { if (existingTimeout) clearTimeout(existingTimeout)
clearTimeout(existingTimeout)
}
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
setActiveRuns((prev) => { setActiveServices((prev) => {
if (!prev.has(nextEvent.runId)) return prev if (!prev.has(nextEvent.runId)) return prev
const next = new Set(prev) const next = new Map(prev)
next.delete(nextEvent.runId) next.delete(nextEvent.runId)
return next return next
}) })
@ -176,8 +189,8 @@ function SyncStatusBar() {
}, RUN_STALE_MS) }, RUN_STALE_MS)
runTimeoutsRef.current.set(nextEvent.runId, timeout) runTimeoutsRef.current.set(nextEvent.runId, timeout)
} else if (nextEvent.type === 'run_complete') { } else if (nextEvent.type === 'run_complete') {
setActiveRuns((prev) => { setActiveServices((prev) => {
const next = new Set(prev) const next = new Map(prev)
next.delete(nextEvent.runId) next.delete(nextEvent.runId)
return next 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" 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 ( return (
<> <>
{!isMobile && isCollapsed && isSyncing && ( {!isMobile && isCollapsed && isSyncing && (
@ -213,37 +265,72 @@ function SyncStatusBar() {
</div> </div>
)} )}
<SidebarFooter className="border-t border-sidebar-border px-2 py-2"> <SidebarFooter className="border-t border-sidebar-border px-2 py-2">
<button <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
type="button" <PopoverTrigger asChild>
onClick={() => setIsExpanded((prev) => !prev)} <button
className="flex w-full items-center justify-between rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-sidebar-accent" type="button"
> className="flex w-full items-center justify-between rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-sidebar-accent"
<span className="flex items-center gap-2"> >
{isSyncing ? ( <span className="flex items-center gap-2 min-w-0">
<LoaderIcon className="h-3 w-3 animate-spin" /> {isSyncing ? (
) : ( <LoaderIcon className="h-3 w-3 shrink-0 animate-spin" />
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60" /> ) : (
)} <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/60" />
{isSyncing ? "Syncing" : "All caught up"} )}
</span> <span className="truncate">{statusLabel}</span>
<ChevronRight className={`h-3 w-3 transition-transform ${isExpanded ? "rotate-90" : ""}`} /> </span>
</button> <ChevronRight className="h-3 w-3 shrink-0" />
{isExpanded && ( </button>
<div className="mt-2 max-h-40 space-y-1 overflow-auto rounded-md border border-border bg-background p-2 text-xs text-muted-foreground"> </PopoverTrigger>
{events.length === 0 ? ( <PopoverContent
<div>No recent activity.</div> side="right"
) : ( align="end"
events.map((event, idx) => ( sideOffset={8}
<div key={`${event.runId}-${event.ts}-${idx}`} className="flex items-start gap-2"> className="w-96 p-0"
<span className="shrink-0 text-[10px] text-muted-foreground/70"> >
{formatEventTime(event.ts)} <div className="p-3 border-b">
</span> <h4 className="font-semibold text-sm">Sync Activity</h4>
<span className="leading-4">{event.message}</span> <p className="text-xs text-muted-foreground mt-0.5">
{isSyncing ? statusLabel : "All services up to date"}
</p>
</div>
<div className="max-h-80 overflow-y-auto p-2">
{logLoading ? (
<div className="flex items-center justify-center py-4">
<LoaderIcon className="h-4 w-4 animate-spin text-muted-foreground" />
</div> </div>
)) ) : logEvents.length === 0 ? (
)} <div className="py-4 text-center text-xs text-muted-foreground">
</div> No recent activity.
)} </div>
) : (
<div className="space-y-0.5">
{logEvents.map((event, idx) => (
<div
key={`${event.runId}-${event.ts}-${idx}`}
className="flex items-start gap-2 rounded px-2 py-1 text-xs hover:bg-accent"
>
<span className="shrink-0 text-[10px] leading-4 text-muted-foreground/70">
{formatEventTime(event.ts)}
</span>
<span className="shrink-0">
<span className={cn(
"inline-block rounded px-1 py-0.5 text-[10px] font-medium leading-none",
event.level === 'error' ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" :
event.level === 'warn' ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" :
"bg-muted text-muted-foreground"
)}>
{SERVICE_LABELS[event.service]?.split(" ").slice(-1)[0] || event.service}
</span>
</span>
<span className="leading-4 text-foreground/80">{event.message}</span>
</div>
))}
</div>
)}
</div>
</PopoverContent>
</Popover>
</SidebarFooter> </SidebarFooter>
</> </>
) )