agent-ecosystem/src/renderer/hooks/useTabUI.ts
Artem Rootman da58917032
perf: offload heavy I/O to worker thread, reduce renderer re-renders
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.
2026-04-05 16:21:05 +00:00

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,
};
}