From e947df56ae3a252b58d28188665378f26bc21cdd Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Thu, 11 Dec 2025 03:54:13 -0600 Subject: [PATCH] # CHANGES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added drag-and-drop reordering support for execution queue items 🎯 - Created new PreparingPlanScreen step in wizard workflow 🚀 - Enhanced document selector dropdown for multi-document support 📄 - Improved process monitor UI with two-line layout design 💅 - Fixed queue processing after interrupting running commands ⚡ - Added lightweight session timestamp fetching for activity graphs 📊 - Separated document generation from review in wizard flow 🔄 - Enhanced file creation tracking with retry logic 🔁 - Added visual feedback for drag operations with shimmer effect ✨ - Fixed tab header layout in right panel interface 🎨 --- .../components/ExecutionQueueBrowser.test.tsx | 308 +++++++ .../components/Wizard/WizardContext.test.tsx | 25 +- .../Wizard/WizardIntegration.test.tsx | 29 +- .../Wizard/WizardKeyboardNavigation.test.tsx | 6 +- src/main/index.ts | 59 ++ src/main/preload.ts | 3 + src/renderer/App.tsx | 216 ++++- .../components/AgentSessionsBrowser.tsx | 21 +- .../components/ExecutionQueueBrowser.tsx | 443 ++++++++-- src/renderer/components/ProcessMonitor.tsx | 136 +-- src/renderer/components/QuickActionsModal.tsx | 15 +- src/renderer/components/RightPanel.tsx | 16 +- .../components/Wizard/MaestroWizard.tsx | 19 +- .../components/Wizard/WizardContext.tsx | 9 +- .../components/Wizard/WizardResumeModal.tsx | 8 +- .../Wizard/screens/AgentSelectionScreen.tsx | 15 +- .../Wizard/screens/ConversationScreen.tsx | 10 +- .../screens/DirectorySelectionScreen.tsx | 25 +- .../Wizard/screens/PhaseReviewScreen.tsx | 822 ++++-------------- .../components/Wizard/screens/index.ts | 1 + .../Wizard/services/phaseGenerator.ts | 132 ++- src/renderer/global.d.ts | 1 + src/renderer/index.css | 10 + 23 files changed, 1432 insertions(+), 897 deletions(-) diff --git a/src/__tests__/renderer/components/ExecutionQueueBrowser.test.tsx b/src/__tests__/renderer/components/ExecutionQueueBrowser.test.tsx index 19fddf21..13c48d0a 100644 --- a/src/__tests__/renderer/components/ExecutionQueueBrowser.test.tsx +++ b/src/__tests__/renderer/components/ExecutionQueueBrowser.test.tsx @@ -1564,4 +1564,312 @@ describe('ExecutionQueueBrowser', () => { expect(initialOnClose).not.toHaveBeenCalled(); }); }); + + describe('drag and drop reordering', () => { + let mockOnReorderItems: ReturnType; + + beforeEach(() => { + mockOnReorderItems = vi.fn(); + }); + + it('should not enable drag when onReorderItems is not provided', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + const { container } = render( + + ); + + // Items should not have grab cursor when onReorderItems is not provided + const itemRows = container.querySelectorAll('.group.select-none'); + itemRows.forEach(row => { + expect(row).not.toHaveStyle({ cursor: 'grab' }); + }); + }); + + it('should not enable drag when session has only one item', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [createQueuedItem({ id: 'item-1' })] + }); + const { container } = render( + + ); + + // Single item should not have grab cursor + const itemRow = container.querySelector('.group.select-none'); + expect(itemRow).not.toHaveStyle({ cursor: 'grab' }); + }); + + it('should enable drag when onReorderItems is provided and session has multiple items', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + const { container } = render( + + ); + + // Items should have grab cursor + const itemRows = container.querySelectorAll('.group.select-none'); + itemRows.forEach(row => { + expect(row).toHaveStyle({ cursor: 'grab' }); + }); + }); + + it('should show drag handle indicator when draggable', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + const { container } = render( + + ); + + // Find the first item row + const itemRow = container.querySelector('.group.select-none'); + expect(itemRow).not.toBeNull(); + + // Drag handle should exist (it's just hidden until hover) + const dragHandle = container.querySelector('.absolute.left-1'); + expect(dragHandle).toBeInTheDocument(); + }); + + it('should render drop zones between items when draggable', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }), + createQueuedItem({ id: 'item-3' }) + ] + }); + const { container } = render( + + ); + + // Should have drop zones: before item 1, before item 2, before item 3, and after item 3 + const dropZones = container.querySelectorAll('.relative.h-1'); + expect(dropZones.length).toBe(4); // n+1 drop zones for n items + }); + + it('should not initiate drag when clicking on remove button', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + render( + + ); + + // Get all remove buttons (there should be 2) + const removeButtons = screen.getAllByTitle('Remove from queue'); + expect(removeButtons.length).toBe(2); + + // Click the first remove button + fireEvent.click(removeButtons[0]); + + // onRemoveItem should be called, not onReorderItems + expect(mockOnRemoveItem).toHaveBeenCalledWith('active-session', 'item-1'); + expect(mockOnReorderItems).not.toHaveBeenCalled(); + }); + + it('should enable drag for sessions in global view', () => { + const session1 = createSession({ + id: 'session-1', + name: 'Project One', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + const session2 = createSession({ + id: 'session-2', + name: 'Project Two', + executionQueue: [ + createQueuedItem({ id: 'item-3' }), + createQueuedItem({ id: 'item-4' }) + ] + }); + const { container } = render( + + ); + + // Switch to global view + const allButton = screen.getByText('All Projects').closest('button'); + fireEvent.click(allButton!); + + // All items should have grab cursor + const itemRows = container.querySelectorAll('.group.select-none'); + expect(itemRows.length).toBe(4); + itemRows.forEach(row => { + expect(row).toHaveStyle({ cursor: 'grab' }); + }); + }); + + it('should have correct number of drop zones in global view', () => { + const session1 = createSession({ + id: 'session-1', + name: 'Project One', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + const session2 = createSession({ + id: 'session-2', + name: 'Project Two', + executionQueue: [ + createQueuedItem({ id: 'item-3' }), + createQueuedItem({ id: 'item-4' }) + ] + }); + const { container } = render( + + ); + + // Switch to global view + const allButton = screen.getByText('All Projects').closest('button'); + fireEvent.click(allButton!); + + // Should have drop zones for each session: 3 for session1 (2 items + 1 after) + 3 for session2 + const dropZones = container.querySelectorAll('.relative.h-1'); + expect(dropZones.length).toBe(6); + }); + + it('should not show drag handle when session has only one item', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [createQueuedItem({ id: 'item-1' })] + }); + const { container } = render( + + ); + + // Drag handle should not exist for single item + const dragHandle = container.querySelector('.absolute.left-1'); + expect(dragHandle).not.toBeInTheDocument(); + }); + + it('should show visual feedback on mousedown', () => { + const session = createSession({ + id: 'active-session', + executionQueue: [ + createQueuedItem({ id: 'item-1' }), + createQueuedItem({ id: 'item-2' }) + ] + }); + const { container } = render( + + ); + + const itemRow = container.querySelector('.group.select-none'); + expect(itemRow).not.toBeNull(); + + // Verify item has grab cursor before interaction + expect(itemRow).toHaveStyle({ cursor: 'grab' }); + }); + }); }); diff --git a/src/__tests__/renderer/components/Wizard/WizardContext.test.tsx b/src/__tests__/renderer/components/Wizard/WizardContext.test.tsx index 49b8eeef..ea18b0a8 100644 --- a/src/__tests__/renderer/components/Wizard/WizardContext.test.tsx +++ b/src/__tests__/renderer/components/Wizard/WizardContext.test.tsx @@ -81,7 +81,7 @@ function createMockDocument( describe('WizardContext', () => { describe('Constants', () => { it('has correct total steps', () => { - expect(WIZARD_TOTAL_STEPS).toBe(4); + expect(WIZARD_TOTAL_STEPS).toBe(5); }); it('has correct step index mapping', () => { @@ -89,7 +89,8 @@ describe('WizardContext', () => { 'agent-selection': 1, 'directory-selection': 2, 'conversation': 3, - 'phase-review': 4, + 'preparing-plan': 4, + 'phase-review': 5, }); }); @@ -98,7 +99,8 @@ describe('WizardContext', () => { 1: 'agent-selection', 2: 'directory-selection', 3: 'conversation', - 4: 'phase-review', + 4: 'preparing-plan', + 5: 'phase-review', }); }); @@ -327,6 +329,11 @@ describe('WizardContext', () => { }); expect(result.current.state.currentStep).toBe('conversation'); + act(() => { + result.current.nextStep(); + }); + expect(result.current.state.currentStep).toBe('preparing-plan'); + act(() => { result.current.nextStep(); }); @@ -370,6 +377,11 @@ describe('WizardContext', () => { result.current.goToStep('phase-review'); }); + act(() => { + result.current.previousStep(); + }); + expect(result.current.state.currentStep).toBe('preparing-plan'); + act(() => { result.current.previousStep(); }); @@ -416,9 +428,14 @@ describe('WizardContext', () => { expect(result.current.getCurrentStepNumber()).toBe(3); act(() => { - result.current.goToStep('phase-review'); + result.current.goToStep('preparing-plan'); }); expect(result.current.getCurrentStepNumber()).toBe(4); + + act(() => { + result.current.goToStep('phase-review'); + }); + expect(result.current.getCurrentStepNumber()).toBe(5); }); }); }); diff --git a/src/__tests__/renderer/components/Wizard/WizardIntegration.test.tsx b/src/__tests__/renderer/components/Wizard/WizardIntegration.test.tsx index f757fc9a..6c32671b 100644 --- a/src/__tests__/renderer/components/Wizard/WizardIntegration.test.tsx +++ b/src/__tests__/renderer/components/Wizard/WizardIntegration.test.tsx @@ -310,7 +310,7 @@ describe('Wizard Integration Tests', () => { await waitFor(() => { expect(screen.getByText('Create a Maestro Agent')).toBeInTheDocument(); - expect(screen.getByText('Step 1 of 4')).toBeInTheDocument(); + expect(screen.getByText('Step 1 of 5')).toBeInTheDocument(); }); }); @@ -362,15 +362,19 @@ describe('Wizard Integration Tests', () => { it('should track step progress through navigation', async () => { function TestWrapper() { - const { openWizard, state, goToStep, setSelectedAgent, setDirectoryPath } = useWizard(); + const { openWizard, state, goToStep, setSelectedAgent, setDirectoryPath, setGeneratedDocuments } = useWizard(); React.useEffect(() => { if (!state.isOpen) { setSelectedAgent('claude-code'); setDirectoryPath('/test/path'); + // Set generated documents so PhaseReviewScreen doesn't redirect back + setGeneratedDocuments([ + { filename: 'Phase-01-Test.md', content: '# Test\n- [ ] Task 1', taskCount: 1 } + ]); openWizard(); } - }, [openWizard, state.isOpen, setSelectedAgent, setDirectoryPath]); + }, [openWizard, state.isOpen, setSelectedAgent, setDirectoryPath, setGeneratedDocuments]); return ( <> @@ -391,14 +395,14 @@ describe('Wizard Integration Tests', () => { renderWithProviders(); await waitFor(() => { - expect(screen.getByText('Step 1 of 4')).toBeInTheDocument(); + expect(screen.getByText('Step 1 of 5')).toBeInTheDocument(); }); // Navigate to step 2 fireEvent.click(screen.getByTestId('go-step-2')); await waitFor(() => { - expect(screen.getByText('Step 2 of 4')).toBeInTheDocument(); + expect(screen.getByText('Step 2 of 5')).toBeInTheDocument(); expect(screen.getByText('Choose Project Directory')).toBeInTheDocument(); }); @@ -406,16 +410,16 @@ describe('Wizard Integration Tests', () => { fireEvent.click(screen.getByTestId('go-step-3')); await waitFor(() => { - expect(screen.getByText('Step 3 of 4')).toBeInTheDocument(); + expect(screen.getByText('Step 3 of 5')).toBeInTheDocument(); expect(screen.getByText('Project Discovery')).toBeInTheDocument(); }); - // Navigate to step 4 + // Navigate to step 5 (phase-review is now step 5) fireEvent.click(screen.getByTestId('go-step-4')); await waitFor(() => { - expect(screen.getByText('Step 4 of 4')).toBeInTheDocument(); - expect(screen.getByText('Review Your Action Plan')).toBeInTheDocument(); + expect(screen.getByText('Step 5 of 5')).toBeInTheDocument(); + expect(screen.getByText('Review Your Action Plans')).toBeInTheDocument(); }); }); @@ -443,13 +447,14 @@ describe('Wizard Integration Tests', () => { await waitFor(() => { const progressDots = screen.getAllByLabelText(/step \d+/i); - expect(progressDots).toHaveLength(4); + expect(progressDots).toHaveLength(5); // Steps 1 and 2 should be completed, step 3 should be current expect(progressDots[0]).toHaveAttribute('aria-label', 'Step 1 (completed - click to go back)'); expect(progressDots[1]).toHaveAttribute('aria-label', 'Step 2 (completed - click to go back)'); expect(progressDots[2]).toHaveAttribute('aria-label', 'Step 3 (current)'); expect(progressDots[3]).toHaveAttribute('aria-label', 'Step 4'); + expect(progressDots[4]).toHaveAttribute('aria-label', 'Step 5'); }); }); }); @@ -728,7 +733,7 @@ describe('Wizard Integration Tests', () => { // Should show saved state info await waitFor(() => { expect(screen.getByText('Resume Setup?')).toBeInTheDocument(); - expect(screen.getByText('Step 3 of 4')).toBeInTheDocument(); + expect(screen.getByText('Step 3 of 5')).toBeInTheDocument(); expect(screen.getByText('Test Project')).toBeInTheDocument(); expect(screen.getByText('/saved/project/path')).toBeInTheDocument(); // The modal shows "X messages exchanged (Y% confidence)" @@ -1266,7 +1271,7 @@ describe('Wizard Integration Tests', () => { renderWithProviders(); await waitFor(() => { - expect(screen.getByText('Review Your Action Plan')).toBeInTheDocument(); + expect(screen.getByText('Review Your Action Plans')).toBeInTheDocument(); }); }); diff --git a/src/__tests__/renderer/components/Wizard/WizardKeyboardNavigation.test.tsx b/src/__tests__/renderer/components/Wizard/WizardKeyboardNavigation.test.tsx index 2e6d11a7..8ebea8fb 100644 --- a/src/__tests__/renderer/components/Wizard/WizardKeyboardNavigation.test.tsx +++ b/src/__tests__/renderer/components/Wizard/WizardKeyboardNavigation.test.tsx @@ -669,12 +669,12 @@ describe('Wizard Keyboard Navigation', () => { renderWithProviders(); await waitFor(() => { - expect(screen.getByText('Step 2 of 4')).toBeInTheDocument(); + expect(screen.getByText('Step 2 of 5')).toBeInTheDocument(); }); - // Should show 4 progress dots + // Should show 5 progress dots const progressDots = screen.getAllByLabelText(/step \d+/i); - expect(progressDots).toHaveLength(4); + expect(progressDots).toHaveLength(5); // Step 1 should be completed, step 2 should be current expect(progressDots[0]).toHaveAttribute('aria-label', 'Step 1 (completed - click to go back)'); diff --git a/src/main/index.ts b/src/main/index.ts index ea2efa36..18034a00 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2908,6 +2908,65 @@ function setupIpcHandlers() { } }); + // Get all session timestamps for activity graph (lightweight, from cache or quick scan) + ipcMain.handle('claude:getSessionTimestamps', async (_event, projectPath: string) => { + try { + // First try to get from cache + const cache = await loadStatsCache(projectPath); + if (cache && Object.keys(cache.sessions).length > 0) { + // Return timestamps from cache + const timestamps = Object.values(cache.sessions) + .map(s => s.oldestTimestamp) + .filter((t): t is string => t !== null); + return { timestamps }; + } + + // Fall back to quick scan of session files (just read first line for timestamp) + const homeDir = os.homedir(); + const claudeProjectsDir = path.join(homeDir, '.claude', 'projects'); + const encodedPath = encodeClaudeProjectPath(projectPath); + const projectDir = path.join(claudeProjectsDir, encodedPath); + + try { + await fs.access(projectDir); + } catch { + return { timestamps: [] }; + } + + const files = await fs.readdir(projectDir); + const sessionFiles = files.filter(f => f.endsWith('.jsonl')); + + const timestamps: string[] = []; + await Promise.all( + sessionFiles.map(async (filename) => { + const filePath = path.join(projectDir, filename); + try { + // Read only first few KB to get the timestamp + const handle = await fs.open(filePath, 'r'); + const buffer = Buffer.alloc(1024); + await handle.read(buffer, 0, 1024, 0); + await handle.close(); + + const firstLine = buffer.toString('utf-8').split('\n')[0]; + if (firstLine) { + const entry = JSON.parse(firstLine); + if (entry.timestamp) { + timestamps.push(entry.timestamp); + } + } + } catch { + // Skip files that can't be read + } + }) + ); + + return { timestamps }; + } catch (error) { + logger.error('Error getting session timestamps', 'ClaudeSessions', error); + return { timestamps: [] }; + } + }); + // Get global stats across ALL Claude projects (uses cache for speed) // Only recalculates stats for new or modified session files ipcMain.handle('claude:getGlobalStats', async () => { diff --git a/src/main/preload.ts b/src/main/preload.ts index 2b430f3c..9cc3fbaf 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -412,6 +412,9 @@ contextBridge.exposeInMainWorld('maestro', { // Get aggregate stats for all sessions in a project (streams progressive updates) getProjectStats: (projectPath: string) => ipcRenderer.invoke('claude:getProjectStats', projectPath), + // Get all session timestamps for activity graph (lightweight) + getSessionTimestamps: (projectPath: string) => + ipcRenderer.invoke('claude:getSessionTimestamps', projectPath) as Promise<{ timestamps: string[] }>, onProjectStatsUpdate: (callback: (stats: { projectPath: string; totalSessions: number; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 684614ae..785b884a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -19,7 +19,7 @@ import { MainPanel } from './components/MainPanel'; import { ProcessMonitor } from './components/ProcessMonitor'; import { GitDiffViewer } from './components/GitDiffViewer'; import { GitLogViewer } from './components/GitLogViewer'; -import { BatchRunnerModal } from './components/BatchRunnerModal'; +import { BatchRunnerModal, DEFAULT_BATCH_PROMPT } from './components/BatchRunnerModal'; import { TabSwitcherModal } from './components/TabSwitcherModal'; import { PromptComposerModal } from './components/PromptComposerModal'; import { ExecutionQueueBrowser } from './components/ExecutionQueueBrowser'; @@ -134,6 +134,7 @@ export default function MaestroConsole() { clearResumeState, completeWizard, closeWizard: closeWizardModal, + goToStep: wizardGoToStep, } = useWizard(); // --- SETTINGS (from useSettings hook) --- @@ -4255,6 +4256,29 @@ export default function MaestroConsole() { // Focus input setActiveFocus('main'); setTimeout(() => inputRef.current?.focus(), 100); + + // Auto-start the batch run with the first document that has tasks + // This is the core purpose of the onboarding wizard - get the user's first Auto Run going + const firstDocWithTasks = generatedDocuments.find(doc => doc.taskCount > 0); + if (firstDocWithTasks && autoRunFolderPath) { + // Create batch config for single document run + const batchConfig: BatchRunConfig = { + documents: [{ + id: generateId(), + filename: firstDocWithTasks.filename.replace(/\.md$/, ''), + resetOnCompletion: false, + isDuplicate: false, + }], + prompt: DEFAULT_BATCH_PROMPT, + loopEnabled: false, + }; + + // Small delay to ensure session state is fully propagated before starting batch + setTimeout(() => { + console.log('[Wizard] Auto-starting batch run with first document:', firstDocWithTasks.filename); + startBatchRun(newId, batchConfig, autoRunFolderPath); + }, 500); + } }, [ wizardState, defaultSaveToHistory, @@ -4266,6 +4290,7 @@ export default function MaestroConsole() { setActiveRightTab, setTourOpen, setActiveFocus, + startBatchRun, ]); const toggleInputMode = () => { @@ -5641,9 +5666,79 @@ export default function MaestroConsole() { // Send interrupt signal (Ctrl+C) await window.maestro.process.interrupt(targetSessionId); - // Set state to idle with full cleanup + // Check if there are queued items to process after interrupt + const currentSession = sessionsRef.current.find(s => s.id === activeSession.id); + let queuedItemToProcess: { sessionId: string; item: QueuedItem } | null = null; + + if (currentSession && currentSession.executionQueue.length > 0) { + queuedItemToProcess = { + sessionId: activeSession.id, + item: currentSession.executionQueue[0] + }; + } + + // Set state to idle with full cleanup, or process next queued item setSessions(prev => prev.map(s => { if (s.id !== activeSession.id) return s; + + // If there are queued items, start processing the next one + if (s.executionQueue.length > 0) { + const [nextItem, ...remainingQueue] = s.executionQueue; + const targetTab = s.aiTabs.find(tab => tab.id === nextItem.tabId) || getActiveTab(s); + + if (!targetTab) { + return { + ...s, + state: 'busy' as SessionState, + busySource: 'ai', + executionQueue: remainingQueue, + thinkingStartTime: Date.now(), + currentCycleTokens: 0, + currentCycleBytes: 0 + }; + } + + // Set the interrupted tab to idle, and the target tab for queued item to busy + let updatedAiTabs = s.aiTabs.map(tab => { + if (tab.id === targetTab.id) { + return { ...tab, state: 'busy' as const, thinkingStartTime: Date.now() }; + } + // Set any other busy tabs to idle (they were interrupted) + if (tab.state === 'busy') { + return { ...tab, state: 'idle' as const, thinkingStartTime: undefined }; + } + return tab; + }); + + // For message items, add a log entry to the target tab + if (nextItem.type === 'message' && nextItem.text) { + const logEntry: LogEntry = { + id: generateId(), + timestamp: Date.now(), + source: 'user', + text: nextItem.text, + images: nextItem.images + }; + updatedAiTabs = updatedAiTabs.map(tab => + tab.id === targetTab.id + ? { ...tab, logs: [...tab.logs, logEntry] } + : tab + ); + } + + return { + ...s, + state: 'busy' as SessionState, + busySource: 'ai', + aiTabs: updatedAiTabs, + executionQueue: remainingQueue, + thinkingStartTime: Date.now(), + currentCycleTokens: 0, + currentCycleBytes: 0 + }; + } + + // No queued items, just go to idle return { ...s, state: 'idle', @@ -5651,6 +5746,13 @@ export default function MaestroConsole() { thinkingStartTime: undefined }; })); + + // Process the queued item after state update + if (queuedItemToProcess) { + setTimeout(() => { + processQueuedItem(queuedItemToProcess!.sessionId, queuedItemToProcess!.item); + }, 0); + } } catch (error) { console.error('Failed to interrupt process:', error); @@ -5670,23 +5772,113 @@ export default function MaestroConsole() { source: 'system', text: 'Process forcefully terminated' }; + + // Check if there are queued items to process after kill + const currentSessionForKill = sessionsRef.current.find(s => s.id === activeSession.id); + let queuedItemAfterKill: { sessionId: string; item: QueuedItem } | null = null; + + if (currentSessionForKill && currentSessionForKill.executionQueue.length > 0) { + queuedItemAfterKill = { + sessionId: activeSession.id, + item: currentSessionForKill.executionQueue[0] + }; + } + setSessions(prev => prev.map(s => { if (s.id !== activeSession.id) return s; + + // Add kill log to the appropriate place + let updatedSession = { ...s }; if (currentMode === 'ai') { const tab = getActiveTab(s); - if (!tab) return { ...s, state: 'idle', busySource: undefined, thinkingStartTime: undefined }; + if (tab) { + updatedSession.aiTabs = s.aiTabs.map(t => + t.id === tab.id ? { ...t, logs: [...t.logs, killLog] } : t + ); + } + } else { + updatedSession.shellLogs = [...s.shellLogs, killLog]; + } + + // If there are queued items, start processing the next one + if (s.executionQueue.length > 0) { + const [nextItem, ...remainingQueue] = s.executionQueue; + const targetTab = s.aiTabs.find(tab => tab.id === nextItem.tabId) || getActiveTab(s); + + if (!targetTab) { + return { + ...updatedSession, + state: 'busy' as SessionState, + busySource: 'ai', + executionQueue: remainingQueue, + thinkingStartTime: Date.now(), + currentCycleTokens: 0, + currentCycleBytes: 0 + }; + } + + // Set tabs appropriately + let updatedAiTabs = updatedSession.aiTabs.map(tab => { + if (tab.id === targetTab.id) { + return { ...tab, state: 'busy' as const, thinkingStartTime: Date.now() }; + } + if (tab.state === 'busy') { + return { ...tab, state: 'idle' as const, thinkingStartTime: undefined }; + } + return tab; + }); + + // For message items, add a log entry to the target tab + if (nextItem.type === 'message' && nextItem.text) { + const logEntry: LogEntry = { + id: generateId(), + timestamp: Date.now(), + source: 'user', + text: nextItem.text, + images: nextItem.images + }; + updatedAiTabs = updatedAiTabs.map(tab => + tab.id === targetTab.id + ? { ...tab, logs: [...tab.logs, logEntry] } + : tab + ); + } + return { - ...s, + ...updatedSession, + state: 'busy' as SessionState, + busySource: 'ai', + aiTabs: updatedAiTabs, + executionQueue: remainingQueue, + thinkingStartTime: Date.now(), + currentCycleTokens: 0, + currentCycleBytes: 0 + }; + } + + // No queued items, just go to idle + if (currentMode === 'ai') { + const tab = getActiveTab(s); + if (!tab) return { ...updatedSession, state: 'idle', busySource: undefined, thinkingStartTime: undefined }; + return { + ...updatedSession, state: 'idle', busySource: undefined, thinkingStartTime: undefined, - aiTabs: s.aiTabs.map(t => - t.id === tab.id ? { ...t, state: 'idle' as const, thinkingStartTime: undefined, logs: [...t.logs, killLog] } : t + aiTabs: updatedSession.aiTabs.map(t => + t.id === tab.id ? { ...t, state: 'idle' as const, thinkingStartTime: undefined } : t ) }; } - return { ...s, shellLogs: [...s.shellLogs, killLog], state: 'idle', busySource: undefined, thinkingStartTime: undefined }; + return { ...updatedSession, state: 'idle', busySource: undefined, thinkingStartTime: undefined }; })); + + // Process the queued item after state update + if (queuedItemAfterKill) { + setTimeout(() => { + processQueuedItem(queuedItemAfterKill!.sessionId, queuedItemAfterKill!.item); + }, 0); + } } catch (killError) { console.error('Failed to kill process:', killError); const errorLog: LogEntry = { @@ -6455,6 +6647,7 @@ export default function MaestroConsole() { onToggleMarkdownRawMode={() => setMarkdownRawMode(!markdownRawMode)} setUpdateCheckModalOpen={setUpdateCheckModalOpen} openWizard={openWizardModal} + wizardGoToStep={wizardGoToStep} /> )} {lightboxImage && ( @@ -7336,6 +7529,15 @@ export default function MaestroConsole() { onSwitchSession={(sessionId) => { setActiveSessionId(sessionId); }} + onReorderItems={(sessionId, fromIndex, toIndex) => { + setSessions(prev => prev.map(s => { + if (s.id !== sessionId) return s; + const queue = [...s.executionQueue]; + const [removed] = queue.splice(fromIndex, 1); + queue.splice(toIndex, 0, removed); + return { ...s, executionQueue: queue }; + })); + }} /> {/* Old settings modal removed - using new SettingsModal component below */} diff --git a/src/renderer/components/AgentSessionsBrowser.tsx b/src/renderer/components/AgentSessionsBrowser.tsx index bf9dde9e..3d8c4c4a 100644 --- a/src/renderer/components/AgentSessionsBrowser.tsx +++ b/src/renderer/components/AgentSessionsBrowser.tsx @@ -731,22 +731,11 @@ export function AgentSessionsBrowser({ return date.toLocaleDateString(); }; - // Stable activity entries for the graph - only updates when switching to graph view - // This prevents the graph from re-rendering during pagination - const [activityEntries, setActivityEntries] = useState([]); - const prevShowSearchPanelRef = useRef(showSearchPanel); - - useEffect(() => { - const switchingToGraph = prevShowSearchPanelRef.current && !showSearchPanel; - prevShowSearchPanelRef.current = showSearchPanel; - - // Update entries when: switching TO graph view, OR initial load completes while graph is shown - if ((switchingToGraph || (!loading && activityEntries.length === 0)) && sessions.length > 0) { - setActivityEntries(sessions.map(s => ({ - timestamp: s.modifiedAt, - }))); - } - }, [showSearchPanel, loading, sessions, activityEntries.length]); + // Activity entries for the graph - derived from filteredSessions so filters apply + // Since we auto-load all sessions in background, this will eventually include all data + const activityEntries = useMemo(() => { + return filteredSessions.map(s => ({ timestamp: s.modifiedAt })); + }, [filteredSessions]); // Handle activity graph bar click - scroll to first session in that time range const handleGraphBarClick = useCallback((bucketStart: number, bucketEnd: number) => { diff --git a/src/renderer/components/ExecutionQueueBrowser.tsx b/src/renderer/components/ExecutionQueueBrowser.tsx index 8affa301..735c1910 100644 --- a/src/renderer/components/ExecutionQueueBrowser.tsx +++ b/src/renderer/components/ExecutionQueueBrowser.tsx @@ -12,6 +12,18 @@ interface ExecutionQueueBrowserProps { theme: Theme; onRemoveItem: (sessionId: string, itemId: string) => void; onSwitchSession: (sessionId: string) => void; + onReorderItems?: (sessionId: string, fromIndex: number, toIndex: number) => void; +} + +interface DragState { + sessionId: string; + itemId: string; + fromIndex: number; +} + +interface DropIndicator { + sessionId: string; + index: number; } /** @@ -25,13 +37,49 @@ export function ExecutionQueueBrowser({ activeSessionId, theme, onRemoveItem, - onSwitchSession + onSwitchSession, + onReorderItems }: ExecutionQueueBrowserProps) { const [viewMode, setViewMode] = useState<'current' | 'global'>('current'); + const [dragState, setDragState] = useState(null); + const [dropIndicator, setDropIndicator] = useState(null); const { registerLayer, unregisterLayer } = useLayerStack(); const onCloseRef = useRef(onClose); onCloseRef.current = onClose; + // Drag handlers + const handleDragStart = (sessionId: string, itemId: string, index: number) => { + setDragState({ sessionId, itemId, fromIndex: index }); + }; + + const handleDragOver = (sessionId: string, index: number) => { + // Allow dropping within the same session only (cross-session would require moving items) + if (dragState && dragState.sessionId === sessionId) { + setDropIndicator({ sessionId, index }); + } + }; + + const handleDragEnd = () => { + if (dragState && dropIndicator && onReorderItems) { + const { sessionId, fromIndex } = dragState; + const toIndex = dropIndicator.index; + + // Only reorder if indices differ + if (fromIndex !== toIndex && fromIndex !== toIndex - 1) { + // Adjust toIndex if dropping after the dragged item + const adjustedToIndex = toIndex > fromIndex ? toIndex - 1 : toIndex; + onReorderItems(sessionId, fromIndex, adjustedToIndex); + } + } + setDragState(null); + setDropIndicator(null); + }; + + const handleDragCancel = () => { + setDragState(null); + setDropIndicator(null); + }; + // Register with layer stack for proper escape handling useEffect(() => { if (isOpen) { @@ -182,20 +230,45 @@ export function ExecutionQueueBrowser({ )} {/* Queue Items */} -
+
{session.executionQueue?.map((item, index) => ( - onRemoveItem(session.id, item.id)} - onSwitchToSession={() => { - onSwitchSession(session.id); - onClose(); - }} - /> + + {/* Drop indicator before this item */} + handleDragOver(session.id, index)} + /> + onRemoveItem(session.id, item.id)} + onSwitchToSession={() => { + onSwitchSession(session.id); + onClose(); + }} + isDragging={dragState?.itemId === item.id} + canDrag={!!onReorderItems && (session.executionQueue?.length || 0) > 1} + isAnyDragging={!!dragState} + onDragStart={() => handleDragStart(session.id, item.id, index)} + onDragEnd={handleDragEnd} + onDragCancel={handleDragCancel} + /> + ))} + {/* Final drop zone after all items */} + handleDragOver(session.id, session.executionQueue?.length || 0)} + />
)) @@ -214,15 +287,62 @@ export function ExecutionQueueBrowser({ ); } +interface DropZoneProps { + theme: Theme; + isActive: boolean; + onDragOver: () => void; +} + +function DropZone({ theme, isActive, onDragOver }: DropZoneProps) { + return ( +
+
+
+ ); +} + interface QueueItemRowProps { item: QueuedItem; index: number; theme: Theme; onRemove: () => void; onSwitchToSession: () => void; + isDragging?: boolean; + canDrag?: boolean; + isAnyDragging?: boolean; + onDragStart?: () => void; + onDragEnd?: () => void; + onDragCancel?: () => void; } -function QueueItemRow({ item, index, theme, onRemove, onSwitchToSession }: QueueItemRowProps) { +function QueueItemRow({ + item, + index, + theme, + onRemove, + onSwitchToSession, + isDragging, + canDrag, + isAnyDragging, + onDragStart, + onDragEnd, + onDragCancel +}: QueueItemRowProps) { + const [isPressed, setIsPressed] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const pressTimerRef = useRef(null); + const isDraggingRef = useRef(false); + const isCommand = item.type === 'command'; const displayText = isCommand ? item.command @@ -234,88 +354,249 @@ function QueueItemRow({ item, index, theme, onRemove, onSwitchToSession }: Queue const minutes = Math.floor(timeSinceQueued / 60000); const timeDisplay = minutes < 1 ? 'Just now' : `${minutes}m ago`; + // Handle mouse down for drag initiation + const handleMouseDown = (e: React.MouseEvent) => { + if (!canDrag || e.button !== 0) return; + + // Don't start drag if clicking on buttons + if ((e.target as HTMLElement).closest('button')) return; + + setIsPressed(true); + + // Small delay before initiating drag to allow for click detection + pressTimerRef.current = setTimeout(() => { + isDraggingRef.current = true; + onDragStart?.(); + }, 150); + }; + + const handleMouseUp = () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + pressTimerRef.current = null; + } + + if (isDraggingRef.current) { + onDragEnd?.(); + isDraggingRef.current = false; + } + + setIsPressed(false); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + pressTimerRef.current = null; + } + + // Don't cancel drag on leave - let mouse up handle it + if (!isDraggingRef.current) { + setIsPressed(false); + } + }; + + // Cleanup timer on unmount + useEffect(() => { + return () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current); + } + }; + }, []); + + // Handle escape key to cancel drag + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isDragging) { + onDragCancel?.(); + isDraggingRef.current = false; + setIsPressed(false); + } + }; + + if (isDragging) { + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('mouseup', handleMouseUp); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('mouseup', handleMouseUp); + }; + } + }, [isDragging, onDragCancel]); + + // Visual states + const showDragReady = canDrag && isHovered && !isDragging && !isAnyDragging; + const showGrabbed = isPressed || isDragging; + const isDimmed = isAnyDragging && !isDragging; + return (
- {/* Position indicator */} - setIsHovered(true)} + onMouseLeave={handleMouseLeave} > - #{index + 1} - - - {/* Type icon */} -
- {isCommand ? ( - - ) : ( - - )} -
- - {/* Content */} -
-
- {item.tabName && ( - - )} - - - {timeDisplay} - -
-
+
+
+
+
+
+
+
+
+
+
+
+
+ )} + + {/* Position indicator */} + - {displayText} + #{index + 1} + + + {/* Type icon */} +
+ {isCommand ? ( + + ) : ( + + )}
- {isCommand && item.commandDescription && ( -
- {item.commandDescription} + + {/* Content */} +
+
+ {item.tabName && ( + + )} + + + {timeDisplay} +
- )} - {item.images && item.images.length > 0 && (
- + {item.images.length} image{item.images.length > 1 ? 's' : ''} + {displayText}
- )} + {isCommand && item.commandDescription && ( +
+ {item.commandDescription} +
+ )} + {item.images && item.images.length > 0 && ( +
+ + {item.images.length} image{item.images.length > 1 ? 's' : ''} +
+ )} +
+ + {/* Remove button */} +
- {/* Remove button */} - + {/* Shimmer effect when grabbed */} + {showGrabbed && ( +
+ )}
); } diff --git a/src/renderer/components/ProcessMonitor.tsx b/src/renderer/components/ProcessMonitor.tsx index 1a0da7db..76db3f4b 100644 --- a/src/renderer/components/ProcessMonitor.tsx +++ b/src/renderer/components/ProcessMonitor.tsx @@ -620,7 +620,7 @@ export function ProcessMonitor(props: ProcessMonitorProps) { ref={isSelected ? selectedNodeRef as React.RefObject : null} key={node.id} tabIndex={0} - className="px-4 py-2 flex items-center gap-2 cursor-default group" + className="px-4 py-1.5 cursor-default group" style={{ paddingLeft: `${paddingLeft}px`, color: theme.colors.textMain, @@ -632,75 +632,81 @@ export function ProcessMonitor(props: ProcessMonitorProps) { onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = `${theme.colors.accent}15`; }} onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = 'transparent'; }} > -
-
- {node.label} - {node.isAutoRun && ( + {/* First line: status dot, label, AUTO badge, kill button */} +
+
+
+ {node.label} + {node.isAutoRun && ( + + AUTO + + )} + {/* Kill button */} + {node.processSessionId && ( + + )} +
+ {/* Second line: Claude session ID, PID, runtime, status - indented */} +
+ {node.claudeSessionId && node.sessionId && onNavigateToSession && ( + + )} + {node.claudeSessionId && (!node.sessionId || !onNavigateToSession) && ( + + {node.claudeSessionId.substring(0, 8)}... + + )} + + PID: {node.pid} + + {node.startTime && ( + + {formatRuntime(node.startTime)} + + )} - AUTO + Running - )} - {node.claudeSessionId && node.sessionId && onNavigateToSession && ( - - )} - {node.claudeSessionId && (!node.sessionId || !onNavigateToSession) && ( - - {node.claudeSessionId.substring(0, 8)}... - - )} - - PID: {node.pid} - - {node.startTime && ( - - {formatRuntime(node.startTime)} - - )} - - Running - - {/* Kill button */} - {node.processSessionId && ( - - )} +
); } diff --git a/src/renderer/components/QuickActionsModal.tsx b/src/renderer/components/QuickActionsModal.tsx index 966cc703..d5d4260f 100644 --- a/src/renderer/components/QuickActionsModal.tsx +++ b/src/renderer/components/QuickActionsModal.tsx @@ -5,6 +5,7 @@ import { useLayerStack } from '../contexts/LayerStackContext'; import { MODAL_PRIORITIES } from '../constants/modalPriorities'; import { gitService } from '../services/git'; import { formatShortcutKeys } from '../utils/shortcutFormatter'; +import type { WizardStep } from './Wizard/WizardContext'; interface QuickAction { id: string; @@ -62,6 +63,7 @@ interface QuickActionsModalProps { onToggleMarkdownRawMode?: () => void; setUpdateCheckModalOpen?: (open: boolean) => void; openWizard?: () => void; + wizardGoToStep?: (step: WizardStep) => void; } export function QuickActionsModal(props: QuickActionsModalProps) { @@ -76,7 +78,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) { setShortcutsHelpOpen, setAboutModalOpen, setLogViewerOpen, setProcessMonitorOpen, setAgentSessionsOpen, setActiveClaudeSessionId, setGitDiffPreview, setGitLogOpen, onRenameTab, onToggleReadOnlyMode, onOpenTabSwitcher, tabShortcuts, isAiMode, setPlaygroundOpen, onRefreshGitFileState, - onDebugReleaseQueuedItem, markdownRawMode, onToggleMarkdownRawMode, setUpdateCheckModalOpen, openWizard + onDebugReleaseQueuedItem, markdownRawMode, onToggleMarkdownRawMode, setUpdateCheckModalOpen, openWizard, wizardGoToStep } = props; const [search, setSearch] = useState(''); @@ -319,6 +321,17 @@ export function QuickActionsModal(props: QuickActionsModalProps) { setQuickActionOpen(false); } }] : []), + ...(openWizard && wizardGoToStep ? [{ + id: 'debugWizardPhaseReview', + label: 'Debug: Wizard → Review Action Plans', + subtext: 'Jump directly to Phase Review step (requires existing Auto Run docs)', + action: () => { + openWizard(); + // Small delay to ensure wizard is open before navigating + setTimeout(() => wizardGoToStep('phase-review'), 50); + setQuickActionOpen(false); + } + }] : []), ]; const groupActions: QuickAction[] = [ diff --git a/src/renderer/components/RightPanel.tsx b/src/renderer/components/RightPanel.tsx index 49a1a4b1..71aaea10 100644 --- a/src/renderer/components/RightPanel.tsx +++ b/src/renderer/components/RightPanel.tsx @@ -213,14 +213,6 @@ export const RightPanel = forwardRef(function {/* Tab Header */}
- - {['files', 'history', 'autorun'].map(tab => ( ))} + +
{/* Tab Content */} diff --git a/src/renderer/components/Wizard/MaestroWizard.tsx b/src/renderer/components/Wizard/MaestroWizard.tsx index 50958320..e00c02ed 100644 --- a/src/renderer/components/Wizard/MaestroWizard.tsx +++ b/src/renderer/components/Wizard/MaestroWizard.tsx @@ -25,6 +25,7 @@ import { AgentSelectionScreen, DirectorySelectionScreen, ConversationScreen, + PreparingPlanScreen, PhaseReviewScreen, } from './screens'; @@ -58,8 +59,10 @@ function getStepTitle(step: WizardStep): string { return 'Choose Project Directory'; case 'conversation': return 'Project Discovery'; + case 'preparing-plan': + return 'Preparing Action Plans'; case 'phase-review': - return 'Review Your Action Plan'; + return 'Review Your Action Plans'; default: return 'Setup Wizard'; } @@ -99,8 +102,6 @@ export function MaestroWizard({ const wizardStartTimeRef = useRef(0); // Track if wizard start has been recorded for this open session const wizardStartedRef = useRef(false); - // Track if we resumed directly into phase-review (needs special handling) - const [resumedAtPhaseReview, setResumedAtPhaseReview] = useState(false); // State for screen transition animations // displayedStep is the step actually being rendered (lags behind currentStep during transitions) @@ -223,14 +224,10 @@ export function MaestroWizard({ // Determine if this is a fresh start or resume based on current step // If we're on step 1, it's a fresh start. Otherwise, it's a resume. if (getCurrentStepNumber() === 1) { - setResumedAtPhaseReview(false); if (onWizardStart) { onWizardStart(); } } else { - // Track if we resumed directly into phase-review - // This needs special handling to re-run the agent with resume context - setResumedAtPhaseReview(state.currentStep === 'phase-review'); if (onWizardResume) { onWizardResume(); } @@ -238,9 +235,8 @@ export function MaestroWizard({ } else if (!state.isOpen) { // Reset when wizard closes wizardStartedRef.current = false; - setResumedAtPhaseReview(false); } - }, [state.isOpen, state.currentStep, getCurrentStepNumber, onWizardStart, onWizardResume]); + }, [state.isOpen, getCurrentStepNumber, onWizardStart, onWizardResume]); // Announce step changes to screen readers useEffect(() => { @@ -283,6 +279,8 @@ export function MaestroWizard({ return ; case 'conversation': return ; + case 'preparing-plan': + return ; case 'phase-review': return ( {})} onWizardComplete={onWizardComplete} wizardStartTime={wizardStartTimeRef.current} - isResuming={resumedAtPhaseReview} /> ); default: return null; } - }, [displayedStep, theme, onLaunchSession, onWizardComplete, resumedAtPhaseReview]); + }, [displayedStep, theme, onLaunchSession, onWizardComplete]); // Don't render if wizard is not open if (!state.isOpen) { diff --git a/src/renderer/components/Wizard/WizardContext.tsx b/src/renderer/components/Wizard/WizardContext.tsx index 95e24831..81864847 100644 --- a/src/renderer/components/Wizard/WizardContext.tsx +++ b/src/renderer/components/Wizard/WizardContext.tsx @@ -25,12 +25,13 @@ export type WizardStep = | 'agent-selection' | 'directory-selection' | 'conversation' + | 'preparing-plan' | 'phase-review'; /** * Total number of steps in the wizard */ -export const WIZARD_TOTAL_STEPS = 4; +export const WIZARD_TOTAL_STEPS = 5; /** * Map step names to their numeric index (1-based for display) @@ -39,7 +40,8 @@ export const STEP_INDEX: Record = { 'agent-selection': 1, 'directory-selection': 2, 'conversation': 3, - 'phase-review': 4, + 'preparing-plan': 4, + 'phase-review': 5, }; /** @@ -49,7 +51,8 @@ export const INDEX_TO_STEP: Record = { 1: 'agent-selection', 2: 'directory-selection', 3: 'conversation', - 4: 'phase-review', + 4: 'preparing-plan', + 5: 'phase-review', }; /** diff --git a/src/renderer/components/Wizard/WizardResumeModal.tsx b/src/renderer/components/Wizard/WizardResumeModal.tsx index 042a2919..08f93406 100644 --- a/src/renderer/components/Wizard/WizardResumeModal.tsx +++ b/src/renderer/components/Wizard/WizardResumeModal.tsx @@ -11,7 +11,7 @@ import type { Theme, AgentConfig } from '../../types'; import { useLayerStack } from '../../contexts/LayerStackContext'; import { MODAL_PRIORITIES } from '../../constants/modalPriorities'; import type { SerializableWizardState, WizardStep } from './WizardContext'; -import { STEP_INDEX } from './WizardContext'; +import { STEP_INDEX, WIZARD_TOTAL_STEPS } from './WizardContext'; interface WizardResumeModalProps { theme: Theme; @@ -32,6 +32,8 @@ function getStepDescription(step: WizardStep): string { return 'Directory Selection'; case 'conversation': return 'Project Discovery'; + case 'preparing-plan': + return 'Preparing Action Plans'; case 'phase-review': return 'Phase Review'; default: @@ -43,7 +45,7 @@ function getStepDescription(step: WizardStep): string { * Get progress percentage based on step */ function getProgressPercentage(step: WizardStep): number { - return ((STEP_INDEX[step] - 1) / 3) * 100; + return ((STEP_INDEX[step] - 1) / (WIZARD_TOTAL_STEPS - 1)) * 100; } export function WizardResumeModal({ @@ -212,7 +214,7 @@ export function WizardResumeModal({ Progress - Step {STEP_INDEX[resumeState.currentStep]} of 4 + Step {STEP_INDEX[resumeState.currentStep]} of {WIZARD_TOTAL_STEPS}
@@ -527,7 +527,7 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX. className="text-2xl font-semibold mb-2" style={{ color: theme.colors.textMain }} > - Choose Your AI Assistant + Choose Your Provider

- {/* Spacer */} -
- {/* Section 2: Agent Grid */}
@@ -658,14 +655,11 @@ export function AgentSelectionScreen({ theme }: AgentSelectionScreenProps): JSX.
- {/* Spacer */} -
- {/* Section 3: Name Your Agent - Prominent */}
- {/* Flexible spacer to push footer down */} -
- {/* Section 4: Keyboard hints (footer) */}
getInitialQuestion()); const [errorRetryCount, setErrorRetryCount] = useState(0); const [streamingText, setStreamingText] = useState(''); const [fillerPhrase, setFillerPhrase] = useState(''); @@ -536,7 +538,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem initialQuestionAddedRef.current = true; addMessage({ role: 'assistant', - content: getInitialQuestion(), + content: initialQuestion, }); // Hide the direct JSX render immediately - the message is now in history setShowInitialQuestion(false); @@ -780,7 +782,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem {formatAgentName(state.agentName || '')}
- {getInitialQuestion()} + {initialQuestion}
@@ -866,7 +868,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem className="px-6 py-2.5 rounded-lg text-sm font-bold transition-all hover:scale-105" style={{ backgroundColor: theme.colors.success, - color: 'white', + color: theme.colors.bgMain, boxShadow: `0 4px 12px ${theme.colors.success}40`, }} > @@ -929,7 +931,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem + + {isOpen && documents.length > 1 && ( +
- Files Created ({files.length}) - -
-
- {files.map((file, index) => ( -
-
- - - {file.filename} - -
- - {formatFileSize(file.size)} - -
- ))} -
-
- ); -} - -/** - * Loading indicator with animated spinner and message - */ -function LoadingIndicator({ - message, - theme, - createdFiles = [], -}: { - message: string; - theme: Theme; - createdFiles?: CreatedFileInfo[]; -}): JSX.Element { - return ( -
- {/* Main loading content - pushed up from center */} -
- {/* Animated spinner */} -
-
- {/* Inner pulsing circle */} -
-
-
-
- - {/* Message */} -

- {message} -

- - {/* Subtitle */} -

- This may take a while. We're creating detailed task documents based on your project requirements. -

- - {/* Animated dots */} -
- {[0, 1, 2].map((i) => ( -
( + ))}
- - {/* Created files list */} - - - {/* Austin Fact */} - -
- - {/* Animation styles */} - -
- ); -} - -/** - * Error display with retry option - */ -function ErrorDisplay({ - error, - onRetry, - onSkip, - theme, -}: { - error: string; - onRetry: () => void; - onSkip: () => void; - theme: Theme; -}): JSX.Element { - return ( -
- {/* Error icon */} -
- - - -
- - {/* Error message */} -

- Generation Failed -

-

- {error} -

- - {/* Action buttons */} -
- - -
+ )}
); } @@ -1005,8 +755,8 @@ function DocumentEditor({
)} - {/* Content area */} -
+ {/* Content area - uses flex-1 to fill remaining space */} +
{mode === 'edit' ? (