diff --git a/src/__tests__/renderer/components/SessionList.test.tsx b/src/__tests__/renderer/components/SessionList.test.tsx index 989b8bc5e0..fba855c3e7 100644 --- a/src/__tests__/renderer/components/SessionList.test.tsx +++ b/src/__tests__/renderer/components/SessionList.test.tsx @@ -17,7 +17,10 @@ import { SessionList } from '../../../renderer/components/SessionList'; import type { Session, Group, Theme } from '../../../renderer/types'; import { useUIStore } from '../../../renderer/stores/uiStore'; import { useSessionStore } from '../../../renderer/stores/sessionStore'; -import { useSettingsStore, DEFAULT_AUTO_RUN_STATS } from '../../../renderer/stores/settingsStore'; +import { useSettingsStore } from '../../../renderer/stores/settingsStore'; + +// Deep-cloned default autoRunStats captured from a fresh store (no longer exported). +const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(useSettingsStore.getState().autoRunStats)); import { useBatchStore } from '../../../renderer/stores/batchStore'; import { useModalStore } from '../../../renderer/stores/modalStore'; import type { BatchRunState } from '../../../renderer/types'; diff --git a/src/__tests__/renderer/fonts-and-sizing.test.ts b/src/__tests__/renderer/fonts-and-sizing.test.ts index 34ac68ce9d..7534f2bd9d 100644 --- a/src/__tests__/renderer/fonts-and-sizing.test.ts +++ b/src/__tests__/renderer/fonts-and-sizing.test.ts @@ -18,15 +18,21 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { render, screen } from '@testing-library/react'; import { useSettings } from '../../renderer/hooks'; import React from 'react'; -import { - useSettingsStore, - DEFAULT_CONTEXT_MANAGEMENT_SETTINGS, - DEFAULT_AUTO_RUN_STATS, - DEFAULT_USAGE_STATS, - DEFAULT_KEYBOARD_MASTERY_STATS, - DEFAULT_ONBOARDING_STATS, - DEFAULT_AI_COMMANDS, -} from '../../renderer/stores/settingsStore'; +import { useSettingsStore } from '../../renderer/stores/settingsStore'; + +// Deep-cloned defaults captured from a fresh store so mutations in tests can't +// leak back into the reference. The store no longer exports these defaults. +const _INITIAL_STATE = useSettingsStore.getState(); +const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS = JSON.parse( + JSON.stringify(_INITIAL_STATE.contextManagementSettings) +); +const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.autoRunStats)); +const DEFAULT_USAGE_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.usageStats)); +const DEFAULT_KEYBOARD_MASTERY_STATS = JSON.parse( + JSON.stringify(_INITIAL_STATE.keyboardMasteryStats) +); +const DEFAULT_ONBOARDING_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.onboardingStats)); +const DEFAULT_AI_COMMANDS = JSON.parse(JSON.stringify(_INITIAL_STATE.customAICommands)); import { DEFAULT_SHORTCUTS, TAB_SHORTCUTS } from '../../renderer/constants/shortcuts'; import { DEFAULT_CUSTOM_THEME_COLORS } from '../../renderer/constants/themes'; diff --git a/src/__tests__/renderer/hooks/useSettings.test.ts b/src/__tests__/renderer/hooks/useSettings.test.ts index 19ddaac459..373c9de2a2 100644 --- a/src/__tests__/renderer/hooks/useSettings.test.ts +++ b/src/__tests__/renderer/hooks/useSettings.test.ts @@ -3,15 +3,21 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { useSettings } from '../../../renderer/hooks'; import type { AutoRunStats, OnboardingStats, CustomAICommand } from '../../../renderer/types'; import { DEFAULT_SHORTCUTS } from '../../../renderer/constants/shortcuts'; -import { - useSettingsStore, - DEFAULT_CONTEXT_MANAGEMENT_SETTINGS, - DEFAULT_AUTO_RUN_STATS, - DEFAULT_USAGE_STATS, - DEFAULT_KEYBOARD_MASTERY_STATS, - DEFAULT_ONBOARDING_STATS, - DEFAULT_AI_COMMANDS, -} from '../../../renderer/stores/settingsStore'; +import { useSettingsStore } from '../../../renderer/stores/settingsStore'; + +// Deep-cloned defaults captured from a fresh store so mutations in tests can't +// leak back into the reference. The store no longer exports these defaults. +const _INITIAL_STATE = useSettingsStore.getState(); +const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS = JSON.parse( + JSON.stringify(_INITIAL_STATE.contextManagementSettings) +); +const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.autoRunStats)); +const DEFAULT_USAGE_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.usageStats)); +const DEFAULT_KEYBOARD_MASTERY_STATS = JSON.parse( + JSON.stringify(_INITIAL_STATE.keyboardMasteryStats) +); +const DEFAULT_ONBOARDING_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.onboardingStats)); +const DEFAULT_AI_COMMANDS = JSON.parse(JSON.stringify(_INITIAL_STATE.customAICommands)); import { TAB_SHORTCUTS } from '../../../renderer/constants/shortcuts'; import { DEFAULT_CUSTOM_THEME_COLORS } from '../../../renderer/constants/themes'; diff --git a/src/__tests__/renderer/stores/agentStore.test.ts b/src/__tests__/renderer/stores/agentStore.test.ts index c1f350cd53..ea386b243e 100644 --- a/src/__tests__/renderer/stores/agentStore.test.ts +++ b/src/__tests__/renderer/stores/agentStore.test.ts @@ -7,13 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -import { - useAgentStore, - selectAvailableAgents, - selectAgentsDetected, - getAgentState, - getAgentActions, -} from '../../../renderer/stores/agentStore'; +import { useAgentStore } from '../../../renderer/stores/agentStore'; import type { ProcessQueuedItemDeps } from '../../../renderer/stores/agentStore'; import { useSessionStore } from '../../../renderer/stores/sessionStore'; import type { Session, AgentConfig, QueuedItem } from '../../../renderer/types'; @@ -919,60 +913,57 @@ describe('agentStore', () => { }); }); - describe('selectors', () => { - it('selectAvailableAgents returns the agents list', () => { + describe('store state access', () => { + it('availableAgents reflects setState updates', () => { const agents = [createMockAgentConfig({ id: 'claude-code' })]; useAgentStore.setState({ availableAgents: agents }); - expect(selectAvailableAgents(useAgentStore.getState())).toEqual(agents); + expect(useAgentStore.getState().availableAgents).toEqual(agents); }); - it('selectAgentsDetected returns detection status', () => { - expect(selectAgentsDetected(useAgentStore.getState())).toBe(false); + it('agentsDetected reflects setState updates', () => { + expect(useAgentStore.getState().agentsDetected).toBe(false); useAgentStore.setState({ agentsDetected: true }); - expect(selectAgentsDetected(useAgentStore.getState())).toBe(true); + expect(useAgentStore.getState().agentsDetected).toBe(true); }); }); describe('non-React access', () => { - it('getAgentState returns current snapshot', () => { + it('getState returns current snapshot', () => { const agents = [createMockAgentConfig()]; useAgentStore.setState({ availableAgents: agents, agentsDetected: true }); - const state = getAgentState(); + const state = useAgentStore.getState(); expect(state.availableAgents).toEqual(agents); expect(state.agentsDetected).toBe(true); }); - it('getAgentState reflects latest mutations', () => { - expect(getAgentState().agentsDetected).toBe(false); + it('getState reflects latest mutations', () => { + expect(useAgentStore.getState().agentsDetected).toBe(false); useAgentStore.setState({ agentsDetected: true }); - expect(getAgentState().agentsDetected).toBe(true); + expect(useAgentStore.getState().agentsDetected).toBe(true); }); - it('getAgentActions returns all 10 action functions', () => { - const actions = getAgentActions(); - - expect(typeof actions.refreshAgents).toBe('function'); - expect(typeof actions.getAgentConfig).toBe('function'); - expect(typeof actions.processQueuedItem).toBe('function'); - expect(typeof actions.clearAgentError).toBe('function'); - expect(typeof actions.startNewSessionAfterError).toBe('function'); - expect(typeof actions.retryAfterError).toBe('function'); - expect(typeof actions.restartAgentAfterError).toBe('function'); - expect(typeof actions.authenticateAfterError).toBe('function'); - expect(typeof actions.killAgent).toBe('function'); - expect(typeof actions.interruptAgent).toBe('function'); + it('getState exposes all 10 action functions', () => { + const state = useAgentStore.getState(); - // Verify exactly 10 actions (no extras, no missing) - expect(Object.keys(actions)).toHaveLength(10); + expect(typeof state.refreshAgents).toBe('function'); + expect(typeof state.getAgentConfig).toBe('function'); + expect(typeof state.processQueuedItem).toBe('function'); + expect(typeof state.clearAgentError).toBe('function'); + expect(typeof state.startNewSessionAfterError).toBe('function'); + expect(typeof state.retryAfterError).toBe('function'); + expect(typeof state.restartAgentAfterError).toBe('function'); + expect(typeof state.authenticateAfterError).toBe('function'); + expect(typeof state.killAgent).toBe('function'); + expect(typeof state.interruptAgent).toBe('function'); }); - it('getAgentActions clearAgentError works end-to-end', () => { + it('clearAgentError works end-to-end', () => { const session = createMockSession({ id: 'session-1', state: 'error', @@ -980,16 +971,14 @@ describe('agentStore', () => { }); useSessionStore.getState().setSessions([session]); - const { clearAgentError } = getAgentActions(); - clearAgentError('session-1'); + useAgentStore.getState().clearAgentError('session-1'); expect(useSessionStore.getState().sessions[0].state).toBe('idle'); expect(mockClearError).toHaveBeenCalledWith('session-1'); }); - it('getAgentActions killAgent works end-to-end', async () => { - const { killAgent } = getAgentActions(); - await killAgent('session-1', 'terminal'); + it('killAgent works end-to-end', async () => { + await useAgentStore.getState().killAgent('session-1', 'terminal'); expect(mockKill).toHaveBeenCalledWith('session-1-terminal'); }); @@ -997,7 +986,7 @@ describe('agentStore', () => { describe('React hook integration', () => { it('useAgentStore with selector re-renders on agent detection', async () => { - const { result } = renderHook(() => useAgentStore(selectAgentsDetected)); + const { result } = renderHook(() => useAgentStore((s) => s.agentsDetected)); expect(result.current).toBe(false); @@ -1012,7 +1001,7 @@ describe('agentStore', () => { }); it('useAgentStore with availableAgents selector updates on refresh', async () => { - const { result } = renderHook(() => useAgentStore(selectAvailableAgents)); + const { result } = renderHook(() => useAgentStore((s) => s.availableAgents)); expect(result.current).toEqual([]); diff --git a/src/__tests__/renderer/stores/batchStore.test.ts b/src/__tests__/renderer/stores/batchStore.test.ts index 05d62eef9f..34812088d6 100644 --- a/src/__tests__/renderer/stores/batchStore.test.ts +++ b/src/__tests__/renderer/stores/batchStore.test.ts @@ -13,11 +13,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { useBatchStore, getBatchState, - getBatchActions, selectHasAnyActiveBatch, selectActiveBatchSessionIds, - selectStoppingBatchSessionIds, - selectBatchRunState, } from '../../../renderer/stores/batchStore'; import type { TaskCountEntry } from '../../../renderer/stores/batchStore'; import type { AutoRunTreeNode } from '../../../renderer/hooks/batch/useAutoRunHandlers'; @@ -565,40 +562,6 @@ describe('batchStore', () => { expect(ids).toContain('sess-3'); }); }); - - describe('selectStoppingBatchSessionIds', () => { - it('returns empty array when no stopping batches', () => { - expect(selectStoppingBatchSessionIds(useBatchStore.getState())).toEqual([]); - }); - - it('returns only stopping session IDs', () => { - useBatchStore.getState().setBatchRunStates({ - 'sess-1': { ...DEFAULT_BATCH_STATE, isRunning: true, isStopping: true }, - 'sess-2': { ...DEFAULT_BATCH_STATE, isRunning: true, isStopping: false }, - 'sess-3': { ...DEFAULT_BATCH_STATE, isRunning: false, isStopping: true }, - }); - const ids = selectStoppingBatchSessionIds(useBatchStore.getState()); - // Only sess-1: isRunning=true AND isStopping=true - expect(ids).toEqual(['sess-1']); - }); - }); - - describe('selectBatchRunState', () => { - it('returns undefined for non-existent session', () => { - expect(selectBatchRunState(useBatchStore.getState(), 'nope')).toBeUndefined(); - }); - - it('returns batch state for existing session', () => { - useBatchStore.getState().dispatchBatch({ - type: 'START_BATCH', - sessionId: 'sess-1', - payload: createStartBatchPayload({ documents: ['x.md'] }), - }); - const state = selectBatchRunState(useBatchStore.getState(), 'sess-1'); - expect(state).toBeDefined(); - expect(state!.documents).toEqual(['x.md']); - }); - }); }); // ========================================================================== @@ -612,15 +575,13 @@ describe('batchStore', () => { expect(state.documentList).toEqual(['test.md']); }); - it('getBatchActions returns working action references', () => { - const actions = getBatchActions(); - actions.setDocumentList(['via-actions.md']); + it('useBatchStore.getState exposes working action references', () => { + useBatchStore.getState().setDocumentList(['via-actions.md']); expect(useBatchStore.getState().documentList).toEqual(['via-actions.md']); }); - it('getBatchActions.dispatchBatch works', () => { - const actions = getBatchActions(); - actions.dispatchBatch({ + it('useBatchStore.getState().dispatchBatch works', () => { + useBatchStore.getState().dispatchBatch({ type: 'START_BATCH', sessionId: 'sess-1', payload: createStartBatchPayload(), diff --git a/src/__tests__/renderer/stores/fileExplorerStore.test.ts b/src/__tests__/renderer/stores/fileExplorerStore.test.ts index 27575f7e4c..766301744f 100644 --- a/src/__tests__/renderer/stores/fileExplorerStore.test.ts +++ b/src/__tests__/renderer/stores/fileExplorerStore.test.ts @@ -7,11 +7,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { - useFileExplorerStore, - getFileExplorerState, - getFileExplorerActions, -} from '../../../renderer/stores/fileExplorerStore'; +import { useFileExplorerStore } from '../../../renderer/stores/fileExplorerStore'; import type { FlatTreeNode } from '../../../renderer/utils/fileExplorer'; // ============================================================================ @@ -244,32 +240,32 @@ describe('fileExplorerStore', () => { }); describe('non-React access', () => { - it('getFileExplorerState returns current state', () => { + it('useFileExplorerStore.getState() returns current state', () => { useFileExplorerStore.getState().setFileTreeFilter('search'); - const state = getFileExplorerState(); + const state = useFileExplorerStore.getState(); expect(state.fileTreeFilter).toBe('search'); }); - it('getFileExplorerActions returns action functions', () => { - const actions = getFileExplorerActions(); - expect(typeof actions.setSelectedFileIndex).toBe('function'); - expect(typeof actions.setFileTreeFilter).toBe('function'); - expect(typeof actions.setFileTreeFilterOpen).toBe('function'); - expect(typeof actions.setFilePreviewLoading).toBe('function'); - expect(typeof actions.setFlatFileList).toBe('function'); - expect(typeof actions.focusFileInGraph).toBe('function'); - expect(typeof actions.openLastDocumentGraph).toBe('function'); - expect(typeof actions.closeGraphView).toBe('function'); - expect(typeof actions.setIsGraphViewOpen).toBe('function'); + it('useFileExplorerStore.getState() exposes action functions', () => { + const state = useFileExplorerStore.getState(); + expect(typeof state.setSelectedFileIndex).toBe('function'); + expect(typeof state.setFileTreeFilter).toBe('function'); + expect(typeof state.setFileTreeFilterOpen).toBe('function'); + expect(typeof state.setFilePreviewLoading).toBe('function'); + expect(typeof state.setFlatFileList).toBe('function'); + expect(typeof state.focusFileInGraph).toBe('function'); + expect(typeof state.openLastDocumentGraph).toBe('function'); + expect(typeof state.closeGraphView).toBe('function'); + expect(typeof state.setIsGraphViewOpen).toBe('function'); }); - it('actions from getFileExplorerActions update state', () => { - const actions = getFileExplorerActions(); + it('actions from useFileExplorerStore.getState() update state', () => { + const actions = useFileExplorerStore.getState(); actions.setSelectedFileIndex(10); actions.setFileTreeFilter('test'); actions.focusFileInGraph('via-actions.ts'); - const state = getFileExplorerState(); + const state = useFileExplorerStore.getState(); expect(state.selectedFileIndex).toBe(10); expect(state.fileTreeFilter).toBe('test'); expect(state.graphFocusFilePath).toBe('via-actions.ts'); diff --git a/src/__tests__/renderer/stores/groupChatStore.test.ts b/src/__tests__/renderer/stores/groupChatStore.test.ts index 7334c6c78b..e3f7db9123 100644 --- a/src/__tests__/renderer/stores/groupChatStore.test.ts +++ b/src/__tests__/renderer/stores/groupChatStore.test.ts @@ -7,11 +7,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { - useGroupChatStore, - getGroupChatState, - getGroupChatActions, -} from '../../../renderer/stores/groupChatStore'; +import { useGroupChatStore } from '../../../renderer/stores/groupChatStore'; import type { GroupChatRightTab, GroupChatErrorState, @@ -480,43 +476,42 @@ describe('groupChatStore', () => { // ========================================================================== describe('non-React access', () => { - it('getGroupChatState returns current state', () => { + it('useGroupChatStore.getState() returns current state', () => { useGroupChatStore.getState().setActiveGroupChatId('gc-99'); - const state = getGroupChatState(); + const state = useGroupChatStore.getState(); expect(state.activeGroupChatId).toBe('gc-99'); }); - it('getGroupChatActions returns all actions', () => { - const actions = getGroupChatActions(); - expect(typeof actions.setGroupChats).toBe('function'); - expect(typeof actions.setActiveGroupChatId).toBe('function'); - expect(typeof actions.setGroupChatMessages).toBe('function'); - expect(typeof actions.setGroupChatState).toBe('function'); - expect(typeof actions.setParticipantStates).toBe('function'); - expect(typeof actions.setModeratorUsage).toBe('function'); - expect(typeof actions.setGroupChatStates).toBe('function'); - expect(typeof actions.setAllGroupChatParticipantStates).toBe('function'); - expect(typeof actions.setGroupChatExecutionQueue).toBe('function'); - expect(typeof actions.setGroupChatReadOnlyMode).toBe('function'); - expect(typeof actions.setGroupChatRightTab).toBe('function'); - expect(typeof actions.setGroupChatParticipantColors).toBe('function'); - expect(typeof actions.setGroupChatStagedImages).toBe('function'); - expect(typeof actions.setGroupChatError).toBe('function'); - expect(typeof actions.clearGroupChatError).toBe('function'); - expect(typeof actions.resetGroupChatState).toBe('function'); - }); - - it('getGroupChatActions returns stable references', () => { - const actions1 = getGroupChatActions(); + it('useGroupChatStore.getState() exposes all actions', () => { + const state = useGroupChatStore.getState(); + expect(typeof state.setGroupChats).toBe('function'); + expect(typeof state.setActiveGroupChatId).toBe('function'); + expect(typeof state.setGroupChatMessages).toBe('function'); + expect(typeof state.setGroupChatState).toBe('function'); + expect(typeof state.setParticipantStates).toBe('function'); + expect(typeof state.setModeratorUsage).toBe('function'); + expect(typeof state.setGroupChatStates).toBe('function'); + expect(typeof state.setAllGroupChatParticipantStates).toBe('function'); + expect(typeof state.setGroupChatExecutionQueue).toBe('function'); + expect(typeof state.setGroupChatReadOnlyMode).toBe('function'); + expect(typeof state.setGroupChatRightTab).toBe('function'); + expect(typeof state.setGroupChatParticipantColors).toBe('function'); + expect(typeof state.setGroupChatStagedImages).toBe('function'); + expect(typeof state.setGroupChatError).toBe('function'); + expect(typeof state.clearGroupChatError).toBe('function'); + expect(typeof state.resetGroupChatState).toBe('function'); + }); + + it('action references are stable across state changes', () => { + const actions1 = useGroupChatStore.getState(); useGroupChatStore.getState().setGroupChatState('agent-working'); - const actions2 = getGroupChatActions(); + const actions2 = useGroupChatStore.getState(); expect(actions1.setGroupChats).toBe(actions2.setGroupChats); expect(actions1.clearGroupChatError).toBe(actions2.clearGroupChatError); }); - it('actions from getGroupChatActions mutate state correctly', () => { - const actions = getGroupChatActions(); - actions.setActiveGroupChatId('gc-from-actions'); + it('actions from useGroupChatStore.getState() mutate state correctly', () => { + useGroupChatStore.getState().setActiveGroupChatId('gc-from-actions'); expect(useGroupChatStore.getState().activeGroupChatId).toBe('gc-from-actions'); }); }); diff --git a/src/__tests__/renderer/stores/modalStore.test.ts b/src/__tests__/renderer/stores/modalStore.test.ts index e683e73715..085511dc80 100644 --- a/src/__tests__/renderer/stores/modalStore.test.ts +++ b/src/__tests__/renderer/stores/modalStore.test.ts @@ -9,7 +9,6 @@ import { useModalActions, selectModalOpen, selectModalData, - selectModal, getModalActions, type ModalId, type SettingsModalData, @@ -300,8 +299,8 @@ describe('modalStore', () => { expect(result.current).toEqual({ tab: 'theme' }); }); - it('provides full entry via selectModal', () => { - const { result } = renderHook(() => useModalStore(selectModal('settings'))); + it('provides full entry via modals map', () => { + const { result } = renderHook(() => useModalStore((s) => s.modals.get('settings'))); expect(result.current).toBeUndefined(); diff --git a/src/__tests__/renderer/stores/notificationStore.test.ts b/src/__tests__/renderer/stores/notificationStore.test.ts index 1c8f85e1e3..518353fe13 100644 --- a/src/__tests__/renderer/stores/notificationStore.test.ts +++ b/src/__tests__/renderer/stores/notificationStore.test.ts @@ -10,16 +10,7 @@ */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { - useNotificationStore, - notifyToast, - resetToastIdCounter, - getNotificationState, - getNotificationActions, - selectToasts, - selectToastCount, - selectConfig, -} from '../../../renderer/stores/notificationStore'; +import { useNotificationStore, notifyToast } from '../../../renderer/stores/notificationStore'; import type { Toast } from '../../../renderer/stores/notificationStore'; // ============================================================================ @@ -43,7 +34,6 @@ beforeEach(() => { idleNotificationCommand: '', }, }); - resetToastIdCounter(); // Mock window.maestro (globalThis as any).window = { @@ -245,9 +235,9 @@ describe('notificationStore', () => { describe('notifyToast', () => { describe('ID generation', () => { - it('returns generated toast ID', () => { + it('returns generated toast ID with expected shape', () => { const id = notifyToast({ type: 'success', title: 'Test', message: 'msg' }); - expect(id).toMatch(/^toast-\d+-0$/); + expect(id).toMatch(/^toast-\d+-\d+$/); }); it('generates unique IDs', () => { @@ -256,10 +246,12 @@ describe('notificationStore', () => { expect(id1).not.toBe(id2); }); - it('increments counter', () => { - notifyToast({ type: 'success', title: 'A', message: 'a' }); + it('counter portion increments between consecutive calls', () => { + const id1 = notifyToast({ type: 'success', title: 'A', message: 'a' }); const id2 = notifyToast({ type: 'success', title: 'B', message: 'b' }); - expect(id2).toMatch(/^toast-\d+-1$/); + const counter1 = Number(id1.split('-').pop()); + const counter2 = Number(id2.split('-').pop()); + expect(counter2).toBe(counter1 + 1); }); }); @@ -530,20 +522,20 @@ describe('notificationStore', () => { // Selectors // ========================================================================== - describe('selectors', () => { - it('selectToasts returns toasts array', () => { + describe('store state access', () => { + it('toasts array reflects addToast calls', () => { useNotificationStore.getState().addToast(createToast({ id: 'a' })); - expect(selectToasts(useNotificationStore.getState())).toHaveLength(1); + expect(useNotificationStore.getState().toasts).toHaveLength(1); }); - it('selectToastCount returns count', () => { + it('toasts length reflects count', () => { useNotificationStore.getState().addToast(createToast({ id: 'a' })); useNotificationStore.getState().addToast(createToast({ id: 'b' })); - expect(selectToastCount(useNotificationStore.getState())).toBe(2); + expect(useNotificationStore.getState().toasts).toHaveLength(2); }); - it('selectConfig returns config object', () => { - const config = selectConfig(useNotificationStore.getState()); + it('config object exposes defaults', () => { + const { config } = useNotificationStore.getState(); expect(config.defaultDuration).toBe(20); expect(config.osNotificationsEnabled).toBe(true); }); @@ -554,20 +546,19 @@ describe('notificationStore', () => { // ========================================================================== describe('non-React access', () => { - it('getNotificationState returns current state', () => { + it('useNotificationStore.getState() returns current state', () => { notifyToast({ type: 'info', title: 'Test', message: 'msg' }); - expect(getNotificationState().toasts).toHaveLength(1); + expect(useNotificationStore.getState().toasts).toHaveLength(1); }); - it('getNotificationActions returns working action references', () => { - const actions = getNotificationActions(); - actions.addToast(createToast({ id: 'from-actions' })); + it('useNotificationStore.getState() exposes working action references', () => { + useNotificationStore.getState().addToast(createToast({ id: 'from-actions' })); expect(useNotificationStore.getState().toasts[0].id).toBe('from-actions'); }); - it('getNotificationActions.clearToasts works', () => { + it('useNotificationStore.getState().clearToasts works', () => { notifyToast({ type: 'info', title: 'A', message: 'a' }); - getNotificationActions().clearToasts(); + useNotificationStore.getState().clearToasts(); expect(useNotificationStore.getState().toasts).toHaveLength(0); }); }); diff --git a/src/__tests__/renderer/stores/operationStore.test.ts b/src/__tests__/renderer/stores/operationStore.test.ts index f74ab52987..4861181cc5 100644 --- a/src/__tests__/renderer/stores/operationStore.test.ts +++ b/src/__tests__/renderer/stores/operationStore.test.ts @@ -9,11 +9,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { useOperationStore, - getOperationState, - getOperationActions, selectIsAnySummarizing, selectIsAnyMerging, - selectIsAnyOperationInProgress, } from '../../../renderer/stores/operationStore'; import type { TabSummarizeState, @@ -429,43 +426,6 @@ describe('operationStore', () => { expect(selectIsAnyMerging(useOperationStore.getState())).toBe(true); }); }); - - describe('selectIsAnyOperationInProgress', () => { - it('returns false when all idle', () => { - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(false); - }); - - it('returns true when summarizing', () => { - useOperationStore - .getState() - .setSummarizeTabState('tab-1', createSummarizeState({ state: 'summarizing' })); - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(true); - }); - - it('returns true when merging', () => { - useOperationStore - .getState() - .setMergeTabState('tab-1', createMergeState({ state: 'merging' })); - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(true); - }); - - it('returns true when transfer is grooming', () => { - useOperationStore.getState().setTransferState({ state: 'grooming' }); - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(true); - }); - - it('returns true when transfer is creating', () => { - useOperationStore.getState().setTransferState({ state: 'creating' }); - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(true); - }); - - it('returns false when transfer is error or complete', () => { - useOperationStore.getState().setTransferState({ state: 'error' }); - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(false); - useOperationStore.getState().setTransferState({ state: 'complete' }); - expect(selectIsAnyOperationInProgress(useOperationStore.getState())).toBe(false); - }); - }); }); // ========================================================================== @@ -536,19 +496,20 @@ describe('operationStore', () => { // ========================================================================== describe('non-React access', () => { - it('getOperationState returns current snapshot', () => { + it('useOperationStore.getState() returns current snapshot', () => { useOperationStore.getState().setGlobalMergeInProgress(true); - expect(getOperationState().globalMergeInProgress).toBe(true); + expect(useOperationStore.getState().globalMergeInProgress).toBe(true); }); - it('getOperationActions returns working methods', () => { - const actions = getOperationActions(); - actions.setSummarizeTabState('tab-1', createSummarizeState({ state: 'summarizing' })); + it('useOperationStore.getState() exposes working methods', () => { + useOperationStore + .getState() + .setSummarizeTabState('tab-1', createSummarizeState({ state: 'summarizing' })); expect(useOperationStore.getState().summarizeStates.get('tab-1')?.state).toBe('summarizing'); }); - it('getOperationActions returns all expected methods', () => { - const actions = getOperationActions(); + it('useOperationStore.getState() exposes all expected methods', () => { + const state = useOperationStore.getState(); const expectedMethods = [ 'setSummarizeTabState', 'updateSummarizeTabState', @@ -565,7 +526,7 @@ describe('operationStore', () => { 'resetAll', ]; for (const method of expectedMethods) { - expect(typeof (actions as any)[method]).toBe('function'); + expect(typeof (state as any)[method]).toBe('function'); } }); }); diff --git a/src/__tests__/renderer/stores/sessionStore.test.ts b/src/__tests__/renderer/stores/sessionStore.test.ts index a6fa0e1155..ddb8b54691 100644 --- a/src/__tests__/renderer/stores/sessionStore.test.ts +++ b/src/__tests__/renderer/stores/sessionStore.test.ts @@ -4,15 +4,6 @@ import { useSessionStore, selectActiveSession, selectSessionById, - selectBookmarkedSessions, - selectSessionsByGroup, - selectUngroupedSessions, - selectGroupById, - selectSessionCount, - selectIsReady, - selectIsAnySessionBusy, - getSessionState, - getSessionActions, } from '../../../renderer/stores/sessionStore'; import type { Session, Group, FilePreviewTab } from '../../../renderer/types'; @@ -511,144 +502,6 @@ describe('sessionStore', () => { expect(session).toBeUndefined(); }); }); - - describe('selectBookmarkedSessions', () => { - it('returns only bookmarked sessions', () => { - useSessionStore - .getState() - .setSessions([ - createMockSession({ id: 'a', bookmarked: true }), - createMockSession({ id: 'b', bookmarked: false }), - createMockSession({ id: 'c', bookmarked: true }), - ]); - - const bookmarked = selectBookmarkedSessions(useSessionStore.getState()); - expect(bookmarked).toHaveLength(2); - expect(bookmarked.map((s) => s.id)).toEqual(['a', 'c']); - }); - - it('returns empty array when none bookmarked', () => { - useSessionStore.getState().setSessions([createMockSession({ id: 'a', bookmarked: false })]); - - expect(selectBookmarkedSessions(useSessionStore.getState())).toHaveLength(0); - }); - }); - - describe('selectSessionsByGroup', () => { - it('returns sessions belonging to a group', () => { - useSessionStore - .getState() - .setSessions([ - createMockSession({ id: 'a', groupId: 'g1' }), - createMockSession({ id: 'b', groupId: 'g2' }), - createMockSession({ id: 'c', groupId: 'g1' }), - ]); - - const group1 = selectSessionsByGroup('g1')(useSessionStore.getState()); - expect(group1).toHaveLength(2); - expect(group1.map((s) => s.id)).toEqual(['a', 'c']); - }); - }); - - describe('selectUngroupedSessions', () => { - it('returns sessions without a group and not worktree children', () => { - useSessionStore.getState().setSessions([ - createMockSession({ id: 'a' }), // ungrouped - createMockSession({ id: 'b', groupId: 'g1' }), // grouped - createMockSession({ id: 'c', parentSessionId: 'a' }), // worktree child - createMockSession({ id: 'd' }), // ungrouped - ]); - - const ungrouped = selectUngroupedSessions(useSessionStore.getState()); - expect(ungrouped).toHaveLength(2); - expect(ungrouped.map((s) => s.id)).toEqual(['a', 'd']); - }); - }); - - describe('selectGroupById', () => { - it('returns the group with the given ID', () => { - useSessionStore.getState().setGroups([createMockGroup({ id: 'g1', name: 'Group One' })]); - - const group = selectGroupById('g1')(useSessionStore.getState()); - expect(group?.name).toBe('Group One'); - }); - - it('returns undefined if not found', () => { - const group = selectGroupById('nope')(useSessionStore.getState()); - expect(group).toBeUndefined(); - }); - }); - - describe('selectSessionCount', () => { - it('returns the number of sessions', () => { - expect(selectSessionCount(useSessionStore.getState())).toBe(0); - - useSessionStore - .getState() - .setSessions([createMockSession({ id: 'a' }), createMockSession({ id: 'b' })]); - - expect(selectSessionCount(useSessionStore.getState())).toBe(2); - }); - }); - - describe('selectIsReady', () => { - it('returns false when neither flag is set', () => { - expect(selectIsReady(useSessionStore.getState())).toBe(false); - }); - - it('returns false when only sessionsLoaded is true', () => { - useSessionStore.getState().setSessionsLoaded(true); - expect(selectIsReady(useSessionStore.getState())).toBe(false); - }); - - it('returns false when only initialLoadComplete is true', () => { - useSessionStore.getState().setInitialLoadComplete(true); - expect(selectIsReady(useSessionStore.getState())).toBe(false); - }); - - it('returns true when both flags are set', () => { - useSessionStore.getState().setSessionsLoaded(true); - useSessionStore.getState().setInitialLoadComplete(true); - expect(selectIsReady(useSessionStore.getState())).toBe(true); - }); - }); - - describe('selectIsAnySessionBusy', () => { - it('returns false when no sessions exist', () => { - expect(selectIsAnySessionBusy(useSessionStore.getState())).toBe(false); - }); - - it('returns false when all sessions are idle', () => { - useSessionStore - .getState() - .setSessions([ - createMockSession({ id: 'a', state: 'idle' }), - createMockSession({ id: 'b', state: 'idle' }), - ]); - expect(selectIsAnySessionBusy(useSessionStore.getState())).toBe(false); - }); - - it('returns true when at least one session is busy', () => { - useSessionStore - .getState() - .setSessions([ - createMockSession({ id: 'a', state: 'idle' }), - createMockSession({ id: 'b', state: 'busy' }), - ]); - expect(selectIsAnySessionBusy(useSessionStore.getState())).toBe(true); - }); - - it('returns false for non-busy active states', () => { - useSessionStore - .getState() - .setSessions([ - createMockSession({ id: 'a', state: 'waiting_input' }), - createMockSession({ id: 'b', state: 'connecting' }), - createMockSession({ id: 'c', state: 'error' }), - ]); - expect(selectIsAnySessionBusy(useSessionStore.getState())).toBe(false); - }); - }); }); // ======================================================================== @@ -767,25 +620,23 @@ describe('sessionStore', () => { // ======================================================================== describe('non-React access', () => { - it('getSessionState returns current state', () => { + it('useSessionStore.getState() returns current state', () => { useSessionStore.getState().setSessions([createMockSession({ id: 'a' })]); useSessionStore.getState().setActiveSessionId('a'); - const state = getSessionState(); + const state = useSessionStore.getState(); expect(state.sessions).toHaveLength(1); expect(state.activeSessionId).toBe('a'); }); - it('getSessionActions returns working action references', () => { - const actions = getSessionActions(); - - actions.setSessions([createMockSession({ id: 'a' })]); + it('useSessionStore.getState() exposes working action references', () => { + useSessionStore.getState().setSessions([createMockSession({ id: 'a' })]); expect(useSessionStore.getState().sessions).toHaveLength(1); - actions.setActiveSessionId('a'); + useSessionStore.getState().setActiveSessionId('a'); expect(useSessionStore.getState().activeSessionId).toBe('a'); - actions.toggleBookmark('a'); + useSessionStore.getState().toggleBookmark('a'); expect(useSessionStore.getState().sessions[0].bookmarked).toBe(true); }); @@ -828,12 +679,12 @@ describe('sessionStore', () => { // Bookmark useSessionStore.getState().toggleBookmark('lifecycle'); - expect(selectBookmarkedSessions(useSessionStore.getState())).toHaveLength(1); + expect(useSessionStore.getState().sessions.filter((s) => s.bookmarked)).toHaveLength(1); // Remove useSessionStore.getState().removeSession('lifecycle'); expect(useSessionStore.getState().sessions).toHaveLength(0); - expect(selectBookmarkedSessions(useSessionStore.getState())).toHaveLength(0); + expect(useSessionStore.getState().sessions.filter((s) => s.bookmarked)).toHaveLength(0); }); it('handles group lifecycle: create → add sessions → collapse → remove', () => { @@ -848,7 +699,7 @@ describe('sessionStore', () => { .getState() .addSession(createMockSession({ id: 'b', groupId: 'g1', name: 'DB' })); - expect(selectSessionsByGroup('g1')(useSessionStore.getState())).toHaveLength(2); + expect(useSessionStore.getState().sessions.filter((s) => s.groupId === 'g1')).toHaveLength(2); // Collapse useSessionStore.getState().toggleGroupCollapsed('g1'); @@ -878,7 +729,11 @@ describe('sessionStore', () => { it('handles initialization flow: load → set loaded → set complete', () => { // Simulate the startup flow - expect(selectIsReady(useSessionStore.getState())).toBe(false); + const isReady = () => { + const s = useSessionStore.getState(); + return s.sessionsLoaded && s.initialLoadComplete; + }; + expect(isReady()).toBe(false); // Step 1: Load sessions from disk useSessionStore @@ -898,7 +753,7 @@ describe('sessionStore', () => { // Step 4: Mark initial load complete useSessionStore.getState().setInitialLoadComplete(true); - expect(selectIsReady(useSessionStore.getState())).toBe(true); + expect(isReady()).toBe(true); expect(selectActiveSession(useSessionStore.getState())?.id).toBe('restored-1'); }); }); @@ -1645,9 +1500,8 @@ describe('sessionStore', () => { consoleSpy.mockRestore(); }); - it('is available via getSessionActions()', () => { - const actions = getSessionActions(); - expect(typeof actions.addLogToTab).toBe('function'); + it('is available via useSessionStore.getState()', () => { + expect(typeof useSessionStore.getState().addLogToTab).toBe('function'); }); }); }); diff --git a/src/__tests__/renderer/stores/settingsStore.test.ts b/src/__tests__/renderer/stores/settingsStore.test.ts index 26078062fe..0d2c21f2eb 100644 --- a/src/__tests__/renderer/stores/settingsStore.test.ts +++ b/src/__tests__/renderer/stores/settingsStore.test.ts @@ -2,22 +2,60 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { useSettingsStore, loadAllSettings, - getBadgeLevelForTime, selectIsLeaderboardRegistered, - getSettingsState, - getSettingsActions, - DEFAULT_CONTEXT_MANAGEMENT_SETTINGS, - DEFAULT_AUTO_RUN_STATS, - DEFAULT_USAGE_STATS, - DEFAULT_KEYBOARD_MASTERY_STATS, - DEFAULT_ONBOARDING_STATS, - DEFAULT_AI_COMMANDS, } from '../../../renderer/stores/settingsStore'; import type { SettingsStoreState } from '../../../renderer/stores/settingsStore'; import type { FileExplorerIconTheme } from '../../../renderer/utils/fileExplorerIcons/shared'; import { DEFAULT_SHORTCUTS, TAB_SHORTCUTS } from '../../../renderer/constants/shortcuts'; import { DEFAULT_CUSTOM_THEME_COLORS } from '../../../renderer/constants/themes'; +// Pull defaults from a freshly-initialized store so tests don't need to re-import them. +// Deep-cloned so test mutations can't affect the captured reference. +// These constants match what the store uses internally (kept non-exported to prevent fan-out). +const _INITIAL_STATE = useSettingsStore.getState(); +const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS = JSON.parse( + JSON.stringify(_INITIAL_STATE.contextManagementSettings) +); +const DEFAULT_AUTO_RUN_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.autoRunStats)); +const DEFAULT_USAGE_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.usageStats)); +const DEFAULT_KEYBOARD_MASTERY_STATS = JSON.parse( + JSON.stringify(_INITIAL_STATE.keyboardMasteryStats) +); +const DEFAULT_ONBOARDING_STATS = JSON.parse(JSON.stringify(_INITIAL_STATE.onboardingStats)); +const DEFAULT_AI_COMMANDS = JSON.parse(JSON.stringify(_INITIAL_STATE.customAICommands)); + +// Inlined badge level calculator matching settingsStore's internal function. +// Kept local so removing the export from the store doesn't break this test. +function getBadgeLevelForTime(cumulativeTimeMs: number): number { + const MINUTE = 60 * 1000; + const HOUR = 60 * MINUTE; + const DAY = 24 * HOUR; + const WEEK = 7 * DAY; + const MONTH = 30 * DAY; + const thresholds = [ + 15 * MINUTE, + 1 * HOUR, + 8 * HOUR, + 1 * DAY, + 1 * WEEK, + 1 * MONTH, + 3 * MONTH, + 6 * MONTH, + 365 * DAY, + 5 * 365 * DAY, + 10 * 365 * DAY, + ]; + let level = 0; + for (let i = 0; i < thresholds.length; i++) { + if (cumulativeTimeMs >= thresholds[i]) { + level = i + 1; + } else { + break; + } + } + return level; +} + /** * Reset the Zustand store to initial state between tests. * Zustand stores are singletons, so state persists across tests unless explicitly reset. @@ -1828,17 +1866,16 @@ describe('settingsStore', () => { // ======================================================================== describe('non-React access', () => { - it('getSettingsState returns current state', () => { + it('useSettingsStore.getState() returns current state', () => { useSettingsStore.setState({ fontSize: 20 }); - const state = getSettingsState(); + const state = useSettingsStore.getState(); expect(state.fontSize).toBe(20); }); - it('getSettingsActions returns action functions that work', () => { - const actions = getSettingsActions(); - expect(typeof actions.setFontSize).toBe('function'); + it('useSettingsStore.getState() exposes action functions that work', () => { + expect(typeof useSettingsStore.getState().setFontSize).toBe('function'); - actions.setFontSize(22); + useSettingsStore.getState().setFontSize(22); expect(useSettingsStore.getState().fontSize).toBe(22); expect(window.maestro.settings.set).toHaveBeenCalledWith('fontSize', 22); }); diff --git a/src/__tests__/renderer/stores/tabStore.test.ts b/src/__tests__/renderer/stores/tabStore.test.ts index d62e62018c..2148a755d6 100644 --- a/src/__tests__/renderer/stores/tabStore.test.ts +++ b/src/__tests__/renderer/stores/tabStore.test.ts @@ -1,18 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { renderHook, act } from '@testing-library/react'; -import { - useTabStore, - selectActiveTab, - selectActiveFileTab, - selectUnifiedTabs, - selectTabById, - selectFileTabById, - selectTabCount, - selectAllTabs, - selectAllFileTabs, - getTabState, - getTabActions, -} from '../../../renderer/stores/tabStore'; +import { useTabStore } from '../../../renderer/stores/tabStore'; import { useSessionStore } from '../../../renderer/stores/sessionStore'; import type { Session, AITab, FilePreviewTab, TerminalTab } from '../../../renderer/types'; @@ -633,177 +621,61 @@ describe('tabStore', () => { // Selectors // ======================================================================== - describe('selectors', () => { - describe('selectActiveTab', () => { - it('should return the active AI tab', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const tab2 = createMockAITab({ id: 'tab-2' }); - setupSessionWithTabs([tab1, tab2], [], 'tab-2'); - - const result = selectActiveTab(useSessionStore.getState()); - expect(result).toBeDefined(); - expect(result!.id).toBe('tab-2'); - }); - - it('should fall back to first tab if activeTabId not found', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - setupSessionWithTabs([tab1], [], 'non-existent'); - - const result = selectActiveTab(useSessionStore.getState()); - expect(result).toBeDefined(); - expect(result!.id).toBe('tab-1'); - }); - - it('should return undefined with no active session', () => { - const result = selectActiveTab(useSessionStore.getState()); - expect(result).toBeUndefined(); - }); - }); - - describe('selectActiveFileTab', () => { - it('should return the active file tab', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const fileTab1 = createMockFileTab({ id: 'file-1' }); - setupSessionWithTabs([tab1], [fileTab1], 'tab-1', 'file-1'); - - const result = selectActiveFileTab(useSessionStore.getState()); - expect(result).toBeDefined(); - expect(result!.id).toBe('file-1'); - }); - - it('should return undefined when no file tab is active', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - setupSessionWithTabs([tab1]); + describe('session state access (tab derivations)', () => { + it('exposes the active AI tab via activeTabId', () => { + const tab1 = createMockAITab({ id: 'tab-1' }); + const tab2 = createMockAITab({ id: 'tab-2' }); + setupSessionWithTabs([tab1, tab2], [], 'tab-2'); - const result = selectActiveFileTab(useSessionStore.getState()); - expect(result).toBeUndefined(); - }); + const session = useSessionStore.getState().sessions[0]; + const active = session.aiTabs.find((t) => t.id === session.activeTabId); + expect(active?.id).toBe('tab-2'); }); - describe('selectUnifiedTabs', () => { - it('should return tabs in unified order', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const tab2 = createMockAITab({ id: 'tab-2' }); - const fileTab1 = createMockFileTab({ id: 'file-1' }); - - const sessionId = 'test-session'; - const session = createMockSession({ - id: sessionId, - aiTabs: [tab1, tab2], - activeTabId: 'tab-1', - filePreviewTabs: [fileTab1], - unifiedTabOrder: [ - { type: 'ai', id: 'tab-1' }, - { type: 'file', id: 'file-1' }, - { type: 'ai', id: 'tab-2' }, - ], - }); - - useSessionStore.setState({ - sessions: [session], - activeSessionId: sessionId, - }); - - const result = selectUnifiedTabs(useSessionStore.getState()); - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ type: 'ai', id: 'tab-1', data: tab1 }); - expect(result[1]).toEqual({ type: 'file', id: 'file-1', data: fileTab1 }); - expect(result[2]).toEqual({ type: 'ai', id: 'tab-2', data: tab2 }); - }); - - it('should include orphan tabs not in unified order', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const tab2 = createMockAITab({ id: 'tab-2' }); - - const session = createMockSession({ - id: 'test', - aiTabs: [tab1, tab2], - activeTabId: 'tab-1', - unifiedTabOrder: [{ type: 'ai', id: 'tab-1' }], - // tab-2 is NOT in unified order - }); - - useSessionStore.setState({ - sessions: [session], - activeSessionId: 'test', - }); - - const result = selectUnifiedTabs(useSessionStore.getState()); - expect(result).toHaveLength(2); - expect(result[0].id).toBe('tab-1'); - expect(result[1].id).toBe('tab-2'); - }); + it('exposes the active file tab via activeFileTabId', () => { + const tab1 = createMockAITab({ id: 'tab-1' }); + const fileTab1 = createMockFileTab({ id: 'file-1' }); + setupSessionWithTabs([tab1], [fileTab1], 'tab-1', 'file-1'); - it('should return empty array with no active session', () => { - const result = selectUnifiedTabs(useSessionStore.getState()); - expect(result).toEqual([]); - }); + const session = useSessionStore.getState().sessions[0]; + const activeFile = session.filePreviewTabs.find((t) => t.id === session.activeFileTabId); + expect(activeFile?.id).toBe('file-1'); }); - describe('selectTabById', () => { - it('should find tab by ID', () => { - const tab1 = createMockAITab({ id: 'tab-1', name: 'Found' }); - setupSessionWithTabs([tab1]); - - const result = selectTabById('tab-1')(useSessionStore.getState()); - expect(result).toBeDefined(); - expect(result!.name).toBe('Found'); - }); - - it('should return undefined for non-existent tab', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - setupSessionWithTabs([tab1]); + it('looks up AI tab by ID', () => { + const tab1 = createMockAITab({ id: 'tab-1', name: 'Found' }); + setupSessionWithTabs([tab1]); - const result = selectTabById('non-existent')(useSessionStore.getState()); - expect(result).toBeUndefined(); - }); + const session = useSessionStore.getState().sessions[0]; + const found = session.aiTabs.find((t) => t.id === 'tab-1'); + expect(found?.name).toBe('Found'); }); - describe('selectFileTabById', () => { - it('should find file tab by ID', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const fileTab = createMockFileTab({ id: 'file-1', name: 'app' }); - setupSessionWithTabs([tab1], [fileTab]); + it('looks up file tab by ID', () => { + const tab1 = createMockAITab({ id: 'tab-1' }); + const fileTab = createMockFileTab({ id: 'file-1', name: 'app' }); + setupSessionWithTabs([tab1], [fileTab]); - const result = selectFileTabById('file-1')(useSessionStore.getState()); - expect(result).toBeDefined(); - expect(result!.name).toBe('app'); - }); + const session = useSessionStore.getState().sessions[0]; + const found = session.filePreviewTabs.find((t) => t.id === 'file-1'); + expect(found?.name).toBe('app'); }); - describe('selectTabCount', () => { - it('should return count of AI tabs', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const tab2 = createMockAITab({ id: 'tab-2' }); - setupSessionWithTabs([tab1, tab2]); - - expect(selectTabCount(useSessionStore.getState())).toBe(2); - }); + it('reports AI tab count', () => { + const tab1 = createMockAITab({ id: 'tab-1' }); + const tab2 = createMockAITab({ id: 'tab-2' }); + setupSessionWithTabs([tab1, tab2]); - it('should return 0 with no active session', () => { - expect(selectTabCount(useSessionStore.getState())).toBe(0); - }); + expect(useSessionStore.getState().sessions[0].aiTabs).toHaveLength(2); }); - describe('selectAllTabs / selectAllFileTabs', () => { - it('should return all AI tabs', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const tab2 = createMockAITab({ id: 'tab-2' }); - setupSessionWithTabs([tab1, tab2]); - - const result = selectAllTabs(useSessionStore.getState()); - expect(result).toHaveLength(2); - }); - - it('should return all file tabs', () => { - const tab1 = createMockAITab({ id: 'tab-1' }); - const fileTab1 = createMockFileTab({ id: 'file-1' }); - const fileTab2 = createMockFileTab({ id: 'file-2' }); - setupSessionWithTabs([tab1], [fileTab1, fileTab2]); + it('exposes all file tabs', () => { + const tab1 = createMockAITab({ id: 'tab-1' }); + const fileTab1 = createMockFileTab({ id: 'file-1' }); + const fileTab2 = createMockFileTab({ id: 'file-2' }); + setupSessionWithTabs([tab1], [fileTab1, fileTab2]); - const result = selectAllFileTabs(useSessionStore.getState()); - expect(result).toHaveLength(2); - }); + expect(useSessionStore.getState().sessions[0].filePreviewTabs).toHaveLength(2); }); }); @@ -824,11 +696,16 @@ describe('tabStore', () => { expect(result.current).toEqual({ filename: 'test.md', content: 'hello' }); }); - it('should subscribe to tab selectors via sessionStore', () => { + it('should subscribe to tab state via sessionStore', () => { const tab1 = createMockAITab({ id: 'tab-1', name: 'First' }); setupSessionWithTabs([tab1]); - const { result } = renderHook(() => useSessionStore(selectActiveTab)); + const { result } = renderHook(() => + useSessionStore((s) => { + const active = s.sessions.find((sess) => sess.id === s.activeSessionId); + return active?.aiTabs.find((t) => t.id === active.activeTabId); + }) + ); expect(result.current).toBeDefined(); expect(result.current!.id).toBe('tab-1'); @@ -847,9 +724,9 @@ describe('tabStore', () => { // ======================================================================== describe('action stability', () => { - it('should return stable action references from getTabActions', () => { - const actions1 = getTabActions(); - const actions2 = getTabActions(); + it('should return stable action references from useTabStore.getState()', () => { + const actions1 = useTabStore.getState(); + const actions2 = useTabStore.getState(); expect(actions1.createTab).toBe(actions2.createTab); expect(actions1.closeTab).toBe(actions2.closeTab); @@ -864,20 +741,19 @@ describe('tabStore', () => { // ======================================================================== describe('non-React access', () => { - it('should provide current state via getTabState', () => { + it('should provide current state via useTabStore.getState()', () => { const { setTabGistContent } = useTabStore.getState(); setTabGistContent({ filename: 'a.ts', content: 'code' }); - const state = getTabState(); + const state = useTabStore.getState(); expect(state.tabGistContent).toEqual({ filename: 'a.ts', content: 'code' }); }); - it('should provide working actions via getTabActions', () => { + it('should provide working actions via useTabStore.getState()', () => { const tab1 = createMockAITab({ id: 'tab-1', starred: false }); setupSessionWithTabs([tab1]); - const actions = getTabActions(); - actions.starTab('tab-1'); + useTabStore.getState().starTab('tab-1'); const session = useSessionStore.getState().sessions[0]; expect(session.aiTabs[0].starred).toBe(true); @@ -1037,7 +913,7 @@ describe('closeTerminalTab', () => { setupSessionWithTerminalTabs([tab1, tab2]); act(() => { - getTabActions().closeTerminalTab('term-2'); + useTabStore.getState().closeTerminalTab('term-2'); }); expect(window.maestro.process.kill).toHaveBeenCalledTimes(1); @@ -1053,7 +929,7 @@ describe('closeTerminalTab', () => { setupSessionWithTerminalTabs([tab1]); act(() => { - getTabActions().closeTerminalTab('term-1'); + useTabStore.getState().closeTerminalTab('term-1'); }); // PTY should be killed diff --git a/src/renderer/stores/agentStore.ts b/src/renderer/stores/agentStore.ts index 1fb0e2c592..b7434bfc75 100644 --- a/src/renderer/stores/agentStore.ts +++ b/src/renderer/stores/agentStore.ts @@ -11,7 +11,7 @@ * 2. Error recovery actions — clearError, restart, retry, newSession, authenticate * 3. Agent lifecycle actions — kill, interrupt * - * Can be used outside React via useAgentStore.getState() / getAgentActions(). + * Can be used outside React via useAgentStore.getState(). */ import { create } from 'zustand'; @@ -535,44 +535,3 @@ export const useAgentStore = create()((set, get) => ({ } }, })); - -// ============================================================================ -// Selectors -// ============================================================================ - -/** Select the list of available (detected) agents */ -export const selectAvailableAgents = (state: AgentStore): AgentConfig[] => state.availableAgents; - -/** Select whether agent detection has completed */ -export const selectAgentsDetected = (state: AgentStore): boolean => state.agentsDetected; - -// ============================================================================ -// Non-React Access -// ============================================================================ - -/** - * Get the current agent store state snapshot. - * Use outside React (services, orchestrators, IPC handlers). - */ -export function getAgentState() { - return useAgentStore.getState(); -} - -/** - * Get stable agent action references outside React. - */ -export function getAgentActions() { - const state = useAgentStore.getState(); - return { - refreshAgents: state.refreshAgents, - getAgentConfig: state.getAgentConfig, - processQueuedItem: state.processQueuedItem, - clearAgentError: state.clearAgentError, - startNewSessionAfterError: state.startNewSessionAfterError, - retryAfterError: state.retryAfterError, - restartAgentAfterError: state.restartAgentAfterError, - authenticateAfterError: state.authenticateAfterError, - killAgent: state.killAgent, - interruptAgent: state.interruptAgent, - }; -} diff --git a/src/renderer/stores/batchStore.ts b/src/renderer/stores/batchStore.ts index ae182b6eb5..6da6d66f1c 100644 --- a/src/renderer/stores/batchStore.ts +++ b/src/renderer/stores/batchStore.ts @@ -9,7 +9,7 @@ * existing batchReducer function to the current state. Hooks retain their * async orchestration; this store owns the state layer only. * - * Can be used outside React via getBatchState() / getBatchActions(). + * Can be used outside React via getBatchState(). */ import { create } from 'zustand'; @@ -103,21 +103,6 @@ export function selectActiveBatchSessionIds(s: BatchStoreState): string[] { .map(([sessionId]) => sessionId); } -/** List of session IDs that are in stopping state */ -export function selectStoppingBatchSessionIds(s: BatchStoreState): string[] { - return Object.entries(s.batchRunStates) - .filter(([, state]) => state.isRunning && state.isStopping) - .map(([sessionId]) => sessionId); -} - -/** Get batch run state for a specific session */ -export function selectBatchRunState( - s: BatchStoreState, - sessionId: string -): BatchRunState | undefined { - return s.batchRunStates[sessionId]; -} - // ============================================================================ // Store // ============================================================================ @@ -180,22 +165,3 @@ export const useBatchStore = create()((set) => ({ export function getBatchState() { return useBatchStore.getState(); } - -/** - * Get stable batch action references outside React. - */ -export function getBatchActions() { - const state = useBatchStore.getState(); - return { - setDocumentList: state.setDocumentList, - setDocumentTree: state.setDocumentTree, - setIsLoadingDocuments: state.setIsLoadingDocuments, - setDocumentTaskCounts: state.setDocumentTaskCounts, - updateTaskCount: state.updateTaskCount, - clearDocumentList: state.clearDocumentList, - dispatchBatch: state.dispatchBatch, - setBatchRunStates: state.setBatchRunStates, - setCustomPrompt: state.setCustomPrompt, - clearCustomPrompts: state.clearCustomPrompts, - }; -} diff --git a/src/renderer/stores/fileExplorerStore.ts b/src/renderer/stores/fileExplorerStore.ts index 039542e057..36254e63f7 100644 --- a/src/renderer/stores/fileExplorerStore.ts +++ b/src/renderer/stores/fileExplorerStore.ts @@ -8,7 +8,7 @@ * Per-session file tree DATA (fileTree, fileExplorerExpanded, etc.) stays * in sessionStore — deeply embedded in the Session type with 200+ call sites. * - * Can be used outside React via getFileExplorerState() / getFileExplorerActions(). + * Can be used outside React via useFileExplorerStore.getState(). */ import { create } from 'zustand'; @@ -134,34 +134,3 @@ export const useFileExplorerStore = create()((set, get) => ({ setIsGraphViewOpen: (open) => set({ isGraphViewOpen: open }), })); - -// ============================================================================ -// Non-React access -// ============================================================================ - -/** - * Get current file explorer state snapshot. - * Use outside React (services, orchestrators, IPC handlers). - */ -export function getFileExplorerState() { - return useFileExplorerStore.getState(); -} - -/** - * Get stable file explorer action references outside React. - */ -export function getFileExplorerActions() { - const state = useFileExplorerStore.getState(); - return { - setSelectedFileIndex: state.setSelectedFileIndex, - setFileTreeFilter: state.setFileTreeFilter, - setFileTreeFilterOpen: state.setFileTreeFilterOpen, - setFilePreviewLoading: state.setFilePreviewLoading, - setFilteredFileTree: state.setFilteredFileTree, - setFlatFileList: state.setFlatFileList, - focusFileInGraph: state.focusFileInGraph, - openLastDocumentGraph: state.openLastDocumentGraph, - closeGraphView: state.closeGraphView, - setIsGraphViewOpen: state.setIsGraphViewOpen, - }; -} diff --git a/src/renderer/stores/groupChatStore.ts b/src/renderer/stores/groupChatStore.ts index 6bb4f9e381..de3ae06ff8 100644 --- a/src/renderer/stores/groupChatStore.ts +++ b/src/renderer/stores/groupChatStore.ts @@ -9,7 +9,7 @@ * Refs (groupChatInputRef, groupChatMessagesRef) stay outside the store * since they are React-specific and don't trigger re-renders. * - * Can be used outside React via getGroupChatState() / getGroupChatActions(). + * Can be used outside React via useGroupChatStore.getState(). */ import { create } from 'zustand'; @@ -221,42 +221,3 @@ export const useGroupChatStore = create()((set) => ({ groupChatError: null, }), })); - -// ============================================================================ -// Non-React access -// ============================================================================ - -/** - * Get current group chat state snapshot. - * Use outside React (services, orchestrators, IPC handlers). - */ -export function getGroupChatState() { - return useGroupChatStore.getState(); -} - -/** - * Get stable group chat action references outside React. - */ -export function getGroupChatActions() { - const state = useGroupChatStore.getState(); - return { - setGroupChats: state.setGroupChats, - setActiveGroupChatId: state.setActiveGroupChatId, - setGroupChatMessages: state.setGroupChatMessages, - setGroupChatState: state.setGroupChatState, - setParticipantStates: state.setParticipantStates, - setModeratorUsage: state.setModeratorUsage, - setGroupChatStates: state.setGroupChatStates, - setAllGroupChatParticipantStates: state.setAllGroupChatParticipantStates, - setGroupChatExecutionQueue: state.setGroupChatExecutionQueue, - setGroupChatReadOnlyMode: state.setGroupChatReadOnlyMode, - setGroupChatRightTab: state.setGroupChatRightTab, - setGroupChatParticipantColors: state.setGroupChatParticipantColors, - setGroupChatStagedImages: state.setGroupChatStagedImages, - setGroupChatError: state.setGroupChatError, - appendParticipantLiveOutput: state.appendParticipantLiveOutput, - clearParticipantLiveOutput: state.clearParticipantLiveOutput, - clearGroupChatError: state.clearGroupChatError, - resetGroupChatState: state.resetGroupChatState, - }; -} diff --git a/src/renderer/stores/modalStore.ts b/src/renderer/stores/modalStore.ts index 206f9d973c..1734798db4 100644 --- a/src/renderer/stores/modalStore.ts +++ b/src/renderer/stores/modalStore.ts @@ -463,18 +463,6 @@ export const selectModalData = (state: ModalStore): ModalDataFor | undefined => state.modals.get(id)?.data as ModalDataFor | undefined; -/** - * Create a selector for a specific modal's full entry (open + data). - * - * @example - * const settings = useModalStore(selectModal('settings')); - * if (settings?.open) { ... } - */ -export const selectModal = - (id: T) => - (state: ModalStore): ModalEntry> | undefined => - state.modals.get(id) as ModalEntry> | undefined; - // ============================================================================ // ModalContext Compatibility Layer // ============================================================================ diff --git a/src/renderer/stores/notificationStore.ts b/src/renderer/stores/notificationStore.ts index 43fba45c26..4bf0efe909 100644 --- a/src/renderer/stores/notificationStore.ts +++ b/src/renderer/stores/notificationStore.ts @@ -8,7 +8,7 @@ * Side effects (logging, audio TTS, OS notifications, auto-dismiss timers) * live in the notifyToast() wrapper function, not in the store itself. * - * Can be used outside React via getNotificationState() / getNotificationActions(). + * Can be used outside React via useNotificationStore.getState(). * notifyToast() is callable from anywhere (React components, services, orchestrators). */ @@ -84,14 +84,6 @@ export type NotificationStore = NotificationStoreState & NotificationStoreAction // Selectors // ============================================================================ -export function selectToasts(s: NotificationStoreState): Toast[] { - return s.toasts; -} - -export function selectToastCount(s: NotificationStoreState): number { - return s.toasts.length; -} - export function selectConfig(s: NotificationStoreState): NotificationConfig { return s.config; } @@ -159,11 +151,6 @@ let toastIdCounter = 0; /** Active auto-dismiss timers keyed by toast ID. Cleared on manual removal. */ const autoDismissTimers = new Map>(); -/** Reset the toast ID counter (for tests). */ -export function resetToastIdCounter(): void { - toastIdCounter = 0; -} - /** * Fire a toast notification. Handles: * 1. ID generation @@ -299,31 +286,3 @@ export function notifyToast(toast: Omit): string { return id; } - -// ============================================================================ -// Non-React access -// ============================================================================ - -/** - * Get current notification state snapshot. - * Use outside React (services, orchestrators, IPC handlers). - */ -export function getNotificationState() { - return useNotificationStore.getState(); -} - -/** - * Get stable notification action references outside React. - */ -export function getNotificationActions() { - const state = useNotificationStore.getState(); - return { - addToast: state.addToast, - removeToast: state.removeToast, - clearToasts: state.clearToasts, - setDefaultDuration: state.setDefaultDuration, - setAudioFeedback: state.setAudioFeedback, - setOsNotifications: state.setOsNotifications, - setIdleNotification: state.setIdleNotification, - }; -} diff --git a/src/renderer/stores/operationStore.ts b/src/renderer/stores/operationStore.ts index aac73cae4d..5d1fbf93f1 100644 --- a/src/renderer/stores/operationStore.ts +++ b/src/renderer/stores/operationStore.ts @@ -11,7 +11,7 @@ * globalTransferInProgress) become proper store state, making them * testable and resettable. * - * Can be used outside React via getOperationState() / getOperationActions(). + * Can be used outside React via useOperationStore.getState(). */ import { create } from 'zustand'; @@ -135,16 +135,6 @@ export function selectIsAnyMerging(s: OperationStoreState): boolean { return false; } -/** True if any operation (summarize, merge, or transfer) is in progress. */ -export function selectIsAnyOperationInProgress(s: OperationStoreState): boolean { - return ( - selectIsAnySummarizing(s) || - selectIsAnyMerging(s) || - s.transferState === 'grooming' || - s.transferState === 'creating' - ); -} - // ============================================================================ // Initial state // ============================================================================ @@ -262,37 +252,3 @@ export const useOperationStore = create()((set) => ({ globalTransferInProgress: false, }), })); - -// ============================================================================ -// Non-React access -// ============================================================================ - -/** - * Get current operation state snapshot. - * Use outside React (services, orchestrators, IPC handlers). - */ -export function getOperationState() { - return useOperationStore.getState(); -} - -/** - * Get stable operation action references outside React. - */ -export function getOperationActions() { - const state = useOperationStore.getState(); - return { - setSummarizeTabState: state.setSummarizeTabState, - updateSummarizeTabState: state.updateSummarizeTabState, - clearSummarizeTabState: state.clearSummarizeTabState, - clearAllSummarizeStates: state.clearAllSummarizeStates, - setMergeTabState: state.setMergeTabState, - updateMergeTabState: state.updateMergeTabState, - clearMergeTabState: state.clearMergeTabState, - clearAllMergeStates: state.clearAllMergeStates, - setGlobalMergeInProgress: state.setGlobalMergeInProgress, - setTransferState: state.setTransferState, - resetTransferState: state.resetTransferState, - setGlobalTransferInProgress: state.setGlobalTransferInProgress, - resetAll: state.resetAll, - }; -} diff --git a/src/renderer/stores/sessionStore.ts b/src/renderer/stores/sessionStore.ts index c4d0d27e95..2f60873f0b 100644 --- a/src/renderer/stores/sessionStore.ts +++ b/src/renderer/stores/sessionStore.ts @@ -350,69 +350,6 @@ export const selectSessionById = (state: SessionStore): Session | undefined => state.sessions.find((s) => s.id === id); -/** - * Select all bookmarked sessions. - * - * @example - * const bookmarked = useSessionStore(selectBookmarkedSessions); - */ -export const selectBookmarkedSessions = (state: SessionStore): Session[] => - state.sessions.filter((s) => s.bookmarked); - -/** - * Select sessions belonging to a specific group. - * - * @example - * const groupSessions = useSessionStore(selectSessionsByGroup('group-1')); - */ -export const selectSessionsByGroup = - (groupId: string) => - (state: SessionStore): Session[] => - state.sessions.filter((s) => s.groupId === groupId); - -/** - * Select ungrouped sessions (no groupId set). - * - * @example - * const ungrouped = useSessionStore(selectUngroupedSessions); - */ -export const selectUngroupedSessions = (state: SessionStore): Session[] => - state.sessions.filter((s) => !s.groupId && !s.parentSessionId); - -/** - * Select a group by ID. - * - * @example - * const group = useSessionStore(selectGroupById('group-1')); - */ -export const selectGroupById = - (id: string) => - (state: SessionStore): Group | undefined => - state.groups.find((g) => g.id === id); - -/** - * Select session count. - * - * @example - * const count = useSessionStore(selectSessionCount); - */ -export const selectSessionCount = (state: SessionStore): number => state.sessions.length; - -/** - * Select whether initial load is complete (sessions loaded from disk). - * - * @example - * const ready = useSessionStore(selectIsReady); - */ -export const selectIsReady = (state: SessionStore): boolean => - state.sessionsLoaded && state.initialLoadComplete; - -/** - * Select whether any session is currently busy (agent actively processing). - * - * @example - * const anyBusy = useSessionStore(selectIsAnySessionBusy); - */ export const selectIsAnySessionBusy = (state: SessionStore): boolean => state.sessions.some((s) => s.state === 'busy'); @@ -420,17 +357,6 @@ export const selectIsAnySessionBusy = (state: SessionStore): boolean => // Non-React Access // ============================================================================ -/** - * Get current session store state outside React. - * Replaces sessionsRef.current, groupsRef.current, activeSessionIdRef.current. - * - * @example - * const { sessions, activeSessionId } = getSessionState(); - */ -export function getSessionState() { - return useSessionStore.getState(); -} - /** * Update a session by ID using a mapper function. * Convenience helper for call sites that need a full session → session transform @@ -471,36 +397,3 @@ export function updateAiTab( }) ); } - -/** - * Get stable action references outside React. - * These never change, so they're safe to call from anywhere. - * - * @example - * const { setSessions, setActiveSessionId } = getSessionActions(); - */ -export function getSessionActions() { - const state = useSessionStore.getState(); - return { - setSessions: state.setSessions, - addSession: state.addSession, - removeSession: state.removeSession, - updateSession: state.updateSession, - setActiveSessionId: state.setActiveSessionId, - setActiveSessionIdInternal: state.setActiveSessionIdInternal, - setGroups: state.setGroups, - addGroup: state.addGroup, - removeGroup: state.removeGroup, - updateGroup: state.updateGroup, - toggleGroupCollapsed: state.toggleGroupCollapsed, - setSessionsLoaded: state.setSessionsLoaded, - setInitialLoadComplete: state.setInitialLoadComplete, - setInitialFileTreeReady: state.setInitialFileTreeReady, - toggleBookmark: state.toggleBookmark, - addRemovedWorktreePath: state.addRemovedWorktreePath, - setRemovedWorktreePaths: state.setRemovedWorktreePaths, - setCyclePosition: state.setCyclePosition, - resetCyclePosition: state.resetCyclePosition, - addLogToTab: state.addLogToTab, - }; -} diff --git a/src/renderer/stores/settingsStore.ts b/src/renderer/stores/settingsStore.ts index 9d1a96fa29..60b0b63faa 100644 --- a/src/renderer/stores/settingsStore.ts +++ b/src/renderer/stores/settingsStore.ts @@ -8,7 +8,7 @@ * Key advantages: * - Selector-based subscriptions: components only re-render when their slice changes * - No refs needed: store.getState() gives current state synchronously - * - Works outside React: services can read/write via getSettingsState()/getSettingsActions() + * - Works outside React: services can read/write via useSettingsStore.getState() * - Single batch load on startup eliminates ~60 individual IPC calls * * Can be used outside React via useSettingsStore.getState() / useSettingsStore.setState(). @@ -106,7 +106,7 @@ const DOCUMENT_GRAPH_LAYOUT_TYPES: DocumentGraphLayoutType[] = ['mindmap', 'radi /** Default local ignore patterns for new installations (includes .git, node_modules, __pycache__) */ export const DEFAULT_LOCAL_IGNORE_PATTERNS = ['.git', 'node_modules', '__pycache__']; -export const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS: ContextManagementSettings = { +const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS: ContextManagementSettings = { autoGroomContexts: true, maxContextTokens: 100000, showMergePreview: true, @@ -117,7 +117,7 @@ export const DEFAULT_CONTEXT_MANAGEMENT_SETTINGS: ContextManagementSettings = { contextWarningRedThreshold: 90, }; -export const DEFAULT_AUTO_RUN_STATS: AutoRunStats = { +const DEFAULT_AUTO_RUN_STATS: AutoRunStats = { cumulativeTimeMs: 0, longestRunMs: 0, longestRunTimestamp: 0, @@ -128,7 +128,7 @@ export const DEFAULT_AUTO_RUN_STATS: AutoRunStats = { badgeHistory: [], }; -export const DEFAULT_USAGE_STATS: MaestroUsageStats = { +const DEFAULT_USAGE_STATS: MaestroUsageStats = { maxAgents: 0, maxDefinedAgents: 0, maxSimultaneousAutoRuns: 0, @@ -136,7 +136,7 @@ export const DEFAULT_USAGE_STATS: MaestroUsageStats = { maxQueueDepth: 0, }; -export const DEFAULT_KEYBOARD_MASTERY_STATS: KeyboardMasteryStats = { +const DEFAULT_KEYBOARD_MASTERY_STATS: KeyboardMasteryStats = { usedShortcuts: [], currentLevel: 0, lastLevelUpTimestamp: 0, @@ -148,7 +148,7 @@ const TOTAL_SHORTCUTS_COUNT = Object.keys(TAB_SHORTCUTS).length + Object.keys(FIXED_SHORTCUTS).length; -export const DEFAULT_ONBOARDING_STATS: OnboardingStats = { +const DEFAULT_ONBOARDING_STATS: OnboardingStats = { wizardStartCount: 0, wizardCompletionCount: 0, wizardAbandonCount: 0, @@ -170,20 +170,20 @@ export const DEFAULT_ONBOARDING_STATS: OnboardingStats = { averageTasksPerPhase: 0, }; -export const DEFAULT_ENCORE_FEATURES: EncoreFeatureFlags = { +const DEFAULT_ENCORE_FEATURES: EncoreFeatureFlags = { directorNotes: false, usageStats: true, symphony: true, maestroCue: false, }; -export const DEFAULT_DIRECTOR_NOTES_SETTINGS: DirectorNotesSettings = { +const DEFAULT_DIRECTOR_NOTES_SETTINGS: DirectorNotesSettings = { provider: 'claude-code', defaultLookbackDays: 7, }; // Uses `let` so the binding updates after loadSettingsStorePrompts() populates the cache -export let DEFAULT_AI_COMMANDS: CustomAICommand[] = [ +let DEFAULT_AI_COMMANDS: CustomAICommand[] = [ { id: 'commit', command: '/commit', @@ -197,7 +197,7 @@ export let DEFAULT_AI_COMMANDS: CustomAICommand[] = [ // Helper Functions // ============================================================================ -export function getBadgeLevelForTime(cumulativeTimeMs: number): number { +function getBadgeLevelForTime(cumulativeTimeMs: number): number { const MINUTE = 60 * 1000; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; @@ -1986,121 +1986,3 @@ export async function loadAllSettings(): Promise { useSettingsStore.setState({ settingsLoaded: true }); } } - -// ============================================================================ -// Non-React Access -// ============================================================================ - -export function getSettingsState(): SettingsStoreState { - return useSettingsStore.getState(); -} - -export function getSettingsActions() { - const state = useSettingsStore.getState(); - return { - setConductorProfile: state.setConductorProfile, - setLlmProvider: state.setLlmProvider, - setModelSlug: state.setModelSlug, - setApiKey: state.setApiKey, - setDefaultShell: state.setDefaultShell, - setCustomShellPath: state.setCustomShellPath, - setShellArgs: state.setShellArgs, - setShellEnvVars: state.setShellEnvVars, - setGhPath: state.setGhPath, - setFontFamily: state.setFontFamily, - setFontSize: state.setFontSize, - setActiveThemeId: state.setActiveThemeId, - setCustomThemeColors: state.setCustomThemeColors, - setCustomThemeBaseId: state.setCustomThemeBaseId, - setEnterToSendAI: state.setEnterToSendAI, - setForcedParallelExecution: state.setForcedParallelExecution, - setForcedParallelAcknowledged: state.setForcedParallelAcknowledged, - setDefaultSaveToHistory: state.setDefaultSaveToHistory, - setDefaultShowThinking: state.setDefaultShowThinking, - setLeftSidebarWidth: state.setLeftSidebarWidth, - setRightPanelWidth: state.setRightPanelWidth, - setMarkdownEditMode: state.setMarkdownEditMode, - setChatRawTextMode: state.setChatRawTextMode, - setShowHiddenFiles: state.setShowHiddenFiles, - setFileExplorerIconTheme: state.setFileExplorerIconTheme, - setTerminalWidth: state.setTerminalWidth, - setLogLevel: state.setLogLevel, - setMaxLogBuffer: state.setMaxLogBuffer, - setMaxOutputLines: state.setMaxOutputLines, - setOsNotificationsEnabled: state.setOsNotificationsEnabled, - setAudioFeedbackEnabled: state.setAudioFeedbackEnabled, - setAudioFeedbackCommand: state.setAudioFeedbackCommand, - setToastDuration: state.setToastDuration, - setIdleNotificationEnabled: state.setIdleNotificationEnabled, - setIdleNotificationCommand: state.setIdleNotificationCommand, - setCheckForUpdatesOnStartup: state.setCheckForUpdatesOnStartup, - setEnableBetaUpdates: state.setEnableBetaUpdates, - setCrashReportingEnabled: state.setCrashReportingEnabled, - setLogViewerSelectedLevels: state.setLogViewerSelectedLevels, - setShortcuts: state.setShortcuts, - setTabShortcuts: state.setTabShortcuts, - setCustomAICommands: state.setCustomAICommands, - setTotalActiveTimeMs: state.setTotalActiveTimeMs, - addTotalActiveTimeMs: state.addTotalActiveTimeMs, - setAutoRunStats: state.setAutoRunStats, - recordAutoRunComplete: state.recordAutoRunComplete, - updateAutoRunProgress: state.updateAutoRunProgress, - acknowledgeBadge: state.acknowledgeBadge, - getUnacknowledgedBadgeLevel: state.getUnacknowledgedBadgeLevel, - setUsageStats: state.setUsageStats, - updateUsageStats: state.updateUsageStats, - setUngroupedCollapsed: state.setUngroupedCollapsed, - setTourCompleted: state.setTourCompleted, - setFirstAutoRunCompleted: state.setFirstAutoRunCompleted, - setOnboardingStats: state.setOnboardingStats, - recordWizardStart: state.recordWizardStart, - recordWizardComplete: state.recordWizardComplete, - recordWizardAbandon: state.recordWizardAbandon, - recordWizardResume: state.recordWizardResume, - recordTourStart: state.recordTourStart, - recordTourComplete: state.recordTourComplete, - recordTourSkip: state.recordTourSkip, - getOnboardingAnalytics: state.getOnboardingAnalytics, - setLeaderboardRegistration: state.setLeaderboardRegistration, - setPersistentWebLink: state.setPersistentWebLink, - setWebInterfaceUseCustomPort: state.setWebInterfaceUseCustomPort, - setWebInterfaceCustomPort: state.setWebInterfaceCustomPort, - setContextManagementSettings: state.setContextManagementSettings, - updateContextManagementSettings: state.updateContextManagementSettings, - setKeyboardMasteryStats: state.setKeyboardMasteryStats, - recordShortcutUsage: state.recordShortcutUsage, - acknowledgeKeyboardMasteryLevel: state.acknowledgeKeyboardMasteryLevel, - getUnacknowledgedKeyboardMasteryLevel: state.getUnacknowledgedKeyboardMasteryLevel, - setColorBlindMode: state.setColorBlindMode, - setShowStarredInUnreadFilter: state.setShowStarredInUnreadFilter, - setShowFilePreviewsInUnreadFilter: state.setShowFilePreviewsInUnreadFilter, - setDocumentGraphShowExternalLinks: state.setDocumentGraphShowExternalLinks, - setDocumentGraphMaxNodes: state.setDocumentGraphMaxNodes, - setDocumentGraphPreviewCharLimit: state.setDocumentGraphPreviewCharLimit, - setDocumentGraphLayoutType: state.setDocumentGraphLayoutType, - setStatsCollectionEnabled: state.setStatsCollectionEnabled, - setDefaultStatsTimeRange: state.setDefaultStatsTimeRange, - setPreventSleepEnabled: state.setPreventSleepEnabled, - setDisableGpuAcceleration: state.setDisableGpuAcceleration, - setDisableConfetti: state.setDisableConfetti, - setLocalIgnorePatterns: state.setLocalIgnorePatterns, - setLocalHonorGitignore: state.setLocalHonorGitignore, - setSshRemoteIgnorePatterns: state.setSshRemoteIgnorePatterns, - setSshRemoteHonorGitignore: state.setSshRemoteHonorGitignore, - setUseSystemBrowser: state.setUseSystemBrowser, - setBrowserHomeUrl: state.setBrowserHomeUrl, - setAutomaticTabNamingEnabled: state.setAutomaticTabNamingEnabled, - setFileTabAutoRefreshEnabled: state.setFileTabAutoRefreshEnabled, - setSuppressWindowsWarning: state.setSuppressWindowsWarning, - setEncoreFeatures: state.setEncoreFeatures, - setSymphonyRegistryUrls: state.setSymphonyRegistryUrls, - setDirectorNotesSettings: state.setDirectorNotesSettings, - setWakatimeApiKey: state.setWakatimeApiKey, - setWakatimeEnabled: state.setWakatimeEnabled, - setWakatimeDetailedTracking: state.setWakatimeDetailedTracking, - setUseNativeTitleBar: state.setUseNativeTitleBar, - setAutoHideMenuBar: state.setAutoHideMenuBar, - setModeratorStandingInstructions: state.setModeratorStandingInstructions, - setAutoRunDisabled: state.setAutoRunDisabled, - }; -} diff --git a/src/renderer/stores/tabStore.ts b/src/renderer/stores/tabStore.ts index 24e2d87dcc..6af01c51d9 100644 --- a/src/renderer/stores/tabStore.ts +++ b/src/renderer/stores/tabStore.ts @@ -25,7 +25,7 @@ */ import { create } from 'zustand'; -import type { AITab, FilePreviewTab, UnifiedTab, TerminalTab, Session } from '../types'; +import type { AITab, FilePreviewTab, Session } from '../types'; import type { GistInfo } from '../components/GistPublishModal'; import { createTab as createTabHelper, @@ -33,12 +33,10 @@ import { closeFileTab as closeFileTabHelper, reopenUnifiedClosedTab as reopenUnifiedClosedTabHelper, setActiveTab as setActiveTabHelper, - getActiveTab, navigateToNextUnifiedTab as navigateToNextHelper, navigateToPrevUnifiedTab as navigateToPrevHelper, navigateToUnifiedTabByIndex as navigateToIndexHelper, navigateToLastUnifiedTab as navigateToLastHelper, - buildUnifiedTabs, type CreateTabOptions, type CreateTabResult, type CloseTabOptions, @@ -529,204 +527,3 @@ export const useTabStore = create()((set) => ({ updateFileTab(tabId, { editMode: !tab.editMode }); }, })); - -// ============================================================================ -// Selectors (derive from sessionStore) -// ============================================================================ - -/** - * Select the active AI tab from the active session. - * Use with useSessionStore: `useSessionStore(selectActiveTab)` - * - * @example - * const activeTab = useSessionStore(selectActiveTab); - */ -export const selectActiveTab = ( - state: ReturnType -): AITab | undefined => { - const session = selectActiveSession(state); - return session ? getActiveTab(session) : undefined; -}; - -/** - * Select the active file preview tab from the active session. - * Use with useSessionStore: `useSessionStore(selectActiveFileTab)` - * - * @example - * const activeFileTab = useSessionStore(selectActiveFileTab); - */ -export const selectActiveFileTab = ( - state: ReturnType -): FilePreviewTab | undefined => { - const session = selectActiveSession(state); - if (!session || !session.activeFileTabId) return undefined; - return session.filePreviewTabs.find((t) => t.id === session.activeFileTabId); -}; - -/** - * Select unified tabs (AI + file) in order for the active session. - * Use with useSessionStore: `useSessionStore(selectUnifiedTabs)` - * - * @example - * const unifiedTabs = useSessionStore(selectUnifiedTabs); - */ -export const selectUnifiedTabs = ( - state: ReturnType -): UnifiedTab[] => { - const session = selectActiveSession(state); - if (!session) return []; - return buildUnifiedTabs(session); -}; - -/** - * Select a specific AI tab by ID from the active session. - * - * @example - * const tab = useSessionStore(selectTabById('tab-123')); - */ -export const selectTabById = - (tabId: string) => - (state: ReturnType): AITab | undefined => { - const session = selectActiveSession(state); - return session?.aiTabs.find((t) => t.id === tabId); - }; - -/** - * Select a specific file preview tab by ID from the active session. - * - * @example - * const fileTab = useSessionStore(selectFileTabById('file-tab-123')); - */ -export const selectFileTabById = - (tabId: string) => - (state: ReturnType): FilePreviewTab | undefined => { - const session = selectActiveSession(state); - return session?.filePreviewTabs.find((t) => t.id === tabId); - }; - -/** - * Select the count of AI tabs in the active session. - * - * @example - * const tabCount = useSessionStore(selectTabCount); - */ -export const selectTabCount = (state: ReturnType): number => { - const session = selectActiveSession(state); - return session?.aiTabs.length ?? 0; -}; - -/** - * Select all AI tabs in the active session. - * - * @example - * const tabs = useSessionStore(selectAllTabs); - */ -export const selectAllTabs = (state: ReturnType): AITab[] => { - const session = selectActiveSession(state); - return session?.aiTabs ?? []; -}; - -/** - * Select all file preview tabs in the active session. - * - * @example - * const fileTabs = useSessionStore(selectAllFileTabs); - */ -export const selectAllFileTabs = ( - state: ReturnType -): FilePreviewTab[] => { - const session = selectActiveSession(state); - return session?.filePreviewTabs ?? []; -}; - -/** - * Select the active terminal tab from the active session. - * Use with useSessionStore: `useSessionStore(selectActiveTerminalTab)` - * - * @example - * const activeTerminalTab = useSessionStore(selectActiveTerminalTab); - */ -export const selectActiveTerminalTab = ( - state: ReturnType -): TerminalTab | undefined => { - const session = selectActiveSession(state); - if (!session || !session.activeTerminalTabId) return undefined; - return session.terminalTabs?.find((t) => t.id === session.activeTerminalTabId); -}; - -/** - * Select all terminal tabs in the active session. - * Use with useSessionStore: `useSessionStore(selectTerminalTabs)` - * - * @example - * const terminalTabs = useSessionStore(selectTerminalTabs); - */ -export const selectTerminalTabs = ( - state: ReturnType -): TerminalTab[] => { - const session = selectActiveSession(state); - return session?.terminalTabs ?? []; -}; - -// ============================================================================ -// Non-React Access -// ============================================================================ - -/** - * Get current tab store state outside React. - * - * @example - * const { tabGistContent, fileGistUrls } = getTabState(); - */ -export function getTabState() { - return useTabStore.getState(); -} - -/** - * Get stable tab action references outside React. - * - * @example - * const { createTab, closeTab, selectTab } = getTabActions(); - */ -export function getTabActions() { - const state = useTabStore.getState(); - return { - // Gist state - setTabGistContent: state.setTabGistContent, - setFileGistUrls: state.setFileGistUrls, - setFileGistUrl: state.setFileGistUrl, - clearFileGistUrl: state.clearFileGistUrl, - // Tab CRUD - createTab: state.createTab, - closeTab: state.closeTab, - closeFileTab: state.closeFileTab, - reopenClosedTab: state.reopenClosedTab, - // Tab navigation - selectTab: state.selectTab, - selectFileTab: state.selectFileTab, - navigateToNext: state.navigateToNext, - navigateToPrev: state.navigateToPrev, - navigateToIndex: state.navigateToIndex, - navigateToLast: state.navigateToLast, - // Tab metadata - starTab: state.starTab, - markUnread: state.markUnread, - updateTabName: state.updateTabName, - toggleReadOnly: state.toggleReadOnly, - toggleSaveToHistory: state.toggleSaveToHistory, - cycleThinkingMode: state.cycleThinkingMode, - // Tab reordering - reorderTabs: state.reorderTabs, - reorderUnifiedTabs: state.reorderUnifiedTabs, - // File tab operations - updateFileTabEditContent: state.updateFileTabEditContent, - updateFileTabScrollPosition: state.updateFileTabScrollPosition, - updateFileTabSearchQuery: state.updateFileTabSearchQuery, - toggleFileTabEditMode: state.toggleFileTabEditMode, - // Terminal tab CRUD - createTerminalTab: state.createTerminalTab, - closeTerminalTab: state.closeTerminalTab, - selectTerminalTab: state.selectTerminalTab, - renameTerminalTab: state.renameTerminalTab, - }; -}