diff --git a/web/src/components/SessionActionMenu.test.tsx b/web/src/components/SessionActionMenu.test.tsx index b08e6037..e4eac884 100644 --- a/web/src/components/SessionActionMenu.test.tsx +++ b/web/src/components/SessionActionMenu.test.tsx @@ -3,12 +3,20 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { I18nProvider } from '@/lib/i18n-context' import { SessionActionMenu } from '@/components/SessionActionMenu' +vi.mock('@/hooks/usePlatform', () => ({ + usePlatform: () => ({ + haptic: { notification: vi.fn(), impact: vi.fn() }, + }), +})) + afterEach(() => cleanup()) function renderMenu(overrides: Partial> = {}) { const defaults: React.ComponentProps = { isOpen: true, onClose: vi.fn(), + sessionId: 'sess-123', + sessionTitle: 'Test session', sessionActive: false, onRename: vi.fn(), onArchive: vi.fn(), @@ -101,6 +109,8 @@ describe('SessionActionMenu - Codex sync action', () => { { expect(onClose).toHaveBeenCalledTimes(1) }) }) + +describe('SessionActionMenu - Copy reference action', () => { + it('renders the Copy reference item', () => { + renderMenu() + + expect(screen.getByRole('menuitem', { name: /Copy reference/ })).toBeInTheDocument() + }) + + it('copies a session citation and closes the menu when Copy reference is clicked', async () => { + const writeText = vi.fn().mockResolvedValue(undefined) + Object.defineProperty(navigator, 'clipboard', { + value: { writeText }, + configurable: true, + }) + + const onClose = vi.fn() + renderMenu({ + sessionId: 'abc-def', + sessionTitle: 'upstream issue/pr discovery', + onClose, + }) + + fireEvent.click(screen.getByRole('menuitem', { name: /Copy reference/ })) + + expect(onClose).toHaveBeenCalledTimes(1) + await vi.waitFor(() => { + expect(writeText).toHaveBeenCalledWith( + 'See session "upstream issue/pr discovery" (/sessions/abc-def) for context' + ) + }) + }) +}) diff --git a/web/src/components/SessionActionMenu.tsx b/web/src/components/SessionActionMenu.tsx index d18acbd8..daf43b3e 100644 --- a/web/src/components/SessionActionMenu.tsx +++ b/web/src/components/SessionActionMenu.tsx @@ -9,10 +9,16 @@ import { } from 'react' import { useTranslation } from '@/lib/use-translation' import { HoverTooltip } from '@/components/HoverTooltip' +import { safeCopyToClipboard } from '@/lib/clipboard' +import { buildSessionReferenceText } from '@/lib/sessionReference' +import { usePlatform } from '@/hooks/usePlatform' +import { CopyIcon } from '@/components/icons' type SessionActionMenuProps = { isOpen: boolean onClose: () => void + sessionId: string + sessionTitle: string sessionActive: boolean onRename: () => void onExport?: () => void @@ -160,9 +166,12 @@ type MenuPosition = { export function SessionActionMenu(props: SessionActionMenuProps) { const { t } = useTranslation() + const { haptic } = usePlatform() const { isOpen, onClose, + sessionId, + sessionTitle, sessionActive, onRename, onExport, @@ -185,6 +194,16 @@ export function SessionActionMenu(props: SessionActionMenuProps) { onRename() } + const handleCopyReference = async () => { + onClose() + try { + await safeCopyToClipboard(buildSessionReferenceText(sessionTitle, sessionId)) + haptic.notification('success') + } catch { + haptic.notification('error') + } + } + const handleArchive = () => { onClose() onArchive() @@ -326,6 +345,16 @@ export function SessionActionMenu(props: SessionActionMenuProps) { {t('session.action.rename')} + + {onExport ? (