diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 667edde5c0..c13ee75b29 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -16,7 +16,12 @@ import { DebugWizardModal } from './components/DebugWizardModal'; import { DebugPackageModal } from './components/DebugPackageModal'; import { WindowsWarningModal } from './components/WindowsWarningModal'; import { GistPublishModal } from './components/GistPublishModal'; -import { MaestroWizard, useWizard, WizardResumeModal } from './components/Wizard'; +import { + MaestroWizard, + useWizard, + WizardResumeModal, + type SerializableWizardState, +} from './components/Wizard'; import { TourOverlay } from './components/Wizard/tour'; // CONDUCTOR_BADGES moved to useAutoRunAchievements hook import { EmptyStateView } from './components/EmptyStateView'; @@ -42,6 +47,8 @@ const DirectorNotesModal = lazy(() => import('./components/DirectorNotes').then((m) => ({ default: m.DirectorNotesModal })) ); +import { captureException } from './utils/sentry'; + // SymphonyContributionData type moved to useSymphonyContribution hook // Group Chat Components @@ -337,7 +344,7 @@ function MaestroConsoleInner() { // --- WIZARD (onboarding wizard for new users) --- const { state: wizardState, - openWizard: openWizardModal, + openWizard: _baseOpenWizardModal, restoreState: restoreWizardState, loadResumeState: _loadResumeState, clearResumeState, @@ -346,6 +353,35 @@ function MaestroConsoleInner() { goToStep: wizardGoToStep, } = useWizard(); + // Wrapper for openWizard that checks for resume state + const openWizardModal = useCallback(async () => { + try { + const saved = await window.maestro.settings.get('wizardResumeState'); + // Validate saved state has a resumable step before casting + const resumableSteps = [ + 'directory-selection', + 'conversation', + 'preparing-plan', + 'phase-review', + ]; + if ( + saved && + typeof saved === 'object' && + 'currentStep' in saved && + typeof saved.currentStep === 'string' && + resumableSteps.includes(saved.currentStep) + ) { + useModalStore + .getState() + .openModal('wizardResume', { state: saved as SerializableWizardState }); + return; + } + } catch (e) { + captureException(e, { extra: { context: 'openWizardModal', setting: 'wizardResumeState' } }); + console.error('[App] Failed to check wizard resume state:', e); + } + _baseOpenWizardModal(); + }, [_baseOpenWizardModal]); // --- SETTINGS (from useSettings hook) --- const settings = useSettings(); const { diff --git a/src/renderer/components/Wizard/screens/PreparingPlanScreen.tsx b/src/renderer/components/Wizard/screens/PreparingPlanScreen.tsx index 5af28810b0..412f3a88da 100644 --- a/src/renderer/components/Wizard/screens/PreparingPlanScreen.tsx +++ b/src/renderer/components/Wizard/screens/PreparingPlanScreen.tsx @@ -768,6 +768,9 @@ export function PreparingPlanScreen({ theme }: PreparingPlanScreenProps): JSX.El // Save documents to disk in "Initiation" subfolder setProgressMessage('Saving documents...'); + const sshRemoteId = state.sessionSshRemoteConfig?.enabled + ? (state.sessionSshRemoteConfig.remoteId ?? undefined) + : undefined; const saveResult = await phaseGenerator.saveDocuments( state.directoryPath, genResult.documents, @@ -775,7 +778,8 @@ export function PreparingPlanScreen({ theme }: PreparingPlanScreenProps): JSX.El // Add file to the created files list as it's saved addCreatedFile(file); }, - 'Initiation' // Save in Initiation subfolder + 'Initiation', // Save in Initiation subfolder + sshRemoteId ); if (saveResult.success) { @@ -828,6 +832,7 @@ export function PreparingPlanScreen({ theme }: PreparingPlanScreenProps): JSX.El state.directoryPath, state.agentName, state.conversationHistory, + state.sessionSshRemoteConfig, setGeneratingDocuments, setGeneratedDocuments, setGenerationError, diff --git a/src/renderer/components/Wizard/services/phaseGenerator.ts b/src/renderer/components/Wizard/services/phaseGenerator.ts index f7fac0d6c1..3b69372037 100644 --- a/src/renderer/components/Wizard/services/phaseGenerator.ts +++ b/src/renderer/components/Wizard/services/phaseGenerator.ts @@ -586,6 +586,9 @@ class PhaseGenerator { // For SSH remote sessions, skip the availability check since we're executing remotely // The agent detector checks for binaries locally, but we need to execute on the remote host const isRemoteSession = config.sshRemoteConfig?.enabled && config.sshRemoteConfig?.remoteId; + const sshRemoteId = config.sshRemoteConfig?.enabled + ? (config.sshRemoteConfig.remoteId ?? undefined) + : undefined; if (!agent) { wizardDebugLogger.log('error', 'Agent configuration not found', { @@ -706,7 +709,7 @@ class PhaseGenerator { if (!hasValidParsedDocs) { callbacks?.onProgress?.('Checking for documents on disk...'); wizardDebugLogger.log('info', 'Checking for documents on disk (parsed docs invalid)'); - const diskDocs = await this.readDocumentsFromDisk(config.directoryPath); + const diskDocs = await this.readDocumentsFromDisk(config.directoryPath, sshRemoteId); if (diskDocs.length > 0) { console.log('[PhaseGenerator] Found documents on disk:', diskDocs.length); wizardDebugLogger.log('info', 'Found documents on disk', { @@ -976,9 +979,14 @@ class PhaseGenerator { subfolder: config.subfolder, }); + // Extract sshRemoteId for remote sessions + const sshRemoteId = config.sshRemoteConfig?.enabled + ? (config.sshRemoteConfig.remoteId ?? undefined) + : undefined; + // Start watching the folder for file changes window.maestro.autorun - .watchFolder(autoRunPath) + .watchFolder(autoRunPath, sshRemoteId) .then((result) => { if (result.success) { console.log('[PhaseGenerator] Started watching folder:', autoRunPath); @@ -1017,7 +1025,7 @@ class PhaseGenerator { const readWithRetry = async (retries = 3, delayMs = 200): Promise => { for (let attempt = 1; attempt <= retries; attempt++) { try { - const content = await window.maestro.fs.readFile(fullPath); + const content = await window.maestro.fs.readFile(fullPath, sshRemoteId); if (content && typeof content === 'string' && content.length > 0) { console.log( '[PhaseGenerator] File read successful:', @@ -1152,13 +1160,16 @@ class PhaseGenerator { * This is a fallback for when the agent writes files directly * instead of outputting them with markers. */ - private async readDocumentsFromDisk(directoryPath: string): Promise { + private async readDocumentsFromDisk( + directoryPath: string, + sshRemoteId?: string + ): Promise { const autoRunPath = `${directoryPath}/${AUTO_RUN_FOLDER_NAME}`; const documents: ParsedDocument[] = []; try { // List files in the Auto Run folder - const listResult = await window.maestro.autorun.listDocs(autoRunPath); + const listResult = await window.maestro.autorun.listDocs(autoRunPath, sshRemoteId); if (!listResult.success || !listResult.files) { return []; } @@ -1169,7 +1180,11 @@ class PhaseGenerator { for (const fileBaseName of listResult.files) { const filename = fileBaseName.endsWith('.md') ? fileBaseName : `${fileBaseName}.md`; - const readResult = await window.maestro.autorun.readDoc(autoRunPath, fileBaseName); + const readResult = await window.maestro.autorun.readDoc( + autoRunPath, + fileBaseName, + sshRemoteId + ); if (readResult.success && readResult.content) { // Extract phase number from filename const phaseMatch = filename.match(/Phase-(\d+)/i); @@ -1225,7 +1240,8 @@ class PhaseGenerator { directoryPath: string, documents: GeneratedDocument[], onFileCreated?: (file: CreatedFileInfo) => void, - subfolder?: string + subfolder?: string, + sshRemoteId?: string ): Promise<{ success: boolean; savedPaths: string[]; error?: string; subfolderPath?: string }> { const baseAutoRunPath = `${directoryPath}/${AUTO_RUN_FOLDER_NAME}`; const autoRunPath = subfolder ? `${baseAutoRunPath}/${subfolder}` : baseAutoRunPath; @@ -1242,7 +1258,12 @@ class PhaseGenerator { console.log('[PhaseGenerator] Saving document:', filename); // Write the document (autorun:writeDoc creates the folder if needed) - const result = await window.maestro.autorun.writeDoc(autoRunPath, filename, doc.content); + const result = await window.maestro.autorun.writeDoc( + autoRunPath, + filename, + doc.content, + sshRemoteId + ); if (result.success) { const fullPath = `${autoRunPath}/${filename}`; diff --git a/src/renderer/services/inlineWizardDocumentGeneration.ts b/src/renderer/services/inlineWizardDocumentGeneration.ts index 5f84ab8e52..5bb53b1d06 100644 --- a/src/renderer/services/inlineWizardDocumentGeneration.ts +++ b/src/renderer/services/inlineWizardDocumentGeneration.ts @@ -678,7 +678,7 @@ async function saveDocument( autoRunFolderPath, filename, doc.content, - sshRemoteId || undefined + sshRemoteId ); if (!result.success) { @@ -721,7 +721,7 @@ export async function generateInlineDocuments( // Create a date-prefixed subfolder name: "YYYY-MM-DD-Feature-Name" (with -2, -3, etc. if needed) const baseFolderName = generateWizardFolderBaseName(projectName); const sshRemoteId = config.sessionSshRemoteConfig?.enabled - ? config.sessionSshRemoteConfig.remoteId + ? (config.sessionSshRemoteConfig.remoteId ?? undefined) : undefined; // Only attempt to check existing folders if we're local OR if listDocs supports remote @@ -843,7 +843,7 @@ export async function generateInlineDocuments( // Set up file watcher for real-time document streaming // The agent writes files directly, and we detect them here window.maestro.autorun - .watchFolder(subfolderPath) + .watchFolder(subfolderPath, sshRemoteId) .then((watchResult) => { if (watchResult.success) { console.log('[InlineWizardDocGen] Started watching folder:', subfolderPath); @@ -871,7 +871,7 @@ export async function generateInlineDocuments( const readWithRetry = async (retries = 3, delayMs = 200): Promise => { for (let attempt = 1; attempt <= retries; attempt++) { try { - const content = await window.maestro.fs.readFile(fullPath); + const content = await window.maestro.fs.readFile(fullPath, sshRemoteId); if (content && typeof content === 'string' && content.length > 0) { console.log( '[InlineWizardDocGen] File read successful:', @@ -1096,7 +1096,7 @@ export async function generateInlineDocuments( if (documents.length === 0 || totalTasks === 0) { // Check for files on disk (agent may have written directly) callbacks?.onProgress?.('Checking for documents on disk...'); - const diskDocs = await readDocumentsFromDisk(subfolderPath); + const diskDocs = await readDocumentsFromDisk(subfolderPath, sshRemoteId); if (diskDocs.length > 0) { console.log('[InlineWizardDocGen] Found documents on disk:', diskDocs.length); documents = diskDocs; @@ -1113,7 +1113,7 @@ export async function generateInlineDocuments( const savedDocuments: InlineGeneratedDocument[] = []; for (const doc of documents) { try { - const savedDoc = await saveDocument(subfolderPath, doc, sshRemoteId || undefined); + const savedDoc = await saveDocument(subfolderPath, doc, sshRemoteId); savedDocuments.push(savedDoc); callbacks?.onDocumentComplete?.(savedDoc); } catch (error) { @@ -1239,12 +1239,15 @@ async function createPlaybookForDocuments( * Note: Documents read from disk are treated as new (isUpdate: false) * since they were written directly by the agent. */ -async function readDocumentsFromDisk(autoRunFolderPath: string): Promise { +async function readDocumentsFromDisk( + autoRunFolderPath: string, + sshRemoteId?: string +): Promise { const documents: ParsedDocument[] = []; try { // List files in the Auto Run folder - const listResult = await window.maestro.autorun.listDocs(autoRunFolderPath); + const listResult = await window.maestro.autorun.listDocs(autoRunFolderPath, sshRemoteId); if (!listResult.success || !listResult.files) { return []; } @@ -1254,7 +1257,11 @@ async function readDocumentsFromDisk(autoRunFolderPath: string): Promise