Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/__tests__/renderer/hooks/useWizardHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,76 @@ describe('useWizardHandlers', () => {
);
});

it('auto-starts batch run with all documents when runAllDocuments is true', async () => {
useSessionStore.setState({ sessions: [], activeSessionId: null });

const deps = createMockDeps({
wizardContext: {
state: {
currentStep: 'review' as any,
isOpen: true,
selectedAgent: 'claude-code',
availableAgents: [],
agentName: 'Test',
directoryPath: '/projects/test',
isGitRepo: false,
detectedAgentPath: null,
directoryError: null,
hasExistingAutoRunDocs: false,
existingDocsCount: 0,
existingDocsChoice: null,
conversationHistory: [],
confidenceLevel: 90,
isReadyToProceed: true,
isConversationLoading: false,
conversationError: null,
generatedDocuments: [
{ filename: 'phase-1.md', content: '# Phase 1', taskCount: 3 },
{ filename: 'phase-2.md', content: '# Phase 2', taskCount: 5 },
{ filename: 'phase-3.md', content: '# Phase 3', taskCount: 2 },
],
currentDocumentIndex: 0,
isGeneratingDocuments: false,
generationError: null,
editedPhase1Content: null,
runAllDocuments: true,
wantsTour: false,
isComplete: false,
createdSessionId: null,
} as any,
completeWizard: vi.fn(),
clearResumeState: vi.fn(),
},
});

const { result } = renderHook(() => useWizardHandlers(deps));

await act(async () => {
await result.current.handleWizardLaunchSession(false);
});

// Wait for the setTimeout batch run
await act(async () => {
await new Promise((r) => setTimeout(r, 600));
});

expect(deps.startBatchRun).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
documents: expect.arrayContaining([
expect.objectContaining({ filename: 'phase-1' }),
expect.objectContaining({ filename: 'phase-2' }),
expect.objectContaining({ filename: 'phase-3' }),
]),
}),
expect.stringContaining('Auto Run Docs')
);

// Should have exactly 3 documents in the batch
const batchConfig = deps.startBatchRun.mock.calls[0][1];
expect(batchConfig.documents).toHaveLength(3);
});

