Skip to content

feat: stream real-time updates into Director's Notes#508

Open
pedramamini wants to merge 2 commits intomainfrom
directors-notes-stream
Open

feat: stream real-time updates into Director's Notes#508
pedramamini wants to merge 2 commits intomainfrom
directors-notes-stream

Conversation

@pedramamini
Copy link
Collaborator

@pedramamini pedramamini commented Mar 4, 2026

Summary

  • Broadcast new history entries via IPC (history:entryAdded) after historyManager.addEntry() so the Director's Notes modal receives live updates while open
  • Extend HistoryStatsBar with spinning Active agent count (warning color) and Queued message count indicators, backward-compatible with the per-session History panel
  • Subscribe in UnifiedHistoryTab on mount with requestAnimationFrame batching, Set-based deduplication, lookback filtering, and incremental stats counter updates — live counts derived from two primitive Zustand selectors against the session store

Changed files

File Change
src/main/ipc/handlers/history.ts Add HistoryHandlerDependencies with safeSend, broadcast after addEntry()
src/main/ipc/handlers/index.ts Wire createSafeSend(deps.getMainWindow) to history handlers
src/main/preload/directorNotes.ts Add onHistoryEntryAdded() IPC subscription
src/renderer/global.d.ts Type declaration for new API
src/renderer/components/History/HistoryStatsBar.tsx Optional activeAgentCount + totalQueuedItems with spinning/queued indicators
src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx Streaming subscription, RAF batching, Zustand live counts
src/__tests__/main/ipc/handlers/history.test.ts Updated for deps param, added broadcast test

Test plan

  • Open Director's Notes, trigger an agent completion — verify entry streams in without refresh
  • Verify spinning indicator shows count of busy agents, disappears when all idle
  • Verify queued count reflects pending messages across all agents
  • Per-session History panel renders identically (no regressions from optional fields)

Summary by CodeRabbit

  • New Features
    • Live history streaming: new entries appear in real time across sessions and are deduplicated and merged into the view.
    • Live indicators in history stats: shows active agent and queued item counts.
    • History events now include source session info and agent name enrichment for clearer context.
    • Public subscription API: consumers can subscribe/unsubscribe to receive live history entry updates.

…e activity indicators

Broadcast new history entries via IPC when they are added, subscribe in
the UnifiedHistoryTab with RAF batching and deduplication, and extend
the HistoryStatsBar with spinning Active agent count and Queued message
count indicators derived from the Zustand session store.
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This PR adds real-time history streaming: the main process now broadcasts new history entries via IPC, preload exposes an onHistoryEntryAdded subscription, IPC handler registration accepts a safeSend dependency, and renderer UI components subscribe and display live history and activity counts.

Changes

Cohort / File(s) Summary
IPC Handler Infrastructure
src/main/ipc/handlers/history.ts, src/main/ipc/handlers/index.ts, src/__tests__/main/ipc/handlers/history.test.ts
Changed history handlers to accept a HistoryHandlerDependencies object exposing safeSend. Handlers now broadcast history:entryAdded after adding entries. Updated tests to assert broadcast via a mock safeSend.
Preload API
src/main/preload/directorNotes.ts
Added onHistoryEntryAdded(callback) to expose renderer subscription to history:entryAdded IPC messages, returning a disposer to remove the listener.
Global Types / Declarations
src/renderer/global.d.ts
Added onHistoryEntryAdded to public API surfaces and switched history payload usageStats to the shared UsageStats type for consistency.
Renderer UI
src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx, src/renderer/components/History/HistoryStatsBar.tsx
UnifiedHistoryTab subscribes to live entries, buffers via requestAnimationFrame, enriches entries with session/agent names, deduplicates and merges into history while respecting lookback filters. HistoryStatsBar adds activeAgentCount and totalQueuedItems and renders live activity indicators.

Sequence Diagram(s)

