mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## 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:
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user