hotkey for copy to clipboard in image carousel

This commit is contained in:
Pedram Amini
2026-01-06 18:29:46 -06:00
parent e9bd57a9a8
commit 40f83cf772
6 changed files with 124 additions and 18 deletions

View File

@@ -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:

View File

@@ -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();
});

View File

@@ -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');

View File

@@ -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>}

View File

@@ -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>

View File

@@ -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>}