sequenceDiagram
    participant Main as Main Process
    participant Handlers as IPC Handlers
    participant Preload as Preload Layer
    participant Renderer as Renderer (UnifiedHistoryTab)
    participant SessionStore as Session Store

    Main->>Handlers: add history entry
    Handlers->>Handlers: persist entry
    Handlers->>Main: call deps.safeSend(entry, sourceSessionId)
    Main->>Preload: IPC message 'history:entryAdded' (entry, sourceSessionId)
    Preload->>Renderer: invoke onHistoryEntryAdded callback
    Renderer->>SessionStore: lookup agent/session names
    SessionStore-->>Renderer: return sessionNameMap
    Renderer->>Renderer: buffer entry (pendingEntriesRef)
    Renderer->>Renderer: requestAnimationFrame flush -> dedupe & enrich
    Renderer->>Renderer: merge into entries & update stats
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature: streaming real-time updates into the Director's Notes modal. It directly relates to the primary changes across the codebase (IPC broadcasting, live subscription, real-time UI updates).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch directors-notes-stream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR adds real-time streaming of history entries into Director's Notes by broadcasting history:entryAdded IPC events from the main process after each addEntry() call, subscribing in the renderer with requestAnimationFrame batching, and enriching the stats bar with live active-agent and queued-message counters sourced from Zustand.

Key observations:

  • Main-process / preload plumbing (history.ts, index.ts) is clean and well-structured; dependency injection of safeSend is a good pattern.
  • HistoryStatsBar changes are purely additive and backward-compatible.
  • UnifiedHistoryTab contains two logic bugs in the streaming path:
    (1) the sessionNameMap Zustand selector constructs a new Map on every call, which always fails Zustand's strict equality check and triggers constant re-renders and IPC re-subscriptions;
    (2) setTotalEntries increments by the raw batch.length rather than the deduplicated delta, so the entry count badge will diverge from reality when duplicate IDs arrive.
  • directorNotes.ts incorrectly types the IPC handler's entry argument as UnifiedHistoryEntry (which requires sourceSessionId) when the wire payload is a plain HistoryEntry.

Confidence Score: 2/5

  • Not safe to merge as-is due to two logic bugs in UnifiedHistoryTab that will cause re-subscription churn and entry count drift in the streaming path.
  • The main-process plumbing and stats-bar changes are solid (confidence 5), but UnifiedHistoryTab contains two critical logic bugs that will manifest under active agent load: (1) the sessionNameMap Zustand selector returns a fresh Map reference on every render, causing the streaming effect to constantly tear down and re-subscribe, wasting CPU and creating an unstable subscription; (2) setTotalEntries increments by full batch size rather than the deduplicated delta, causing the entry counter badge to drift upward when duplicates arrive. The preload type error is low-severity (TypeScript-only) but contributes to erosion of type safety. Both component issues are high-value fixes that should be addressed before merge.
  • src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx (sessionNameMap selector and totalEntries counter) requires the most attention; src/main/preload/directorNotes.ts needs a minor type fix.

Sequence Diagram

sequenceDiagram
    participant Agent as Agent (Main Process)
    participant HistoryHandler as history:add IPC Handler
    participant HistoryManager as HistoryManager
    participant SafeSend as safeSend
    participant Renderer as Renderer (BrowserWindow)
    participant Preload as directorNotes preload
    participant Component as UnifiedHistoryTab

    Agent->>HistoryHandler: ipcRenderer.invoke('history:add', entry)
    HistoryHandler->>HistoryManager: addEntry(sessionId, projectPath, entry)
    HistoryManager-->>HistoryHandler: stored
    HistoryHandler->>SafeSend: safeSend('history:entryAdded', entry, sessionId)
    SafeSend->>Renderer: webContents.send('history:entryAdded', entry, sessionId)
    Renderer->>Preload: ipcRenderer.on('history:entryAdded', handler)
    Preload->>Component: callback(entry, sessionId)
    Component->>Component: push to pendingEntriesRef
    Component->>Component: requestAnimationFrame(flushPending)
    Component->>Component: flushPending() — deduplicate, sort, setState
    Component->>Component: setEntries / setGraphEntries / setHistoryStats (batched)
Loading

Last reviewed commit: a9952f4

Comment on lines +113 to +119
const sessionNameMap = useSessionStore((s) => {
const map = new Map<string, string>();
for (const sess of s.sessions) {
map.set(sess.id, sess.name);
}
return map;
});
Copy link

Choose a reason for hiding this comment

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

The sessionNameMap Zustand selector constructs a new Map on every invocation. Since Zustand uses strict reference equality checks for selector memoization, this fresh Map reference is never equal to the previous one, causing the selector result to change on every render.

