-
Notifications
You must be signed in to change notification settings - Fork 237
feat: stream real-time updates into Director's Notes #508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: UnifiedHistoryEntry, sourceSessionId: string) => void | ||
| ): (() => void) => { | ||
| const handler = (_event: unknown, entry: UnifiedHistoryEntry, sessionId: string) => { | ||
| callback(entry, sessionId); | ||
|
Comment on lines
+114
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix On Line 115, 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 |
||
| }; | ||
| ipcRenderer.on('history:entryAdded', handler); | ||
| return () => { | ||
| ipcRenderer.removeListener('history:entryAdded', handler); | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 */ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -86,6 +87,114 @@ | |||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Build session name map for enriching streamed entries with agentName | ||||||||||||||||||||||||||||||||||||||||||||||
| 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 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.
Outdated
There was a problem hiding this comment.
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:
| 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.)
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Check failure on line 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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 167 in src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
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:167: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
There was a problem hiding this comment.
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
entryargument asUnifiedHistoryEntry, which declaressourceSessionIdas a required field. However, the main process broadcasts a plainHistoryEntry(which lackssourceSessionId); thesourceSessionIdis passed separately as thesessionIdparameter. This type mismatch means TypeScript will not catch downstream code that accidentally readsentry.sourceSessionIddirectly, believing it to be populated from the wire when it's actually absent.Use the correct source type:
(You may need to import
HistoryEntryfrom../../shared/typesif not already imported.)