feat(ui): add copy path actions (#3913)

This commit is contained in:
Carlos Florêncio
2026-09-24 19:05:37 +01:00
committed by GitHub
parent 6b86b3482c
commit 9a5614963a
25 changed files with 991 additions and 52 deletions
+168
View File
@@ -0,0 +1,168 @@
---
created: 2026-09-24
status: implemented
requirements:
- REQ-UI-COPY-FILE-PATH-ACTIONS-001
system_design:
- ../../specs/ui/system-design/copy-file-path-actions.md
legacy_specs: []
---
# Implementation Plan: Copy File Path Actions
## Overview
Add Copy path to working-tree rows in the task Changes panel and replace Copy
diff with Copy path in the Review diff toolbar. Implement the two surfaces
together so they use the same repository-relative path meaning and clipboard
behavior. Refuse to copy paths containing C0 or DEL ASCII control characters
and report that outcome to prevent terminal input injection.
## Scope
### In scope
- Add Copy path to desktop hover and keyboard-focus actions for Changes rows.
- Add Copy path to the existing touch action menu for Changes rows.
- Replace Copy diff with Copy path in desktop and phone Review diff file actions.
- Reuse `copyToClipboard` for safe paths and the existing `task:copyPath`
translation.
- Reject C0 and DEL ASCII control characters from every Copy path entry point
and show localized refusal feedback.
- Add focused unit and desktop/phone E2E coverage for the copied value and
action reachability.
### Out of scope
- Copy diff controls in task diff toolbars, Monaco, or other editor surfaces.
- Path normalization, repository data, Git behavior, APIs, or persistence.
- Public documentation changes.
## Technical approach
- Extend `FileRowActions` and `TouchFileRowActions` with
the same copy handler for `ChangedFile.path`. Stop propagation so
copying does not open the diff. Ensure desktop actions become visible on
keyboard focus as well as hover.
- Replace the Review toolbar's desktop Copy diff button and phone menu item in
`review-diff-toolbar.tsx`. Copy the current `filePath`,
including the new path for renamed files.
- Use `copyPathToClipboard` in both surfaces to reject paths with C0 or DEL
ASCII control characters, then use `copyToClipboard` for safe paths. Use
`task:copyPath` for localized accessible names and a localized refusal
message for rejected paths.
- Suppress the shared editor menu's absolute-worktree Copy path item in the
Review toolbar menus, so the Review surface exposes one Copy path action with
the repository-relative value. Keep those shared menus unchanged elsewhere.
- Update existing row and Review toolbar unit tests, then extend the existing
Changes and Review browser scenarios for desktop and phone.
## ASCII UI preview
UI-01: Changes panel working-tree file row, desktop pointer hover or keyboard
focus. The path and statistics stay in the row; Copy path joins the existing
secondary actions. Copy path and row activation are separate controls.
```text
[go] browser/programme_test.go +12 -12 [M] [undo] [edit] [copy]
```
UI-02: Changes panel working-tree file row, phone. Tapping the row opens the
diff; the visible 44px menu trigger opens a menu whose rows are at least 44px.
Copy path is a menu item, not a hover action.
```text
[go] programme_test.go [+12 -12 M] [more]
go/internal/.../browser
Menu: Full path
Copy path
Stage / Unstage
Edit
Discard
```
UI-03: Review diff file toolbar, desktop. Copy path replaces Copy diff and
copies the current repository-relative path.
```text
[comment] path/to/programme_test.go +12/-12 [copy path] [external] [view] [edit] [...]
```
UI-04: Review diff file toolbar, phone. The existing 44px action trigger opens
the file menu; Copy path replaces Copy diff there. The diff keeps its existing
single content scroll region.
```text
Diff header: .../path/to/programme_test.go [status]
[more]
Menu: Copy path
Edit
Preview / diff controls
```
UI-05: If either surface receives a path containing a C0 or DEL ASCII control
character, Copy path leaves the clipboard unchanged and shows a localized
refusal notice. The notice does not change the row or active diff.
```text
Copy path: refused
Notice: This path contains control characters and cannot be copied.
```
The drawings show action order, access, and hierarchy. Icon appearance and
spacing are illustrative. UI-01 through UI-04 map to
`AC-UI-COPY-FILE-PATH-ACTIONS-001.1` through `.4`; the
targeted browser checks prove the copied value and desktop/phone access.
UI-05 maps to `AC-UI-COPY-FILE-PATH-ACTIONS-001.6`.
## Tests
| Acceptance criterion | Evidence |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.1`, `.5` | `components/task/changes-panel-file-row.test.tsx`: desktop and phone tests assert the exact path and no diff navigation |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.2`, `.3`, `.5` | `components/review/review-diff-toolbar.test.tsx`: desktop copies the current path for a rename; phone copies its active path |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.4` | Desktop and phone E2E checks below assert keyboard/pointer or menu reachability and 44px phone controls |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.6` | Component suites reject newline, ESC, and DEL across all four entry points; utility tests cover the full C0/DEL range |
## E2E tests
- `tests/git/git-changes-panel.spec.ts` (chromium):
`copies a changed file path without opening its diff`.
- `tests/task/mobile-changes-panel.spec.ts` (mobile-chrome):
`compact file rows preserve names and expose touch actions` also copies from
the row menu.
- `tests/review/review-file-status.spec.ts` (chromium):
the Review status scenario copies the new path for a renamed file.
- `tests/review/mobile-review-file-status.spec.ts` (mobile-chrome):
the mobile header scenario copies the current path from its file menu.
Component tests also verify control-character paths are rejected from all four
desktop and phone entry points with localized refusal feedback.
Each flow asserts the exact repository-relative clipboard value. The phone flows
also assert the relevant menu or control has a 44px hit area and the page has no
horizontal overflow.
## Work orders
- [x] [Task 01: Add copy path actions](task-01-copy-path-actions.md) (done)
## Verification results
- `pnpm exec vitest run components/task/changes-panel-file-row.test.tsx components/review/review-diff-toolbar.test.tsx lib/utils/copy-repository-path.test.ts`: passed (39 tests, including control-character rejection across all four desktop and phone entry points).
- `pnpm e2e:run --host --project chromium tests/git/git-changes-panel.spec.ts tests/review/review-file-status.spec.ts`: passed (28 tests; production builds succeeded).
- `pnpm e2e:run --no-build --host --project mobile-chrome tests/task/mobile-changes-panel.spec.ts tests/review/mobile-review-file-status.spec.ts`: passed (10 tests; using the production bundle built by the preceding managed mobile run).
- After formatting, `pnpm run build` passed, and focused desktop and mobile E2E reruns each passed (2 tests per project).
- Targeted ESLint, Prettier, typecheck, and `pnpm run i18n:ratchet`: passed.
- `python3 scripts/list-docs.py validate`, `python3 scripts/lint-spec-files.py --all`, and `git diff --check`: passed.
- After PR review remediation, the focused desktop Chromium E2E suite passed (28 tests) and the phone mobile-chrome suite passed (10 tests); both managed runs rebuilt the backend and Vite assets.
- After remediation, web typecheck, targeted ESLint and Prettier, `i18n:check`, `i18n:ratchet`, specification validation and lint all passed.
## Risks
- The additional Changes row control must not reduce the visible path or
overlap the trailing statistics at the panel's supported minimum width.
- Renamed files must copy the current path from the diff header, not the
previous path retained for rename context.
@@ -0,0 +1,177 @@
---
id: "01-copy-path-actions"
title: "Add copy path actions"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-UI-COPY-FILE-PATH-ACTIONS-001
acceptance_criteria:
- AC-UI-COPY-FILE-PATH-ACTIONS-001.1
- AC-UI-COPY-FILE-PATH-ACTIONS-001.2
- AC-UI-COPY-FILE-PATH-ACTIONS-001.3
- AC-UI-COPY-FILE-PATH-ACTIONS-001.4
- AC-UI-COPY-FILE-PATH-ACTIONS-001.5
- AC-UI-COPY-FILE-PATH-ACTIONS-001.6
system_design:
- ../../specs/ui/system-design/copy-file-path-actions.md
---
# Task 01: Add copy path actions
## Summary
Add Copy path to working-tree file rows in the Changes panel and replace the
Review diff toolbar's Copy diff action with Copy path. Both actions copy the
current repository-relative path through the shared clipboard helper when it
contains no C0 or DEL ASCII control character. Unsafe paths are refused with
localized feedback.
## In scope
- Add a Copy path action to desktop Changes row hover and keyboard-focus
actions.
- Add Copy path to the touch Changes row action menu without changing row-tap
diff navigation.
- Replace Copy diff in the desktop and phone Review diff file actions.
- Refuse paths containing C0 or DEL ASCII control characters from each Copy
path entry point and report the refusal.
- Suppress the Review toolbar's existing absolute-worktree Copy path item so
the repository-relative action is the single Copy path choice there.
- Add focused unit and desktop/phone browser coverage for path value and access.
- Add desktop/phone component coverage for control-character rejection.
## Out of scope
- Copy diff actions in task diff, Monaco, and other editor surfaces.
- Path conversion, Git behavior, backend/API changes, and persistence.
- Public documentation changes.
## Acceptance
- Changes row Copy path copies the exact repository-relative path on desktop
and phone; using it does not open the diff.
- Review diff exposes Copy path in place of Copy diff on desktop and phone and
copies the active path, including the current path for a rename.
- Paths with C0 or DEL ASCII control characters are not copied from either
surface, and the UI reports the refusal.
- Phone controls and menu items meet the 44px target requirement, and desktop
row actions are available by keyboard focus as well as pointer hover.
## ASCII UI preview
UI-01 desktop and UI-02 phone from the
[full preview](plan.md#ascii-ui-preview):
```text
Desktop: [go] browser/programme_test.go +12 -12 [M] [undo] [edit] [copy]
Phone: [go] programme_test.go [+12 -12 M] [more]
go/internal/.../browser
Menu: Full path / Copy path / Stage or Unstage / Edit / Discard
```
UI-03 desktop and UI-04 phone from the
[full preview](plan.md#ascii-ui-preview):
```text
Desktop: [comment] path/to/programme_test.go [copy path] [external] [view] [...]
Phone: Diff header .../path/to/programme_test.go [more]
Menu: Copy path / Edit / Preview / diff controls
```
UI-05 from the [full preview](plan.md#ascii-ui-preview) shows the refusal
notice when a path contains a C0 or DEL ASCII control character. The clipboard
and the selected row or diff remain unchanged.
```text
Copy path: refused
Notice: This path contains control characters and cannot be copied.
```
AC-001.1 through AC-001.6 require the current repository-relative value for
safe paths, separate row and copy actions, current rename path, desktop/phone
access, and control-character rejection. The full plan preview defines fixed
versus scrollable content and each control's placement.
## Verification
Run from the repository root after `pnpm install --frozen-lockfile` in `apps`:
```bash
(cd apps/web && pnpm exec vitest run components/task/changes-panel-file-row.test.tsx components/review/review-diff-toolbar.test.tsx lib/utils/copy-repository-path.test.ts)
(cd apps/web && pnpm e2e:run --host --project chromium tests/git/git-changes-panel.spec.ts tests/review/review-file-status.spec.ts)
(cd apps/web && pnpm e2e:run --host --project mobile-chrome tests/task/mobile-changes-panel.spec.ts tests/review/mobile-review-file-status.spec.ts)
(cd apps/web && pnpm exec eslint components/task/changes-panel-file-row.tsx components/task/changes-panel-touch-file-row.tsx components/review/review-diff-toolbar.tsx components/task/changes-panel-file-row.test.tsx components/review/review-diff-toolbar.test.tsx)
(cd apps/web && pnpm run typecheck)
(cd apps/web && pnpm run i18n:ratchet)
```
## Files likely touched
- `apps/web/components/task/changes-panel-file-row.tsx`
- `apps/web/components/task/changes-panel-touch-file-row.tsx`
- `apps/web/components/task/changes-panel-file-row.test.tsx`
- `apps/web/components/review/review-diff-toolbar.tsx`
- `apps/web/components/review/review-diff-toolbar.test.tsx`
- `apps/web/hooks/use-copy-repository-path.ts`
- `apps/web/lib/utils/copy-repository-path.ts`
- `apps/web/lib/utils/copy-repository-path.test.ts`
- `apps/web/src/locales/*/task.json`
- `apps/web/components/editors/file-actions-dropdown.tsx`
- `apps/web/e2e/tests/git/git-changes-panel.spec.ts`
- `apps/web/e2e/tests/task/mobile-changes-panel.spec.ts`
- `apps/web/e2e/tests/review/review-file-status.spec.ts`
- `apps/web/e2e/tests/review/mobile-review-file-status.spec.ts`
## Dependencies
None.
## Risks
Keep the Changes row statistics contained at the minimum panel width. Copy the
current path for renamed files and retain the previous path only as context.
## Parallelism
`sequential`
## Inputs
- [Copy File Path Actions requirements](../../specs/ui/requirements/copy-file-path-actions.md)
- [Copy File Path Actions system design](../../specs/ui/system-design/copy-file-path-actions.md)
- Existing `copyToClipboard`, Changes row, touch action menu, and
Review file toolbar patterns.
## Results
Implemented desktop and phone Copy path actions for Changes rows, replaced
Copy diff in the Review toolbar, and suppressed the duplicate absolute-path
copy item inside Review file menus.
Verification passed:
- Focused Vitest suites: 25 tests passed.
- Desktop Chromium E2E: 28 tests passed; managed backend and Vite builds passed.
- Mobile Chrome E2E: 10 tests passed after the menu animation settled before
hit-area measurement.
- After formatting, `pnpm run build` and focused desktop/mobile browser reruns
passed (2 tests per project).
- Targeted ESLint and Prettier, web typecheck, i18n ratchet, specification
validation, specification lint, and `git diff --check` passed.
PR review remediation removed the unused Review toolbar `diff` prop, made the
keyboard-focus E2E assertion find Copy path without a fixed Tab count, and
added a shared C0/DEL path-copy guard with localized refusal feedback. The
system design now has valid requirement-mapping anchors, and acceptance
criterion AC-UI-COPY-FILE-PATH-ACTIONS-001.6 records the refusal contract.
Verification after remediation passed:
- Focused Changes row, Review toolbar, and path-copy utility Vitest suites: 39
tests.
- Desktop Chromium E2E: 28 tests; the managed backend and Vite builds passed.
- Phone mobile-chrome E2E: 10 tests; the managed backend and Vite builds passed.
- Web typecheck, targeted ESLint and Prettier, `i18n:check`,
`i18n:ratchet`, specification validation and lint, and `git diff --check`.
@@ -0,0 +1,64 @@
---
status: active
system: ui
created: 2026-09-24
owners:
- kandev
---
# Copy File Path Actions Requirements
## Overview
People can open a changed file's diff but must select its path manually to copy
it. The UI system owns the transient copy actions and their responsive
presentation. Task and workspace systems continue to own the file and
repository identities supplied to the UI.
## Requirements
### REQ-UI-COPY-FILE-PATH-ACTIONS-001: Copy a changed file path
**Intent:** Let people copy the repository-relative path of a changed file from
the Changes list or its Review diff header.
**User story:** As a person reviewing file changes, I want to copy a file path
directly so that I can reuse it without selecting the text.
#### Acceptance criteria
- **AC-UI-COPY-FILE-PATH-ACTIONS-001.1:** Given a working-tree file row in the
Changes panel whose path has no C0 or DEL ASCII control character, when a
user activates Copy path, the clipboard shall receive the exact
repository-relative path represented by that row, and the action shall not
open the file diff.
- **AC-UI-COPY-FILE-PATH-ACTIONS-001.2:** Given a file in the Review diff
panel whose path has no C0 or DEL ASCII control character, when the user
activates Copy path, the clipboard shall receive the current
repository-relative file path. Copy path shall replace Copy diff in that
panel's file toolbar.
- **AC-UI-COPY-FILE-PATH-ACTIONS-001.3:** For a renamed file, Copy path shall
copy the current path shown by the file row or diff header when that path has
no C0 or DEL ASCII control character. The previous path remains available
only through the existing rename context.
- **AC-UI-COPY-FILE-PATH-ACTIONS-001.4:** Copy path shall be keyboard and
pointer reachable on desktop and touch reachable on phones. It shall not
depend on hover as the only access path, and its phone controls shall have
hit areas of at least 44px in both dimensions.
- **AC-UI-COPY-FILE-PATH-ACTIONS-001.5:** The copied value shall not include a
repository label or an absolute worktree path, including in a multi-repository
task.
- **AC-UI-COPY-FILE-PATH-ACTIONS-001.6:** Given a file path containing a C0 or
DEL ASCII control character, when a user activates Copy path in the Changes
panel or Review diff panel, the clipboard shall remain unchanged and the UI
shall report that the path cannot be copied.
## Out of scope
- Changing file-path data, repository routing, diff selection, or Git actions.
- Copying the entire diff from other editor or diff surfaces.
- Adding clipboard persistence or a new clipboard implementation.
## Implementation plans
- [Copy file path actions](../../../plans/copy-file-path-actions/plan.md)
@@ -0,0 +1,136 @@
---
status: current
system: ui
requirements:
- REQ-UI-COPY-FILE-PATH-ACTIONS-001
---
# Copy File Path Actions System Design
## Purpose and boundaries
The UI system owns the transient copy controls and their desktop and phone
presentation. Existing task and workspace data remains authoritative for file
paths, repository identity, and diff selection. The change adds no backend
contract, persistent state, or path conversion.
## Requirement mapping
| Criterion | Design section |
| --------------------------------------------- | -------------------------------------------------------------------- |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.1` | [Changes row action](#changes-row-action) |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.2`, `001.3` | [Review diff action](#review-diff-action), [Path value](#path-value) |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.4` | [Responsive composition](#responsive-composition) |
| `AC-UI-COPY-FILE-PATH-ACTIONS-001.5` | [Path value](#path-value) |
## Components and responsibilities
- `ChangesPanel` continues to provide each working-tree
`ChangedFile.path`.
### Changes row action
- `FileRowActions` in
`apps/web/components/task/changes-panel-file-row.tsx` adds the
desktop Copy path control beside existing row actions.
- `TouchFileRowActions` in
`apps/web/components/task/changes-panel-touch-file-row.tsx` adds
Copy path to the existing row action menu. The row itself remains the primary
diff action.
### Review diff action
- `DesktopFileDiffToolbar` and `MobileFileMenuItems` in
`apps/web/components/review/review-diff-toolbar.tsx` replace the
Review toolbar's Copy diff action with Copy path.
- The shared file-action menus also offer Copy path, but that action copies an
absolute worktree path. The Review toolbar suppresses that generic item in
its desktop and phone menus so the Review surface has one unambiguous Copy
path action. Other editor menus keep their existing behavior.
- `copyToClipboard` in
`apps/web/lib/utils/copy-to-clipboard.ts` remains the clipboard
boundary, including its secure-context fallback.
- The existing `task:copyPath` translation supplies the accessible
name and tooltip. `task:copyPathWithControlCharacters` supplies refusal
feedback when a path contains control characters.
## Path value
Copy path passes the repository-relative path already represented by the
surface: `file.path` for a Changes row and the current
`filePath` for the Review diff toolbar. It does not prepend the
repository name, resolve a local filesystem root, or normalize the value. A
renamed file copies its current path; the existing previous-path value remains
reserved for rename cues and external links. Each copy action stops row-click
propagation where applicable.
Paths containing C0 or DEL ASCII control characters are rejected before the
clipboard write. For these paths the action reports the refusal and leaves the
clipboard unchanged. Other paths are copied exactly as represented by the
surface.
## Control flow
1. The row or active diff supplies its current path.
2. The shared path-copy helper rejects values containing C0 or DEL ASCII
control characters and reports the refusal.
3. For other values, the helper calls the shared clipboard helper with the
exact repository-relative path.
4. The Changes row remains selected and the Review diff remains open.
5. If the modern clipboard API is unavailable or rejects the write, the shared
helper attempts its existing DOM fallback. The action remains available for
retry if copying still fails.
## Responsive composition
- **Desktop Changes outcome:** the existing file row keeps its path and
statistics layout. Copy path joins the row's hover actions and becomes
visible while keyboard focus is within the actions. It does not replace the
row's click-to-open behavior.
- **Phone Changes entry point and surface:** the existing Changes bottom
navigation opens `MobileChangesPanel`. The visible 44px row menu
trigger exposes Copy path with the existing Stage/Unstage, Edit, and Discard
actions. The row identity continues to open the diff.
- **Nearest mobile exemplar:** `TouchFileRowActions` already presents
temporary row actions in a responsive menu with 44px controls. Reuse that
menu and its existing full-path label.
- **Review diff outcome:** fine-pointer desktop keeps a direct Copy path icon
in the file toolbar. Phone users open the existing 44px file-actions trigger
and select Copy path from its menu.
- **Shared behavior:** desktop and phone use the same path value and clipboard
helper. No new scroll owner, drawer, saved preference, or page-level
horizontal overflow is introduced.
## Failure and recovery
Clipboard writes use the shared helper and its current fallback. A failed
attempt does not alter the selected file, current diff, or path value; users can
retry from the same control. A path containing C0 or DEL ASCII control
characters is not copied and displays localized refusal feedback. A normal
clipboard failure remains available for retry from the same control.
## Persistence
None. Clipboard actions are transient.
## Security
Repository filenames are untrusted input. C0 and DEL ASCII control characters
are refused before clipboard access to prevent a copied name from injecting
additional terminal input. For other paths, only the repository-relative value
already shown by the UI is copied. No filesystem root, credential, or
repository label is added.
## Observability
No new logs or metrics are needed.
## Related specifications
- [Clipboard browser fallbacks](../../auth/requirements/secure-context-browser-fallbacks.md)
- [Changes file row containment](../requirements/changes-file-row-containment.md)
- [Review file status cues](../requirements/review-file-status.md)
## Implementation plan
- [Copy file path actions](../../../plans/copy-file-path-actions/plan.md)