Files
claude-mem/src/ui/viewer/App.tsx
T
Alex Newman 74c8afd0e0 feat: Add real-time queue depth indicator to viewer UI
Implements a visual badge that displays the count of active work items (queued + currently processing) in the worker service. The badge appears next to the claude-mem logo and updates in real-time via SSE.

Features:
- Shows count of pending messages + active SDK generators
- Only displays when queueDepth > 0
- Subtle pulse animation for visual feedback
- Theme-aware styling

Backend changes:
- Added getTotalActiveWork() method to SessionManager
- Updated worker-service to broadcast queueDepth via SSE
- Enhanced processing status API endpoint

Frontend changes:
- Updated Header component to display queue bubble
- Enhanced useSSE hook to track queueDepth state
- Added CSS styling with pulse animation

Closes #122
Closes #96
Closes #97

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 15:11:37 -05:00

127 lines
4.6 KiB
TypeScript

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Header } from './components/Header';
import { Feed } from './components/Feed';
import { Sidebar } from './components/Sidebar';
import { useSSE } from './hooks/useSSE';
import { useSettings } from './hooks/useSettings';
import { useStats } from './hooks/useStats';
import { usePagination } from './hooks/usePagination';
import { useTheme } from './hooks/useTheme';
import { Observation, Summary, UserPrompt } from './types';
import { mergeAndDeduplicateByProject } from './utils/data';
export function App() {
const [currentFilter, setCurrentFilter] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(false);
const [paginatedObservations, setPaginatedObservations] = useState<Observation[]>([]);
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
const { observations, summaries, prompts, projects, isProcessing, queueDepth, isConnected } = useSSE();
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
const { stats, refreshStats } = useStats();
const { preference, resolvedTheme, setThemePreference } = useTheme();
const pagination = usePagination(currentFilter);
// When filtering by project: ONLY use paginated data (API-filtered)
// When showing all projects: merge SSE live data with paginated data
const allObservations = useMemo(() => {
if (currentFilter) {
// Project filter active: API handles filtering, ignore SSE items
return paginatedObservations;
}
// No filter: merge SSE + paginated, deduplicate by ID
return mergeAndDeduplicateByProject(observations, paginatedObservations);
}, [observations, paginatedObservations, currentFilter]);
const allSummaries = useMemo(() => {
if (currentFilter) {
return paginatedSummaries;
}
return mergeAndDeduplicateByProject(summaries, paginatedSummaries);
}, [summaries, paginatedSummaries, currentFilter]);
const allPrompts = useMemo(() => {
if (currentFilter) {
return paginatedPrompts;
}
return mergeAndDeduplicateByProject(prompts, paginatedPrompts);
}, [prompts, paginatedPrompts, currentFilter]);
// Toggle sidebar
const toggleSidebar = useCallback(() => {
setSidebarOpen(prev => !prev);
}, []);
// Handle loading more data
const handleLoadMore = useCallback(async () => {
try {
const [newObservations, newSummaries, newPrompts] = await Promise.all([
pagination.observations.loadMore(),
pagination.summaries.loadMore(),
pagination.prompts.loadMore()
]);
if (newObservations.length > 0) {
setPaginatedObservations(prev => [...prev, ...newObservations]);
}
if (newSummaries.length > 0) {
setPaginatedSummaries(prev => [...prev, ...newSummaries]);
}
if (newPrompts.length > 0) {
setPaginatedPrompts(prev => [...prev, ...newPrompts]);
}
} catch (error) {
console.error('Failed to load more data:', error);
}
}, [currentFilter, pagination.observations, pagination.summaries, pagination.prompts]);
// Reset paginated data and load first page when filter changes
useEffect(() => {
setPaginatedObservations([]);
setPaginatedSummaries([]);
setPaginatedPrompts([]);
handleLoadMore();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentFilter]);
return (
<div className="container">
<div className="main-col">
<Header
isConnected={isConnected}
projects={projects}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSettingsToggle={toggleSidebar}
sidebarOpen={sidebarOpen}
isProcessing={isProcessing}
queueDepth={queueDepth}
themePreference={preference}
onThemeChange={setThemePreference}
/>
<Feed
observations={allObservations}
summaries={allSummaries}
prompts={allPrompts}
onLoadMore={handleLoadMore}
isLoading={pagination.observations.isLoading || pagination.summaries.isLoading || pagination.prompts.isLoading}
hasMore={pagination.observations.hasMore || pagination.summaries.hasMore || pagination.prompts.hasMore}
/>
</div>
<Sidebar
isOpen={sidebarOpen}
settings={settings}
stats={stats}
isSaving={isSaving}
saveStatus={saveStatus}
isConnected={isConnected}
onSave={saveSettings}
onClose={toggleSidebar}
onRefreshStats={refreshStats}
/>
</div>
);
}