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