This makes sessionNameMap an unstable dependency for the streaming useEffect below (line 121), which tears down and re-subscribes to the IPC listener on every render cycle. Under active agent load, this creates constant re-subscription churn, making the streaming connection effectively unstable and wasting CPU cycles.

Use a stable ref pattern instead to read session data without making it a reactive dependency:

Suggested change
const sessionNameMap = useSessionStore((s) => {
const map = new Map<string, string>();
for (const sess of s.sessions) {
map.set(sess.id, sess.name);
}
return map;
});
const sessionsRef = useRef(useSessionStore.getState().sessions);
useEffect(() => {
// Subscribe to future changes but keep current value in a ref
return useSessionStore.subscribe((s) => {
sessionsRef.current = s.sessions;
});
}, []);
// Then in the onHistoryEntryAdded callback below, read from sessionsRef:
const enriched = {
...rawEntry,
sourceSessionId,
agentName: sessionsRef.current.find((s) => s.id === sourceSessionId)?.name,
} as UnifiedHistoryEntry;

This way, the streaming effect has no reactive dependencies on session data and remains stable.

return merged;
});

setTotalEntries((prev) => prev + batch.length);
Copy link

Choose a reason for hiding this comment

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

setEntries correctly deduplicates the batch—only adding entries whose IDs aren't already in the list—but setTotalEntries increments by the full batch.length, counting duplicate IDs that were silently dropped. Over time, especially if IPC redelivers the same entry due to the re-subscription churn described above, the entry count will drift upward while the actual entry list does not grow, breaking the badge display.

The count should mirror the number of entries actually added:

Suggested change
setTotalEntries((prev) => prev + batch.length);
setEntries((prev) => {
const existingIds = new Set(prev.map((e) => e.id));
const newEntries = batch.filter((e) => !existingIds.has(e.id));
if (newEntries.length === 0) return prev;
// Track exactly how many new entries were added
setTotalEntries((t) => t + newEntries.length);
const merged = [...newEntries, ...prev];
merged.sort((a, b) => b.timestamp - a.timestamp);
return merged;
});

