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