diff --git a/src/__tests__/renderer/hooks/useAutoRunAutoFollow.test.ts b/src/__tests__/renderer/hooks/useAutoRunAutoFollow.test.ts new file mode 100644 index 000000000..1e804268a --- /dev/null +++ b/src/__tests__/renderer/hooks/useAutoRunAutoFollow.test.ts @@ -0,0 +1,358 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useAutoRunAutoFollow } from '../../../renderer/hooks/batch/useAutoRunAutoFollow'; +import type { UseAutoRunAutoFollowDeps } from '../../../renderer/hooks/batch/useAutoRunAutoFollow'; +import type { BatchRunState } from '../../../renderer/types'; +import { useUIStore } from '../../../renderer/stores/uiStore'; + +function createBatchState(overrides: Partial = {}): BatchRunState { + return { + isRunning: false, + isStopping: false, + documents: [], + lockedDocuments: [], + currentDocumentIndex: 0, + currentDocTasksTotal: 0, + currentDocTasksCompleted: 0, + totalTasksAcrossAllDocs: 0, + completedTasksAcrossAllDocs: 0, + loopEnabled: false, + loopIteration: 0, + folderPath: '/tmp', + worktreeActive: false, + totalTasks: 0, + completedTasks: 0, + currentTaskIndex: 0, + originalContent: '', + sessionIds: [], + ...overrides, + }; +} + +function createDeps(overrides: Partial = {}): UseAutoRunAutoFollowDeps { + return { + currentSessionBatchState: null, + onAutoRunSelectDocument: vi.fn(), + selectedFile: null, + setActiveRightTab: vi.fn(), + rightPanelOpen: true, + setRightPanelOpen: vi.fn(), + onAutoRunModeChange: vi.fn(), + currentMode: 'preview', + ...overrides, + }; +} + +describe('useAutoRunAutoFollow', () => { + beforeEach(() => { + // Reset the zustand store between tests + useUIStore.setState({ autoFollowEnabled: false }); + }); + + it('should not auto-select when autoFollowEnabled is false', () => { + const onAutoRunSelectDocument = vi.fn(); + const deps = createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b'], + currentDocumentIndex: 0, + }), + }); + + renderHook(() => useAutoRunAutoFollow(deps)); + + expect(onAutoRunSelectDocument).not.toHaveBeenCalled(); + }); + + it('should auto-select document when batch starts and autoFollow is enabled', () => { + const onAutoRunSelectDocument = vi.fn(); + const deps = createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: null, + }); + + const { result, rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + // Enable auto-follow + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + // Simulate batch start + const runningDeps = createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b'], + currentDocumentIndex: 0, + }), + }); + + rerender(runningDeps); + + expect(onAutoRunSelectDocument).toHaveBeenCalledWith('doc-a'); + }); + + it('should auto-select next document on index change', () => { + const onAutoRunSelectDocument = vi.fn(); + const initialDeps = createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b'], + currentDocumentIndex: 0, + }), + }); + + const { result, rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: initialDeps } + ); + + // Enable auto-follow + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + // Move to next document + const nextDeps = createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b'], + currentDocumentIndex: 1, + }), + }); + + rerender(nextDeps); + + expect(onAutoRunSelectDocument).toHaveBeenCalledWith('doc-b'); + }); + + it('should not auto-select if already on correct document', () => { + const onAutoRunSelectDocument = vi.fn(); + const deps = createDeps({ + onAutoRunSelectDocument, + selectedFile: 'doc-a', + currentSessionBatchState: null, + }); + + const { result, rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + // Enable auto-follow + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + // Start batch with doc-a at index 0, but selectedFile is already doc-a + const runningDeps = createDeps({ + onAutoRunSelectDocument, + selectedFile: 'doc-a', + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b'], + currentDocumentIndex: 0, + }), + }); + + rerender(runningDeps); + + expect(onAutoRunSelectDocument).not.toHaveBeenCalled(); + }); + + it('should switch to autorun tab on batch start when autoFollow enabled', () => { + const setActiveRightTab = vi.fn(); + const deps = createDeps({ + setActiveRightTab, + currentSessionBatchState: createBatchState({ isRunning: false }), + }); + + const { result, rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + // Enable auto-follow + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + // Transition to running + const runningDeps = createDeps({ + setActiveRightTab, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a'], + currentDocumentIndex: 0, + }), + }); + + rerender(runningDeps); + + expect(setActiveRightTab).toHaveBeenCalledWith('autorun'); + }); + + it('should not switch tab when autoFollow is disabled', () => { + const setActiveRightTab = vi.fn(); + const deps = createDeps({ + setActiveRightTab, + currentSessionBatchState: createBatchState({ isRunning: false }), + }); + + const { rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + // Leave auto-follow off, transition to running + const runningDeps = createDeps({ + setActiveRightTab, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a'], + currentDocumentIndex: 0, + }), + }); + + rerender(runningDeps); + + expect(setActiveRightTab).not.toHaveBeenCalled(); + }); + + it('should open right panel on batch start when closed and autoFollow enabled', () => { + const setRightPanelOpen = vi.fn(); + const deps = createDeps({ + rightPanelOpen: false, + setRightPanelOpen, + currentSessionBatchState: createBatchState({ isRunning: false }), + }); + + const { result, rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + // Enable auto-follow + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + // Transition to running with panel closed + const runningDeps = createDeps({ + rightPanelOpen: false, + setRightPanelOpen, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a'], + currentDocumentIndex: 0, + }), + }); + + rerender(runningDeps); + + expect(setRightPanelOpen).toHaveBeenCalledWith(true); + }); + + it('should immediately jump to active document when enabling during a running batch', () => { + const onAutoRunSelectDocument = vi.fn(); + const setActiveRightTab = vi.fn(); + const setRightPanelOpen = vi.fn(); + const deps = createDeps({ + onAutoRunSelectDocument, + setActiveRightTab, + rightPanelOpen: false, + setRightPanelOpen, + currentMode: 'edit', + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b'], + currentDocumentIndex: 1, + }), + }); + + const { result } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + expect(onAutoRunSelectDocument).toHaveBeenCalledWith('doc-b'); + expect(setActiveRightTab).toHaveBeenCalledWith('autorun'); + expect(setRightPanelOpen).toHaveBeenCalledWith(true); + }); + + it('should reset refs when batch ends', () => { + const onAutoRunSelectDocument = vi.fn(); + const deps = createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b', 'doc-c'], + currentDocumentIndex: 0, + }), + }); + + const { result, rerender } = renderHook( + (props: UseAutoRunAutoFollowDeps) => useAutoRunAutoFollow(props), + { initialProps: deps } + ); + + // Enable auto-follow + act(() => { + result.current.setAutoFollowEnabled(true); + }); + + // Advance to index 2 + rerender( + createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-a', 'doc-b', 'doc-c'], + currentDocumentIndex: 2, + }), + }) + ); + + expect(onAutoRunSelectDocument).toHaveBeenCalledWith('doc-c'); + onAutoRunSelectDocument.mockClear(); + + // End batch + rerender( + createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: false, + documents: [], + currentDocumentIndex: 0, + }), + }) + ); + + onAutoRunSelectDocument.mockClear(); + + // Start new batch from index 0 — should auto-select again + rerender( + createDeps({ + onAutoRunSelectDocument, + currentSessionBatchState: createBatchState({ + isRunning: true, + documents: ['doc-x', 'doc-y'], + currentDocumentIndex: 0, + }), + }) + ); + + expect(onAutoRunSelectDocument).toHaveBeenCalledWith('doc-x'); + }); +}); diff --git a/src/renderer/components/AutoRun.tsx b/src/renderer/components/AutoRun.tsx index 706b19bce..3e652ec98 100644 --- a/src/renderer/components/AutoRun.tsx +++ b/src/renderer/components/AutoRun.tsx @@ -113,6 +113,9 @@ interface AutoRunProps { batchRunState?: BatchRunState; onOpenBatchRunner?: () => void; onStopBatchRun?: (sessionId?: string) => void; + + // Auto-follow: when enabled during a batch run, suppresses focus-stealing and scrolls to active task + autoFollowEnabled?: boolean; // Error handling callbacks (Phase 5.10) onSkipCurrentDocument?: () => void; onAbortBatchOnError?: () => void; @@ -482,6 +485,7 @@ const AutoRunInner = forwardRef(function AutoRunInn batchRunState, onOpenBatchRunner, onStopBatchRun, + autoFollowEnabled, // Error handling callbacks (Phase 5.10) onSkipCurrentDocument: _onSkipCurrentDocument, onAbortBatchOnError, @@ -939,12 +943,15 @@ const AutoRunInner = forwardRef(function AutoRunInn // Auto-focus the active element after mode change useEffect(() => { + // Skip focus when auto-follow is driving changes during a batch run + if (autoFollowEnabled && batchRunState?.isRunning) return; + if (mode === 'edit' && textareaRef.current) { textareaRef.current.focus(); } else if (mode === 'preview' && previewRef.current) { previewRef.current.focus(); } - }, [mode]); + }, [mode, autoFollowEnabled, batchRunState?.isRunning]); // Handle document selection change - focus the appropriate element // Note: Content syncing and editing state reset is handled by the main sync effect above @@ -957,6 +964,9 @@ const AutoRunInner = forwardRef(function AutoRunInn prevFocusSelectedFileRef.current = selectedFile; if (isNewDocument) { + // Skip focus when auto-follow is driving changes during a batch run + if (autoFollowEnabled && batchRunState?.isRunning) return; + // Focus on document change requestAnimationFrame(() => { if (mode === 'edit' && textareaRef.current) { @@ -966,7 +976,39 @@ const AutoRunInner = forwardRef(function AutoRunInn } }); } - }, [selectedFile, mode]); + }, [selectedFile, mode, autoFollowEnabled, batchRunState?.isRunning]); + + // Auto-follow: scroll to the first unchecked task when batch is running + useEffect(() => { + if (!autoFollowEnabled || !batchRunState?.isRunning || mode !== 'preview') return; + + const timeout = setTimeout(() => { + // Wait for React to commit new content before querying the DOM + requestAnimationFrame(() => { + if (!previewRef.current) return; + + const checkboxes = previewRef.current.querySelectorAll('input[type="checkbox"]'); + if (checkboxes.length === 0) return; + for (const checkbox of checkboxes) { + if (!(checkbox as HTMLInputElement).checked) { + const li = (checkbox as HTMLElement).closest('li'); + if (li) { + li.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + break; + } + } + }); + }, 150); + + return () => clearTimeout(timeout); + }, [ + batchRunState?.currentDocumentIndex, + batchRunState?.currentTaskIndex, + batchRunState?.isRunning, + autoFollowEnabled, + mode, + ]); // Debounced preview scroll handler to avoid triggering re-renders on every scroll event // We only save scroll position to ref immediately (for local use), but delay parent notification @@ -2231,7 +2273,9 @@ export const AutoRun = memo(AutoRunInner, (prevProps, nextProps) => { // UI control props prevProps.hideTopControls === nextProps.hideTopControls && // External change detection - prevProps.contentVersion === nextProps.contentVersion + prevProps.contentVersion === nextProps.contentVersion && + // Auto-follow state + prevProps.autoFollowEnabled === nextProps.autoFollowEnabled // Note: initialCursorPosition, initialEditScrollPos, initialPreviewScrollPos // are intentionally NOT compared - they're only used on mount // Note: documentTree is derived from documentList, comparing documentList is sufficient diff --git a/src/renderer/components/BatchRunnerModal.tsx b/src/renderer/components/BatchRunnerModal.tsx index e1ff1bef9..35875f743 100644 --- a/src/renderer/components/BatchRunnerModal.tsx +++ b/src/renderer/components/BatchRunnerModal.tsx @@ -25,6 +25,7 @@ import { AgentPromptComposerModal } from './AgentPromptComposerModal'; import { DocumentsPanel } from './DocumentsPanel'; import { WorktreeRunSection } from './WorktreeRunSection'; import { useSessionStore } from '../stores/sessionStore'; +import { useUIStore } from '../stores/uiStore'; import { getModalActions } from '../stores/modalStore'; import { usePlaybookManagement, @@ -100,6 +101,10 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { onOpenMarketplace, } = props; + // Auto-follow state (read/write directly from store to avoid stale local copy) + const autoFollowEnabled = useUIStore((s) => s.autoFollowEnabled); + const setAutoFollowEnabled = useUIStore((s) => s.setAutoFollowEnabled); + // Worktree run target state const [worktreeTarget, setWorktreeTarget] = useState(null); const [isPreparingWorktree, setIsPreparingWorktree] = useState(false); @@ -811,15 +816,35 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { className="p-4 border-t flex items-center justify-between shrink-0" style={{ borderColor: theme.colors.border }} > - {/* Left side: Hint */} -
- + +
- {formatMetaKey()} + Drag - - to copy document + + {formatMetaKey()} + Drag + + to copy document +
{/* Right side: Buttons */} diff --git a/src/renderer/components/RightPanel.tsx b/src/renderer/components/RightPanel.tsx index 5ac2d9d4d..e66ffb6ed 100644 --- a/src/renderer/components/RightPanel.tsx +++ b/src/renderer/components/RightPanel.tsx @@ -24,6 +24,7 @@ import { AutoRunExpandedModal } from './AutoRunExpandedModal'; import { formatShortcutKeys } from '../utils/shortcutFormatter'; import { ConfirmModal } from './ConfirmModal'; import { useResizablePanel } from '../hooks'; +import { useAutoRunAutoFollow } from '../hooks/batch/useAutoRunAutoFollow'; import { useUIStore } from '../stores/uiStore'; import { useSettingsStore } from '../stores/settingsStore'; import { useFileExplorerStore } from '../stores/fileExplorerStore'; @@ -235,6 +236,18 @@ export const RightPanel = memo( } }, [autoRunContent, autoRunContentVersion, session?.id, session?.autoRunSelectedFile]); + // Auto-follow: automatically select the active document during batch runs + const { autoFollowEnabled, setAutoFollowEnabled } = useAutoRunAutoFollow({ + currentSessionBatchState, + onAutoRunSelectDocument, + selectedFile: session?.autoRunSelectedFile ?? null, + setActiveRightTab, + rightPanelOpen, + setRightPanelOpen, + onAutoRunModeChange, + currentMode: session?.autoRunMode, + }); + // Expanded modal state for Auto Run const [autoRunExpanded, setAutoRunExpanded] = useState(false); const handleExpandAutoRun = useCallback(() => setAutoRunExpanded(true), []); @@ -375,6 +388,7 @@ export const RightPanel = memo( onOpenMarketplace, onLaunchWizard, onShowFlash, + autoFollowEnabled, }; return ( @@ -730,6 +744,20 @@ export const RightPanel = memo( )} +
+ +
)} diff --git a/src/renderer/hooks/batch/index.ts b/src/renderer/hooks/batch/index.ts index 0bc5e3015..1040251df 100644 --- a/src/renderer/hooks/batch/index.ts +++ b/src/renderer/hooks/batch/index.ts @@ -142,5 +142,9 @@ export type { UseAutoRunAchievementsDeps } from './useAutoRunAchievements'; export { useAutoRunDocumentLoader } from './useAutoRunDocumentLoader'; export type { UseAutoRunDocumentLoaderReturn } from './useAutoRunDocumentLoader'; +// Auto Run auto-follow (document tracking during batch runs) +export { useAutoRunAutoFollow } from './useAutoRunAutoFollow'; +export type { UseAutoRunAutoFollowDeps, UseAutoRunAutoFollowReturn } from './useAutoRunAutoFollow'; + // Re-export ExistingDocument type from existingDocsDetector for convenience export type { ExistingDocument } from '../../utils/existingDocsDetector'; diff --git a/src/renderer/hooks/batch/useAutoRunAutoFollow.ts b/src/renderer/hooks/batch/useAutoRunAutoFollow.ts new file mode 100644 index 000000000..077a3cd68 --- /dev/null +++ b/src/renderer/hooks/batch/useAutoRunAutoFollow.ts @@ -0,0 +1,123 @@ +import { useRef, useEffect, useCallback } from 'react'; +import type { BatchRunState, RightPanelTab } from '../../types'; +import { useUIStore } from '../../stores/uiStore'; + +export interface UseAutoRunAutoFollowDeps { + currentSessionBatchState: BatchRunState | null | undefined; + onAutoRunSelectDocument: (filename: string) => void | Promise; + selectedFile: string | null; + setActiveRightTab: (tab: RightPanelTab) => void; + rightPanelOpen: boolean; + setRightPanelOpen?: (open: boolean) => void; + onAutoRunModeChange?: (mode: 'edit' | 'preview') => void; + currentMode?: 'edit' | 'preview'; +} + +export interface UseAutoRunAutoFollowReturn { + autoFollowEnabled: boolean; + setAutoFollowEnabled: (enabled: boolean) => void; +} + +export function useAutoRunAutoFollow(deps: UseAutoRunAutoFollowDeps): UseAutoRunAutoFollowReturn { + const { + currentSessionBatchState, + onAutoRunSelectDocument, + selectedFile, + setActiveRightTab, + rightPanelOpen, + setRightPanelOpen, + onAutoRunModeChange, + currentMode, + } = deps; + + const autoFollowEnabled = useUIStore((s) => s.autoFollowEnabled); + const setAutoFollowStoreRaw = useUIStore((s) => s.setAutoFollowEnabled); + const prevBatchDocIndexRef = useRef(-1); + const prevIsRunningRef = useRef(false); + + // Wrap setAutoFollowEnabled to immediately jump to active task when toggling on during a running batch + const setAutoFollowEnabled = useCallback( + (enabled: boolean) => { + setAutoFollowStoreRaw(enabled); + if (enabled && currentSessionBatchState?.isRunning) { + const currentDocumentIndex = currentSessionBatchState.currentDocumentIndex ?? -1; + const activeDoc = currentSessionBatchState.documents?.[currentDocumentIndex]; + if (activeDoc && activeDoc !== selectedFile) { + onAutoRunSelectDocument(activeDoc); + } + setActiveRightTab('autorun'); + if (!rightPanelOpen) { + setRightPanelOpen?.(true); + } + if (currentMode === 'edit') { + onAutoRunModeChange?.('preview'); + } + } + }, + [ + setAutoFollowStoreRaw, + currentSessionBatchState, + selectedFile, + onAutoRunSelectDocument, + setActiveRightTab, + rightPanelOpen, + setRightPanelOpen, + onAutoRunModeChange, + currentMode, + ] + ); + + useEffect(() => { + const isRunning = currentSessionBatchState?.isRunning ?? false; + const currentDocumentIndex = currentSessionBatchState?.currentDocumentIndex ?? -1; + const documents = currentSessionBatchState?.documents; + + // Detect batch start + const batchJustStarted = isRunning && !prevIsRunningRef.current; + + // Detect document transition + const docChanged = currentDocumentIndex !== prevBatchDocIndexRef.current; + + // Auto-follow on batch start or document transition (only while running) + if (autoFollowEnabled && isRunning && (batchJustStarted || docChanged)) { + const activeDoc = documents?.[currentDocumentIndex]; + if (activeDoc && activeDoc !== selectedFile) { + onAutoRunSelectDocument(activeDoc); + } + } + + // On batch start with auto-follow: switch to autorun tab, open panel, switch to preview mode + if (autoFollowEnabled && batchJustStarted) { + setActiveRightTab('autorun'); + if (!rightPanelOpen) { + setRightPanelOpen?.(true); + } + // Switch to preview mode so the user sees rendered markdown with scrolling tasks + if (currentMode === 'edit') { + onAutoRunModeChange?.('preview'); + } + } + + // Reset on batch end + if (!isRunning) { + prevBatchDocIndexRef.current = -1; + } else { + prevBatchDocIndexRef.current = currentDocumentIndex ?? -1; + } + prevIsRunningRef.current = !!isRunning; + }, [ + currentSessionBatchState?.isRunning, + currentSessionBatchState?.currentDocumentIndex, + currentSessionBatchState?.documents, + autoFollowEnabled, + onAutoRunSelectDocument, + selectedFile, + setActiveRightTab, + rightPanelOpen, + setRightPanelOpen, + onAutoRunModeChange, + currentMode, + ]); + + return { autoFollowEnabled, setAutoFollowEnabled }; +} diff --git a/src/renderer/stores/uiStore.ts b/src/renderer/stores/uiStore.ts index 40e7aaf9e..85cd47b2f 100644 --- a/src/renderer/stores/uiStore.ts +++ b/src/renderer/stores/uiStore.ts @@ -57,6 +57,9 @@ export interface UIStoreState { // Editing (inline renaming in sidebar) editingGroupId: string | null; editingSessionId: string | null; + + // Auto-follow active task during batch runs + autoFollowEnabled: boolean; } export interface UIStoreActions { @@ -110,6 +113,9 @@ export interface UIStoreActions { // Editing setEditingGroupId: (id: string | null | ((prev: string | null) => string | null)) => void; setEditingSessionId: (id: string | null | ((prev: string | null) => string | null)) => void; + + // Auto-follow + setAutoFollowEnabled: (enabled: boolean | ((prev: boolean) => boolean)) => void; } export type UIStore = UIStoreState & UIStoreActions; @@ -143,6 +149,7 @@ export const useUIStore = create()((set) => ({ draggingSessionId: null, editingGroupId: null, editingSessionId: null, + autoFollowEnabled: false, // --- Actions --- setLeftSidebarOpen: (v) => set((s) => ({ leftSidebarOpen: resolve(v, s.leftSidebarOpen) })), @@ -187,4 +194,6 @@ export const useUIStore = create()((set) => ({ setEditingGroupId: (v) => set((s) => ({ editingGroupId: resolve(v, s.editingGroupId) })), setEditingSessionId: (v) => set((s) => ({ editingSessionId: resolve(v, s.editingSessionId) })), + + setAutoFollowEnabled: (v) => set((s) => ({ autoFollowEnabled: resolve(v, s.autoFollowEnabled) })), }));