Merge pull request #76536 from NousResearch/bb/composer-path-copy

The branch-bar path copy confirms in place
This commit is contained in:
brooklyn!
2026-08-01 21:52:39 -05:00
committed by GitHub
2 changed files with 54 additions and 17 deletions

View File

@@ -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(<CodingStatusRow onOpen={() => 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(<CodingStatusRow onOpen={() => 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)
})
})

View File

@@ -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 && (
<div className="flex min-w-0 flex-1 items-center gap-0.5 opacity-0 transition-opacity group-hover/status-row:opacity-100 group-focus-within/status-row:opacity-100">
<span
className="min-w-0 flex-1 truncate font-mono text-[0.62rem] leading-4 text-muted-foreground/50"
className="min-w-0 truncate font-mono text-[0.62rem] leading-4 text-muted-foreground/50"
data-slot="coding-status-cwd"
>
{displayPath(resolvedRepoPath)}
</span>
<Button
aria-label={fileMenu.copyPath}
<CopyButton
appearance="icon"
buttonSize="icon-xs"
className="pointer-events-none size-4 shrink-0 text-muted-foreground/50 hover:text-foreground group-hover/status-row:pointer-events-auto group-focus-within/status-row:pointer-events-auto"
onClick={event => {
event.stopPropagation()
void copyFilePath(resolvedRepoPath)
}}
size="icon-xs"
type="button"
variant="ghost"
>
<Codicon name="copy" size="0.7rem" />
</Button>
iconClassName="size-3"
label={fileMenu.copyPath}
side="top"
stopPropagation
text={resolvedRepoPath}
/>
</div>
)}