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>
This commit is contained in:
Alex Newman
2025-11-17 15:11:37 -05:00
parent 02444392da
commit 74c8afd0e0
9 changed files with 111 additions and 18 deletions
+30
View File
@@ -376,6 +376,36 @@
animation: spin 1.5s linear infinite;
}
.queue-bubble {
position: absolute;
top: -8px;
right: -8px;
background: var(--color-accent-primary);
color: var(--color-text-button);
font-size: 10px;
font-weight: 600;
font-family: 'Monaspace Radon', monospace;
min-width: 18px;
height: 18px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
animation: pulse 2s ease-in-out infinite;
z-index: 10;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.logo-text {
font-family: 'Monaspace Radon', monospace;
font-weight: 100;
+2 -1
View File
@@ -17,7 +17,7 @@ export function App() {
const [paginatedSummaries, setPaginatedSummaries] = useState<Summary[]>([]);
const [paginatedPrompts, setPaginatedPrompts] = useState<UserPrompt[]>([]);
const { observations, summaries, prompts, projects, isProcessing, isConnected } = useSSE();
const { observations, summaries, prompts, projects, isProcessing, queueDepth, isConnected } = useSSE();
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
const { stats, refreshStats } = useStats();
const { preference, resolvedTheme, setThemePreference } = useTheme();
@@ -96,6 +96,7 @@ export function App() {
onSettingsToggle={toggleSidebar}
sidebarOpen={sidebarOpen}
isProcessing={isProcessing}
queueDepth={queueDepth}
themePreference={preference}
onThemeChange={setThemePreference}
/>
+10 -1
View File
@@ -10,6 +10,7 @@ interface HeaderProps {
onSettingsToggle: () => void;
sidebarOpen: boolean;
isProcessing: boolean;
queueDepth: number;
themePreference: ThemePreference;
onThemeChange: (theme: ThemePreference) => void;
}
@@ -22,13 +23,21 @@ export function Header({
onSettingsToggle,
sidebarOpen,
isProcessing,
queueDepth,
themePreference,
onThemeChange
}: HeaderProps) {
return (
<div className="header">
<h1>
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
<div style={{ position: 'relative', display: 'inline-block' }}>
<img src="claude-mem-logomark.webp" alt="" className={`logomark ${isProcessing ? 'spinning' : ''}`} />
{queueDepth > 0 && (
<div className="queue-bubble">
{queueDepth}
</div>
)}
</div>
<span className="logo-text">claude-mem</span>
</h1>
<div className="status">
+4 -2
View File
@@ -10,6 +10,7 @@ export function useSSE() {
const [projects, setProjects] = useState<string[]>([]);
const [isConnected, setIsConnected] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [queueDepth, setQueueDepth] = useState(0);
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
@@ -83,8 +84,9 @@ export function useSSE() {
case 'processing_status':
if (typeof data.isProcessing === 'boolean') {
console.log('[SSE] Processing status:', data.isProcessing);
console.log('[SSE] Processing status:', data.isProcessing, 'Queue depth:', data.queueDepth);
setIsProcessing(data.isProcessing);
setQueueDepth(data.queueDepth || 0);
}
break;
}
@@ -107,5 +109,5 @@ export function useSSE() {
};
}, []);
return { observations, summaries, prompts, projects, isProcessing, isConnected };
return { observations, summaries, prompts, projects, isProcessing, queueDepth, isConnected };
}