agent-ecosystem/src/renderer/store/slices/tabUISlice.ts

319 lines
12 KiB
TypeScript

/**
* Tab UI slice - manages per-tab UI state (expansion states, scroll positions, etc.)
*
* This slice provides COMPLETE isolation of UI state between tabs. Each tab has its
* own independent state for:
* - AI group expansion (collapsed/expanded)
* - Display item expansion within AI groups
* - Subagent trace expansion
* - Context panel visibility
* - Scroll position
*
* The state is keyed by tabId, so opening the same session in two tabs gives each
* tab its own independent UI state.
*/
import type { AppState } from '../types';
import type { StateCreator } from 'zustand';
// =============================================================================
// Types
// =============================================================================
/**
* UI state for a single tab.
* All values are optional - defaults are applied when reading.
*/
export interface TabUIState {
/** Which AI groups are expanded (by aiGroupId) */
expandedAIGroupIds: Set<string>;
/** Which display items within AI groups are expanded: Map<aiGroupId, Set<itemId>> */
expandedDisplayItemIds: Map<string, Set<string>>;
/** Which subagent traces are manually expanded (by subagentId) */
expandedSubagentTraceIds: Set<string>;
/** Whether the context panel is visible */
showContextPanel: boolean;
/** Selected context phase for filtering (null = current/latest phase) */
selectedContextPhase: number | null;
/** Saved scroll position for restoring when switching back to this tab */
savedScrollTop?: number;
}
/**
* Creates a default/empty TabUIState.
*/
function createDefaultTabUIState(): TabUIState {
return {
expandedAIGroupIds: new Set(),
expandedDisplayItemIds: new Map(),
expandedSubagentTraceIds: new Set(),
showContextPanel: false,
selectedContextPhase: null,
savedScrollTop: undefined,
};
}
// =============================================================================
// Slice Interface
// =============================================================================
export interface TabUISlice {
/** Per-tab UI states: Map<tabId, TabUIState> */
tabUIStates: Map<string, TabUIState>;
// Initialization & cleanup
/** Initialize UI state for a new tab */
initTabUIState: (tabId: string) => void;
/** Clean up UI state when a tab is closed */
cleanupTabUIState: (tabId: string) => void;
// AI Group expansion (per-tab)
/** Toggle AI group expansion for a specific tab */
toggleAIGroupExpansionForTab: (tabId: string, aiGroupId: string) => void;
/** Check if AI group is expanded for a specific tab */
isAIGroupExpandedForTab: (tabId: string, aiGroupId: string) => boolean;
/** Expand AI group for a specific tab (for auto-expand scenarios) */
expandAIGroupForTab: (tabId: string, aiGroupId: string) => void;
// Display item expansion (per-tab)
/** Toggle display item expansion within an AI group for a specific tab */
toggleDisplayItemExpansionForTab: (tabId: string, aiGroupId: string, itemId: string) => void;
/** Get expanded display item IDs for an AI group in a specific tab */
getExpandedDisplayItemIdsForTab: (tabId: string, aiGroupId: string) => Set<string>;
/** Expand a display item for a specific tab (for auto-expand scenarios) */
expandDisplayItemForTab: (tabId: string, aiGroupId: string, itemId: string) => void;
// Subagent trace expansion (per-tab)
/** Toggle subagent trace expansion for a specific tab */
toggleSubagentTraceExpansionForTab: (tabId: string, subagentId: string) => void;
/** Expand subagent trace for a specific tab (no-op if already expanded) */
expandSubagentTraceForTab: (tabId: string, subagentId: string) => void;
/** Check if subagent trace is expanded for a specific tab */
isSubagentTraceExpandedForTab: (tabId: string, subagentId: string) => boolean;
// Context panel (per-tab)
/** Set context panel visibility for a specific tab */
setContextPanelVisibleForTab: (tabId: string, visible: boolean) => void;
/** Get context panel visibility for a specific tab */
isContextPanelVisibleForTab: (tabId: string) => boolean;
// Context phase selection (per-tab)
/** Set the selected context phase for a specific tab */
setSelectedContextPhaseForTab: (tabId: string, phase: number | null) => void;
// Scroll position (per-tab)
/** Save scroll position for a specific tab */
saveScrollPositionForTab: (tabId: string, scrollTop: number) => void;
/** Get saved scroll position for a specific tab */
getScrollPositionForTab: (tabId: string) => number | undefined;
}
// =============================================================================
// Slice Creator
// =============================================================================
export const createTabUISlice: StateCreator<AppState, [], [], TabUISlice> = (set, get) => ({
tabUIStates: new Map<string, TabUIState>(),
// ==========================================================================
// Initialization & Cleanup
// ==========================================================================
initTabUIState: (tabId: string) => {
const state = get();
if (state.tabUIStates.has(tabId)) return; // Already initialized
const newMap = new Map(state.tabUIStates);
newMap.set(tabId, createDefaultTabUIState());
set({ tabUIStates: newMap });
},
cleanupTabUIState: (tabId: string) => {
const state = get();
if (!state.tabUIStates.has(tabId)) return;
const newMap = new Map(state.tabUIStates);
newMap.delete(tabId);
set({ tabUIStates: newMap });
},
// ==========================================================================
// AI Group Expansion
// ==========================================================================
toggleAIGroupExpansionForTab: (tabId: string, aiGroupId: string) => {
const state = get();
const newMap = new Map(state.tabUIStates);
const tabState = newMap.get(tabId) ?? createDefaultTabUIState();
const newExpandedIds = new Set(tabState.expandedAIGroupIds);
if (newExpandedIds.has(aiGroupId)) {
newExpandedIds.delete(aiGroupId);
} else {
newExpandedIds.add(aiGroupId);
}
newMap.set(tabId, { ...tabState, expandedAIGroupIds: newExpandedIds });
set({ tabUIStates: newMap });
},
isAIGroupExpandedForTab: (tabId: string, aiGroupId: string) => {
const tabState = get().tabUIStates.get(tabId);
return tabState?.expandedAIGroupIds.has(aiGroupId) ?? false;
},
expandAIGroupForTab: (tabId: string, aiGroupId: string) => {
const state = get();
const tabState = state.tabUIStates.get(tabId);
if (tabState?.expandedAIGroupIds.has(aiGroupId)) return; // Already expanded
const newMap = new Map(state.tabUIStates);
const currentTabState = newMap.get(tabId) ?? createDefaultTabUIState();
const newExpandedIds = new Set(currentTabState.expandedAIGroupIds);
newExpandedIds.add(aiGroupId);
newMap.set(tabId, { ...currentTabState, expandedAIGroupIds: newExpandedIds });
set({ tabUIStates: newMap });
},
// ==========================================================================
// Display Item Expansion
// ==========================================================================
toggleDisplayItemExpansionForTab: (tabId: string, aiGroupId: string, itemId: string) => {
const state = get();
const newMap = new Map(state.tabUIStates);
const tabState = newMap.get(tabId) ?? createDefaultTabUIState();
const newDisplayItemMap = new Map(tabState.expandedDisplayItemIds);
const currentSet = newDisplayItemMap.get(aiGroupId) ?? new Set<string>();
const newSet = new Set(currentSet);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
newSet.add(itemId);
}
newDisplayItemMap.set(aiGroupId, newSet);
newMap.set(tabId, { ...tabState, expandedDisplayItemIds: newDisplayItemMap });
set({ tabUIStates: newMap });
},
getExpandedDisplayItemIdsForTab: (tabId: string, aiGroupId: string) => {
const tabState = get().tabUIStates.get(tabId);
return tabState?.expandedDisplayItemIds.get(aiGroupId) ?? new Set<string>();
},
expandDisplayItemForTab: (tabId: string, aiGroupId: string, itemId: string) => {
const state = get();
const tabState = state.tabUIStates.get(tabId);
const currentSet = tabState?.expandedDisplayItemIds.get(aiGroupId);
if (currentSet?.has(itemId)) return; // Already expanded
const newMap = new Map(state.tabUIStates);
const currentTabState = newMap.get(tabId) ?? createDefaultTabUIState();
const newDisplayItemMap = new Map(currentTabState.expandedDisplayItemIds);
const newSet = new Set(newDisplayItemMap.get(aiGroupId) ?? new Set<string>());
newSet.add(itemId);
newDisplayItemMap.set(aiGroupId, newSet);
newMap.set(tabId, { ...currentTabState, expandedDisplayItemIds: newDisplayItemMap });
set({ tabUIStates: newMap });
},
// ==========================================================================
// Subagent Trace Expansion
// ==========================================================================
toggleSubagentTraceExpansionForTab: (tabId: string, subagentId: string) => {
const state = get();
const newMap = new Map(state.tabUIStates);
const tabState = newMap.get(tabId) ?? createDefaultTabUIState();
const newExpandedIds = new Set(tabState.expandedSubagentTraceIds);
if (newExpandedIds.has(subagentId)) {
newExpandedIds.delete(subagentId);
} else {
newExpandedIds.add(subagentId);
}
newMap.set(tabId, { ...tabState, expandedSubagentTraceIds: newExpandedIds });
set({ tabUIStates: newMap });
},
expandSubagentTraceForTab: (tabId: string, subagentId: string) => {
const state = get();
const tabState = state.tabUIStates.get(tabId) ?? createDefaultTabUIState();
// No-op if already expanded
if (tabState.expandedSubagentTraceIds.has(subagentId)) return;
const newExpandedIds = new Set(tabState.expandedSubagentTraceIds);
newExpandedIds.add(subagentId);
const newMap = new Map(state.tabUIStates);
newMap.set(tabId, { ...tabState, expandedSubagentTraceIds: newExpandedIds });
set({ tabUIStates: newMap });
},
isSubagentTraceExpandedForTab: (tabId: string, subagentId: string) => {
const tabState = get().tabUIStates.get(tabId);
return tabState?.expandedSubagentTraceIds.has(subagentId) ?? false;
},
// ==========================================================================
// Context Panel
// ==========================================================================
setContextPanelVisibleForTab: (tabId: string, visible: boolean) => {
const state = get();
const newMap = new Map(state.tabUIStates);
const tabState = newMap.get(tabId) ?? createDefaultTabUIState();
newMap.set(tabId, { ...tabState, showContextPanel: visible });
set({ tabUIStates: newMap });
},
isContextPanelVisibleForTab: (tabId: string) => {
const tabState = get().tabUIStates.get(tabId);
return tabState?.showContextPanel ?? false;
},
// ==========================================================================
// Context Phase Selection
// ==========================================================================
setSelectedContextPhaseForTab: (tabId: string, phase: number | null) => {
const state = get();
const newMap = new Map(state.tabUIStates);
const tabState = newMap.get(tabId) ?? createDefaultTabUIState();
newMap.set(tabId, { ...tabState, selectedContextPhase: phase });
set({ tabUIStates: newMap });
},
// ==========================================================================
// Scroll Position
// ==========================================================================
saveScrollPositionForTab: (tabId: string, scrollTop: number) => {
const state = get();
const newMap = new Map(state.tabUIStates);
const tabState = newMap.get(tabId) ?? createDefaultTabUIState();
newMap.set(tabId, { ...tabState, savedScrollTop: scrollTop });
set({ tabUIStates: newMap });
},
getScrollPositionForTab: (tabId: string) => {
const tabState = get().tabUIStates.get(tabId);
return tabState?.savedScrollTop;
},
});