## CHANGES

- Auto-run stop tooltips now say “stop auto-run” consistently everywhere 🛑
- AutoRun pill shows “AutoRun Stopping…” with warning color feedback 
- Cmd/Ctrl+Shift+K thinking toggle now works globally in wizard modal ⌨️
- ConversationScreen thinking toggle state is lifted to MaestroWizard 📤
- Thinking toggle tests updated to click button, not keyboard shortcut 🖱️
- BatchRunner modal rebranded as “Auto Run Configuration” for clarity 🏷️
- Auto-run prompt placeholder clarifies it’s a system prompt 🧠
- “Run batch processing” copy replaced with clearer “Start auto-run” 🚀
- Batch processor now bypasses debouncing for direct progress updates 
- Stop auto-run requests always honored; extra logging aids debugging 🔍
This commit is contained in:
Pedram Amini
2026-01-04 15:24:31 -06:00
parent 351299bdd1
commit 7f1729fcd7
15 changed files with 165 additions and 83 deletions

View File

@@ -1269,7 +1269,7 @@ describe('MainPanel', () => {
render(<MainPanel {...defaultProps} currentSessionBatchState={currentSessionBatchState} />);
const button = screen.getByText('Auto').closest('button');
expect(button).toHaveAttribute('title', 'Click to stop batch run');
expect(button).toHaveAttribute('title', 'Click to stop auto-run');
});
it('should display correct tooltip when stopping', () => {

View File

@@ -820,7 +820,7 @@ describe('ThinkingStatusPill', () => {
expect(onStopAutoRun).toHaveBeenCalledTimes(1);
});
it('shows AutoRun label and Stopping button when isStopping is true', () => {
it('shows AutoRun Stopping label and Stopping button when isStopping is true', () => {
const autoRunState: BatchRunState = {
isRunning: true,
isPaused: false,
@@ -840,8 +840,8 @@ describe('ThinkingStatusPill', () => {
onStopAutoRun={() => {}}
/>
);
// AutoRun label should still be visible
expect(screen.getByText('AutoRun')).toBeInTheDocument();
// AutoRun label should show stopping state
expect(screen.getByText('AutoRun Stopping...')).toBeInTheDocument();
// Button should show "Stopping" text
expect(screen.getByText('Stopping')).toBeInTheDocument();
});

View File

@@ -456,6 +456,7 @@ describe('Wizard Keyboard Navigation', () => {
describe('ConversationScreen', () => {
function ConversationScreenWrapper({ theme }: { theme: Theme }) {
const { goToStep, setSelectedAgent, setDirectoryPath } = useWizard();
const [showThinking, setShowThinking] = React.useState(false);
React.useEffect(() => {
setSelectedAgent('claude-code');
@@ -463,7 +464,7 @@ describe('Wizard Keyboard Navigation', () => {
goToStep('conversation');
}, [goToStep, setSelectedAgent, setDirectoryPath]);
return <ConversationScreen theme={theme} />;
return <ConversationScreen theme={theme} showThinking={showThinking} setShowThinking={setShowThinking} />;
}
it('should focus textarea on mount', async () => {
@@ -492,6 +493,7 @@ describe('Wizard Keyboard Navigation', () => {
it('should go to previous step with Escape', async () => {
function ConversationWithEscape({ theme }: { theme: Theme }) {
const { goToStep, setSelectedAgent, setDirectoryPath, state } = useWizard();
const [showThinking, setShowThinking] = React.useState(false);
React.useEffect(() => {
setSelectedAgent('claude-code');
@@ -502,7 +504,7 @@ describe('Wizard Keyboard Navigation', () => {
return (
<>
<div data-testid="current-step">{state.currentStep}</div>
<ConversationScreen theme={theme} />
<ConversationScreen theme={theme} showThinking={showThinking} setShowThinking={setShowThinking} />
</>
);
}
@@ -519,17 +521,17 @@ describe('Wizard Keyboard Navigation', () => {
});
});
it('should toggle thinking display with Cmd+Shift+K', async () => {
it('should toggle thinking display when clicking the thinking button', async () => {
// Note: Cmd+Shift+K shortcut is now handled at MaestroWizard level, not in ConversationScreen
// This test verifies the button click toggle works correctly
renderWithProviders(<ConversationScreenWrapper theme={mockTheme} />);
const container = screen.getByText('Project Understanding Confidence').closest('div[tabindex]');
// Find the thinking button - initially should be dim (off)
const thinkingButton = screen.getByTitle(/show ai thinking/i);
expect(thinkingButton).toBeInTheDocument();
// Press Cmd+Shift+K to toggle thinking display on
fireEvent.keyDown(container!, { key: 'k', metaKey: true, shiftKey: true });
// Click the button to toggle thinking display on
fireEvent.click(thinkingButton);
// Find the thinking button again - should now be highlighted (on)
await waitFor(() => {
@@ -537,8 +539,9 @@ describe('Wizard Keyboard Navigation', () => {
expect(thinkingButtonOn).toBeInTheDocument();
});
// Press Cmd+Shift+K again to toggle thinking display off
fireEvent.keyDown(container!, { key: 'k', metaKey: true, shiftKey: true });
// Click again to toggle thinking display off
const thinkingButtonOn = screen.getByTitle(/hide ai thinking/i);
fireEvent.click(thinkingButtonOn);
// Should be back to off state
await waitFor(() => {

View File

@@ -5303,11 +5303,15 @@ You are taking over this conversation. Based on the context above, provide a bri
// Use provided targetSessionId, or fall back to first active batch, or active session
const sessionId = targetSessionId
?? (activeBatchSessionIds.length > 0 ? activeBatchSessionIds[0] : activeSession?.id);
console.log('[App:handleStopBatchRun] targetSessionId:', targetSessionId, 'resolved sessionId:', sessionId);
if (!sessionId) return;
const session = sessions.find(s => s.id === sessionId);
const agentName = session?.name || 'this session';
setConfirmModalMessage(`Stop Auto Run for "${agentName}" after the current task completes?`);
setConfirmModalOnConfirm(() => () => stopBatchRun(sessionId));
setConfirmModalOnConfirm(() => () => {
console.log('[App:handleStopBatchRun] Confirmation callback executing for sessionId:', sessionId);
stopBatchRun(sessionId);
});
setConfirmModalOpen(true);
}, [activeBatchSessionIds, activeSession, sessions, stopBatchRun]);

View File

@@ -1452,7 +1452,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
border: `1px solid ${isStopping ? theme.colors.warning : theme.colors.error}`,
pointerEvents: isStopping ? 'none' : 'auto'
}}
title={isStopping ? 'Stopping after current task...' : 'Stop batch run'}
title={isStopping ? 'Stopping after current task...' : 'Stop auto-run'}
>
{isStopping ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -1477,7 +1477,7 @@ const AutoRunInner = forwardRef<AutoRunHandle, AutoRunProps>(function AutoRunInn
color: theme.colors.accentForeground,
border: `1px solid ${theme.colors.accent}`
}}
title={isAgentBusy ? "Cannot run while agent is thinking" : "Run batch processing on Auto Run tasks"}
title={isAgentBusy ? "Cannot run while agent is thinking" : "Run auto-run on tasks"}
>
<Play className="w-3.5 h-3.5" />
Run

View File

@@ -321,7 +321,7 @@ export function AutoRunExpandedModal({
border: `1px solid ${isStopping ? theme.colors.warning : theme.colors.error}`,
pointerEvents: isStopping ? 'none' : 'auto'
}}
title={isStopping ? 'Stopping after current task...' : 'Stop batch run'}
title={isStopping ? 'Stopping after current task...' : 'Stop auto-run'}
>
{isStopping ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -346,7 +346,7 @@ export function AutoRunExpandedModal({
color: theme.colors.accentForeground,
border: `1px solid ${theme.colors.accent}`
}}
title={isAgentBusy ? "Cannot run while agent is thinking" : "Run batch processing on Auto Run tasks"}
title={isAgentBusy ? "Cannot run while agent is thinking" : "Run auto-run on tasks"}
>
<Play className="w-3.5 h-3.5" />
Run

View File

@@ -176,7 +176,7 @@ export function AutoRunnerHelpModal({ theme, onClose }: AutoRunnerHelpModalProps
style={{ color: theme.colors.textDim }}
>
<p>
Click <strong style={{ color: theme.colors.textMain }}>Run</strong> to open the batch runner.
Click <strong style={{ color: theme.colors.textMain }}>Run</strong> to configure auto-run.
By default, the currently selected document is ready to run.
</p>
<p>
@@ -200,7 +200,7 @@ export function AutoRunnerHelpModal({ theme, onClose }: AutoRunnerHelpModalProps
style={{ color: theme.colors.textDim }}
>
<p>
Click <strong style={{ color: theme.colors.textMain }}>"+ Add Docs"</strong> in the batch runner
Click <strong style={{ color: theme.colors.textMain }}>"+ Add Docs"</strong> in the configuration
to select additional documents. Documents are processed sequentially in the order shown.
</p>
<p>

View File

@@ -293,7 +293,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
className="fixed inset-0 modal-overlay flex items-center justify-center z-[9999] animate-in fade-in duration-200"
role="dialog"
aria-modal="true"
aria-label="Batch Runner"
aria-label="Auto Run Configuration"
tabIndex={-1}
>
<div
@@ -617,7 +617,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
color: theme.colors.textMain,
minHeight: '200px'
}}
placeholder="Enter the prompt for the batch agent..."
placeholder="Enter the system prompt for auto-run..."
/>
<button
onClick={() => setPromptComposerOpen(true)}
@@ -669,7 +669,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
documents.length === 0 ? 'No documents selected' :
documents.length === missingDocCount ? 'All selected documents are missing' :
hasNoTasks ? 'No unchecked tasks in documents' :
'Run batch processing'
'Start auto-run'
}
>
<Play className="w-4 h-4" />

View File

@@ -801,7 +801,7 @@ export const MainPanel = React.memo(forwardRef<MainPanelHandle, MainPanelProps>(
color: isCurrentSessionStopping ? theme.colors.bgMain : 'white',
pointerEvents: isCurrentSessionStopping ? 'none' : 'auto'
}}
title={isCurrentSessionStopping ? 'Stopping after current task...' : 'Click to stop batch run'}
title={isCurrentSessionStopping ? 'Stopping after current task...' : 'Click to stop auto-run'}
>
{isCurrentSessionStopping ? (
<Loader2 className="w-4 h-4 animate-spin" />

View File

@@ -230,7 +230,7 @@ export function SessionListItem({
<span
className="text-[10px] font-bold px-1.5 py-0.5 rounded"
style={{ backgroundColor: theme.colors.warning + '30', color: theme.colors.warning }}
title="Auto-batch session through Maestro"
title="Auto-run session"
>
AUTO
</span>

View File

@@ -185,9 +185,9 @@ const AutoRunPill = memo(({
{/* AutoRun label */}
<span
className="text-xs font-semibold shrink-0"
style={{ color: theme.colors.accent }}
style={{ color: isStopping ? theme.colors.warning : theme.colors.accent }}
>
AutoRun
{isStopping ? 'AutoRun Stopping...' : 'AutoRun'}
</span>
{/* Worktree indicator */}

View File

@@ -110,6 +110,9 @@ export function MaestroWizard({
// State for exit confirmation modal
const [showExitConfirm, setShowExitConfirm] = useState(false);
// State for thinking toggle (shared across screens via ref callback)
const [showThinking, setShowThinking] = useState(false);
// Track wizard start time for duration calculation
const wizardStartTimeRef = useRef<number>(0);
// Track if wizard start has been recorded for this open session
@@ -281,6 +284,30 @@ export function MaestroWizard({
}
}, [state.isOpen, showExitConfirm, registerLayer, unregisterLayer, handleCloseRequest]);
// Capture-phase handler for global shortcuts that should work anywhere in the modal
// This ensures Cmd+Shift+K (thinking toggle) works even when focus is on header elements
useEffect(() => {
if (!state.isOpen) return;
const modal = modalRef.current;
if (!modal) return;
const handleCaptureKeyDown = (e: KeyboardEvent) => {
// Cmd+Shift+K to toggle thinking display (only on conversation step)
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'k') {
if (state.currentStep === 'conversation') {
e.preventDefault();
e.stopPropagation();
setShowThinking((prev) => !prev);
}
}
};
// Use capture phase to intercept before any other handlers
modal.addEventListener('keydown', handleCaptureKeyDown, { capture: true });
return () => modal.removeEventListener('keydown', handleCaptureKeyDown, { capture: true });
}, [state.isOpen, state.currentStep]);
// Bubble-phase handler to stop Cmd+E from reaching the main app after wizard handles it
// This prevents the wizard's edit/preview toggle from leaking to the AutoRun component
useEffect(() => {
@@ -372,7 +399,7 @@ export function MaestroWizard({
case 'directory-selection':
return <DirectorySelectionScreen theme={theme} />;
case 'conversation':
return <ConversationScreen theme={theme} />;
return <ConversationScreen theme={theme} showThinking={showThinking} setShowThinking={setShowThinking} />;
case 'preparing-plan':
return <PreparingPlanScreen theme={theme} />;
case 'phase-review':
@@ -387,7 +414,7 @@ export function MaestroWizard({
default:
return null;
}
}, [displayedStep, theme, onLaunchSession, onWizardComplete]);
}, [displayedStep, theme, onLaunchSession, onWizardComplete, showThinking]);
// Don't render if wizard is not open
if (!state.isOpen) {

View File

@@ -37,6 +37,10 @@ import { ScreenReaderAnnouncement } from '../ScreenReaderAnnouncement';
interface ConversationScreenProps {
theme: Theme;
/** Whether to show AI thinking content instead of filler phrases */
showThinking: boolean;
/** Callback to toggle thinking display (controlled by parent for global shortcut) */
setShowThinking: (value: boolean | ((prev: boolean) => boolean)) => void;
}
/**
@@ -408,7 +412,7 @@ function ThinkingDisplay({
/**
* ConversationScreen - Project discovery conversation
*/
export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Element {
export function ConversationScreen({ theme, showThinking, setShowThinking }: ConversationScreenProps): JSX.Element {
const {
state,
addMessage,
@@ -436,9 +440,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
const [fillerPhrase, setFillerPhrase] = useState('');
// Track detected provider error for showing recovery hints
const [detectedError, setDetectedError] = useState<WizardError | null>(null);
// Show Thinking toggle - shows raw AI reasoning instead of filler phrases
const [showThinking, setShowThinking] = useState(false);
// Accumulated thinking content when showThinking is enabled
// Accumulated thinking content when showThinking is enabled (showThinking prop controls display)
const [thinkingContent, setThinkingContent] = useState('');
// Screen reader announcement state
@@ -983,6 +985,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
/**
* Handle keyboard events at container level
* Note: Cmd+Enter is handled by the textarea directly to avoid double-firing
* Note: Cmd+Shift+K is handled at the MaestroWizard level to work from anywhere in modal
*/
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -991,11 +994,6 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
e.preventDefault();
previousStep();
}
// Cmd+Shift+K to toggle thinking display
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'k') {
e.preventDefault();
setShowThinking((prev) => !prev);
}
},
[previousStep]
);

View File

@@ -194,10 +194,12 @@ export function useBatchProcessor({
batchRunStatesRef.current = batchReducer(batchRunStatesRef.current, action);
// DEBUG: Log dispatch to trace state updates
if (action.type === 'START_BATCH' || action.type === 'UPDATE_PROGRESS') {
if (action.type === 'START_BATCH' || action.type === 'UPDATE_PROGRESS' || action.type === 'SET_STOPPING') {
const sessionId = action.sessionId;
console.log('[BatchProcessor:dispatch]', action.type, {
sessionId,
prevIsStopping: prevRef[sessionId]?.isStopping,
newIsStopping: batchRunStatesRef.current[sessionId]?.isStopping,
prevCompleted: prevRef[sessionId]?.completedTasksAcrossAllDocs,
newCompleted: batchRunStatesRef.current[sessionId]?.completedTasksAcrossAllDocs,
payload: action.type === 'UPDATE_PROGRESS' ? (action as { payload?: { completedTasksAcrossAllDocs?: number } }).payload?.completedTasksAcrossAllDocs : 'N/A',
@@ -274,48 +276,52 @@ export function useBatchProcessor({
// Note: We use a ref to capture the new state since dispatch doesn't return it
let newStateForSession: BatchRunState | null = null;
// For reducer, we need to convert the updater to an action
// Since the updater pattern doesn't map directly to actions, we wrap it
// by reading current state and computing the new state
const currentState = batchRunStatesRef.current;
const newState = updater(currentState);
newStateForSession = newState[sessionId] || null;
try {
// For reducer, we need to convert the updater to an action
// Since the updater pattern doesn't map directly to actions, we wrap it
// by reading current state and computing the new state
const currentState = batchRunStatesRef.current;
const newState = updater(currentState);
newStateForSession = newState[sessionId] || null;
// DEBUG: Log to trace progress updates
console.log('[BatchProcessor:onUpdate] Debounce fired:', {
sessionId,
refHasSession: !!currentState[sessionId],
refCompletedTasks: currentState[sessionId]?.completedTasksAcrossAllDocs,
newCompletedTasks: newStateForSession?.completedTasksAcrossAllDocs,
});
// Dispatch UPDATE_PROGRESS with the computed changes
// For complex state changes, we extract the session's new state and dispatch appropriately
if (newStateForSession) {
const prevSessionState = currentState[sessionId] || DEFAULT_BATCH_STATE;
// Dispatch UPDATE_PROGRESS with any changed fields
dispatch({
type: 'UPDATE_PROGRESS',
// DEBUG: Log to trace progress updates
console.log('[BatchProcessor:onUpdate] Debounce fired:', {
sessionId,
payload: {
currentDocumentIndex: newStateForSession.currentDocumentIndex !== prevSessionState.currentDocumentIndex ? newStateForSession.currentDocumentIndex : undefined,
currentDocTasksTotal: newStateForSession.currentDocTasksTotal !== prevSessionState.currentDocTasksTotal ? newStateForSession.currentDocTasksTotal : undefined,
currentDocTasksCompleted: newStateForSession.currentDocTasksCompleted !== prevSessionState.currentDocTasksCompleted ? newStateForSession.currentDocTasksCompleted : undefined,
totalTasksAcrossAllDocs: newStateForSession.totalTasksAcrossAllDocs !== prevSessionState.totalTasksAcrossAllDocs ? newStateForSession.totalTasksAcrossAllDocs : undefined,
completedTasksAcrossAllDocs: newStateForSession.completedTasksAcrossAllDocs !== prevSessionState.completedTasksAcrossAllDocs ? newStateForSession.completedTasksAcrossAllDocs : undefined,
totalTasks: newStateForSession.totalTasks !== prevSessionState.totalTasks ? newStateForSession.totalTasks : undefined,
completedTasks: newStateForSession.completedTasks !== prevSessionState.completedTasks ? newStateForSession.completedTasks : undefined,
currentTaskIndex: newStateForSession.currentTaskIndex !== prevSessionState.currentTaskIndex ? newStateForSession.currentTaskIndex : undefined,
sessionIds: newStateForSession.sessionIds !== prevSessionState.sessionIds ? newStateForSession.sessionIds : undefined,
accumulatedElapsedMs: newStateForSession.accumulatedElapsedMs !== prevSessionState.accumulatedElapsedMs ? newStateForSession.accumulatedElapsedMs : undefined,
lastActiveTimestamp: newStateForSession.lastActiveTimestamp !== prevSessionState.lastActiveTimestamp ? newStateForSession.lastActiveTimestamp : undefined,
loopIteration: newStateForSession.loopIteration !== prevSessionState.loopIteration ? newStateForSession.loopIteration : undefined,
}
refHasSession: !!currentState[sessionId],
refCompletedTasks: currentState[sessionId]?.completedTasksAcrossAllDocs,
newCompletedTasks: newStateForSession?.completedTasksAcrossAllDocs,
});
}
broadcastAutoRunState(sessionId, newStateForSession);
// Dispatch UPDATE_PROGRESS with the computed changes
// For complex state changes, we extract the session's new state and dispatch appropriately
if (newStateForSession) {
const prevSessionState = currentState[sessionId] || DEFAULT_BATCH_STATE;
// Dispatch UPDATE_PROGRESS with any changed fields
dispatch({
type: 'UPDATE_PROGRESS',
sessionId,
payload: {
currentDocumentIndex: newStateForSession.currentDocumentIndex !== prevSessionState.currentDocumentIndex ? newStateForSession.currentDocumentIndex : undefined,
currentDocTasksTotal: newStateForSession.currentDocTasksTotal !== prevSessionState.currentDocTasksTotal ? newStateForSession.currentDocTasksTotal : undefined,
currentDocTasksCompleted: newStateForSession.currentDocTasksCompleted !== prevSessionState.currentDocTasksCompleted ? newStateForSession.currentDocTasksCompleted : undefined,
totalTasksAcrossAllDocs: newStateForSession.totalTasksAcrossAllDocs !== prevSessionState.totalTasksAcrossAllDocs ? newStateForSession.totalTasksAcrossAllDocs : undefined,
completedTasksAcrossAllDocs: newStateForSession.completedTasksAcrossAllDocs !== prevSessionState.completedTasksAcrossAllDocs ? newStateForSession.completedTasksAcrossAllDocs : undefined,
totalTasks: newStateForSession.totalTasks !== prevSessionState.totalTasks ? newStateForSession.totalTasks : undefined,
completedTasks: newStateForSession.completedTasks !== prevSessionState.completedTasks ? newStateForSession.completedTasks : undefined,
currentTaskIndex: newStateForSession.currentTaskIndex !== prevSessionState.currentTaskIndex ? newStateForSession.currentTaskIndex : undefined,
sessionIds: newStateForSession.sessionIds !== prevSessionState.sessionIds ? newStateForSession.sessionIds : undefined,
accumulatedElapsedMs: newStateForSession.accumulatedElapsedMs !== prevSessionState.accumulatedElapsedMs ? newStateForSession.accumulatedElapsedMs : undefined,
lastActiveTimestamp: newStateForSession.lastActiveTimestamp !== prevSessionState.lastActiveTimestamp ? newStateForSession.lastActiveTimestamp : undefined,
loopIteration: newStateForSession.loopIteration !== prevSessionState.loopIteration ? newStateForSession.loopIteration : undefined,
}
});
}
broadcastAutoRunState(sessionId, newStateForSession);
} catch (error) {
console.error('[BatchProcessor:onUpdate] ERROR in debounce callback:', error);
}
}, [broadcastAutoRunState])
});
@@ -393,10 +399,43 @@ export function useBatchProcessor({
updater: (prev: Record<string, BatchRunState>) => Record<string, BatchRunState>,
immediate: boolean = false
) => {
// DEBUG: Log when updates are scheduled
console.log('[BatchProcessor:updateBatchStateAndBroadcast] Scheduling update', { sessionId, immediate });
scheduleDebouncedUpdate(sessionId, updater, immediate);
}, [scheduleDebouncedUpdate]);
// DEBUG: Bypass debouncing entirely to test if that's the issue
// Apply update directly without debouncing
const currentState = batchRunStatesRef.current;
const newState = updater(currentState);
const newStateForSession = newState[sessionId] || null;
console.log('[BatchProcessor:updateBatchStateAndBroadcast] DIRECT update (no debounce)', {
sessionId,
prevCompleted: currentState[sessionId]?.completedTasksAcrossAllDocs,
newCompleted: newStateForSession?.completedTasksAcrossAllDocs,
});
if (newStateForSession) {
const prevSessionState = currentState[sessionId] || DEFAULT_BATCH_STATE;
dispatch({
type: 'UPDATE_PROGRESS',
sessionId,
payload: {
currentDocumentIndex: newStateForSession.currentDocumentIndex !== prevSessionState.currentDocumentIndex ? newStateForSession.currentDocumentIndex : undefined,
currentDocTasksTotal: newStateForSession.currentDocTasksTotal !== prevSessionState.currentDocTasksTotal ? newStateForSession.currentDocTasksTotal : undefined,
currentDocTasksCompleted: newStateForSession.currentDocTasksCompleted !== prevSessionState.currentDocTasksCompleted ? newStateForSession.currentDocTasksCompleted : undefined,
totalTasksAcrossAllDocs: newStateForSession.totalTasksAcrossAllDocs !== prevSessionState.totalTasksAcrossAllDocs ? newStateForSession.totalTasksAcrossAllDocs : undefined,
completedTasksAcrossAllDocs: newStateForSession.completedTasksAcrossAllDocs !== prevSessionState.completedTasksAcrossAllDocs ? newStateForSession.completedTasksAcrossAllDocs : undefined,
totalTasks: newStateForSession.totalTasks !== prevSessionState.totalTasks ? newStateForSession.totalTasks : undefined,
completedTasks: newStateForSession.completedTasks !== prevSessionState.completedTasks ? newStateForSession.completedTasks : undefined,
currentTaskIndex: newStateForSession.currentTaskIndex !== prevSessionState.currentTaskIndex ? newStateForSession.currentTaskIndex : undefined,
sessionIds: newStateForSession.sessionIds !== prevSessionState.sessionIds ? newStateForSession.sessionIds : undefined,
accumulatedElapsedMs: newStateForSession.accumulatedElapsedMs !== prevSessionState.accumulatedElapsedMs ? newStateForSession.accumulatedElapsedMs : undefined,
lastActiveTimestamp: newStateForSession.lastActiveTimestamp !== prevSessionState.lastActiveTimestamp ? newStateForSession.lastActiveTimestamp : undefined,
loopIteration: newStateForSession.loopIteration !== prevSessionState.loopIteration ? newStateForSession.loopIteration : undefined,
}
});
}
broadcastAutoRunState(sessionId, newStateForSession);
}, [broadcastAutoRunState]);
// Update ref to always have latest updateBatchStateAndBroadcast (fixes HMR stale closure)
updateBatchStateAndBroadcastRef.current = updateBatchStateAndBroadcast;
@@ -1388,10 +1427,11 @@ export function useBatchProcessor({
/**
* Request to stop the batch run for a specific session after current task completes
* Note: No isMountedRef check here - stop requests should always be honored.
* All operations are safe: ref updates, reducer dispatch (React handles gracefully), and broadcasts.
*/
const stopBatchRun = useCallback((sessionId: string) => {
if (!isMountedRef.current) return;
console.log('[BatchProcessor:stopBatchRun] Called with sessionId:', sessionId);
stopRequestedRefs.current[sessionId] = true;
const errorResolution = errorResolutionRefs.current[sessionId];
if (errorResolution) {

View File

@@ -90,6 +90,8 @@ export function useSessionDebounce<T>(
// Cleanup effect: clear all timers synchronously on unmount
useEffect(() => {
return () => {
// DEBUG: Log unmount
console.log('[useSessionDebounce] UNMOUNTING - clearing all timers and pending updates');
isMountedRef.current = false;
// Clear all timers synchronously
@@ -146,9 +148,17 @@ export function useSessionDebounce<T>(
}
debounceTimerRefs.current[sessionId] = setTimeout(() => {
// DEBUG: Log when timer fires
const hasUpdater = !!pendingUpdatesRef.current[sessionId];
const mounted = isMountedRef.current;
console.log('[useSessionDebounce:timer] Timer fired', { sessionId, hasUpdater, mounted });
const composedUpdater = pendingUpdatesRef.current[sessionId];
if (composedUpdater && isMountedRef.current) {
console.log('[useSessionDebounce:timer] Calling onUpdate');
onUpdate(sessionId, composedUpdater);
} else {
console.log('[useSessionDebounce:timer] Skipping onUpdate - composedUpdater:', !!composedUpdater, 'isMounted:', isMountedRef.current);
}
delete pendingUpdatesRef.current[sessionId];
delete debounceTimerRefs.current[sessionId];