Skip to content

feat(home): group workspace folders by parent in the dropdown (#129)#1368

Open
georgeglarson wants to merge 4 commits into
OpenHands:mainfrom
georgeglarson:feat-129-group-workspace-folders
Open

feat(home): group workspace folders by parent in the dropdown (#129)#1368
georgeglarson wants to merge 4 commits into
OpenHands:mainfrom
georgeglarson:feat-129-group-workspace-folders

Conversation

@georgeglarson

@georgeglarson georgeglarson commented Jun 15, 2026

Copy link
Copy Markdown

HUMAN:

Tested locally with dev:mock: grouped dropdown renders parent headers, stays flat under 2 groups, and keyboard nav skips the headers.

  • A human has tested these changes.

AGENT:

End-to-end evidence below. The branch was rebased onto current main (7 commits) before opening; verification was re-run post-rebase.

  • npm run lint → clean: react-router typegen + tsc + eslint src + prettier --check all pass, exit 0.
  • npm test (full vitest run) → 3213 passed, 5 skipped, 9 todo (3227 total), exit 0. Includes the new __tests__/components/features/home/workspace-dropdown.test.tsx suite covering grouping order, labeling, accessibility names, keyboard nav (ArrowDown+Enter lands on a real folder, never a header), and fallback cases.
  • Real-browser check via npm run dev:mock (VITE_MOCK_API): the Home Repository/Workspace dropdown renders parent group headers, collapses to a flat list at <2 groups, floats the "Other" group last, and keyboard navigation skips headers.

Why

The Home Repository/Workspace dropdown lists every workspace folder flat, so when folders live under different parents there's no visual ownership grouping. Closes #129. The approach was posted on the issue and approved by @DevinVinson before this PR.

Summary

  • Group dropdown folders under a per-parent header using each folder's parentPath plus the merged parents from useResolvedWorkspaces; grouping activates only at 2+ groups, otherwise the list stays flat.
  • Headers are presentational siblings (no Downshift index consumed); each option carries its group label in its accessible name, so screen-reader content stays correct while keyboard nav/highlight stays aligned to visible order.
  • Catch-all "Other" group for unparented folders, always sorted last; fall back to the path basename when a parent name is missing.

Issue Number

#129

How to Test

  1. npm install
  2. npm run dev:mock (runs with mocked API data)
  3. Open the Home view; click the Repository/Workspace dropdown.
  4. With workspaces under 2+ distinct parents, confirm a header per parent and an "Other" group last for unparented folders.
  5. Confirm that with 0–1 groups the list renders flat (no headers).
  6. Keyboard: ArrowDown/ArrowUp moves only across selectable folders (headers are skipped); Enter selects the highlighted folder.

Automated: npm run lint and npm test both pass (counts above).

Video/Screenshots

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • Rebased onto current main before opening; single commit. The only touched-file overlap with upstream drift (src/i18n/translation.json) merged cleanly.
  • src/i18n/declaration.ts is a gitignored generated artifact (regenerated by make-i18n (which runs in prelint/test/build)); the new HOME$WORKSPACE_GROUP_OTHER key resolves under tsc.

Copilot AI review requested due to automatic review settings June 15, 2026 18:03
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

@georgeglarson is attempting to deploy a commit to the openhands Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds grouped workspace rendering in the Home workspace dropdown, including an “Other” bucket for standalone workspaces, and exposes resolved parent metadata so group headers can use parent display names.

Changes:

  • Added a new i18n key for the standalone “Other” workspace group label.
  • Extended useResolvedWorkspaces to return merged workspace parents and wired that into the workspace selection flow.
  • Implemented grouped rendering + accessibility labeling in WorkspaceDropdown, with supporting dropdown component enhancements and new tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/i18n/translation.json Adds HOME$WORKSPACE_GROUP_OTHER translations for the new standalone group header.
src/hooks/query/use-resolved-workspaces.ts Returns parents so UI can label groups using parent names (incl. implicit parents).
src/components/features/home/workspace-selection-form.tsx Passes resolved parents into WorkspaceDropdown.
src/components/features/home/workspace-dropdown/workspace-dropdown.tsx Groups workspaces by parent, adds headers, and augments option a11y names.
src/components/features/home/shared/generic-dropdown-menu.tsx Adds renderItemPrefix to support non-item UI (e.g., headers) in the list.
src/components/features/home/shared/dropdown-item.tsx Allows overriding option accessible name via ariaLabel.
tests/components/features/home/workspace-dropdown.test.tsx Adds coverage for grouping behavior, ordering, and a11y labeling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

} from "#/utils/dropdown-classes";
import { formControlFieldClassName } from "#/utils/form-control-classes";
import { LocalWorkspace } from "#/types/workspace";
import { LocalWorkspace, LocalWorkspaceParent } from "#/types/workspace";
import { describe, expect, it, vi } from "vitest";

import { WorkspaceDropdown } from "../../../../src/components/features/home/workspace-dropdown/workspace-dropdown";
import { LocalWorkspace, LocalWorkspaceParent } from "#/types/workspace";
Comment on lines +216 to +220
ariaLabel={
isGrouped
? `${groupLabelByIndex.get(index) ?? ""}, ${item.name}`.trim()
: undefined
}
Comment on lines +370 to +376
<li
role="presentation"
data-testid="workspace-group-header"
className="px-2 pt-2 pb-1 text-xs font-medium text-[var(--oh-muted)] select-none"
>
{label}
</li>
…nds#129)

The Repository/Workspace dropdown rendered every folder flat, so with more
than one workspace it was unclear which folder belonged to which. Group
folders by their parent workspace under a header.

- useResolvedWorkspaces now also returns the merged `parents` (stored +
  implicit /projects), so the dropdown can label each group by the parent's
  real name rather than a path basename.
- WorkspaceDropdown reorders the filtered list into contiguous groups and
  feeds that single array to both downshift's `items` and the menu, keeping
  highlightedIndex/keyboard nav in lockstep with the visible order. Headers
  are presentational siblings injected via a new opt-in GenericDropdownMenu
  `renderItemPrefix` slot — they consume no downshift index (modeled on the
  existing numberOfRecentItems divider). Flat fallback preserved for <=1 group.
- Static (no-parent) workspaces group under an 'Other' header (new i18n key,
  all locales).

Tests: grouping by parent name, contiguity, keyboard nav skips headers,
flat fallback, static-group handling. Full suite + typecheck + lint green.
The group label is already folded into each option's accessible name
("group, name"), so announcing the visual header re-reads the group.
role="presentation" strips the <li> semantics but leaves its text in
the accessibility tree; aria-hidden="true" removes the redundant
announcement.
@georgeglarson georgeglarson force-pushed the feat-129-group-workspace-folders branch from 302fa22 to 335a7f6 Compare June 16, 2026 13:54
The recent-items divider in GenericDropdownMenu was a <div> rendered as a
direct child of the listbox <ul> — invalid list markup that accessibility
tooling flags (surfaced by Copilot on PR #1). Make it an
<li role="presentation" aria-hidden="true">, matching the existing group-header
pattern, so the <ul> contains only <li> children and the divider stays out
of the a11y tree.

Add generic-dropdown-menu.test.tsx asserting both invariants.
Match the recent-items divider by role="presentation" rather than
"not an option", and assert the role explicitly, so the accessibility
contract stays pinned if another non-option child is added later
(CodeRabbit, PR #1).
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.

[Feature] Group workspace folders by workspace in the Repository/Workspace dropdown on the home view

2 participants