diff --git a/docs/screenshots/symphony-history.png b/docs/screenshots/symphony-history.png new file mode 100644 index 00000000..bd5e5fdb Binary files /dev/null and b/docs/screenshots/symphony-history.png differ diff --git a/docs/screenshots/symphony-stats.png b/docs/screenshots/symphony-stats.png new file mode 100644 index 00000000..fc56e779 Binary files /dev/null and b/docs/screenshots/symphony-stats.png differ diff --git a/docs/symphony.md b/docs/symphony.md index b9c1c55d..ae013abc 100644 --- a/docs/symphony.md +++ b/docs/symphony.md @@ -92,20 +92,40 @@ Each active contribution shows: ### History Tab -Review completed contributions: -- PR links with merge status (Open or Merged) -- Completion date -- Documents processed count -- Total cost for the contribution -- Summary statistics at the top (total PRs, merged count, tokens, cost) +Review completed contributions with aggregate stats at a glance: + +![Contribution History](./screenshots/symphony-history.png) + +The header shows your totals: PRs created, merged count, tasks completed, tokens used, and dollar value donated. Each contribution card displays: +- **Issue and repository** — What you worked on +- **Merge status** — Whether the PR was merged +- **Completion date** — When you finished +- **PR link** — Direct link to the pull request +- **Detailed metrics** — Documents processed, tasks completed, tokens used, and cost ### Stats Tab -Track your overall impact: -- **Overview metrics**: Total contributions, merged PRs, tokens donated, estimated cost -- **Repositories contributed to**: Unique project count -- **Streak tracking**: Current and longest contribution streaks -- **Achievement badges**: Unlock achievements like "First Contribution", "Week Warrior", "Token Titan" +Track your overall impact and unlock achievements: + +![Symphony Stats](./screenshots/symphony-stats.png) + +**Summary cards** show your cumulative contributions: +- **Tokens Donated** — Total tokens contributed with dollar value +- **Time Contributed** — Hours spent and repositories helped +- **Streak** — Current and best contribution streaks + +**Achievements** reward milestones in your Symphony journey: + +| Achievement | Requirement | +|-------------|-------------| +| **First Steps** | Complete your first Symphony contribution | +| **Merged Melody** | Have a contribution merged | +| **Weekly Rhythm** | Maintain a 7-day contribution streak | +| **Harmony Seeker** | Complete 10 contributions | +| **Ensemble Player** | Contribute to 5 different repositories | +| **Virtuoso** | Complete 1000 tasks across all contributions | +| **Token Millionaire** | Donate over 10 million tokens | +| **Early Adopter** | Join Symphony in its first month | ## Session Integration @@ -192,6 +212,14 @@ Your project entry should include: Once merged, your project will appear in the Symphony Projects tab (registry cached for 2 hours, issues cached for 5 minutes). +## Available Issues + +Browse Symphony-ready issues on Maestro itself: + + + View all issues labeled `runmaestro.ai` — ready for AI-assisted contribution + + --- **Maestro Symphony — Advancing open source, together.** diff --git a/src/__tests__/main/ipc/handlers/system.test.ts b/src/__tests__/main/ipc/handlers/system.test.ts index 0a0f1fec..5b1bfa44 100644 --- a/src/__tests__/main/ipc/handlers/system.test.ts +++ b/src/__tests__/main/ipc/handlers/system.test.ts @@ -206,6 +206,7 @@ describe('system IPC handlers', () => { // Shell handlers 'shells:detect', 'shell:openExternal', + 'shell:trashItem', // Tunnel handlers 'tunnel:isCloudflaredInstalled', 'tunnel:start', diff --git a/src/__tests__/renderer/components/DeleteAgentConfirmModal.test.tsx b/src/__tests__/renderer/components/DeleteAgentConfirmModal.test.tsx new file mode 100644 index 00000000..12652cd9 --- /dev/null +++ b/src/__tests__/renderer/components/DeleteAgentConfirmModal.test.tsx @@ -0,0 +1,356 @@ +/** + * Tests for DeleteAgentConfirmModal component + * + * Tests the core behavior of the delete agent confirmation dialog: + * - Rendering with agent name, working directory, and three buttons + * - Button click handlers (Cancel, Confirm, Confirm and Erase) + * - Focus management + * - Layer stack integration + * - Accessibility + */ + +import React from 'react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { DeleteAgentConfirmModal } from '../../../renderer/components/DeleteAgentConfirmModal'; +import { LayerStackProvider } from '../../../renderer/contexts/LayerStackContext'; +import type { Theme } from '../../../renderer/types'; + +// Mock lucide-react +vi.mock('lucide-react', () => ({ + X: () => , + AlertTriangle: () => , + Trash2: ({ className, style }: { className?: string; style?: React.CSSProperties }) => ( + + ), +})); + +// Create a test theme +const testTheme: Theme = { + id: 'test-theme', + name: 'Test Theme', + mode: 'dark', + colors: { + bgMain: '#1e1e1e', + bgSidebar: '#252526', + bgActivity: '#333333', + textMain: '#d4d4d4', + textDim: '#808080', + accent: '#007acc', + accentDim: '#007acc80', + accentText: '#ffffff', + accentForeground: '#ffffff', + border: '#404040', + error: '#f14c4c', + warning: '#cca700', + success: '#89d185', + }, +}; + +// Helper to render with LayerStackProvider +const renderWithLayerStack = (ui: React.ReactElement) => { + return render({ui}); +}; + +describe('DeleteAgentConfirmModal', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('rendering', () => { + it('renders with agent name and three action buttons', () => { + renderWithLayerStack( + + ); + + expect(screen.getByText(/TestAgent/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Confirm and Erase' })).toBeInTheDocument(); + }); + + it('renders working directory path', () => { + renderWithLayerStack( + + ); + + expect(screen.getByText('/home/user/project')).toBeInTheDocument(); + }); + + it('renders explanatory text about Confirm and Erase', () => { + renderWithLayerStack( + + ); + + // Check the explanatory text (in a tag within a

) + expect(screen.getByText(/will also move the working directory to the trash/)).toBeInTheDocument(); + }); + + it('renders header with title and close button', () => { + renderWithLayerStack( + + ); + + expect(screen.getByText('Confirm Delete')).toBeInTheDocument(); + expect(screen.getByTestId('x-icon')).toBeInTheDocument(); + }); + + it('has correct ARIA attributes', () => { + renderWithLayerStack( + + ); + + const dialog = screen.getByRole('dialog'); + expect(dialog).toHaveAttribute('aria-modal', 'true'); + expect(dialog).toHaveAttribute('aria-label', 'Confirm Delete'); + }); + }); + + describe('focus management', () => { + it('focuses Confirm button on mount (not Confirm and Erase)', async () => { + renderWithLayerStack( + + ); + + await waitFor(() => { + expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Confirm' })); + }); + }); + }); + + describe('button handlers', () => { + it('calls onClose when X button is clicked', () => { + const onClose = vi.fn(); + renderWithLayerStack( + + ); + + const closeButton = screen.getByTestId('x-icon').closest('button'); + fireEvent.click(closeButton!); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('calls onClose when Cancel is clicked', () => { + const onClose = vi.fn(); + renderWithLayerStack( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('calls onConfirm then onClose when Confirm is clicked', () => { + const callOrder: string[] = []; + const onClose = vi.fn(() => callOrder.push('close')); + const onConfirm = vi.fn(() => callOrder.push('confirm')); + const onConfirmAndErase = vi.fn(); + + renderWithLayerStack( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Confirm' })); + expect(callOrder).toEqual(['confirm', 'close']); + expect(onConfirmAndErase).not.toHaveBeenCalled(); + }); + + it('calls onConfirmAndErase then onClose when Confirm and Erase is clicked', () => { + const callOrder: string[] = []; + const onClose = vi.fn(() => callOrder.push('close')); + const onConfirm = vi.fn(); + const onConfirmAndErase = vi.fn(() => callOrder.push('confirmAndErase')); + + renderWithLayerStack( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Confirm and Erase' })); + expect(callOrder).toEqual(['confirmAndErase', 'close']); + expect(onConfirm).not.toHaveBeenCalled(); + }); + + it('Cancel does not call onConfirm or onConfirmAndErase', () => { + const onConfirm = vi.fn(); + const onConfirmAndErase = vi.fn(); + renderWithLayerStack( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onConfirm).not.toHaveBeenCalled(); + expect(onConfirmAndErase).not.toHaveBeenCalled(); + }); + }); + + describe('keyboard interaction', () => { + it('stops propagation of keydown events', () => { + const parentHandler = vi.fn(); + + render( +

+ + + +
+ ); + + fireEvent.keyDown(screen.getByRole('dialog'), { key: 'a' }); + expect(parentHandler).not.toHaveBeenCalled(); + }); + }); + + describe('layer stack integration', () => { + it('registers and unregisters without errors', () => { + const { unmount } = renderWithLayerStack( + + ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(() => unmount()).not.toThrow(); + }); + }); + + describe('accessibility', () => { + it('has tabIndex on dialog for focus', () => { + renderWithLayerStack( + + ); + + expect(screen.getByRole('dialog')).toHaveAttribute('tabIndex', '-1'); + }); + + it('has semantic button elements', () => { + renderWithLayerStack( + + ); + + // X, Cancel, Confirm, Confirm and Erase + expect(screen.getAllByRole('button')).toHaveLength(4); + }); + + it('has heading for modal title', () => { + renderWithLayerStack( + + ); + + expect(screen.getByRole('heading', { name: 'Confirm Delete' })).toBeInTheDocument(); + }); + }); +}); diff --git a/src/__tests__/renderer/services/inlineWizardDocumentGeneration.test.ts b/src/__tests__/renderer/services/inlineWizardDocumentGeneration.test.ts index 4049b743..fb34f75c 100644 --- a/src/__tests__/renderer/services/inlineWizardDocumentGeneration.test.ts +++ b/src/__tests__/renderer/services/inlineWizardDocumentGeneration.test.ts @@ -11,6 +11,8 @@ import { sanitizeFilename, generateWizardFolderBaseName, countTasks, + generateDocumentPrompt, + type DocumentGenerationConfig, } from '../../../renderer/services/inlineWizardDocumentGeneration'; describe('inlineWizardDocumentGeneration', () => { @@ -390,4 +392,138 @@ CONTENT: expect(day).toHaveLength(2); }); }); + + describe('generateDocumentPrompt', () => { + // Helper to create a minimal config for testing + const createTestConfig = (overrides: Partial = {}): DocumentGenerationConfig => ({ + agentType: 'claude-code', + directoryPath: '/project/root', + projectName: 'Test Project', + conversationHistory: [ + { id: '1', role: 'user', content: 'Build a web app', timestamp: Date.now() }, + { id: '2', role: 'assistant', content: 'I can help with that', timestamp: Date.now() }, + ], + mode: 'new', + autoRunFolderPath: '/project/root/Auto Run Docs', + ...overrides, + }); + + it('should use the configured autoRunFolderPath in the prompt', () => { + const config = createTestConfig({ + autoRunFolderPath: '/custom/autorun/path', + }); + + const prompt = generateDocumentPrompt(config); + + // The prompt should contain the custom path, not the default 'Auto Run Docs' + expect(prompt).toContain('/custom/autorun/path'); + // Should NOT contain the hardcoded pattern with directoryPath + default folder + expect(prompt).not.toContain('/project/root/Auto Run Docs'); + }); + + it('should use external autoRunFolderPath when different from directoryPath', () => { + const config = createTestConfig({ + directoryPath: '/main/repo', + autoRunFolderPath: '/worktrees/autorun/feature-branch', + }); + + const prompt = generateDocumentPrompt(config); + + // The prompt should instruct writing to the external path + expect(prompt).toContain('/worktrees/autorun/feature-branch'); + // Read access should still reference the project directory + expect(prompt).toContain('/main/repo'); + }); + + it('should append subfolder to autoRunFolderPath when provided', () => { + const config = createTestConfig({ + autoRunFolderPath: '/custom/autorun', + }); + + const prompt = generateDocumentPrompt(config, 'Wizard-2026-01-11'); + + // Should contain the full path with subfolder + expect(prompt).toContain('/custom/autorun/Wizard-2026-01-11'); + }); + + it('should handle autoRunFolderPath that is inside directoryPath', () => { + const config = createTestConfig({ + directoryPath: '/project/root', + autoRunFolderPath: '/project/root/Auto Run Docs', + }); + + const prompt = generateDocumentPrompt(config); + + // Should still work correctly when path is inside project + expect(prompt).toContain('/project/root/Auto Run Docs'); + }); + + it('should include project name in the prompt', () => { + const config = createTestConfig({ + projectName: 'My Awesome Project', + }); + + const prompt = generateDocumentPrompt(config); + + expect(prompt).toContain('My Awesome Project'); + }); + + it('should include conversation summary in the prompt', () => { + const config = createTestConfig({ + conversationHistory: [ + { id: '1', role: 'user', content: 'I want to build a dashboard', timestamp: Date.now() }, + { id: '2', role: 'assistant', content: 'What metrics should it display?', timestamp: Date.now() }, + ], + }); + + const prompt = generateDocumentPrompt(config); + + expect(prompt).toContain('User: I want to build a dashboard'); + expect(prompt).toContain('Assistant: What metrics should it display?'); + }); + + it('should use iterate prompt template when mode is iterate', () => { + const config = createTestConfig({ + mode: 'iterate', + goal: 'Add authentication', + existingDocuments: [ + { name: 'Phase-01-Setup', filename: 'Phase-01-Setup.md', path: '/path/Phase-01-Setup.md' }, + ], + }); + + const prompt = generateDocumentPrompt(config); + + // Iterate mode has specific markers + expect(prompt).toContain('Add authentication'); + expect(prompt).toContain('Existing Documents'); + }); + + it('should NOT contain hardcoded Auto Run Docs when custom path is configured', () => { + const config = createTestConfig({ + directoryPath: '/my/project', + autoRunFolderPath: '/completely/different/path', + }); + + const prompt = generateDocumentPrompt(config); + + // The combined pattern should be replaced with custom path + // Check that we don't have the default path in write instructions + expect(prompt).not.toMatch(/\/my\/project\/Auto Run Docs/); + expect(prompt).toContain('/completely/different/path'); + }); + + it('should preserve directoryPath for read access instructions', () => { + const config = createTestConfig({ + directoryPath: '/project/source', + autoRunFolderPath: '/external/autorun', + }); + + const prompt = generateDocumentPrompt(config); + + // Read access should reference project directory + expect(prompt).toContain('Read any file in: `/project/source`'); + // Write access should reference autorun path + expect(prompt).toContain('/external/autorun'); + }); + }); }); diff --git a/src/main/ipc/handlers/system.ts b/src/main/ipc/handlers/system.ts index 0ff67bf9..a83865e8 100644 --- a/src/main/ipc/handlers/system.ts +++ b/src/main/ipc/handlers/system.ts @@ -182,6 +182,19 @@ export function registerSystemHandlers(deps: SystemHandlerDependencies): void { await shell.openExternal(url); }); + // Shell operations - move item to system trash + ipcMain.handle('shell:trashItem', async (_event, itemPath: string) => { + if (!itemPath || typeof itemPath !== 'string') { + throw new Error('Invalid path: path must be a non-empty string'); + } + // Resolve to absolute path and verify it exists + const absolutePath = path.resolve(itemPath); + if (!fsSync.existsSync(absolutePath)) { + throw new Error(`Path does not exist: ${absolutePath}`); + } + await shell.trashItem(absolutePath); + }); + // ============ Tunnel Handlers (Cloudflare) ============ ipcMain.handle('tunnel:isCloudflaredInstalled', async () => { diff --git a/src/main/preload.ts b/src/main/preload.ts index 128e6d20..38ebcb8d 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -622,6 +622,7 @@ contextBridge.exposeInMainWorld('maestro', { // Shell API shell: { openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url), + trashItem: (itemPath: string) => ipcRenderer.invoke('shell:trashItem', itemPath), }, // Tunnel API (Cloudflare tunnel support) @@ -2004,6 +2005,7 @@ export interface MaestroAPI { }; shell: { openExternal: (url: string) => Promise; + trashItem: (itemPath: string) => Promise; }; tunnel: { isCloudflaredInstalled: () => Promise; diff --git a/src/prompts/wizard-document-generation.md b/src/prompts/wizard-document-generation.md index d2aebe74..aaac7341 100644 --- a/src/prompts/wizard-document-generation.md +++ b/src/prompts/wizard-document-generation.md @@ -267,6 +267,8 @@ File naming convention: **IMPORTANT**: Write files one at a time, IN ORDER (Phase-01 first, then Phase-02, etc.). Do NOT wait until you've finished all documents to write them - save each one as soon as it's complete. +**DO NOT create any additional files** such as summary documents, README files, recap files, or "what I did" files. Only create the Phase-XX-[Description].md documents. The user can see your generated documents in real-time and does not need a summary. + ## Project Discovery Conversation {{CONVERSATION_SUMMARY}} diff --git a/src/prompts/wizard-inline-iterate-generation.md b/src/prompts/wizard-inline-iterate-generation.md index faf970bb..40fc3ee6 100644 --- a/src/prompts/wizard-inline-iterate-generation.md +++ b/src/prompts/wizard-inline-iterate-generation.md @@ -194,6 +194,8 @@ File paths for the Auto Run folder: - When updating, provide the COMPLETE updated document content, not just the additions - New phases should use the next available phase number +**DO NOT create any additional files** such as summary documents, README files, recap files, or "what I did" files. Only create the Phase-XX-[Description].md documents. The user can see your generated documents in real-time and does not need a summary. + ## Project Discovery Conversation {{CONVERSATION_SUMMARY}} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 97987315..7a34f443 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -25,6 +25,7 @@ import { CONDUCTOR_BADGES, getBadgeForTime } from './constants/conductorBadges'; import { EmptyStateView } from './components/EmptyStateView'; import { MarketplaceModal } from './components/MarketplaceModal'; import { DocumentGraphView } from './components/DocumentGraph/DocumentGraphView'; +import { DeleteAgentConfirmModal } from './components/DeleteAgentConfirmModal'; // Group Chat Components import { GroupChatPanel } from './components/GroupChatPanel'; @@ -472,6 +473,10 @@ function MaestroConsoleInner() { // File gist URL storage - maps file paths to their published gist info const [fileGistUrls, setFileGistUrls] = useState>({}); + // Delete Agent Modal State + const [deleteAgentModalOpen, setDeleteAgentModalOpen] = useState(false); + const [deleteAgentSession, setDeleteAgentSession] = useState(null); + // Note: Git Diff State, Tour Overlay State, and Git Log Viewer State are now from ModalContext // Renaming State @@ -501,6 +506,12 @@ function MaestroConsoleInner() { // Confirm modal close handler const handleCloseConfirmModal = useCallback(() => setConfirmModalOpen(false), []); + // Delete agent modal handlers + const handleCloseDeleteAgentModal = useCallback(() => { + setDeleteAgentModalOpen(false); + setDeleteAgentSession(null); + }, []); + // Quit confirm modal handlers const handleConfirmQuit = useCallback(() => { setQuitConfirmModalOpen(false); @@ -6378,51 +6389,70 @@ You are taking over this conversation. Based on the context above, provide a bri const session = sessions.find(s => s.id === id); if (!session) return; - showConfirmation( - `Are you sure you want to delete the agent "${session.name}"? This action cannot be undone.`, - async () => { - // Record session closure for Usage Dashboard (before cleanup) - window.maestro.stats.recordSessionClosed(id, Date.now()); - - // Kill both processes for this session - try { - await window.maestro.process.kill(`${id}-ai`); - } catch (error) { - console.error('Failed to kill AI process:', error); - } - - try { - await window.maestro.process.kill(`${id}-terminal`); - } catch (error) { - console.error('Failed to kill terminal process:', error); - } - - // Delete associated playbooks - try { - await window.maestro.playbooks.deleteAll(id); - } catch (error) { - console.error('Failed to delete playbooks:', error); - } - - // If this is a worktree session, track its path to prevent re-discovery - if (session.worktreeParentPath && session.cwd) { - setRemovedWorktreePaths(prev => new Set([...prev, session.cwd])); - } - - const newSessions = sessions.filter(s => s.id !== id); - setSessions(newSessions); - // Flush immediately for critical operation (session deletion) - // Note: flushSessionPersistence will pick up the latest state via ref - setTimeout(() => flushSessionPersistence(), 0); - if (newSessions.length > 0) { - setActiveSessionId(newSessions[0].id); - } else { - setActiveSessionId(''); - } - } - ); + // Open the delete agent modal + setDeleteAgentSession(session); + setDeleteAgentModalOpen(true); }; + // Internal function to perform the actual session deletion + const performDeleteSession = useCallback(async (session: Session, eraseWorkingDirectory: boolean) => { + const id = session.id; + + // Record session closure for Usage Dashboard (before cleanup) + window.maestro.stats.recordSessionClosed(id, Date.now()); + + // Kill both processes for this session + try { + await window.maestro.process.kill(`${id}-ai`); + } catch (error) { + console.error('Failed to kill AI process:', error); + } + + try { + await window.maestro.process.kill(`${id}-terminal`); + } catch (error) { + console.error('Failed to kill terminal process:', error); + } + + // Delete associated playbooks + try { + await window.maestro.playbooks.deleteAll(id); + } catch (error) { + console.error('Failed to delete playbooks:', error); + } + + // If this is a worktree session, track its path to prevent re-discovery + if (session.worktreeParentPath && session.cwd) { + setRemovedWorktreePaths(prev => new Set([...prev, session.cwd])); + } + + // Optionally erase the working directory (move to trash) + if (eraseWorkingDirectory && session.cwd) { + try { + await window.maestro.shell.trashItem(session.cwd); + } catch (error) { + console.error('Failed to move working directory to trash:', error); + // Show a toast notification about the failure + addToast({ + title: 'Failed to Erase Directory', + message: error instanceof Error ? error.message : 'Unknown error', + type: 'error', + }); + } + } + + const newSessions = sessions.filter(s => s.id !== id); + setSessions(newSessions); + // Flush immediately for critical operation (session deletion) + // Note: flushSessionPersistence will pick up the latest state via ref + setTimeout(() => flushSessionPersistence(), 0); + if (newSessions.length > 0) { + setActiveSessionId(newSessions[0].id); + } else { + setActiveSessionId(''); + } + }, [sessions, setSessions, setActiveSessionId, flushSessionPersistence, setRemovedWorktreePaths, addToast]); + // Delete an entire worktree group and all its agents const deleteWorktreeGroup = (groupId: string) => { const group = groups.find(g => g.id === groupId); @@ -9931,6 +9961,18 @@ You are taking over this conversation. Based on the context above, provide a bri {/* NOTE: All modals are now rendered via the unified component above */} + {/* Delete Agent Confirmation Modal */} + {deleteAgentModalOpen && deleteAgentSession && ( + performDeleteSession(deleteAgentSession, false)} + onConfirmAndErase={() => performDeleteSession(deleteAgentSession, true)} + onClose={handleCloseDeleteAgentModal} + /> + )} + {/* --- EMPTY STATE VIEW (when no sessions) --- */} {sessions.length === 0 && !isMobileLandscape ? ( void; + onConfirmAndErase: () => void; + onClose: () => void; +} + +export function DeleteAgentConfirmModal({ + theme, + agentName, + workingDirectory, + onConfirm, + onConfirmAndErase, + onClose, +}: DeleteAgentConfirmModalProps) { + const confirmButtonRef = useRef(null); + + const handleConfirm = useCallback(() => { + onConfirm(); + onClose(); + }, [onConfirm, onClose]); + + const handleConfirmAndErase = useCallback(() => { + onConfirmAndErase(); + onClose(); + }, [onConfirmAndErase, onClose]); + + // Stop Enter key propagation to prevent parent handlers from triggering after modal closes + const handleKeyDown = (e: React.KeyboardEvent, action: () => void) => { + if (e.key === 'Enter') { + e.stopPropagation(); + action(); + } + }; + + return ( + } + width={500} + zIndex={10000} + initialFocusRef={confirmButtonRef} + footer={ +
+ + + +
+ } + > +
+
+ +
+
+

+ Are you sure you want to delete the agent "{agentName}"? This action cannot be undone. +

+

+ Confirm and Erase will also move the working directory to the trash: +

+ + {workingDirectory} + +
+
+
+ ); +} diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index df5e7836..c79a45b4 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -599,6 +599,7 @@ interface MaestroAPI { }; shell: { openExternal: (url: string) => Promise; + trashItem: (itemPath: string) => Promise; }; tunnel: { isCloudflaredInstalled: () => Promise; diff --git a/src/renderer/services/inlineWizardDocumentGeneration.ts b/src/renderer/services/inlineWizardDocumentGeneration.ts index e3e0d5c6..2e6376e4 100644 --- a/src/renderer/services/inlineWizardDocumentGeneration.ts +++ b/src/renderer/services/inlineWizardDocumentGeneration.ts @@ -288,8 +288,8 @@ function formatExistingDocsForPrompt(docs: ExistingDocument[]): string { * @param subfolder Optional subfolder name within Auto Run Docs * @returns The complete prompt for the agent */ -function generateDocumentPrompt(config: DocumentGenerationConfig, subfolder?: string): string { - const { projectName, directoryPath, conversationHistory, mode, goal, existingDocuments } = config; +export function generateDocumentPrompt(config: DocumentGenerationConfig, subfolder?: string): string { + const { projectName, directoryPath, conversationHistory, mode, goal, existingDocuments, autoRunFolderPath } = config; const projectDisplay = projectName || 'this project'; // Build conversation summary from the wizard conversation @@ -307,15 +307,22 @@ function generateDocumentPrompt(config: DocumentGenerationConfig, subfolder?: st : wizardDocumentGenerationPrompt; // Build the full Auto Run folder path (including subfolder if specified) - const autoRunFolderPath = subfolder - ? `${AUTO_RUN_FOLDER_NAME}/${subfolder}` - : AUTO_RUN_FOLDER_NAME; + // Use the user-configured autoRunFolderPath (which may be external to directoryPath) + const fullAutoRunPath = subfolder + ? `${autoRunFolderPath}/${subfolder}` + : autoRunFolderPath; - // Handle wizard-specific template variables + // The prompt template uses {{DIRECTORY_PATH}}/{{AUTO_RUN_FOLDER_NAME}} as a combined pattern + // for specifying where documents should be written. Since the user may have configured + // an external Auto Run folder (not inside directoryPath), we replace the combined pattern + // with the full absolute path. let prompt = basePrompt .replace(/\{\{PROJECT_NAME\}\}/gi, projectDisplay) + // Replace the combined pattern first (for write access paths) + .replace(/\{\{DIRECTORY_PATH\}\}\/\{\{AUTO_RUN_FOLDER_NAME\}\}/gi, fullAutoRunPath) + // Then replace remaining individual placeholders (for read access, etc.) .replace(/\{\{DIRECTORY_PATH\}\}/gi, directoryPath) - .replace(/\{\{AUTO_RUN_FOLDER_NAME\}\}/gi, autoRunFolderPath) + .replace(/\{\{AUTO_RUN_FOLDER_NAME\}\}/gi, fullAutoRunPath) .replace(/\{\{CONVERSATION_SUMMARY\}\}/gi, conversationSummary); // Handle iterate-mode specific placeholders