252 lines · 7.9 KB · TypeScript
RawDownload
/** * 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((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,  };}