mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
hotkey for copy to clipboard in image carousel
This commit is contained in:
24
CLAUDE.md
24
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:
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('AutoRunLightbox', () => {
|
||||
const props = createDefaultProps();
|
||||
renderWithProviders(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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(<AutoRunLightbox {...props} />);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<LightboxModal
|
||||
image={mockImage}
|
||||
stagedImages={mockImages}
|
||||
onClose={onClose}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<LightboxModal
|
||||
image={mockImage}
|
||||
stagedImages={mockImages}
|
||||
onClose={onClose}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
);
|
||||
|
||||
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');
|
||||
|
||||
@@ -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(({
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); copyToClipboard(); }}
|
||||
className="bg-white/10 hover:bg-white/20 text-white rounded-full p-3 backdrop-blur-sm transition-colors flex items-center gap-2"
|
||||
title="Copy image to clipboard"
|
||||
title="Copy image to clipboard (⌘C)"
|
||||
>
|
||||
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
|
||||
{copied && <span className="text-sm">Copied!</span>}
|
||||
|
||||
@@ -1226,6 +1226,11 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenFuzzySearch();
|
||||
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey) && isImage) {
|
||||
// Cmd+C: Copy image to clipboard when viewing an image
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
copyContentToClipboard();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1308,7 +1313,7 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
onClick={copyContentToClipboard}
|
||||
className="p-2 rounded hover:bg-white/10 transition-colors"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
title={isImage ? "Copy image to clipboard" : "Copy content to clipboard"}
|
||||
title={isImage ? "Copy image to clipboard (⌘C)" : "Copy content to clipboard"}
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -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
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); copyImageToClipboard(); }}
|
||||
className="bg-white/10 hover:bg-white/20 text-white rounded-full p-3 backdrop-blur-sm transition-colors flex items-center gap-2"
|
||||
title="Copy image to clipboard"
|
||||
title="Copy image to clipboard (⌘C)"
|
||||
>
|
||||
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
|
||||
{copied && <span className="text-sm">Copied!</span>}
|
||||
|
||||
Reference in New Issue
Block a user