From b093e87c89a8d5675dc894bc6520ab63c4bd49fa Mon Sep 17 00:00:00 2001 From: iliya Date: Fri, 6 Mar 2026 10:40:45 +0200 Subject: [PATCH] feat: add options for bypassing cache in session and subagent detail retrieval - Updated handleGetSessionDetail and handleGetSubagentDetail functions to accept an optional options parameter for bypassing cache. - Modified IPC and API methods to support the new options parameter, enhancing flexibility in data retrieval. - Adjusted MemberLogsTab to utilize the bypassCache option when fetching log details, improving data freshness. --- src/main/ipc/sessions.ts | 5 +- src/main/ipc/subagents.ts | 5 +- src/main/services/team/TeamDataService.ts | 71 +++++++++++++------ src/preload/index.ts | 12 ++-- src/renderer/api/httpClient.ts | 9 ++- .../components/team/members/MemberLogsTab.tsx | 64 ++++++++++++++--- src/shared/types/api.ts | 9 ++- 7 files changed, 134 insertions(+), 41 deletions(-) diff --git a/src/main/ipc/sessions.ts b/src/main/ipc/sessions.ts index e99caa7c..cea9d76e 100644 --- a/src/main/ipc/sessions.ts +++ b/src/main/ipc/sessions.ts @@ -198,7 +198,8 @@ async function handleGetSessionsByIds( async function handleGetSessionDetail( _event: IpcMainInvokeEvent, projectId: string, - sessionId: string + sessionId: string, + options?: { bypassCache?: boolean } ): Promise { try { const validatedProject = validateProjectId(projectId); @@ -220,7 +221,7 @@ async function handleGetSessionDetail( // Check cache first let sessionDetail = dataCache.get(cacheKey); - if (sessionDetail) { + if (sessionDetail && !options?.bypassCache) { return sessionDetail; } diff --git a/src/main/ipc/subagents.ts b/src/main/ipc/subagents.ts index 747605d3..9dc10d52 100644 --- a/src/main/ipc/subagents.ts +++ b/src/main/ipc/subagents.ts @@ -56,7 +56,8 @@ async function handleGetSubagentDetail( _event: IpcMainInvokeEvent, projectId: string, sessionId: string, - subagentId: string + subagentId: string, + options?: { bypassCache?: boolean } ): Promise { try { const validatedProject = validateProjectId(projectId); @@ -85,7 +86,7 @@ async function handleGetSubagentDetail( // Check cache first let subagentDetail = dataCache.getSubagent(cacheKey); - if (subagentDetail) { + if (subagentDetail && !options?.bypassCache) { return subagentDetail; } diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index b6edd939..7f9bea48 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -300,30 +300,61 @@ export class TeamDataService { }); } - // Enrich inbox messages without leadSessionId by propagating from neighboring - // messages that have it (lead_session, user_sent). Sort chronologically (asc), - // sweep forward, then sweep backward so orphans at the start also get a session. + // Enrich inbox messages without leadSessionId by assigning the nearest neighbor's + // session ID (by timestamp). This avoids the old forward-only propagation bug where + // messages between two sessions always inherited the *earlier* session, causing a + // spurious "New session" divider even when the message is chronologically closer to + // the later session. if (config.leadSessionId || messages.some((m) => m.leadSessionId)) { messages.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)); - // Forward pass: propagate leadSessionId from earlier messages to later ones - let currentSessionId: string | undefined; - for (const msg of messages) { - if (msg.leadSessionId) { - currentSessionId = msg.leadSessionId; - } else if (currentSessionId) { - msg.leadSessionId = currentSessionId; + + // Collect indices of messages that already have a leadSessionId (anchors). + const anchors: { index: number; time: number; sessionId: string }[] = []; + for (let i = 0; i < messages.length; i++) { + if (messages[i].leadSessionId) { + anchors.push({ + index: i, + time: Date.parse(messages[i].timestamp), + sessionId: messages[i].leadSessionId!, + }); } } - // Backward pass: fill messages before the first known session. - // Seed with config.leadSessionId so that recent messages without an explicit - // session ID inherit the current (most recent) session — this ensures that - // session boundary separators work even when inbox entries lack the field. - currentSessionId = config.leadSessionId; - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].leadSessionId) { - currentSessionId = messages[i].leadSessionId; - } else if (currentSessionId) { - messages[i].leadSessionId = currentSessionId; + + if (anchors.length > 0) { + // For each message without leadSessionId, find the closest anchor by timestamp + // and inherit its sessionId. + let anchorIdx = 0; + for (let i = 0; i < messages.length; i++) { + if (messages[i].leadSessionId) { + // Advance anchorIdx to track current position for efficient lookup + while (anchorIdx < anchors.length - 1 && anchors[anchorIdx].index < i) { + anchorIdx++; + } + continue; + } + + const msgTime = Date.parse(messages[i].timestamp); + + // Find closest anchor by timestamp (binary-search-like scan from current position) + let bestAnchor = anchors[0]; + let bestDist = Math.abs(msgTime - bestAnchor.time); + for (let a = 0; a < anchors.length; a++) { + const dist = Math.abs(msgTime - anchors[a].time); + if (dist < bestDist) { + bestDist = dist; + bestAnchor = anchors[a]; + } else if (dist > bestDist && anchors[a].time > msgTime) { + // Anchors are sorted by index (asc time) — once distance grows past the + // message time, further anchors will only be farther. + break; + } + } + messages[i].leadSessionId = bestAnchor.sessionId; + } + } else if (config.leadSessionId) { + // No anchors at all — fall back to config.leadSessionId for everything. + for (const msg of messages) { + msg.leadSessionId = config.leadSessionId; } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 687132ad..9f361333 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -347,14 +347,18 @@ const electronAPI: ElectronAPI = { ipcRenderer.invoke('search-sessions', projectId, query, maxResults), searchAllProjects: (query: string, maxResults?: number) => ipcRenderer.invoke('search-all-projects', query, maxResults), - getSessionDetail: (projectId: string, sessionId: string) => - ipcRenderer.invoke('get-session-detail', projectId, sessionId), + getSessionDetail: (projectId: string, sessionId: string, options?: { bypassCache?: boolean }) => + ipcRenderer.invoke('get-session-detail', projectId, sessionId, options), getSessionMetrics: (projectId: string, sessionId: string) => ipcRenderer.invoke('get-session-metrics', projectId, sessionId), getWaterfallData: (projectId: string, sessionId: string) => ipcRenderer.invoke('get-waterfall-data', projectId, sessionId), - getSubagentDetail: (projectId: string, sessionId: string, subagentId: string) => - ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId), + getSubagentDetail: ( + projectId: string, + sessionId: string, + subagentId: string, + options?: { bypassCache?: boolean } + ) => ipcRenderer.invoke('get-subagent-detail', projectId, sessionId, subagentId, options), getSessionGroups: (projectId: string, sessionId: string) => ipcRenderer.invoke('get-session-groups', projectId, sessionId), getSessionsByIds: (projectId: string, sessionIds: string[], options?: SessionsByIdsOptions) => diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index 7fc64c7c..033bebe5 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -246,7 +246,11 @@ export class HttpAPIClient implements ElectronAPI { return this.get(`/api/search?${params}`); }; - getSessionDetail = (projectId: string, sessionId: string): Promise => + getSessionDetail = ( + projectId: string, + sessionId: string, + _options?: { bypassCache?: boolean } + ): Promise => this.get( `/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}` ); @@ -264,7 +268,8 @@ export class HttpAPIClient implements ElectronAPI { getSubagentDetail = ( projectId: string, sessionId: string, - subagentId: string + subagentId: string, + _options?: { bypassCache?: boolean } ): Promise => this.get( `/api/projects/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(sessionId)}/subagents/${encodeURIComponent(subagentId)}` diff --git a/src/renderer/components/team/members/MemberLogsTab.tsx b/src/renderer/components/team/members/MemberLogsTab.tsx index c55619e9..3cf05dbf 100644 --- a/src/renderer/components/team/members/MemberLogsTab.tsx +++ b/src/renderer/components/team/members/MemberLogsTab.tsx @@ -57,6 +57,7 @@ export const MemberLogsTab = ({ showLeadPreview = false, onPreviewOnlineChange, }: MemberLogsTabProps): React.JSX.Element => { + const MIN_REFRESH_VISIBLE_MS = 250; const intervalsKey = useMemo( () => (taskWorkIntervals ? JSON.stringify(taskWorkIntervals) : ''), [taskWorkIntervals] @@ -68,6 +69,8 @@ export const MemberLogsTab = ({ const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const refreshCountRef = useRef(0); + const refreshBeganAtRef = useRef(null); + const refreshHideTimeoutRef = useRef | null>(null); const [error, setError] = useState(null); const [expandedId, setExpandedId] = useState(null); const expandedIdRef = useRef(null); @@ -78,6 +81,10 @@ export const MemberLogsTab = ({ useEffect(() => { return () => { isMountedRef.current = false; + if (refreshHideTimeoutRef.current) { + clearTimeout(refreshHideTimeoutRef.current); + refreshHideTimeoutRef.current = null; + } }; }, []); @@ -86,13 +93,40 @@ export const MemberLogsTab = ({ }, [expandedId]); const beginRefreshing = useCallback((): void => { + if (refreshCountRef.current === 0) { + refreshBeganAtRef.current = Date.now(); + if (refreshHideTimeoutRef.current) { + clearTimeout(refreshHideTimeoutRef.current); + refreshHideTimeoutRef.current = null; + } + } refreshCountRef.current += 1; if (isMountedRef.current) setRefreshing(true); }, []); const endRefreshing = useCallback((): void => { refreshCountRef.current = Math.max(0, refreshCountRef.current - 1); - if (isMountedRef.current) setRefreshing(refreshCountRef.current > 0); + if (refreshCountRef.current > 0) { + if (isMountedRef.current) setRefreshing(true); + return; + } + + const beganAt = refreshBeganAtRef.current; + refreshBeganAtRef.current = null; + const elapsed = beganAt ? Date.now() - beganAt : Number.POSITIVE_INFINITY; + + if (!isMountedRef.current) return; + if (elapsed >= MIN_REFRESH_VISIBLE_MS) { + setRefreshing(false); + return; + } + + const remaining = Math.max(0, MIN_REFRESH_VISIBLE_MS - elapsed); + refreshHideTimeoutRef.current = setTimeout(() => { + refreshHideTimeoutRef.current = null; + if (!isMountedRef.current) return; + if (refreshCountRef.current === 0) setRefreshing(false); + }, remaining); }, []); const getRowId = useCallback((log: MemberLogSummary): string => { @@ -258,12 +292,20 @@ export const MemberLogsTab = ({ }, [teamName, memberName, taskId, taskOwner, taskStatus, intervalsKey]); const fetchDetailForLog = useCallback( - async (log: MemberLogSummary): Promise => { + async ( + log: MemberLogSummary, + options?: { bypassCache?: boolean } + ): Promise => { if (log.kind === 'subagent') { - const d = await api.getSubagentDetail(log.projectId, log.sessionId, log.subagentId); + const d = await api.getSubagentDetail( + log.projectId, + log.sessionId, + log.subagentId, + options + ); return d?.chunks ?? null; } - const d = await api.getSessionDetail(log.projectId, log.sessionId); + const d = await api.getSessionDetail(log.projectId, log.sessionId, options); return (d?.chunks ?? null) as unknown as EnhancedChunk[] | null; }, [] @@ -281,7 +323,7 @@ export const MemberLogsTab = ({ const shouldAutoRefreshSummary = taskId != null && taskStatus === 'in_progress'; if (!shouldAutoRefreshSummary && !nextExpanded.isOngoing) return; - const next = await fetchDetailForLog(nextExpanded); + const next = await fetchDetailForLog(nextExpanded, { bypassCache: true }); if (!isMountedRef.current) return; // Ensure new reference so memoized transforms update. setDetailChunks(next ? [...next] : null); @@ -327,7 +369,7 @@ export const MemberLogsTab = ({ const interval = setInterval(async () => { beginRefreshing(); try { - const next = await fetchDetailForLog(previewLog); + const next = await fetchDetailForLog(previewLog, { bypassCache: true }); if (cancelled) return; setPreviewChunks(next ? [...next] : null); } catch { @@ -363,7 +405,7 @@ export const MemberLogsTab = ({ const refreshDetail = async (): Promise => { beginRefreshing(); try { - const next = await fetchDetailForLog(expandedLogSummary); + const next = await fetchDetailForLog(expandedLogSummary, { bypassCache: true }); if (cancelled) return; // Ensure new reference so memoized transforms update. setDetailChunks(next ? [...next] : null); @@ -395,7 +437,11 @@ export const MemberLogsTab = ({ setDetailChunks(null); setDetailLoading(true); try { - const chunks = await fetchDetailForLog(log); + const shouldBypassCache = log.isOngoing || taskStatus === 'in_progress'; + const chunks = await fetchDetailForLog( + log, + shouldBypassCache ? { bypassCache: true } : undefined + ); setDetailChunks(chunks ? [...chunks] : null); } catch { setDetailChunks(null); @@ -403,7 +449,7 @@ export const MemberLogsTab = ({ setDetailLoading(false); } }, - [expandedId, fetchDetailForLog, getRowId] + [expandedId, fetchDetailForLog, getRowId, taskStatus] ); if (loading && logs.length === 0) { diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index d8227f7a..2b43e502 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -614,13 +614,18 @@ export interface ElectronAPI { maxResults?: number ) => Promise; searchAllProjects: (query: string, maxResults?: number) => Promise; - getSessionDetail: (projectId: string, sessionId: string) => Promise; + getSessionDetail: ( + projectId: string, + sessionId: string, + options?: { bypassCache?: boolean } + ) => Promise; getSessionMetrics: (projectId: string, sessionId: string) => Promise; getWaterfallData: (projectId: string, sessionId: string) => Promise; getSubagentDetail: ( projectId: string, sessionId: string, - subagentId: string + subagentId: string, + options?: { bypassCache?: boolean } ) => Promise; getSessionGroups: (projectId: string, sessionId: string) => Promise; getSessionsByIds: (