Skip to content

ux(canvas/files): right-click context menu — Open / Download / Delete (#2999 PR-C) - #3004

Merged
HongmingWang-Rabbit merged 1 commit into
stagingfrom
ux/files-tab-context-menu
May 6, 2026
Merged

HongmingWang-Rabbit merged 1 commit into
stagingfrom
ux/files-tab-context-menu

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

Why

User asked for a VSCode-style right-click menu on file rows (#2999): "right click to have a menu to download". Today the only download affordance is the toolbar's Export-all (bulk JSON dump), and the inline ✕ button is the only delete UX (small click target, easy to miss).

Fix

  1. New FileTreeContextMenu component — fixed-position popover with Open / Download / Delete items composed per-row (files get all three; directories get Delete only since "open a directory in the editor" doesn't apply). Esc + outside-click + Tab + scroll dismiss. ↓/↑ arrow keys rove focus between menu items. role=menu + role=menuitem + autofocus on first item for a11y.
  2. Menu state lifted to the top-level FileTree (not per-row) so opening a second row's menu auto-closes the first — only one menu open at a time, matching VSCode/Theia. Pinned by the "replaces the first" test.
  3. New downloadFileByPath(path) in useFilesApi — fetches via the existing GET /workspaces/<id>/files/<path>?root= endpoint and triggers a browser download. Distinct from the existing handleDownloadFile which downloads the in-editor buffer (round-trips unsaved edits to disk); the context-menu download targets arbitrary tree rows the user hasn't opened.
  4. canDelete prop threaded from FilesTab → FileTree → menu → item. Same gate as the toolbar (Clear/New/Upload all gated to /configs); context menu's Delete renders as disabled with a muted background on other roots.

Test coverage

FileTreeContextMenu.test.tsx (8 tests):

  • File row → menu opens with Open + Download + Delete.
  • Directory row → menu opens with Delete only.
  • Click Download → onDownload(path) fires + menu closes.
  • Click Delete (canDelete=true) → onDelete(path) fires.
  • Click Delete (canDelete=false) → onDelete NOT called + menu stays open (disabled-state UX).
  • Esc dismisses.
  • Outside-click (mousedown on document.body) dismisses.
  • Opening second context menu replaces the first (only-one-open invariant).

Three weakest spots (hostile self-review)

  1. Menu positioned at clientX/clientY without viewport clamping. Right-click at the very bottom-right may overflow a few pixels. FilesTab is fixed-width and the menu is small (~140×80px), so overflow is bounded. Filed as a follow-up.
  2. Auto-focus on first item shifts keyboard focus away from the row. Closing with Esc returns focus to the body, not the row. Matches the canvas's other context menus — uniform if not ideal.
  3. Download request reuses the API client's 15s default timeout. Same risk as the existing toolbar Export.

Verification

  • npx tsc --noEmit clean
  • 176/176 canvas tab tests pass
  • Manual on local dev: right-click a config.yaml row → menu opens → click Download → file lands in Downloads. Right-click on /home root → Delete renders disabled.

Pairing

Pairs with PR #3002 (PR-A, backend EIC) — without PR-A the tree is empty and there's nothing to right-click on a SaaS workspace.

Refs #2999.

🤖 Generated with Claude Code

…#2999 PR-C)

## Why

User asked for a VSCode-style right-click menu on file rows (#2999):
"right click to have a menu to download". Today the only download
affordance is the toolbar's Export-all (bulk JSON dump), and the
inline ✕ button is the only delete UX (small click target, easy to
miss).

## Fix

1. New `FileTreeContextMenu` component — fixed-position popover with
   Open / Download / Delete items composed per-row (files get all
   three; directories get Delete only since "open a directory in the
   editor" doesn't apply). Esc + outside-click + Tab + scroll
   dismiss. ↓/↑ arrow keys rove focus between menu items. role=menu
   + role=menuitem + autofocus on first item for a11y.

2. Menu state lifted to the top-level `FileTree` (not per-row) so
   opening a second row's menu auto-closes the first — only one
   menu open at a time, matching VSCode/Theia. Pinned by the
   `replaces the first` test.

3. New `downloadFileByPath(path)` in `useFilesApi` — fetches via the
   existing GET /workspaces/<id>/files/<path>?root= endpoint and
   triggers a browser download. Distinct from the existing
   `handleDownloadFile` which downloads the in-editor buffer
   (round-trips unsaved edits to disk); the context-menu download
   targets arbitrary tree rows the user hasn't opened.

4. `canDelete` prop threaded from FilesTab → FileTree → menu →
   item. Same gate as the toolbar (Clear/New/Upload all gated to
   /configs); context menu's Delete renders as disabled with a
   muted background on other roots, matching the "feature exists
   but isn't applicable here" pattern.

## Test coverage

`FileTreeContextMenu.test.tsx` (8 tests):

- File row → menu opens with Open + Download + Delete.
- Directory row → menu opens with Delete only.
- Click Download → onDownload(path) fires + menu closes.
- Click Delete (canDelete=true) → onDelete(path) fires.
- Click Delete (canDelete=false) → onDelete NOT called + menu stays
  open (disabled-state UX).
- Esc dismisses.
- Outside-click (mousedown on document.body) dismisses.
- Opening second context menu replaces the first (only-one-open
  invariant).

Each test uses fireEvent + screen.getByRole, so they fail on a
deleted-code regression — none would pass on the pre-PR shape.

## Three weakest spots (hostile self-review)

1. The menu is positioned at `clientX/clientY` without viewport
   clamping. If the user right-clicks at the very bottom-right of
   the panel, part of the menu may overflow off-screen. VSCode
   handles this by flipping the anchor; we don't yet. Acceptable
   v1 because the FilesTab is fixed-width (≤ side-panel width)
   and the menu is small (140×~80px); the overflow would be a few
   pixels of one item. Filed as a follow-up.

2. Auto-focus on the first item shifts keyboard focus away from
   the row that opened the menu. Closing with Esc returns focus
   to the body, not the row. Same behavior as TerminalTab's
   placeholder + the canvas's other context menus; consistent
   isn't ideal but at least uniform. Documented inline.

3. The download request reuses the API client's 15s default
   timeout — large config files (multi-MB skill bundles) on a
   slow connection could time out. Same risk applies to the
   existing toolbar Export. If we see real download failures we
   can add a `timeoutMs` override at the call site without
   touching the menu.

## Verification

- `npx tsc --noEmit` clean
- 176/176 canvas tab tests pass
- Manual on local dev: right-click a config.yaml row → menu opens
  → click Download → file lands in Downloads. Right-click on
  /home root → Delete renders disabled.

Refs #2999. Pairs with PR-A (backend EIC) — without PR-A the tree
is empty and there's nothing to right-click on a SaaS workspace.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@HongmingWang-Rabbit
HongmingWang-Rabbit added this pull request to the merge queue May 6, 2026
Merged via the queue into staging with commit 57bfa40 May 6, 2026
23 of 24 checks passed
@HongmingWang-Rabbit
HongmingWang-Rabbit deleted the ux/files-tab-context-menu branch May 6, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant