mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
- Added extensive DEBUG-level logging for SSH command execution, spawn details, exit codes, and configuration flow - Improved Wizard SSH remote support: - Debounced remote directory validation to reduce excessive SSH calls - Fixed git.isRepo() to correctly pass remoteCwd for remote checks - Persisted SSH config in SerializableWizardState and validated directories over SSH - Ensured ConversationScreen and ConversationSession consistently pass SSH config for remote agent execution - Fixed "agent not available" errors by forwarding stdin via exec and enabling stream-json mode for large prompts - Enhanced remote agent execution logic in ProcessManager with stdin streaming, exec-based forwarding, and useStdin flag - Improved SSH file browser behavior: - Added resolveSshPath() to locate SSH binaries on Windows (Electron spawn PATH issue) - Corrected getSshContext() handling of enabled/remoteId states - Ensured synopsis background tasks run via SSH instead of local paths - Added Windows development improvements: dev:win script and PowerShell launcher for separate renderer/main terminals - Added additional SSH directory debugging logs for remote-fs and wizard flows Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
993 lines
28 KiB
TypeScript
993 lines
28 KiB
TypeScript
/**
|
|
* WizardContext.tsx
|
|
*
|
|
* State management for the onboarding wizard multi-screen flow.
|
|
* Manages wizard progression, agent selection, directory configuration,
|
|
* conversation history, and document generation state.
|
|
*/
|
|
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useCallback,
|
|
useReducer,
|
|
useEffect,
|
|
useRef,
|
|
useMemo,
|
|
ReactNode,
|
|
} from 'react';
|
|
import type { ToolType, AgentConfig } from '../../types';
|
|
|
|
/**
|
|
* Wizard steps in order of progression
|
|
*/
|
|
export type WizardStep =
|
|
| 'agent-selection'
|
|
| 'directory-selection'
|
|
| 'conversation'
|
|
| 'preparing-plan'
|
|
| 'phase-review';
|
|
|
|
/**
|
|
* Total number of steps in the wizard
|
|
*/
|
|
export const WIZARD_TOTAL_STEPS = 5;
|
|
|
|
/**
|
|
* Map step names to their numeric index (1-based for display)
|
|
*/
|
|
export const STEP_INDEX: Record<WizardStep, number> = {
|
|
'agent-selection': 1,
|
|
'directory-selection': 2,
|
|
conversation: 3,
|
|
'preparing-plan': 4,
|
|
'phase-review': 5,
|
|
};
|
|
|
|
/**
|
|
* Map numeric index to step name
|
|
*/
|
|
export const INDEX_TO_STEP: Record<number, WizardStep> = {
|
|
1: 'agent-selection',
|
|
2: 'directory-selection',
|
|
3: 'conversation',
|
|
4: 'preparing-plan',
|
|
5: 'phase-review',
|
|
};
|
|
|
|
/**
|
|
* Conversation message in the wizard conversation flow
|
|
*/
|
|
export interface WizardMessage {
|
|
id: string;
|
|
role: 'user' | 'assistant' | 'system';
|
|
content: string;
|
|
timestamp: number;
|
|
/** Parsed confidence from assistant responses */
|
|
confidence?: number;
|
|
/** Parsed ready flag from assistant responses */
|
|
ready?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Generated document from the phase generation step
|
|
*/
|
|
export interface GeneratedDocument {
|
|
filename: string;
|
|
content: string;
|
|
taskCount: number;
|
|
/** Absolute path after saving */
|
|
savedPath?: string;
|
|
}
|
|
|
|
/**
|
|
* Complete wizard state
|
|
*/
|
|
export interface WizardState {
|
|
/** Current step in the wizard flow */
|
|
currentStep: WizardStep;
|
|
|
|
/** Whether the wizard is open/visible */
|
|
isOpen: boolean;
|
|
|
|
// Agent Selection (Step 1)
|
|
/** Selected agent type */
|
|
selectedAgent: ToolType | null;
|
|
/** Available agents from detection */
|
|
availableAgents: AgentConfig[];
|
|
/** User-provided project name */
|
|
agentName: string;
|
|
/** Per-agent custom path */
|
|
customPath?: string;
|
|
/** Per-agent custom CLI arguments */
|
|
customArgs?: string;
|
|
/** Per-agent custom environment variables */
|
|
customEnvVars?: Record<string, string>;
|
|
/** Per-session SSH remote configuration (stored per-session, not per-agent) */
|
|
sessionSshRemoteConfig?: {
|
|
enabled: boolean;
|
|
remoteId: string | null;
|
|
workingDirOverride?: string;
|
|
};
|
|
|
|
// Directory Selection (Step 2)
|
|
/** Selected directory path */
|
|
directoryPath: string;
|
|
/** Whether the selected path is a git repo */
|
|
isGitRepo: boolean;
|
|
/** Auto-detected agent path (if any) */
|
|
detectedAgentPath: string | null;
|
|
/** Directory selection error (if any) */
|
|
directoryError: string | null;
|
|
/** Whether an Auto Run Docs folder exists in the selected directory */
|
|
hasExistingAutoRunDocs: boolean;
|
|
/** Number of documents in existing Auto Run Docs folder */
|
|
existingDocsCount: number;
|
|
/** User's choice for existing docs: 'continue' to read them, 'fresh' to delete them */
|
|
existingDocsChoice: 'continue' | 'fresh' | null;
|
|
|
|
// Conversation (Step 3)
|
|
/** Conversation history with the agent */
|
|
conversationHistory: WizardMessage[];
|
|
/** Current confidence level (0-100) from agent responses */
|
|
confidenceLevel: number;
|
|
/** Whether agent has indicated ready=true and confidence > 80 */
|
|
isReadyToProceed: boolean;
|
|
/** Whether conversation is in progress (agent is thinking) */
|
|
isConversationLoading: boolean;
|
|
/** Error message if conversation fails */
|
|
conversationError: string | null;
|
|
|
|
// Phase Review (Step 4)
|
|
/** Generated Auto Run documents */
|
|
generatedDocuments: GeneratedDocument[];
|
|
/** Index of the currently displayed document (Phase 1 = 0) */
|
|
currentDocumentIndex: number;
|
|
/** Whether documents are being generated */
|
|
isGeneratingDocuments: boolean;
|
|
/** Error message if generation fails */
|
|
generationError: string | null;
|
|
/** User's edited content for Phase 1 (if modified) */
|
|
editedPhase1Content: string | null;
|
|
|
|
// Tour Preference
|
|
/** Whether user wants the walkthrough tour after setup */
|
|
wantsTour: boolean;
|
|
|
|
// Wizard completion state
|
|
/** Whether wizard completed successfully */
|
|
isComplete: boolean;
|
|
/** Session ID created by the wizard (if any) */
|
|
createdSessionId: string | null;
|
|
}
|
|
|
|
/**
|
|
* Initial/default wizard state
|
|
*/
|
|
const initialState: WizardState = {
|
|
currentStep: 'agent-selection',
|
|
isOpen: false,
|
|
|
|
// Agent Selection
|
|
selectedAgent: null,
|
|
availableAgents: [],
|
|
agentName: '',
|
|
customPath: undefined,
|
|
customArgs: undefined,
|
|
customEnvVars: undefined,
|
|
sessionSshRemoteConfig: undefined,
|
|
|
|
// Directory Selection
|
|
directoryPath: '',
|
|
isGitRepo: false,
|
|
detectedAgentPath: null,
|
|
directoryError: null,
|
|
hasExistingAutoRunDocs: false,
|
|
existingDocsCount: 0,
|
|
existingDocsChoice: null,
|
|
|
|
// Conversation
|
|
conversationHistory: [],
|
|
confidenceLevel: 0,
|
|
isReadyToProceed: false,
|
|
isConversationLoading: false,
|
|
conversationError: null,
|
|
|
|
// Phase Review
|
|
generatedDocuments: [],
|
|
currentDocumentIndex: 0,
|
|
isGeneratingDocuments: false,
|
|
generationError: null,
|
|
editedPhase1Content: null,
|
|
|
|
// Tour
|
|
wantsTour: true, // Default to wanting the tour
|
|
|
|
// Completion
|
|
isComplete: false,
|
|
createdSessionId: null,
|
|
};
|
|
|
|
/**
|
|
* Action types for the wizard reducer
|
|
*/
|
|
type WizardAction =
|
|
| { type: 'OPEN_WIZARD' }
|
|
| { type: 'CLOSE_WIZARD' }
|
|
| { type: 'RESET_WIZARD' }
|
|
| { type: 'SET_STEP'; step: WizardStep }
|
|
| { type: 'NEXT_STEP' }
|
|
| { type: 'PREVIOUS_STEP' }
|
|
| { type: 'SET_SELECTED_AGENT'; agent: ToolType | null }
|
|
| { type: 'SET_AVAILABLE_AGENTS'; agents: AgentConfig[] }
|
|
| { type: 'SET_AGENT_NAME'; name: string }
|
|
| { type: 'SET_CUSTOM_PATH'; path: string | undefined }
|
|
| { type: 'SET_CUSTOM_ARGS'; args: string | undefined }
|
|
| { type: 'SET_CUSTOM_ENV_VARS'; envVars: Record<string, string> | undefined }
|
|
| {
|
|
type: 'SET_SESSION_SSH_REMOTE_CONFIG';
|
|
config:
|
|
| { enabled: boolean; remoteId: string | null; workingDirOverride?: string }
|
|
| undefined;
|
|
}
|
|
| { type: 'SET_DIRECTORY_PATH'; path: string }
|
|
| { type: 'SET_IS_GIT_REPO'; isGitRepo: boolean }
|
|
| { type: 'SET_DETECTED_AGENT_PATH'; path: string | null }
|
|
| { type: 'SET_DIRECTORY_ERROR'; error: string | null }
|
|
| { type: 'SET_HAS_EXISTING_AUTORUN_DOCS'; hasExisting: boolean; count: number }
|
|
| { type: 'SET_EXISTING_DOCS_CHOICE'; choice: 'continue' | 'fresh' | null }
|
|
| { type: 'ADD_MESSAGE'; message: WizardMessage }
|
|
| { type: 'SET_CONVERSATION_HISTORY'; history: WizardMessage[] }
|
|
| { type: 'SET_CONFIDENCE_LEVEL'; level: number }
|
|
| { type: 'SET_IS_READY_TO_PROCEED'; ready: boolean }
|
|
| { type: 'SET_CONVERSATION_LOADING'; loading: boolean }
|
|
| { type: 'SET_CONVERSATION_ERROR'; error: string | null }
|
|
| { type: 'SET_GENERATED_DOCUMENTS'; documents: GeneratedDocument[] }
|
|
| { type: 'SET_CURRENT_DOCUMENT_INDEX'; index: number }
|
|
| { type: 'SET_GENERATING_DOCUMENTS'; generating: boolean }
|
|
| { type: 'SET_GENERATION_ERROR'; error: string | null }
|
|
| { type: 'SET_EDITED_PHASE1_CONTENT'; content: string | null }
|
|
| { type: 'SET_WANTS_TOUR'; wantsTour: boolean }
|
|
| { type: 'SET_COMPLETE'; sessionId: string | null }
|
|
| { type: 'RESTORE_STATE'; state: Partial<WizardState> };
|
|
|
|
/**
|
|
* Get the next step in the wizard flow
|
|
*/
|
|
function getNextStep(current: WizardStep): WizardStep | null {
|
|
const currentIndex = STEP_INDEX[current];
|
|
const nextIndex = currentIndex + 1;
|
|
return nextIndex <= WIZARD_TOTAL_STEPS ? INDEX_TO_STEP[nextIndex] : null;
|
|
}
|
|
|
|
/**
|
|
* Get the previous step in the wizard flow
|
|
*/
|
|
function getPreviousStep(current: WizardStep): WizardStep | null {
|
|
const currentIndex = STEP_INDEX[current];
|
|
const prevIndex = currentIndex - 1;
|
|
return prevIndex >= 1 ? INDEX_TO_STEP[prevIndex] : null;
|
|
}
|
|
|
|
/**
|
|
* Wizard state reducer
|
|
*/
|
|
function wizardReducer(state: WizardState, action: WizardAction): WizardState {
|
|
switch (action.type) {
|
|
case 'OPEN_WIZARD':
|
|
return { ...state, isOpen: true };
|
|
|
|
case 'CLOSE_WIZARD':
|
|
return { ...state, isOpen: false };
|
|
|
|
case 'RESET_WIZARD':
|
|
return { ...initialState };
|
|
|
|
case 'SET_STEP':
|
|
return { ...state, currentStep: action.step };
|
|
|
|
case 'NEXT_STEP': {
|
|
const nextStep = getNextStep(state.currentStep);
|
|
return nextStep ? { ...state, currentStep: nextStep } : state;
|
|
}
|
|
|
|
case 'PREVIOUS_STEP': {
|
|
const prevStep = getPreviousStep(state.currentStep);
|
|
return prevStep ? { ...state, currentStep: prevStep } : state;
|
|
}
|
|
|
|
case 'SET_SELECTED_AGENT':
|
|
return { ...state, selectedAgent: action.agent };
|
|
|
|
case 'SET_AVAILABLE_AGENTS':
|
|
return { ...state, availableAgents: action.agents };
|
|
|
|
case 'SET_AGENT_NAME':
|
|
return { ...state, agentName: action.name };
|
|
|
|
case 'SET_CUSTOM_PATH':
|
|
return { ...state, customPath: action.path };
|
|
|
|
case 'SET_CUSTOM_ARGS':
|
|
return { ...state, customArgs: action.args };
|
|
|
|
case 'SET_CUSTOM_ENV_VARS':
|
|
return { ...state, customEnvVars: action.envVars };
|
|
|
|
case 'SET_SESSION_SSH_REMOTE_CONFIG':
|
|
return { ...state, sessionSshRemoteConfig: action.config };
|
|
|
|
case 'SET_DIRECTORY_PATH':
|
|
return { ...state, directoryPath: action.path, directoryError: null };
|
|
|
|
case 'SET_IS_GIT_REPO':
|
|
return { ...state, isGitRepo: action.isGitRepo };
|
|
|
|
case 'SET_DETECTED_AGENT_PATH':
|
|
return { ...state, detectedAgentPath: action.path };
|
|
|
|
case 'SET_DIRECTORY_ERROR':
|
|
return { ...state, directoryError: action.error };
|
|
|
|
case 'SET_HAS_EXISTING_AUTORUN_DOCS':
|
|
return {
|
|
...state,
|
|
hasExistingAutoRunDocs: action.hasExisting,
|
|
existingDocsCount: action.count,
|
|
};
|
|
|
|
case 'SET_EXISTING_DOCS_CHOICE':
|
|
return { ...state, existingDocsChoice: action.choice };
|
|
|
|
case 'ADD_MESSAGE':
|
|
return {
|
|
...state,
|
|
conversationHistory: [...state.conversationHistory, action.message],
|
|
};
|
|
|
|
case 'SET_CONVERSATION_HISTORY':
|
|
return { ...state, conversationHistory: action.history };
|
|
|
|
case 'SET_CONFIDENCE_LEVEL':
|
|
return { ...state, confidenceLevel: action.level };
|
|
|
|
case 'SET_IS_READY_TO_PROCEED':
|
|
return { ...state, isReadyToProceed: action.ready };
|
|
|
|
case 'SET_CONVERSATION_LOADING':
|
|
return { ...state, isConversationLoading: action.loading };
|
|
|
|
case 'SET_CONVERSATION_ERROR':
|
|
return { ...state, conversationError: action.error };
|
|
|
|
case 'SET_GENERATED_DOCUMENTS':
|
|
return { ...state, generatedDocuments: action.documents };
|
|
|
|
case 'SET_CURRENT_DOCUMENT_INDEX':
|
|
return { ...state, currentDocumentIndex: action.index };
|
|
|
|
case 'SET_GENERATING_DOCUMENTS':
|
|
return { ...state, isGeneratingDocuments: action.generating };
|
|
|
|
case 'SET_GENERATION_ERROR':
|
|
return { ...state, generationError: action.error };
|
|
|
|
case 'SET_EDITED_PHASE1_CONTENT':
|
|
return { ...state, editedPhase1Content: action.content };
|
|
|
|
case 'SET_WANTS_TOUR':
|
|
return { ...state, wantsTour: action.wantsTour };
|
|
|
|
case 'SET_COMPLETE':
|
|
return {
|
|
...state,
|
|
isComplete: true,
|
|
createdSessionId: action.sessionId,
|
|
isOpen: false,
|
|
};
|
|
|
|
case 'RESTORE_STATE':
|
|
return { ...state, ...action.state };
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serializable wizard state for persistence
|
|
*/
|
|
export interface SerializableWizardState {
|
|
currentStep: WizardStep;
|
|
selectedAgent: ToolType | null;
|
|
agentName: string;
|
|
directoryPath: string;
|
|
isGitRepo: boolean;
|
|
conversationHistory: WizardMessage[];
|
|
confidenceLevel: number;
|
|
isReadyToProceed: boolean;
|
|
generatedDocuments: GeneratedDocument[];
|
|
editedPhase1Content: string | null;
|
|
wantsTour: boolean;
|
|
/** Per-session SSH remote configuration (for remote execution) */
|
|
sessionSshRemoteConfig?: {
|
|
enabled: boolean;
|
|
remoteId: string | null;
|
|
workingDirOverride?: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Wizard context API type
|
|
*/
|
|
export interface WizardContextAPI {
|
|
/** Current wizard state */
|
|
state: WizardState;
|
|
|
|
// Wizard lifecycle
|
|
/** Open the wizard */
|
|
openWizard: () => void;
|
|
/** Close the wizard (can be reopened) */
|
|
closeWizard: () => void;
|
|
/** Reset wizard to initial state */
|
|
resetWizard: () => void;
|
|
|
|
// Navigation
|
|
/** Go to a specific step */
|
|
goToStep: (step: WizardStep) => void;
|
|
/** Go to the next step */
|
|
nextStep: () => void;
|
|
/** Go to the previous step */
|
|
previousStep: () => void;
|
|
/** Check if can go to next step (validation) */
|
|
canProceedToNext: () => boolean;
|
|
/** Get current step number (1-based) */
|
|
getCurrentStepNumber: () => number;
|
|
|
|
// Agent Selection
|
|
/** Set the selected agent type */
|
|
setSelectedAgent: (agent: ToolType | null) => void;
|
|
/** Set available agents from detection */
|
|
setAvailableAgents: (agents: AgentConfig[]) => void;
|
|
/** Set the project/agent name */
|
|
setAgentName: (name: string) => void;
|
|
/** Set custom path for the agent */
|
|
setCustomPath: (path: string | undefined) => void;
|
|
/** Set custom CLI arguments for the agent */
|
|
setCustomArgs: (args: string | undefined) => void;
|
|
/** Set custom environment variables for the agent */
|
|
setCustomEnvVars: (envVars: Record<string, string> | undefined) => void;
|
|
/** Set per-session SSH remote configuration */
|
|
setSessionSshRemoteConfig: (
|
|
config: { enabled: boolean; remoteId: string | null; workingDirOverride?: string } | undefined
|
|
) => void;
|
|
|
|
// Directory Selection
|
|
/** Set the directory path */
|
|
setDirectoryPath: (path: string) => void;
|
|
/** Set whether the path is a git repo */
|
|
setIsGitRepo: (isGitRepo: boolean) => void;
|
|
/** Set the auto-detected agent path */
|
|
setDetectedAgentPath: (path: string | null) => void;
|
|
/** Set directory validation error */
|
|
setDirectoryError: (error: string | null) => void;
|
|
/** Set whether existing Auto Run Docs folder was found */
|
|
setHasExistingAutoRunDocs: (hasExisting: boolean, count: number) => void;
|
|
/** Set user's choice for existing docs */
|
|
setExistingDocsChoice: (choice: 'continue' | 'fresh' | null) => void;
|
|
|
|
// Conversation
|
|
/** Add a message to conversation history */
|
|
addMessage: (message: Omit<WizardMessage, 'id' | 'timestamp'>) => void;
|
|
/** Set the entire conversation history (for restore) */
|
|
setConversationHistory: (history: WizardMessage[]) => void;
|
|
/** Set the confidence level from agent response */
|
|
setConfidenceLevel: (level: number) => void;
|
|
/** Set whether ready to proceed (confidence > 80 and ready=true) */
|
|
setIsReadyToProceed: (ready: boolean) => void;
|
|
/** Set conversation loading state */
|
|
setConversationLoading: (loading: boolean) => void;
|
|
/** Set conversation error */
|
|
setConversationError: (error: string | null) => void;
|
|
|
|
// Phase Review
|
|
/** Set generated documents */
|
|
setGeneratedDocuments: (documents: GeneratedDocument[]) => void;
|
|
/** Set current document index for viewing */
|
|
setCurrentDocumentIndex: (index: number) => void;
|
|
/** Set document generation loading state */
|
|
setGeneratingDocuments: (generating: boolean) => void;
|
|
/** Set generation error */
|
|
setGenerationError: (error: string | null) => void;
|
|
/** Set user's edited Phase 1 content */
|
|
setEditedPhase1Content: (content: string | null) => void;
|
|
/** Get current Phase 1 content (edited or original) */
|
|
getPhase1Content: () => string;
|
|
|
|
// Tour
|
|
/** Set whether user wants the tour */
|
|
setWantsTour: (wantsTour: boolean) => void;
|
|
|
|
// Completion
|
|
/** Mark wizard as complete with optional session ID */
|
|
completeWizard: (sessionId: string | null) => void;
|
|
|
|
// State persistence (for resume functionality)
|
|
/** Save current state for later resume */
|
|
saveStateForResume: () => void;
|
|
/** Restore state from saved data */
|
|
restoreState: (state: Partial<WizardState>) => void;
|
|
/** Get serializable state for persistence */
|
|
getSerializableState: () => SerializableWizardState;
|
|
/** Check if there's saved resume state (async) */
|
|
hasResumeState: () => Promise<boolean>;
|
|
/** Load saved resume state (async, returns null if none) */
|
|
loadResumeState: () => Promise<SerializableWizardState | null>;
|
|
/** Clear saved resume state */
|
|
clearResumeState: () => void;
|
|
}
|
|
|
|
// Create the context
|
|
const WizardContext = createContext<WizardContextAPI | null>(null);
|
|
|
|
/**
|
|
* WizardProvider props
|
|
*/
|
|
interface WizardProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Generate a unique message ID
|
|
*/
|
|
function generateMessageId(): string {
|
|
return `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* WizardProvider - Provides wizard state management to the component tree
|
|
*/
|
|
export function WizardProvider({ children }: WizardProviderProps) {
|
|
const [state, dispatch] = useReducer(wizardReducer, initialState);
|
|
|
|
// Wizard lifecycle
|
|
const openWizard = useCallback(() => {
|
|
// If previous wizard was completed, reset state for fresh start
|
|
// This prevents showing stale state when running wizard multiple times in same session
|
|
if (state.isComplete) {
|
|
dispatch({ type: 'RESET_WIZARD' });
|
|
}
|
|
dispatch({ type: 'OPEN_WIZARD' });
|
|
}, [state.isComplete]);
|
|
|
|
const closeWizard = useCallback(() => {
|
|
dispatch({ type: 'CLOSE_WIZARD' });
|
|
}, []);
|
|
|
|
const resetWizard = useCallback(() => {
|
|
dispatch({ type: 'RESET_WIZARD' });
|
|
}, []);
|
|
|
|
// Navigation
|
|
const goToStep = useCallback((step: WizardStep) => {
|
|
dispatch({ type: 'SET_STEP', step });
|
|
}, []);
|
|
|
|
const nextStep = useCallback(() => {
|
|
dispatch({ type: 'NEXT_STEP' });
|
|
}, []);
|
|
|
|
const previousStep = useCallback(() => {
|
|
dispatch({ type: 'PREVIOUS_STEP' });
|
|
}, []);
|
|
|
|
const canProceedToNext = useCallback((): boolean => {
|
|
switch (state.currentStep) {
|
|
case 'agent-selection':
|
|
// Must have selected an agent and provided a name
|
|
return state.selectedAgent !== null && state.agentName.trim() !== '';
|
|
|
|
case 'directory-selection':
|
|
// Must have a valid directory path
|
|
return state.directoryPath.trim() !== '' && state.directoryError === null;
|
|
|
|
case 'conversation':
|
|
// Must have confidence > 80 and ready=true from agent
|
|
return state.isReadyToProceed;
|
|
|
|
case 'phase-review':
|
|
// Must have at least one generated document
|
|
return state.generatedDocuments.length > 0;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}, [
|
|
state.currentStep,
|
|
state.selectedAgent,
|
|
state.agentName,
|
|
state.directoryPath,
|
|
state.directoryError,
|
|
state.isReadyToProceed,
|
|
state.generatedDocuments.length,
|
|
]);
|
|
|
|
const getCurrentStepNumber = useCallback((): number => {
|
|
return STEP_INDEX[state.currentStep];
|
|
}, [state.currentStep]);
|
|
|
|
// Agent Selection
|
|
const setSelectedAgent = useCallback((agent: ToolType | null) => {
|
|
dispatch({ type: 'SET_SELECTED_AGENT', agent });
|
|
}, []);
|
|
|
|
const setAvailableAgents = useCallback((agents: AgentConfig[]) => {
|
|
dispatch({ type: 'SET_AVAILABLE_AGENTS', agents });
|
|
}, []);
|
|
|
|
const setAgentName = useCallback((name: string) => {
|
|
dispatch({ type: 'SET_AGENT_NAME', name });
|
|
}, []);
|
|
|
|
const setCustomPath = useCallback((path: string | undefined) => {
|
|
dispatch({ type: 'SET_CUSTOM_PATH', path });
|
|
}, []);
|
|
|
|
const setCustomArgs = useCallback((args: string | undefined) => {
|
|
dispatch({ type: 'SET_CUSTOM_ARGS', args });
|
|
}, []);
|
|
|
|
const setCustomEnvVars = useCallback((envVars: Record<string, string> | undefined) => {
|
|
dispatch({ type: 'SET_CUSTOM_ENV_VARS', envVars });
|
|
}, []);
|
|
|
|
const setSessionSshRemoteConfig = useCallback(
|
|
(
|
|
config: { enabled: boolean; remoteId: string | null; workingDirOverride?: string } | undefined
|
|
) => {
|
|
dispatch({ type: 'SET_SESSION_SSH_REMOTE_CONFIG', config });
|
|
},
|
|
[]
|
|
);
|
|
|
|
// Directory Selection
|
|
const setDirectoryPath = useCallback((path: string) => {
|
|
dispatch({ type: 'SET_DIRECTORY_PATH', path });
|
|
}, []);
|
|
|
|
const setIsGitRepo = useCallback((isGitRepo: boolean) => {
|
|
dispatch({ type: 'SET_IS_GIT_REPO', isGitRepo });
|
|
}, []);
|
|
|
|
const setDetectedAgentPath = useCallback((path: string | null) => {
|
|
dispatch({ type: 'SET_DETECTED_AGENT_PATH', path });
|
|
}, []);
|
|
|
|
const setDirectoryError = useCallback((error: string | null) => {
|
|
dispatch({ type: 'SET_DIRECTORY_ERROR', error });
|
|
}, []);
|
|
|
|
const setHasExistingAutoRunDocs = useCallback((hasExisting: boolean, count: number) => {
|
|
dispatch({ type: 'SET_HAS_EXISTING_AUTORUN_DOCS', hasExisting, count });
|
|
}, []);
|
|
|
|
const setExistingDocsChoice = useCallback((choice: 'continue' | 'fresh' | null) => {
|
|
dispatch({ type: 'SET_EXISTING_DOCS_CHOICE', choice });
|
|
}, []);
|
|
|
|
// Conversation
|
|
const addMessage = useCallback((message: Omit<WizardMessage, 'id' | 'timestamp'>) => {
|
|
const fullMessage: WizardMessage = {
|
|
...message,
|
|
id: generateMessageId(),
|
|
timestamp: Date.now(),
|
|
};
|
|
dispatch({ type: 'ADD_MESSAGE', message: fullMessage });
|
|
}, []);
|
|
|
|
const setConversationHistory = useCallback((history: WizardMessage[]) => {
|
|
dispatch({ type: 'SET_CONVERSATION_HISTORY', history });
|
|
}, []);
|
|
|
|
const setConfidenceLevel = useCallback((level: number) => {
|
|
dispatch({ type: 'SET_CONFIDENCE_LEVEL', level });
|
|
}, []);
|
|
|
|
const setIsReadyToProceed = useCallback((ready: boolean) => {
|
|
dispatch({ type: 'SET_IS_READY_TO_PROCEED', ready });
|
|
}, []);
|
|
|
|
const setConversationLoading = useCallback((loading: boolean) => {
|
|
dispatch({ type: 'SET_CONVERSATION_LOADING', loading });
|
|
}, []);
|
|
|
|
const setConversationError = useCallback((error: string | null) => {
|
|
dispatch({ type: 'SET_CONVERSATION_ERROR', error });
|
|
}, []);
|
|
|
|
// Phase Review
|
|
const setGeneratedDocuments = useCallback((documents: GeneratedDocument[]) => {
|
|
dispatch({ type: 'SET_GENERATED_DOCUMENTS', documents });
|
|
}, []);
|
|
|
|
const setCurrentDocumentIndex = useCallback((index: number) => {
|
|
dispatch({ type: 'SET_CURRENT_DOCUMENT_INDEX', index });
|
|
}, []);
|
|
|
|
const setGeneratingDocuments = useCallback((generating: boolean) => {
|
|
dispatch({ type: 'SET_GENERATING_DOCUMENTS', generating });
|
|
}, []);
|
|
|
|
const setGenerationError = useCallback((error: string | null) => {
|
|
dispatch({ type: 'SET_GENERATION_ERROR', error });
|
|
}, []);
|
|
|
|
const setEditedPhase1Content = useCallback((content: string | null) => {
|
|
dispatch({ type: 'SET_EDITED_PHASE1_CONTENT', content });
|
|
}, []);
|
|
|
|
const getPhase1Content = useCallback((): string => {
|
|
// Return edited content if available, otherwise original Phase 1 content
|
|
if (state.editedPhase1Content !== null) {
|
|
return state.editedPhase1Content;
|
|
}
|
|
const phase1Doc = state.generatedDocuments[0];
|
|
return phase1Doc?.content || '';
|
|
}, [state.editedPhase1Content, state.generatedDocuments]);
|
|
|
|
// Tour
|
|
const setWantsTour = useCallback((wantsTour: boolean) => {
|
|
dispatch({ type: 'SET_WANTS_TOUR', wantsTour });
|
|
}, []);
|
|
|
|
// Completion
|
|
const completeWizard = useCallback((sessionId: string | null) => {
|
|
// Clear saved resume state since wizard completed successfully
|
|
window.maestro.settings.set('wizardResumeState', null);
|
|
dispatch({ type: 'SET_COMPLETE', sessionId });
|
|
}, []);
|
|
|
|
// State persistence
|
|
const getSerializableState = useCallback((): SerializableWizardState => {
|
|
return {
|
|
currentStep: state.currentStep,
|
|
selectedAgent: state.selectedAgent,
|
|
agentName: state.agentName,
|
|
directoryPath: state.directoryPath,
|
|
isGitRepo: state.isGitRepo,
|
|
conversationHistory: state.conversationHistory,
|
|
confidenceLevel: state.confidenceLevel,
|
|
isReadyToProceed: state.isReadyToProceed,
|
|
generatedDocuments: state.generatedDocuments,
|
|
editedPhase1Content: state.editedPhase1Content,
|
|
wantsTour: state.wantsTour,
|
|
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
|
|
};
|
|
}, [
|
|
state.currentStep,
|
|
state.selectedAgent,
|
|
state.agentName,
|
|
state.directoryPath,
|
|
state.isGitRepo,
|
|
state.conversationHistory,
|
|
state.confidenceLevel,
|
|
state.isReadyToProceed,
|
|
state.generatedDocuments,
|
|
state.editedPhase1Content,
|
|
state.wantsTour,
|
|
state.sessionSshRemoteConfig,
|
|
]);
|
|
|
|
const saveStateForResume = useCallback(() => {
|
|
const serializableState = getSerializableState();
|
|
// Save to settings (async, fire-and-forget)
|
|
window.maestro.settings.set('wizardResumeState', serializableState);
|
|
}, [getSerializableState]);
|
|
|
|
const restoreState = useCallback((savedState: Partial<WizardState>) => {
|
|
dispatch({ type: 'RESTORE_STATE', state: savedState });
|
|
}, []);
|
|
|
|
const hasResumeState = useCallback(async (): Promise<boolean> => {
|
|
try {
|
|
const saved = await window.maestro.settings.get('wizardResumeState');
|
|
// Check if saved state exists and wizard is not complete
|
|
return saved !== undefined && saved !== null && typeof saved === 'object';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
const loadResumeState = useCallback(async (): Promise<SerializableWizardState | null> => {
|
|
try {
|
|
const saved = await window.maestro.settings.get('wizardResumeState');
|
|
if (saved && typeof saved === 'object') {
|
|
// Validate that required fields exist
|
|
const state = saved as SerializableWizardState;
|
|
if (state.currentStep && state.currentStep !== 'agent-selection') {
|
|
// Only return state if past the first step
|
|
return state;
|
|
}
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
const clearResumeState = useCallback(() => {
|
|
window.maestro.settings.set('wizardResumeState', null);
|
|
}, []);
|
|
|
|
// Store full state in a ref so we can access current values without triggering effect re-runs
|
|
const stateRef = useRef(state);
|
|
stateRef.current = state;
|
|
|
|
// Auto-save state when step changes (only for steps past the first)
|
|
// PERF: Only depends on state.currentStep to avoid running on every state change
|
|
useEffect(() => {
|
|
// Save state when advancing past the first step
|
|
// This ensures user progress is preserved for resume
|
|
if (STEP_INDEX[state.currentStep] > 1) {
|
|
// Access current state via ref to get latest values
|
|
const currentState = stateRef.current;
|
|
const serializableState: SerializableWizardState = {
|
|
currentStep: currentState.currentStep,
|
|
selectedAgent: currentState.selectedAgent,
|
|
agentName: currentState.agentName,
|
|
directoryPath: currentState.directoryPath,
|
|
isGitRepo: currentState.isGitRepo,
|
|
conversationHistory: currentState.conversationHistory,
|
|
confidenceLevel: currentState.confidenceLevel,
|
|
isReadyToProceed: currentState.isReadyToProceed,
|
|
generatedDocuments: currentState.generatedDocuments,
|
|
editedPhase1Content: currentState.editedPhase1Content,
|
|
wantsTour: currentState.wantsTour,
|
|
};
|
|
window.maestro.settings.set('wizardResumeState', serializableState);
|
|
}
|
|
}, [state.currentStep]);
|
|
|
|
// Build the context value - memoized to prevent unnecessary re-renders
|
|
const contextValue: WizardContextAPI = useMemo(
|
|
() => ({
|
|
state,
|
|
|
|
// Lifecycle
|
|
openWizard,
|
|
closeWizard,
|
|
resetWizard,
|
|
|
|
// Navigation
|
|
goToStep,
|
|
nextStep,
|
|
previousStep,
|
|
canProceedToNext,
|
|
getCurrentStepNumber,
|
|
|
|
// Agent Selection
|
|
setSelectedAgent,
|
|
setAvailableAgents,
|
|
setAgentName,
|
|
setCustomPath,
|
|
setCustomArgs,
|
|
setCustomEnvVars,
|
|
setSessionSshRemoteConfig,
|
|
|
|
// Directory Selection
|
|
setDirectoryPath,
|
|
setIsGitRepo,
|
|
setDetectedAgentPath,
|
|
setDirectoryError,
|
|
setHasExistingAutoRunDocs,
|
|
setExistingDocsChoice,
|
|
|
|
// Conversation
|
|
addMessage,
|
|
setConversationHistory,
|
|
setConfidenceLevel,
|
|
setIsReadyToProceed,
|
|
setConversationLoading,
|
|
setConversationError,
|
|
|
|
// Phase Review
|
|
setGeneratedDocuments,
|
|
setCurrentDocumentIndex,
|
|
setGeneratingDocuments,
|
|
setGenerationError,
|
|
setEditedPhase1Content,
|
|
getPhase1Content,
|
|
|
|
// Tour
|
|
setWantsTour,
|
|
|
|
// Completion
|
|
completeWizard,
|
|
|
|
// State persistence
|
|
saveStateForResume,
|
|
restoreState,
|
|
getSerializableState,
|
|
hasResumeState,
|
|
loadResumeState,
|
|
clearResumeState,
|
|
}),
|
|
[
|
|
state,
|
|
openWizard,
|
|
closeWizard,
|
|
resetWizard,
|
|
goToStep,
|
|
nextStep,
|
|
previousStep,
|
|
canProceedToNext,
|
|
getCurrentStepNumber,
|
|
setSelectedAgent,
|
|
setAvailableAgents,
|
|
setAgentName,
|
|
setCustomPath,
|
|
setCustomArgs,
|
|
setCustomEnvVars,
|
|
setSessionSshRemoteConfig,
|
|
setDirectoryPath,
|
|
setIsGitRepo,
|
|
setDetectedAgentPath,
|
|
setDirectoryError,
|
|
setHasExistingAutoRunDocs,
|
|
setExistingDocsChoice,
|
|
addMessage,
|
|
setConversationHistory,
|
|
setConfidenceLevel,
|
|
setIsReadyToProceed,
|
|
setConversationLoading,
|
|
setConversationError,
|
|
setGeneratedDocuments,
|
|
setCurrentDocumentIndex,
|
|
setGeneratingDocuments,
|
|
setGenerationError,
|
|
setEditedPhase1Content,
|
|
getPhase1Content,
|
|
setWantsTour,
|
|
completeWizard,
|
|
saveStateForResume,
|
|
restoreState,
|
|
getSerializableState,
|
|
hasResumeState,
|
|
loadResumeState,
|
|
clearResumeState,
|
|
]
|
|
);
|
|
|
|
return <WizardContext.Provider value={contextValue}>{children}</WizardContext.Provider>;
|
|
}
|
|
|
|
/**
|
|
* useWizard - Hook to access the wizard context API
|
|
*
|
|
* Must be used within a WizardProvider. Throws an error if used outside.
|
|
*
|
|
* @returns WizardContextAPI - Methods and state for managing the wizard
|
|
*
|
|
* @example
|
|
* const { state, nextStep, setSelectedAgent } = useWizard();
|
|
*
|
|
* // Check current step
|
|
* if (state.currentStep === 'agent-selection') {
|
|
* setSelectedAgent('claude-code');
|
|
* }
|
|
*
|
|
* // Navigate to next step
|
|
* if (canProceedToNext()) {
|
|
* nextStep();
|
|
* }
|
|
*/
|
|
export function useWizard(): WizardContextAPI {
|
|
const context = useContext(WizardContext);
|
|
|
|
if (!context) {
|
|
throw new Error('useWizard must be used within a WizardProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|