Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/__tests__/main/ipc/handlers/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ vi.mock('../../../../main/utils/logger', () => ({
describe('history IPC handlers', () => {
let handlers: Map<string, Function>;
let mockHistoryManager: Partial<HistoryManager>;
let mockSafeSend: ReturnType<typeof vi.fn>;

// Sample history entries for testing
const createMockEntry = (overrides: Partial<HistoryEntry> = {}): HistoryEntry => ({
Expand All @@ -54,6 +55,8 @@ describe('history IPC handlers', () => {
// Clear mocks
vi.clearAllMocks();

mockSafeSend = vi.fn();

// Create mock history manager
mockHistoryManager = {
getEntries: vi.fn().mockReturnValue([]),
Expand Down Expand Up @@ -101,8 +104,8 @@ describe('history IPC handlers', () => {
handlers.set(channel, handler);
});

// Register handlers
registerHistoryHandlers();
// Register handlers with mock safeSend
registerHistoryHandlers({ safeSend: mockSafeSend });
});

afterEach(() => {
Expand Down Expand Up @@ -282,6 +285,15 @@ describe('history IPC handlers', () => {
expect(result).toBe(true);
});

it('should broadcast entry via safeSend after adding', async () => {
const entry = createMockEntry({ sessionId: 'session-1', projectPath: '/test' });

const handler = handlers.get('history:add');
await handler!({} as any, entry);

expect(mockSafeSend).toHaveBeenCalledWith('history:entryAdded', entry, 'session-1');
});

it('should use orphaned session ID when sessionId is missing', async () => {
const entry = createMockEntry({ sessionId: undefined, projectPath: '/test' });

Expand Down
11 changes: 10 additions & 1 deletion src/main/ipc/handlers/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ import { HistoryEntry } from '../../../shared/types';
import { PaginationOptions, ORPHANED_SESSION_ID } from '../../../shared/history';
import { getHistoryManager } from '../../history-manager';
import { withIpcErrorLogging, CreateHandlerOptions } from '../../utils/ipcHandler';
import type { SafeSendFn } from '../../utils/safe-send';

const LOG_CONTEXT = '[History]';

export interface HistoryHandlerDependencies {
safeSend: SafeSendFn;
}

// Helper to create handler options with consistent context
const handlerOpts = (operation: string): Pick<CreateHandlerOptions, 'context' | 'operation'> => ({
context: LOG_CONTEXT,
Expand All @@ -39,7 +44,7 @@ const handlerOpts = (operation: string): Pick<CreateHandlerOptions, 'context' |
* - Get history file path (for AI context integration)
* - List sessions with history
*/
export function registerHistoryHandlers(): void {
export function registerHistoryHandlers(deps: HistoryHandlerDependencies): void {
const historyManager = getHistoryManager();

// Get all history entries, optionally filtered by project and/or session
Expand Down Expand Up @@ -111,6 +116,10 @@ export function registerHistoryHandlers(): void {
const sessionId = entry.sessionId || ORPHANED_SESSION_ID;
historyManager.addEntry(sessionId, entry.projectPath, entry);
logger.info(`Added history entry: ${entry.type}`, LOG_CONTEXT, { summary: entry.summary });

// Broadcast to renderer for real-time Director's Notes streaming
deps.safeSend('history:entryAdded', entry, sessionId);

return true;
})
);
Expand Down
8 changes: 6 additions & 2 deletions src/main/ipc/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Store from 'electron-store';
import { registerGitHandlers, GitHandlerDependencies } from './git';
import { registerAutorunHandlers } from './autorun';
import { registerPlaybooksHandlers } from './playbooks';
import { registerHistoryHandlers } from './history';
import { registerHistoryHandlers, HistoryHandlerDependencies } from './history';
import { registerAgentsHandlers, AgentsHandlerDependencies } from './agents';
import { registerProcessHandlers, ProcessHandlerDependencies } from './process';
import {
Expand Down Expand Up @@ -57,6 +57,7 @@ import { AgentDetector } from '../../agents';
import { ProcessManager } from '../../process-manager';
import { WebServer } from '../../web-server';
import { tunnelManager as tunnelManagerInstance } from '../../tunnel-manager';
import { createSafeSend } from '../../utils/safe-send';

// Type for tunnel manager instance
type TunnelManagerType = typeof tunnelManagerInstance;
Expand All @@ -66,6 +67,7 @@ export { registerGitHandlers };
export { registerAutorunHandlers };
export { registerPlaybooksHandlers };
export { registerHistoryHandlers };
export type { HistoryHandlerDependencies };
export { registerAgentsHandlers };
export { registerProcessHandlers };
export { registerPersistenceHandlers };
Expand Down Expand Up @@ -172,7 +174,9 @@ export function registerAllHandlers(deps: HandlerDependencies): void {
});
registerAutorunHandlers(deps);
registerPlaybooksHandlers(deps);
registerHistoryHandlers();
registerHistoryHandlers({
safeSend: createSafeSend(deps.getMainWindow),
});
registerAgentsHandlers({
getAgentDetector: deps.getAgentDetector,
agentConfigsStore: deps.agentConfigsStore,
Expand Down
18 changes: 17 additions & 1 deletion src/main/preload/directorNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { ipcRenderer } from 'electron';
import type { ToolType } from '../../shared/types';
import type { ToolType, HistoryEntry } from '../../shared/types';

/** Aggregate stats returned alongside unified history */
export interface UnifiedHistoryStats {
Expand Down Expand Up @@ -106,6 +106,22 @@ export function createDirectorNotesApi() {
// Generate AI synopsis
generateSynopsis: (options: SynopsisOptions): Promise<SynopsisResult> =>
ipcRenderer.invoke('director-notes:generateSynopsis', options),

/**
* Subscribe to new history entries as they are added in real-time.
* Returns a cleanup function to unsubscribe.
*/
onHistoryEntryAdded: (
callback: (entry: HistoryEntry, sourceSessionId: string) => void
): (() => void) => {
const handler = (_event: unknown, entry: HistoryEntry, sessionId: string) => {
callback(entry, sessionId);
Comment on lines +114 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix onHistoryEntryAdded callback payload typing.

On Line 115, entry is typed as UnifiedHistoryEntry (which requires sourceSessionId), but sourceSessionId is delivered as the second callback arg. This contract is misleading and can cause incorrect assumptions in callers.

Suggested fix
-import type { ToolType } from '../../shared/types';
+import type { ToolType, HistoryEntry } from '../../shared/types';
@@
 		onHistoryEntryAdded: (
-			callback: (entry: UnifiedHistoryEntry, sourceSessionId: string) => void
+			callback: (entry: HistoryEntry, sourceSessionId: string) => void
 		): (() => void) => {
-			const handler = (_event: unknown, entry: UnifiedHistoryEntry, sessionId: string) => {
+			const handler = (_event: unknown, entry: HistoryEntry, sessionId: string) => {
 				callback(entry, sessionId);
 			};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/preload/directorNotes.ts` around lines 114 - 118, The callback
currently types its first arg as UnifiedHistoryEntry but the actual payload
lacks sourceSessionId (it's passed as the second arg), so update the callback
and handler signatures to reflect that (e.g. change the first parameter type to
Omit<UnifiedHistoryEntry, 'sourceSessionId'>) in onHistoryEntryAdded and the
inner handler variable so callers can't assume sourceSessionId is already
present; keep passing sessionId as the second argument when invoking callback.

};
ipcRenderer.on('history:entryAdded', handler);
return () => {
ipcRenderer.removeListener('history:entryAdded', handler);
};
},
};
}

Expand Down
125 changes: 123 additions & 2 deletions src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import type { HistoryStats } from '../History';
import { HistoryDetailModal } from '../HistoryDetailModal';
import { useListNavigation, useSettings } from '../../hooks';
import { useSessionStore } from '../../stores/sessionStore';
import type { TabFocusHandle } from './OverviewTab';

/** Page size for progressive loading */
Expand Down Expand Up @@ -86,6 +87,126 @@
const loadingMoreRef = useRef(false); // Guard against concurrent loads
const searchInputRef = useRef<HTMLInputElement>(null);

// --- Live agent activity from Zustand (primitive selectors for efficient re-renders) ---
const activeAgentCount = useSessionStore(
(s) => s.sessions.filter((sess) => sess.state === 'busy').length
);
const totalQueuedItems = useSessionStore((s) =>
s.sessions.reduce((sum, sess) => sum + (sess.executionQueue?.length || 0), 0)
);

// Merge live counts into history stats for the stats bar
const enrichedStats = useMemo<HistoryStats | null>(() => {
if (!historyStats) return null;
return {
...historyStats,
activeAgentCount,
totalQueuedItems,
};
}, [historyStats, activeAgentCount, totalQueuedItems]);

// --- Real-time streaming of new history entries ---
const pendingEntriesRef = useRef<UnifiedHistoryEntry[]>([]);
const rafIdRef = useRef<number | null>(null);

// Stable ref for session names — avoids making the streaming effect depend on session state
const sessionsRef = useRef(useSessionStore.getState().sessions);
useEffect(() => {
return useSessionStore.subscribe((s) => {
sessionsRef.current = s.sessions;
});
}, []);

useEffect(() => {
const flushPending = () => {
rafIdRef.current = null;
const batch = pendingEntriesRef.current;
if (batch.length === 0) return;
pendingEntriesRef.current = [];

// Dedupe within the batch itself
const seen = new Set<string>();
const uniqueBatch: UnifiedHistoryEntry[] = [];
for (const entry of batch) {
if (!seen.has(entry.id)) {
seen.add(entry.id);
uniqueBatch.push(entry);
}
}

setEntries((prev) => {
const existingIds = new Set(prev.map((e) => e.id));
const newEntries = uniqueBatch.filter((e) => !existingIds.has(e.id));
if (newEntries.length === 0) return prev;

// Update total count to match actual additions
setTotalEntries((t) => t + newEntries.length);

// Incrementally update stats counters from deduplicated entries
setHistoryStats((prevStats) => {
if (!prevStats) return prevStats;
let newAuto = 0;
let newUser = 0;
for (const entry of newEntries) {
if (entry.type === 'AUTO') newAuto++;
else if (entry.type === 'USER') newUser++;
}
return {
...prevStats,
autoCount: prevStats.autoCount + newAuto,
userCount: prevStats.userCount + newUser,
totalCount: prevStats.totalCount + newAuto + newUser,
};
});

const merged = [...newEntries, ...prev];
merged.sort((a, b) => b.timestamp - a.timestamp);
return merged;
});

// Update graph entries for ActivityGraph
setGraphEntries((prev) => {
const existingIds = new Set(prev.map((e) => e.id));
const newEntries = uniqueBatch.filter((e) => !existingIds.has(e.id));
if (newEntries.length === 0) return prev;
const merged = [...newEntries, ...prev];
merged.sort((a, b) => b.timestamp - a.timestamp);
return merged;
});
};

const cleanup = window.maestro.directorNotes.onHistoryEntryAdded(

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Stats Bar > does not render stats bar while loading

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Stats Bar > does not render stats bar when no entries exist

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Stats Bar > renders stats bar with aggregate counts after loading

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > displays loaded/total when more entries exist

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > displays total entry count

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > renders entries from all sessions (aggregated)

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > shows empty state when no entries found

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > fetches all-time history when defaultLookbackDays is 0

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > fetches unified history on mount using default lookback from settings

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24

Check failure on line 178 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx

View workflow job for this annotation

GitHub Actions / test

src/__tests__/renderer/components/DirectorNotes/UnifiedHistoryTab.test.tsx > UnifiedHistoryTab > Loading and Data Fetching > shows loading state initially

TypeError: window.maestro.directorNotes.onHistoryEntryAdded is not a function ❯ src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx:178:49 ❯ commitHookEffectListMount node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue node_modules/react/cjs/react.development.js:2667:24
(rawEntry, sourceSessionId) => {
// Check if entry is within lookback window
if (lookbackHours !== null) {
const cutoff = Date.now() - lookbackHours * 60 * 60 * 1000;
if (rawEntry.timestamp < cutoff) return;
}

const enriched = {
...rawEntry,
sourceSessionId,
agentName: sessionsRef.current.find((s) => s.id === sourceSessionId)?.name,
} as UnifiedHistoryEntry;

pendingEntriesRef.current.push(enriched);

// Coalesce into a single frame update
if (rafIdRef.current === null) {
rafIdRef.current = requestAnimationFrame(flushPending);
}
}
);

return () => {
cleanup();
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
pendingEntriesRef.current = [];
};
}, [lookbackHours]);

useImperativeHandle(
ref,
() => ({
Expand Down Expand Up @@ -439,8 +560,8 @@
onScroll={handleScroll}
>
{/* Stats bar — scrolls with entries */}
{!isLoading && historyStats && historyStats.totalCount > 0 && (
<HistoryStatsBar stats={historyStats} theme={theme} />
{!isLoading && enrichedStats && enrichedStats.totalCount > 0 && (
<HistoryStatsBar stats={enrichedStats} theme={theme} />
)}

{isLoading ? (
Expand Down
54 changes: 53 additions & 1 deletion src/renderer/components/History/HistoryStatsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { memo } from 'react';
import { Layers, Hash, Bot, User, BarChart3 } from 'lucide-react';
import { Layers, Hash, Bot, User, BarChart3, Loader2, ListOrdered } from 'lucide-react';
import type { Theme } from '../../types';

export interface HistoryStats {
Expand All @@ -8,6 +8,10 @@ export interface HistoryStats {
autoCount: number;
userCount: number;
totalCount: number;
/** Number of agents currently in 'busy' state (live indicator) */
activeAgentCount?: number;
/** Total queued messages across all agents (live indicator) */
totalQueuedItems?: number;
}

interface HistoryStatsBarProps {
Expand Down Expand Up @@ -45,6 +49,10 @@ function StatItem({ icon, label, value, color, theme }: StatItemProps) {
);
}

const showLiveIndicators = (stats: HistoryStats) =>
(stats.activeAgentCount !== undefined && stats.activeAgentCount > 0) ||
(stats.totalQueuedItems !== undefined && stats.totalQueuedItems > 0);

export const HistoryStatsBar = memo(function HistoryStatsBar({
stats,
theme,
Expand Down Expand Up @@ -88,6 +96,50 @@ export const HistoryStatsBar = memo(function HistoryStatsBar({
color={theme.colors.textMain}
theme={theme}
/>

{/* Live activity indicators — only shown when provided and > 0 */}
{showLiveIndicators(stats) && (
<>
<div
className="w-px h-4 flex-shrink-0"
style={{ backgroundColor: theme.colors.border }}
/>
{stats.activeAgentCount !== undefined && stats.activeAgentCount > 0 && (
<div className="flex items-center gap-1.5">
<span
className="flex items-center justify-center w-5 h-5 rounded"
style={{
backgroundColor: theme.colors.warning + '15',
color: theme.colors.warning,
}}
>
<Loader2 className="w-3 h-3 animate-spin" />
</span>
<span
className="text-[10px] uppercase tracking-wider"
style={{ color: theme.colors.textDim }}
>
Active
</span>
<span
className="text-xs font-bold tabular-nums"
style={{ color: theme.colors.warning }}
>
{stats.activeAgentCount}
</span>
</div>
)}
{stats.totalQueuedItems !== undefined && stats.totalQueuedItems > 0 && (
<StatItem
icon={<ListOrdered className="w-3 h-3" />}
label="Queued"
value={stats.totalQueuedItems}
color={theme.colors.accent}
theme={theme}
/>
)}
</>
)}
</div>
);
});
Loading
Loading