agent-ecosystem/src/renderer/hooks/useTabUI.ts

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