diff --git a/apps/desktop/src/app/chat/composer/status-stack/coding-row.test.tsx b/apps/desktop/src/app/chat/composer/status-stack/coding-row.test.tsx index 018862fc39..326cdfcfd6 100644 --- a/apps/desktop/src/app/chat/composer/status-stack/coding-row.test.tsx +++ b/apps/desktop/src/app/chat/composer/status-stack/coding-row.test.tsx @@ -1,7 +1,9 @@ -import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { atom } from 'nanostores' import { afterEach, describe, expect, it, vi } from 'vitest' +import { $notifications, clearNotifications } from '@/store/notifications' + vi.mock('@/store/coding-status', () => ({ registerRepoStatusCwd: () => undefined, repoStatusForCwd: () => @@ -54,4 +56,38 @@ describe('CodingStatusRow', () => { // The glyph button fills the row's existing 3.5 leading slot exactly. expect(container.querySelector('button[class~="size-3.5"]')).not.toBeNull() }) + + it('parks the copy glyph against the end of the path, not the end of the row', () => { + render( undefined} repoPath="/Users/someone/www/repo" />) + + const path = screen.getByText('~/www/repo') + + // The path sizes to its content and the glyph is its immediate sibling, so + // the pair reads as one unit. `flex-1` belongs to the wrapper (which holds + // the row's slack open) — on the label it stretched the text and pushed the + // glyph out to the kebab. + expect(path.classList.contains('flex-1')).toBe(false) + expect(path.parentElement?.classList.contains('flex-1')).toBe(true) + expect(path.nextElementSibling?.tagName).toBe('BUTTON') + }) + + it('copies the absolute cwd inline — checkmark feedback, no toast', async () => { + const writeText = vi.fn().mockResolvedValue(undefined) + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }) + clearNotifications() + + render( undefined} repoPath="/Users/someone/www/repo" />) + + // Painted tildified, copied raw. + expect(screen.getByText('~/www/repo')).toBeTruthy() + + const copy = screen.getByRole('button', { name: 'Copy Path' }) + + fireEvent.click(copy) + + await waitFor(() => expect(writeText).toHaveBeenCalledWith('/Users/someone/www/repo')) + // Confirmation is the button turning into a checkmark, not a notification. + await waitFor(() => expect(screen.getByRole('button', { name: 'Copied' })).toBeTruthy()) + expect($notifications.get()).toHaveLength(0) + }) }) diff --git a/apps/desktop/src/app/chat/composer/status-stack/coding-row.tsx b/apps/desktop/src/app/chat/composer/status-stack/coding-row.tsx index 5682fb82b5..842b1ac37e 100644 --- a/apps/desktop/src/app/chat/composer/status-stack/coding-row.tsx +++ b/apps/desktop/src/app/chat/composer/status-stack/coding-row.tsx @@ -12,12 +12,12 @@ import { } from '@/components/ui/actions-menu' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' +import { CopyButton } from '@/components/ui/copy-button' import { DiffCount } from '@/components/ui/diff-count' import type { HermesGitBranch } from '@/global' import { useI18n } from '@/i18n' import { displayPath } from '@/lib/display-path' import { registerRepoStatusCwd, repoStatusForCwd, repoWorktreesForCwd } from '@/store/coding-status' -import { copyFilePath } from '@/store/file-actions' import { notifyError } from '@/store/notifications' import { $newWorktreeRequest } from '@/store/projects' @@ -242,29 +242,30 @@ export const CodingStatusRow = memo(function CodingStatusRow({ {/* Worktree path + copy — plain muted text, not a chip. Always in the flex so hover doesn't reflow the row; opacity alone reveals the - pair. `displayPath` collapses home → ~; copy still takes the - real absolute path. */} + pair. The path sizes to its content (the `flex-1` lives on the + wrapper) so the glyph sits against the end of the text instead of + drifting to the far edge of the row. `displayPath` collapses + home → ~; the copy still takes the real absolute path, and it's + the shared `CopyButton` so it confirms with the same inline + checkmark as every other copy in the app. */} {resolvedRepoPath && (
{displayPath(resolvedRepoPath)} - + iconClassName="size-3" + label={fileMenu.copyPath} + side="top" + stopPropagation + text={resolvedRepoPath} + />
)}