Main process — worker thread for team data: - New team-data-worker thread handles getTeamData and findLogsForTask, isolating heavy file I/O (scanning 300+ subagent JSONL files) from Electron's main event loop. getTeamData dropped from ~2000ms on the main thread to ~110ms via the worker. - Worker-side dedup and 10s result cache for findLogsForTask prevents redundant scans when the same task is queried multiple times. - Discovery cache TTL raised from 5s to 30s — avoids re-scanning the entire project directory on every call. - Message cap at 200 in TeamDataService to keep IPC payloads under 1MB (was sending 2200+ messages / ~3MB, stalling Chromium IPC serialization). - IPC handlers fall back to main-thread execution if the worker is unavailable (graceful degradation). Renderer — useShallow and memoization (55 files): - Added useShallow to store selectors across 55 renderer files. Batched individual useStore() calls (e.g. 17 calls in ExtensionStoreView, 10 in ConnectionSection) into single useShallow selectors, cutting unnecessary re-render checks on every store update. - MemberLogsTab: three 5-second polling intervals now pause when the parent tab is hidden (display:none). Previously 5 hidden tabs × 3 intervals = 15 polling timers firing continuously. - KanbanColumn wrapped in React.memo to skip re-renders when props haven't changed. - MemberList: memoized activeMembers/removedMembers/colorMap; replaced O(n×m) per-member task scan with a pre-computed reviewer map. - Bounded timer Maps in store initialization to prevent unbounded growth of debounce/throttle tracking maps during long sessions.
252 lines
7.9 KiB
TypeScript
252 lines
7.9 KiB
TypeScript
/**
|
|
* useTabUI - Hook for accessing per-tab UI state.
|
|
*
|
|
* This hook combines the TabUIContext (for tabId) with the tabUISlice (for state/actions).
|
|
* It provides a simple interface for components to access their tab-specific UI state.
|
|
*
|
|
* IMPORTANT: This hook subscribes to `tabUIStates` directly to ensure proper reactivity.
|
|
* Using getter functions (like isContextPanelVisibleForTab) in useMemo doesn't work
|
|
* because the function reference doesn't change when the underlying state changes.
|
|
*
|
|
* Usage:
|
|
* ```tsx
|
|
* const { isAIGroupExpanded, toggleAIGroupExpansion } = useTabUI();
|
|
*
|
|
* // Check if a group is expanded in THIS tab
|
|
* if (isAIGroupExpanded(groupId)) { ... }
|
|
*
|
|
* // Toggle expansion in THIS tab only
|
|
* toggleAIGroupExpansion(groupId);
|
|
* ```
|
|
*/
|
|
|
|
import { useCallback, useMemo } from 'react';
|
|
|
|
import { useTabIdOptional } from '@renderer/contexts/useTabUIContext';
|
|
import { useStore } from '@renderer/store';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
interface UseTabUIReturn {
|
|
tabId: string | null;
|
|
isAIGroupExpanded: (aiGroupId: string) => boolean;
|
|
toggleAIGroupExpansion: (aiGroupId: string) => void;
|
|
expandAIGroup: (aiGroupId: string) => void;
|
|
getExpandedDisplayItemIds: (aiGroupId: string) => Set<string>;
|
|
toggleDisplayItemExpansion: (aiGroupId: string, itemId: string) => void;
|
|
expandDisplayItem: (aiGroupId: string, itemId: string) => void;
|
|
isSubagentTraceExpanded: (subagentId: string) => boolean;
|
|
toggleSubagentTraceExpansion: (subagentId: string) => void;
|
|
expandSubagentTrace: (subagentId: string) => void;
|
|
isContextPanelVisible: boolean;
|
|
setContextPanelVisible: (visible: boolean) => void;
|
|
selectedContextPhase: number | null;
|
|
setSelectedContextPhase: (phase: number | null) => void;
|
|
savedScrollTop: number | undefined;
|
|
saveScrollPosition: (scrollTop: number) => void;
|
|
initializeTabUI: () => void;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Main Hook
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Hook for accessing per-tab UI state and actions.
|
|
*
|
|
* @returns Object containing per-tab state getters and actions
|
|
*/
|
|
export function useTabUI(): UseTabUIReturn {
|
|
// Get tabId from context (null if not in a tab)
|
|
const tabId = useTabIdOptional();
|
|
|
|
// Subscribe to tabUIStates MAP directly for reactivity
|
|
// This ensures re-renders when any tab state changes
|
|
const tabUIStates = useStore(useShallow((s) => s.tabUIStates));
|
|
|
|
// Get the current tab's state (derived from subscribed state)
|
|
const tabState = useMemo(() => {
|
|
if (!tabId) return null;
|
|
return tabUIStates.get(tabId) ?? null;
|
|
}, [tabId, tabUIStates]);
|
|
|
|
// Get all tab UI actions from store (these are stable function references)
|
|
const {
|
|
toggleAIGroupExpansionForTab,
|
|
expandAIGroupForTab,
|
|
toggleDisplayItemExpansionForTab,
|
|
expandDisplayItemForTab,
|
|
toggleSubagentTraceExpansionForTab,
|
|
expandSubagentTraceForTab,
|
|
setContextPanelVisibleForTab,
|
|
setSelectedContextPhaseForTab,
|
|
saveScrollPositionForTab,
|
|
initTabUIState,
|
|
} = useStore(
|
|
useShallow((s) => ({
|
|
toggleAIGroupExpansionForTab: s.toggleAIGroupExpansionForTab,
|
|
expandAIGroupForTab: s.expandAIGroupForTab,
|
|
toggleDisplayItemExpansionForTab: s.toggleDisplayItemExpansionForTab,
|
|
expandDisplayItemForTab: s.expandDisplayItemForTab,
|
|
toggleSubagentTraceExpansionForTab: s.toggleSubagentTraceExpansionForTab,
|
|
expandSubagentTraceForTab: s.expandSubagentTraceForTab,
|
|
setContextPanelVisibleForTab: s.setContextPanelVisibleForTab,
|
|
setSelectedContextPhaseForTab: s.setSelectedContextPhaseForTab,
|
|
saveScrollPositionForTab: s.saveScrollPositionForTab,
|
|
initTabUIState: s.initTabUIState,
|
|
}))
|
|
);
|
|
|
|
// ==========================================================================
|
|
// Derived state from tabState (reactive!)
|
|
// ==========================================================================
|
|
|
|
// AI Group expansion - check directly from tabState
|
|
const isAIGroupExpanded = useCallback(
|
|
(aiGroupId: string): boolean => {
|
|
return tabState?.expandedAIGroupIds.has(aiGroupId) ?? false;
|
|
},
|
|
[tabState]
|
|
);
|
|
|
|
const toggleAIGroupExpansion = useCallback(
|
|
(aiGroupId: string): void => {
|
|
if (!tabId) return;
|
|
toggleAIGroupExpansionForTab(tabId, aiGroupId);
|
|
},
|
|
[tabId, toggleAIGroupExpansionForTab]
|
|
);
|
|
|
|
const expandAIGroup = useCallback(
|
|
(aiGroupId: string): void => {
|
|
if (!tabId) return;
|
|
expandAIGroupForTab(tabId, aiGroupId);
|
|
},
|
|
[tabId, expandAIGroupForTab]
|
|
);
|
|
|
|
// Display item expansion - derive from tabState
|
|
const getExpandedDisplayItemIds = useCallback(
|
|
(aiGroupId: string): Set<string> => {
|
|
return tabState?.expandedDisplayItemIds.get(aiGroupId) ?? new Set<string>();
|
|
},
|
|
[tabState]
|
|
);
|
|
|
|
const toggleDisplayItemExpansion = useCallback(
|
|
(aiGroupId: string, itemId: string): void => {
|
|
if (!tabId) return;
|
|
toggleDisplayItemExpansionForTab(tabId, aiGroupId, itemId);
|
|
},
|
|
[tabId, toggleDisplayItemExpansionForTab]
|
|
);
|
|
|
|
const expandDisplayItem = useCallback(
|
|
(aiGroupId: string, itemId: string): void => {
|
|
if (!tabId) return;
|
|
expandDisplayItemForTab(tabId, aiGroupId, itemId);
|
|
},
|
|
[tabId, expandDisplayItemForTab]
|
|
);
|
|
|
|
// Subagent trace expansion - derive from tabState
|
|
const isSubagentTraceExpanded = useCallback(
|
|
(subagentId: string): boolean => {
|
|
return tabState?.expandedSubagentTraceIds.has(subagentId) ?? false;
|
|
},
|
|
[tabState]
|
|
);
|
|
|
|
const toggleSubagentTraceExpansion = useCallback(
|
|
(subagentId: string): void => {
|
|
if (!tabId) return;
|
|
toggleSubagentTraceExpansionForTab(tabId, subagentId);
|
|
},
|
|
[tabId, toggleSubagentTraceExpansionForTab]
|
|
);
|
|
|
|
const expandSubagentTrace = useCallback(
|
|
(subagentId: string): void => {
|
|
if (!tabId) return;
|
|
expandSubagentTraceForTab(tabId, subagentId);
|
|
},
|
|
[tabId, expandSubagentTraceForTab]
|
|
);
|
|
|
|
// Context panel - derive directly from tabState (reactive!)
|
|
const isContextPanelVisible = tabState?.showContextPanel ?? false;
|
|
|
|
const setContextPanelVisible = useCallback(
|
|
(visible: boolean): void => {
|
|
if (!tabId) return;
|
|
setContextPanelVisibleForTab(tabId, visible);
|
|
},
|
|
[tabId, setContextPanelVisibleForTab]
|
|
);
|
|
|
|
// Context phase selection - derive from tabState
|
|
const selectedContextPhase = tabState?.selectedContextPhase ?? null;
|
|
|
|
const setSelectedContextPhase = useCallback(
|
|
(phase: number | null): void => {
|
|
if (!tabId) return;
|
|
setSelectedContextPhaseForTab(tabId, phase);
|
|
},
|
|
[tabId, setSelectedContextPhaseForTab]
|
|
);
|
|
|
|
// Scroll position - derive from tabState
|
|
const savedScrollTop = tabState?.savedScrollTop;
|
|
|
|
const saveScrollPosition = useCallback(
|
|
(scrollTop: number): void => {
|
|
if (!tabId) return;
|
|
saveScrollPositionForTab(tabId, scrollTop);
|
|
},
|
|
[tabId, saveScrollPositionForTab]
|
|
);
|
|
|
|
// Initialize tab UI state (call once when tab is mounted)
|
|
const initializeTabUI = useCallback((): void => {
|
|
if (!tabId) return;
|
|
initTabUIState(tabId);
|
|
}, [tabId, initTabUIState]);
|
|
|
|
return {
|
|
// Current tab ID
|
|
tabId,
|
|
|
|
// AI Group expansion
|
|
isAIGroupExpanded,
|
|
toggleAIGroupExpansion,
|
|
expandAIGroup,
|
|
|
|
// Display item expansion
|
|
getExpandedDisplayItemIds,
|
|
toggleDisplayItemExpansion,
|
|
expandDisplayItem,
|
|
|
|
// Subagent trace expansion
|
|
isSubagentTraceExpanded,
|
|
toggleSubagentTraceExpansion,
|
|
expandSubagentTrace,
|
|
|
|
// Context panel
|
|
isContextPanelVisible,
|
|
setContextPanelVisible,
|
|
|
|
// Context phase selection
|
|
selectedContextPhase,
|
|
setSelectedContextPhase,
|
|
|
|
// Scroll position
|
|
savedScrollTop,
|
|
saveScrollPosition,
|
|
|
|
// Initialization
|
|
initializeTabUI,
|
|
};
|
|
}
|