(Nesting setTotalEntries inside the setEntries updater is safe in React 18's automatic batching and ensures the delta always matches the deduplicated count.)

onHistoryEntryAdded: (
callback: (entry: UnifiedHistoryEntry, sourceSessionId: string) => void
): (() => void) => {
const handler = (_event: unknown, entry: UnifiedHistoryEntry, sessionId: string) => {
Copy link

Choose a reason for hiding this comment

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

The IPC handler types the entry argument as UnifiedHistoryEntry, which declares sourceSessionId as a required field. However, the main process broadcasts a plain HistoryEntry (which lacks sourceSessionId); the sourceSessionId is passed separately as the sessionId parameter. This type mismatch means TypeScript will not catch downstream code that accidentally reads entry.sourceSessionId directly, believing it to be populated from the wire when it's actually absent.

Use the correct source type:

Suggested change
const handler = (_event: unknown, entry: UnifiedHistoryEntry, sessionId: string) => {
const handler = (_event: unknown, entry: HistoryEntry, sessionId: string) => {
callback(entry, sessionId);
};

(You may need to import HistoryEntry from ../../shared/types if not already imported.)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/preload/directorNotes.ts`:
- Around line 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.

In `@src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx`:
- Around line 121-165: flushPending currently dedupes only against prior state
and updates totals/stats/graph from the raw batch, and pendingEntriesRef is
never cleared on cleanup which can replay stale items; modify flushPending (and
the similar handler around lines 190-195) to 1) internally dedupe the incoming
batch itself (remove duplicates within batch) and then dedupe that result
against existing IDs from entries/graph before merging, 2) compute
setTotalEntries and setHistoryStats counts from the final newEntries (not the
raw batch) so counters reflect only actually-added items, and 3) ensure on
unmount/resubscribe you clear pendingEntriesRef.current (and cancel rafIdRef) to
avoid flushing stale queued entries.

In `@src/renderer/global.d.ts`:
- Around line 2715-2721: The realtime usageStats shape in the callback currently
uses cacheReadTokens/cacheWriteTokens which diverges from the canonical
UsageStats interface; update the usageStats object to match the exact property
names and types defined on the existing UsageStats interface (e.g., rename
cacheReadTokens/cacheWriteTokens to the canonical property names used by
UsageStats or import/extend UsageStats directly), and ensure the usageStats
declaration (symbol: usageStats) matches the canonical UsageStats type so
streamed entries are correctly typed.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd8d5cc and a9952f4.

📒 Files selected for processing (7)
  • src/__tests__/main/ipc/handlers/history.test.ts
  • src/main/ipc/handlers/history.ts
  • src/main/ipc/handlers/index.ts
  • src/main/preload/directorNotes.ts
  • src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
  • src/renderer/components/History/HistoryStatsBar.tsx
  • src/renderer/global.d.ts

Comment on lines +114 to +118
onHistoryEntryAdded: (
callback: (entry: UnifiedHistoryEntry, sourceSessionId: string) => void
): (() => void) => {
const handler = (_event: unknown, entry: UnifiedHistoryEntry, sessionId: string) => {
callback(entry, sessionId);
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.

…orrect types

- Replace unstable sessionNameMap Zustand selector (new Map per render) with
  a stable ref + subscribe pattern to avoid streaming effect re-subscription
- Dedupe within batch before merging; compute setTotalEntries and
  setHistoryStats from deduplicated entries only (not raw batch)
- Clear pendingEntriesRef on cleanup to prevent stale replay after resubscribe
- Use HistoryEntry (not UnifiedHistoryEntry) in preload callback type since
  the wire payload lacks sourceSessionId
- Use canonical UsageStats interface in global.d.ts (fixes pre-existing
  cacheReadTokens/cacheWriteTokens field name mismatch)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx (1)

137-160: ⚠️ Potential issue | 🟠 Major

Avoid side effects inside the setEntries updater to prevent counter drift.

setTotalEntries and setHistoryStats are invoked from inside the setEntries updater at lines 143 and 146. With React StrictMode enabled in the renderer (src/renderer/main.tsx:88), this can double-apply counter updates even when entry merges are correct, causing drift in totalCount, autoCount, and userCount.

Move both state updates outside the updater function to prevent replay issues:

Proposed fix
+		const knownEntryIdsRef = useRef<Set<string>>(new Set());
+		useEffect(() => {
+			knownEntryIdsRef.current = new Set(entries.map((e) => e.id));
+		}, [entries]);

 		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);
 					}
 				}
+
+				const newEntries = uniqueBatch.filter((e) => !knownEntryIdsRef.current.has(e.id));
+				if (newEntries.length === 0) return;
+				for (const entry of newEntries) knownEntryIdsRef.current.add(entry.id);

 				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;
 				});
+
+				setTotalEntries((t) => t + newEntries.length);
+				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,
+					};
+				});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx` around lines 137
- 160, The setEntries updater currently performs side effects by calling
setTotalEntries and setHistoryStats inside its functional update (see
setEntries, setTotalEntries, setHistoryStats, uniqueBatch, existingIds,
newEntries); move those state updates out of the setEntries updater to avoid
double application under React StrictMode: inside the updater only return the
new entries array (compute newEntries by filtering uniqueBatch against
existingIds), then after setEntries completes compute the increments
(newAuto/newUser and newEntries.length) and call setTotalEntries and
setHistoryStats once with those computed deltas (use the functional form for
setHistoryStats to add deltas to prevStats) so counters are updated exactly once
per batch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx`:
- Around line 137-160: The setEntries updater currently performs side effects by
calling setTotalEntries and setHistoryStats inside its functional update (see
setEntries, setTotalEntries, setHistoryStats, uniqueBatch, existingIds,
newEntries); move those state updates out of the setEntries updater to avoid
double application under React StrictMode: inside the updater only return the
new entries array (compute newEntries by filtering uniqueBatch against
existingIds), then after setEntries completes compute the increments
(newAuto/newUser and newEntries.length) and call setTotalEntries and
setHistoryStats once with those computed deltas (use the functional form for
setHistoryStats to add deltas to prevStats) so counters are updated exactly once
per batch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 068890e1-97a0-4333-a726-a0828a04751c

📥 Commits

Reviewing files that changed from the base of the PR and between a9952f4 and 6c9c9fe.

📒 Files selected for processing (3)
  • src/main/preload/directorNotes.ts
  • src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
  • src/renderer/global.d.ts

@pedramamini pedramamini self-assigned this Mar 7, 2026
@pedramamini pedramamini added the RC Getting soak time in RC branch now label Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RC Getting soak time in RC branch now

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant