diff --git a/CLAUDE.md b/CLAUDE.md index 29b60226..c63699f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,6 +117,8 @@ src/ | Add agent session storage | `src/main/storage/`, `src/main/agent-session-storage.ts` | | Add agent error patterns | `src/main/parsers/error-patterns.ts` | | Add playbook feature | `src/cli/services/playbooks.ts` | +| Add marketplace playbook | `src/main/ipc/handlers/marketplace.ts` (import from GitHub) | +| Playbook import/export | `src/main/ipc/handlers/playbooks.ts` (ZIP handling with assets) | | Modify wizard flow | `src/renderer/components/Wizard/` (see Onboarding Wizard section) | | Add tour step | `src/renderer/components/Wizard/tour/tourSteps.ts` | | Modify file linking | `src/renderer/utils/remarkFileLinks.ts` (remark plugin for `[[wiki]]` and path links) | @@ -259,6 +261,28 @@ window.maestro.autorun.saveDocument(folderPath, filename, content); **Worktree Support:** Auto Run can operate in a git worktree, allowing users to continue interactive editing in the main repo while Auto Run processes tasks in the background. When `batchRunState.worktreeActive` is true, read-only mode is disabled and a git branch icon appears in the UI. See `useBatchProcessor.ts` for worktree setup logic. +**Playbook Assets:** Playbooks can include non-markdown assets (config files, YAML, Dockerfiles, scripts) in an `assets/` subfolder. When installing playbooks from the marketplace or importing from ZIP files, Maestro copies the entire folder structure including assets. See the [Maestro-Playbooks repository](https://github.com/pedramamini/Maestro-Playbooks) for the convention documentation. + +``` +playbook-folder/ +├── 01_TASK.md +├── 02_TASK.md +├── README.md +└── assets/ + ├── config.yaml + ├── Dockerfile + └── setup.sh +``` + +Documents can reference assets using `{{AUTORUN_FOLDER}}/assets/filename`. The manifest lists assets explicitly: +```json +{ + "id": "example-playbook", + "documents": [...], + "assets": ["config.yaml", "Dockerfile", "setup.sh"] +} +``` + ### 9. Tab Hover Overlay Menu AI conversation tabs display a hover overlay menu after a 400ms delay when hovering over tabs with an established session. The overlay includes tab management and context operations: diff --git a/src/__tests__/renderer/components/AutoRunLightbox.test.tsx b/src/__tests__/renderer/components/AutoRunLightbox.test.tsx index 13e27da1..91a1e2ea 100644 --- a/src/__tests__/renderer/components/AutoRunLightbox.test.tsx +++ b/src/__tests__/renderer/components/AutoRunLightbox.test.tsx @@ -186,7 +186,7 @@ describe('AutoRunLightbox', () => { const props = createDefaultProps(); renderWithProviders(); - expect(screen.getByTitle('Copy image to clipboard')).toBeInTheDocument(); + expect(screen.getByTitle('Copy image to clipboard (⌘C)')).toBeInTheDocument(); expect(screen.getByTestId('copy-icon')).toBeInTheDocument(); }); @@ -637,7 +637,33 @@ describe('AutoRunLightbox', () => { const props = createDefaultProps(); renderWithProviders(); - fireEvent.click(screen.getByTitle('Copy image to clipboard')); + fireEvent.click(screen.getByTitle('Copy image to clipboard (⌘C)')); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('data:image/png;base64,mock-data-image1.png'); + expect(mockClipboardWrite).toHaveBeenCalled(); + }); + }); + + it('should copy image to clipboard when Cmd+C is pressed', async () => { + const props = createDefaultProps(); + renderWithProviders(); + + const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]'); + fireEvent.keyDown(backdrop!, { key: 'c', metaKey: true }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('data:image/png;base64,mock-data-image1.png'); + expect(mockClipboardWrite).toHaveBeenCalled(); + }); + }); + + it('should copy image to clipboard when Ctrl+C is pressed', async () => { + const props = createDefaultProps(); + renderWithProviders(); + + const backdrop = document.querySelector('.fixed.inset-0.z-\\[9999\\]'); + fireEvent.keyDown(backdrop!, { key: 'c', ctrlKey: true }); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('data:image/png;base64,mock-data-image1.png'); @@ -652,7 +678,7 @@ describe('AutoRunLightbox', () => { // Initially shows copy icon expect(screen.getByTestId('copy-icon')).toBeInTheDocument(); - fireEvent.click(screen.getByTitle('Copy image to clipboard')); + fireEvent.click(screen.getByTitle('Copy image to clipboard (⌘C)')); await waitFor(() => { expect(screen.getByTestId('check-icon')).toBeInTheDocument(); @@ -666,7 +692,7 @@ describe('AutoRunLightbox', () => { const props = createDefaultProps(); renderWithProviders(); - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); // Trigger copy and immediately resolve the promise chain await act(async () => { @@ -693,7 +719,7 @@ describe('AutoRunLightbox', () => { const props = createDefaultProps({ onClose }); renderWithProviders(); - fireEvent.click(screen.getByTitle('Copy image to clipboard')); + fireEvent.click(screen.getByTitle('Copy image to clipboard (⌘C)')); // onClose should NOT be called because stopPropagation prevents backdrop click expect(onClose).not.toHaveBeenCalled(); @@ -706,7 +732,7 @@ describe('AutoRunLightbox', () => { const props = createDefaultProps(); renderWithProviders(); - fireEvent.click(screen.getByTitle('Copy image to clipboard')); + fireEvent.click(screen.getByTitle('Copy image to clipboard (⌘C)')); await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( @@ -727,7 +753,7 @@ describe('AutoRunLightbox', () => { }); renderWithProviders(); - fireEvent.click(screen.getByTitle('Copy image to clipboard')); + fireEvent.click(screen.getByTitle('Copy image to clipboard (⌘C)')); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('https://example.com/image.png'); @@ -1095,7 +1121,7 @@ describe('AutoRunLightbox', () => { expect(screen.getByTitle('Previous image (←)')).toBeInTheDocument(); expect(screen.getByTitle('Next image (→)')).toBeInTheDocument(); - expect(screen.getByTitle('Copy image to clipboard')).toBeInTheDocument(); + expect(screen.getByTitle('Copy image to clipboard (⌘C)')).toBeInTheDocument(); expect(screen.getByTitle('Delete image (Delete key)')).toBeInTheDocument(); expect(screen.getByTitle('Close (ESC)')).toBeInTheDocument(); }); diff --git a/src/__tests__/renderer/components/LightboxModal.test.tsx b/src/__tests__/renderer/components/LightboxModal.test.tsx index c797d7b7..3b88f649 100644 --- a/src/__tests__/renderer/components/LightboxModal.test.tsx +++ b/src/__tests__/renderer/components/LightboxModal.test.tsx @@ -448,7 +448,7 @@ describe('LightboxModal', () => { ); // Click copy button - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); fireEvent.click(copyButton); await waitFor(() => { @@ -457,6 +457,50 @@ describe('LightboxModal', () => { }); }); + it('copies image to clipboard when Cmd+C pressed', async () => { + const onClose = vi.fn(); + const onNavigate = vi.fn(); + + renderWithLayerStack( + + ); + + const dialog = screen.getByRole('dialog'); + fireEvent.keyDown(dialog, { key: 'c', metaKey: true }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith(mockImage); + expect(mockClipboardWrite).toHaveBeenCalled(); + }); + }); + + it('copies image to clipboard when Ctrl+C pressed', async () => { + const onClose = vi.fn(); + const onNavigate = vi.fn(); + + renderWithLayerStack( + + ); + + const dialog = screen.getByRole('dialog'); + fireEvent.keyDown(dialog, { key: 'c', ctrlKey: true }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith(mockImage); + expect(mockClipboardWrite).toHaveBeenCalled(); + }); + }); + it('shows check icon and "Copied!" after successful copy', async () => { const onClose = vi.fn(); const onNavigate = vi.fn(); @@ -473,7 +517,7 @@ describe('LightboxModal', () => { // Initially shows copy icon expect(screen.getByTestId('copy-icon')).toBeInTheDocument(); - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); fireEvent.click(copyButton); await waitFor(() => { @@ -496,7 +540,7 @@ describe('LightboxModal', () => { /> ); - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); // Trigger copy and immediately resolve the promise chain await act(async () => { @@ -531,7 +575,7 @@ describe('LightboxModal', () => { /> ); - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); fireEvent.click(copyButton); expect(onClose).not.toHaveBeenCalled(); @@ -553,7 +597,7 @@ describe('LightboxModal', () => { /> ); - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); fireEvent.click(copyButton); await waitFor(() => { @@ -775,7 +819,7 @@ describe('LightboxModal', () => { /> ); - const copyButton = screen.getByTitle('Copy image to clipboard'); + const copyButton = screen.getByTitle('Copy image to clipboard (⌘C)'); expect(copyButton).toHaveClass('bg-white/10'); expect(copyButton).toHaveClass('hover:bg-white/20'); expect(copyButton).toHaveClass('rounded-full'); diff --git a/src/renderer/components/AutoRunLightbox.tsx b/src/renderer/components/AutoRunLightbox.tsx index d81b6ffe..b1a316dc 100644 --- a/src/renderer/components/AutoRunLightbox.tsx +++ b/src/renderer/components/AutoRunLightbox.tsx @@ -193,8 +193,11 @@ export const AutoRunLightbox = memo(({ if (!lightboxExternalUrl && onDelete) { promptDelete(); } + } else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + copyToClipboard(); } - }, [goToPrevImage, goToNextImage, lightboxExternalUrl, onDelete, promptDelete]); + }, [goToPrevImage, goToNextImage, lightboxExternalUrl, onDelete, promptDelete, copyToClipboard]); // Don't render if no image is selected const imageUrl = lightboxExternalUrl || (lightboxFilename ? attachmentPreviews.get(lightboxFilename) : undefined); @@ -245,7 +248,7 @@ export const AutoRunLightbox = memo(({ diff --git a/src/renderer/components/LightboxModal.tsx b/src/renderer/components/LightboxModal.tsx index 64b95bad..ece93396 100644 --- a/src/renderer/components/LightboxModal.tsx +++ b/src/renderer/components/LightboxModal.tsx @@ -160,6 +160,10 @@ export function LightboxModal({ image, stagedImages, onClose, onNavigate, onDele e.preventDefault(); promptDelete(); } + else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + copyImageToClipboard(); + } }} tabIndex={-1} role="dialog" @@ -182,7 +186,7 @@ export function LightboxModal({ image, stagedImages, onClose, onNavigate, onDele