it('starts tour when wantsTour is true', async () => {
useSessionStore.setState({ sessions: [], activeSessionId: null });

Expand Down
20 changes: 2 additions & 18 deletions src/renderer/components/SettingCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import type { Theme } from '../types';
import type { LucideIcon } from 'lucide-react';
import { ToggleSwitch } from './ui/ToggleSwitch';

export interface SettingCheckboxProps {
/** The icon to display next to the section label */
Expand Down Expand Up @@ -62,24 +63,7 @@ export function SettingCheckbox({
</div>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onChange(!checked);
}}
className="relative w-10 h-5 rounded-full transition-colors flex-shrink-0"
style={{
backgroundColor: checked ? theme.colors.accent : theme.colors.bgActivity,
}}
role="switch"
aria-checked={checked}
>
<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
checked ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
<ToggleSwitch checked={checked} onChange={onChange} theme={theme} />
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/Wizard/MaestroWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ export function MaestroWizard({

<div
ref={modalRef}
className="w-[90vw] h-[80vh] max-w-5xl rounded-xl border shadow-2xl flex flex-col overflow-hidden wizard-modal"
className="w-[1200px] max-w-[95vw] h-[85vh] rounded-xl border shadow-2xl flex flex-col overflow-hidden wizard-modal"
style={{
backgroundColor: theme.colors.bgMain,
borderColor: theme.colors.border,
Expand Down
28 changes: 28 additions & 0 deletions src/renderer/components/Wizard/WizardContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export interface WizardState {
/** User's edited content for Phase 1 (if modified) */
editedPhase1Content: string | null;

// Launch Options
/** Whether to auto-run all documents in sequence (vs just the first) */
runAllDocuments: boolean;

// Tour Preference
/** Whether user wants the walkthrough tour after setup */
wantsTour: boolean;
Expand Down Expand Up @@ -200,6 +204,9 @@ const initialState: WizardState = {
generationError: null,
editedPhase1Content: null,

// Launch Options
runAllDocuments: false, // Default to running first document only

// Tour
wantsTour: true, // Default to wanting the tour

Expand Down Expand Up @@ -247,6 +254,7 @@ type WizardAction =
| { type: 'SET_GENERATING_DOCUMENTS'; generating: boolean }
| { type: 'SET_GENERATION_ERROR'; error: string | null }
| { type: 'SET_EDITED_PHASE1_CONTENT'; content: string | null }
| { type: 'SET_RUN_ALL_DOCUMENTS'; runAll: boolean }
| { type: 'SET_WANTS_TOUR'; wantsTour: boolean }
| { type: 'SET_COMPLETE'; sessionId: string | null }
| { type: 'RESTORE_STATE'; state: Partial<WizardState> };
Expand Down Expand Up @@ -375,6 +383,9 @@ function wizardReducer(state: WizardState, action: WizardAction): WizardState {
case 'SET_EDITED_PHASE1_CONTENT':
return { ...state, editedPhase1Content: action.content };

case 'SET_RUN_ALL_DOCUMENTS':
return { ...state, runAllDocuments: action.runAll };

case 'SET_WANTS_TOUR':
return { ...state, wantsTour: action.wantsTour };

Expand Down Expand Up @@ -408,6 +419,7 @@ export interface SerializableWizardState {
isReadyToProceed: boolean;
generatedDocuments: GeneratedDocument[];
editedPhase1Content: string | null;
runAllDocuments: boolean;
wantsTour: boolean;
/** Per-session SSH remote configuration (for remote execution) */
sessionSshRemoteConfig?: {
Expand Down Expand Up @@ -504,6 +516,10 @@ export interface WizardContextAPI {
/** Get current Phase 1 content (edited or original) */
getPhase1Content: () => string;

// Launch Options
/** Set whether to run all documents or just the first */
setRunAllDocuments: (runAll: boolean) => void;

// Tour
/** Set whether user wants the tour */
setWantsTour: (wantsTour: boolean) => void;
Expand Down Expand Up @@ -735,6 +751,11 @@ export function WizardProvider({ children }: WizardProviderProps) {
return phase1Doc?.content || '';
}, [state.editedPhase1Content, state.generatedDocuments]);

// Launch Options
const setRunAllDocuments = useCallback((runAll: boolean) => {
dispatch({ type: 'SET_RUN_ALL_DOCUMENTS', runAll });
}, []);

// Tour
const setWantsTour = useCallback((wantsTour: boolean) => {
dispatch({ type: 'SET_WANTS_TOUR', wantsTour });
Expand All @@ -760,6 +781,7 @@ export function WizardProvider({ children }: WizardProviderProps) {
isReadyToProceed: state.isReadyToProceed,
generatedDocuments: state.generatedDocuments,
editedPhase1Content: state.editedPhase1Content,
runAllDocuments: state.runAllDocuments,
wantsTour: state.wantsTour,
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
};
Expand All @@ -774,6 +796,7 @@ export function WizardProvider({ children }: WizardProviderProps) {
state.isReadyToProceed,
state.generatedDocuments,
state.editedPhase1Content,
state.runAllDocuments,
state.wantsTour,
state.sessionSshRemoteConfig,
]);
Expand Down Expand Up @@ -842,6 +865,7 @@ export function WizardProvider({ children }: WizardProviderProps) {
isReadyToProceed: currentState.isReadyToProceed,
generatedDocuments: currentState.generatedDocuments,
editedPhase1Content: currentState.editedPhase1Content,
runAllDocuments: currentState.runAllDocuments,
wantsTour: currentState.wantsTour,
};
window.maestro.settings.set('wizardResumeState', serializableState);
Expand Down Expand Up @@ -898,6 +922,9 @@ export function WizardProvider({ children }: WizardProviderProps) {
setEditedPhase1Content,
getPhase1Content,

// Launch Options
setRunAllDocuments,

// Tour
setWantsTour,

Expand Down Expand Up @@ -947,6 +974,7 @@ export function WizardProvider({ children }: WizardProviderProps) {
setGenerationError,
setEditedPhase1Content,
getPhase1Content,
setRunAllDocuments,
setWantsTour,
completeWizard,
saveStateForResume,
Expand Down
43 changes: 41 additions & 2 deletions src/renderer/components/Wizard/screens/PhaseReviewScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useWizard } from '../WizardContext';
import { AUTO_RUN_FOLDER_NAME } from '../services/phaseGenerator';
import { ScreenReaderAnnouncement } from '../ScreenReaderAnnouncement';
import { DocumentEditor } from '../shared/DocumentEditor';
import { ToggleSwitch } from '../../ui/ToggleSwitch';
import { formatShortcutKeys } from '../../../utils/shortcutFormatter';

// Auto-save debounce delay in milliseconds
Expand Down Expand Up @@ -67,8 +68,14 @@ function DocumentReview({
) => void;
wizardStartTime?: number;
}): JSX.Element {
const { state, setEditedPhase1Content, getPhase1Content, setWantsTour, setCurrentDocumentIndex } =
useWizard();
const {
state,
setEditedPhase1Content,
getPhase1Content,
setWantsTour,
setCurrentDocumentIndex,
setRunAllDocuments,
} = useWizard();

const { generatedDocuments, directoryPath, currentDocumentIndex } = state;
const currentDoc = generatedDocuments[currentDocumentIndex] || generatedDocuments[0];
Expand Down Expand Up @@ -479,6 +486,38 @@ function DocumentReview({
backgroundColor: theme.colors.bgSidebar,
}}
>
{/* Run All toggle - only shown when there are multiple documents */}
{generatedDocuments.length > 1 && (
<div
className="flex items-center gap-3 mb-3 px-3 py-2.5 rounded-lg border cursor-pointer"
style={{
borderColor: theme.colors.border,
backgroundColor: theme.colors.bgMain,
}}
onClick={() => setRunAllDocuments(!state.runAllDocuments)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setRunAllDocuments(!state.runAllDocuments);
}
}}
>
<ToggleSwitch
checked={state.runAllDocuments}
onChange={setRunAllDocuments}
theme={theme}
ariaLabel={
state.runAllDocuments ? 'Auto Run All Phases' : 'Auto Run First Phase Only For Now'
}
/>
<span className="text-sm font-medium" style={{ color: theme.colors.textMain }}>
{state.runAllDocuments ? 'Auto Run All Phases' : 'Auto Run First Phase Only For Now'}
</span>
</div>
)}

<div className="flex flex-col sm:flex-row gap-3">
{/* Primary button - Ready to Go */}
<button
Expand Down
52 changes: 52 additions & 0 deletions src/renderer/components/ui/ToggleSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import type { Theme } from '../../types';

export interface ToggleSwitchProps {
/** Whether the toggle is on */
checked: boolean;
/** Callback when the toggle state changes */
onChange: (checked: boolean) => void;
/** The current theme */
theme: Theme;
/** Optional aria-label for accessibility */
ariaLabel?: string;
/** Whether the toggle is disabled */
disabled?: boolean;
}

/**
* A reusable toggle switch (pill-style) with consistent styling.
* Matches the design used in SettingsModal and other toggle UIs.
*/
export function ToggleSwitch({
checked,
onChange,
theme,
ariaLabel,
disabled = false,
}: ToggleSwitchProps): React.ReactElement {
return (
<button
onClick={(e) => {
e.stopPropagation();
if (!disabled) onChange(!checked);
}}
className={`relative w-10 h-5 rounded-full transition-colors flex-shrink-0 ${
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
}`}
style={{
backgroundColor: checked ? theme.colors.accent : theme.colors.bgActivity,
}}
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
disabled={disabled}
>
Comment on lines +29 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit keyboard focus hooks required by renderer component guidelines.

On Line 29, this interactive control is focusable but currently lacks tabIndex and focus handlers. Please add tabIndex and onFocus/onBlur so focus behavior is explicit and consistently stylable.

Suggested update
 export function ToggleSwitch({
 	checked,
 	onChange,
 	theme,
 	ariaLabel,
 	disabled = false,
 }: ToggleSwitchProps): React.ReactElement {
+	const [isFocused, setIsFocused] = React.useState(false);
+
 	return (
 		<button
 			type="button"
+			tabIndex={disabled ? -1 : 0}
+			onFocus={() => setIsFocused(true)}
+			onBlur={() => setIsFocused(false)}
 			onClick={(e) => {
 				e.stopPropagation();
 				if (!disabled) onChange(!checked);
 			}}
 			className={`relative w-10 h-5 rounded-full transition-colors flex-shrink-0 ${
 				disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
-			}`}
+			} ${isFocused ? 'ring-2 ring-offset-2' : ''}`}
 			style={{
 				backgroundColor: checked ? theme.colors.accent : theme.colors.bgActivity,
 			}}
 			role="switch"
 			aria-checked={checked}
 			aria-label={ariaLabel}
 			disabled={disabled}
 		>

As per coding guidelines, src/renderer/components/**/*.{ts,tsx}: Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/ui/ToggleSwitch.tsx` around lines 29 - 45, The
ToggleSwitch button lacks explicit keyboard focus hooks; add a tabIndex and
focus handlers to make focus behavior explicit and stylable: in the ToggleSwitch
component, add tabIndex={disabled ? -1 : 0} to the button, introduce local state
(e.g., isFocused) and onFocus/onBlur handlers that set/unset it, update the
button className or inline style to reflect focus (e.g., add a focus ring class
when isFocused), and if the component accepts optional onFocus/onBlur props
forward/invoke them from these handlers; keep existing behavior for checked,
onChange, disabled and ariaLabel unchanged.

<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
checked ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
);
}
3 changes: 3 additions & 0 deletions src/renderer/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ export type { FormInputProps } from './FormInput';

export { EmojiPickerField } from './EmojiPickerField';
export type { EmojiPickerFieldProps } from './EmojiPickerField';

export { ToggleSwitch } from './ToggleSwitch';
export type { ToggleSwitchProps } from './ToggleSwitch';
Loading
Loading