319 lines
12 KiB
TypeScript
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;
|
|
},
|
|
});
|