fix: keep macOS artifact preview panel clickable above drag region (#1067)

This commit is contained in:
paisley
2026-05-26 13:00:34 +08:00
committed by GitHub
parent b7d0ca0c53
commit 39c8400421
5 changed files with 65 additions and 8 deletions
+13 -5
View File
@@ -17,11 +17,11 @@
* toolbar buttons, "View file changes →" links) can drive it.
*/
import { useLayoutEffect, useMemo, useRef } from 'react';
import { cn } from '@/lib/utils';
import { Eye, FileEdit, FolderOpen, FolderTree, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { supportsRichDocumentPreview, type GeneratedFile } from '@/lib/generated-files';
import { invokeIpc } from '@/lib/api-client';
import type { AgentSummary } from '@/types/agent';
@@ -44,6 +44,7 @@ export interface ArtifactPanelProps {
export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: ArtifactPanelProps) {
const { t } = useTranslation('chat');
const isMac = window.electron?.platform === 'darwin';
const tab = useArtifactPanel((s) => s.tab);
const setTab = useArtifactPanel((s) => s.setTab);
const focusedFile = useArtifactPanel((s) => s.focusedFile);
@@ -61,9 +62,16 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
};
return (
<div data-testid="artifact-panel" className="flex h-full min-h-0 flex-col bg-background">
<div data-testid="artifact-panel" className={cn('flex h-full min-h-0 flex-col bg-background', isMac && 'no-drag')}>
<div className="relative z-30 flex shrink-0 items-center justify-between gap-2 border-b border-black/5 bg-background px-3 py-2 dark:border-white/10">
<div className="flex min-w-0 items-center gap-1">
{isMac && (
<div
data-testid="artifact-panel-drag-region"
className="drag-region absolute inset-0 z-0"
aria-hidden="true"
/>
)}
<div className={cn('flex min-w-0 items-center gap-1', isMac && 'no-drag relative z-10')}>
{richFocusedFile ? (
<PanelTabButton
testId="artifact-panel-action-open-folder"
@@ -102,7 +110,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0"
className={cn('h-7 w-7 shrink-0', isMac && 'no-drag relative z-10')}
onClick={close}
aria-label={t('filePreview.actions.close', 'Close')}
>
@@ -110,7 +118,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
</Button>
</div>
<div className="relative z-0 min-h-0 flex-1 overflow-hidden">
<div className={cn('relative z-0 min-h-0 flex-1 overflow-hidden', isMac && 'no-drag')}>
<div className={cn('h-full min-h-0', visibleTab !== 'changes' && 'hidden')}>
<ChangesTab
files={files}
+9 -2
View File
@@ -768,7 +768,10 @@ export function Chat() {
className={cn(
'relative flex min-h-0 -m-6 overflow-hidden transition-colors duration-500',
'bg-background',
isMac && 'rounded-tl-2xl shadow-[inset_1px_1px_0_hsl(var(--border)/0.55)]',
// Stack above MainLayout's mac-main-drag-region (z-10) so the right-hand
// artifact/preview pane stays clickable; window drag is handled by the
// sidebar + chat-toolbar drag strips instead.
isMac && 'z-20 rounded-tl-2xl shadow-[inset_1px_1px_0_hsl(var(--border)/0.55)]',
isWindows && 'rounded-tl-2xl',
)}
style={{ height: isMac ? '100vh' : 'calc(100vh - 2.5rem)' }}
@@ -1022,7 +1025,11 @@ export function Chat() {
<PanelResizeDividerLazy containerRef={splitContainerRef} />
</Suspense>
<aside
className="relative z-20 hidden shrink-0 border-l border-black/5 dark:border-white/10 lg:flex lg:flex-col"
data-testid="artifact-panel-aside"
className={cn(
'relative z-20 hidden shrink-0 border-l border-black/5 dark:border-white/10 lg:flex lg:flex-col',
isMac && 'no-drag',
)}
style={{ width: `${panelWidthPct}%` }}
>
<Suspense
+5 -1
View File
@@ -3,7 +3,7 @@ import { expect, test } from './fixtures/electron';
test.describe('macOS frameless chrome', () => {
test.skip(process.platform !== 'darwin', 'macOS drag-region chrome only');
test('keeps a draggable strip above the right content pane', async ({ page }) => {
test('keeps a draggable strip above non-chat pages and stacks chat above it', async ({ page }) => {
await expect(page.getByTestId('setup-page')).toBeVisible();
await page.getByTestId('setup-skip-button').click();
@@ -18,5 +18,9 @@ test.describe('macOS frameless chrome', () => {
expect(box).not.toBeNull();
expect(box!.width).toBeGreaterThan(200);
expect(box!.height).toBeGreaterThanOrEqual(24);
const chatPage = page.getByTestId('chat-page');
await expect(chatPage).toBeVisible();
await expect(chatPage).toHaveCSS('z-index', '20');
});
});
+28
View File
@@ -140,4 +140,32 @@ describe('ArtifactPanel', () => {
fireEvent.pointerDown(changesButton, { button: 0 });
expect(screen.getAllByTestId('file-preview-body')[0]).toHaveTextContent('diff');
});
it('marks macOS chrome so preview tabs and content stay clickable', () => {
window.electron.platform = 'darwin';
useArtifactPanel.setState({
open: true,
tab: 'preview',
focusedFile: {
filePath: '/tmp/demo.md',
fileName: 'demo.md',
ext: '.md',
mimeType: 'text/markdown',
contentType: 'document',
},
widthPct: ARTIFACT_PANEL_DEFAULT_WIDTH,
});
render(
<ArtifactPanel
files={[makeGeneratedFile()]}
agent={null}
/>,
);
expect(screen.getByTestId('artifact-panel')).toHaveClass('no-drag');
expect(screen.getByTestId('artifact-panel-drag-region')).toHaveClass('drag-region');
expect(screen.getByTestId('artifact-panel-tab-preview').parentElement).toHaveClass('no-drag');
});
});
@@ -102,6 +102,15 @@ describe('Chat artifact panel layout', () => {
expect(actions).toHaveClass('no-drag');
});
it('stacks the chat page above the macOS main drag strip', async () => {
window.electron.platform = 'darwin';
render(<Chat />);
const chatPage = await screen.findByTestId('chat-page');
expect(chatPage).toHaveClass('z-20');
});
it('layers the right artifact panel above the macOS drag strip', async () => {
window.electron.platform = 'darwin';
@@ -112,5 +121,6 @@ describe('Chat artifact panel layout', () => {
expect(aside).toHaveClass('relative');
expect(aside).toHaveClass('z-20');
expect(aside).toHaveClass('no-drag');
});
});