Repositories list: recent searches, view options popover, and page size selector (monolith/S04)
Part of epic [Repositories list (monolith/S04)](https://gitlab.com/groups/gitlab-org/-/work_items/22321).
Three list controls that the shipped repositories list does not yet render: the recent-searches history, a view-options popover, and a page-size selector.
## Scope
### 1. Recent searches (plan Step 8)
Switch on the filtered-search bar's recent-searches history by giving it a storage key scoped per user and per organization. `FilteredSearchBarRoot` supplies the history list, the re-apply, and the clear action; this part supplies the key and the EE registry entry it resolves through. This is [plan](https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/plans/monolith/2026-07-28-repositories-list.md) **Step 8**, whose row in the plan's Status table carries no MR.
The design proposal prototype shows this control as the icon button inside the search field on the repositories list.
### 2. View options popover
An icon-button disclosure panel with a **Columns** section: one toggle per optional column, persisted client-side. Modelled on the version list's control from [!251881](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/251881), reusing its decisions (store the **hidden** keys, not the visible ones; intersect a stored value with the columns that actually have a switch; `GlDisclosureDropdown` with `:auto-close="false"`, not `GlPopover`).
**This has no design source.** The prototype's repositories list carries no view-options control — its heading kebab holds Manage access, View Trash, and Migrate. The control is proposed here by analogy with the version list, so the column set and the placement need design sign-off.
### 3. Page-size selector
A page-size dropdown beside the keyset pager, so a user can trade page count against page length.
**This contradicts the current plan.** Plan Step 10 states that "the pager exposes previous and next only, so the view offers no page-size selector", and the prototype's list shows Prev/Next with no selector. Landing this needs a plan amendment MR against [`docs/plans/monolith/2026-07-28-repositories-list.md`](https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/plans/monolith/2026-07-28-repositories-list.md) (and an S04 spec amendment if the spec's states table is read as fixing the page size), not just an implementation MR.
## Out of scope
- Free-text name search and the visibility and downloads filter tokens (Phase 2 contract gaps).
- Sort persistence, which already ships through `REPOSITORIES_SORT_STORAGE_KEY`.
- Any change to the AR list contract. All three parts are presentation-only; the page-size selector moves an argument the connection already accepts.
## Dependencies
- Parts 1 and 2 depend on nothing unmerged: the toolbar, the table, and the pager are all on `master`.
- Part 3 depends on the plan amendment above landing first, per the guardrail that no implementation MR opens ahead of its plan.
## Acceptance
- The bar lists recently applied filter selections, re-applies one, and clears the list, persisting client-side only. Two different `gon` usernames yield disjoint stored lists; two different organizations yield disjoint stored lists; with no `gon` username the bar mounts without a history and stores nothing. (S04 criterion 11, history part.)
- Switching a column off removes it from the table, leaves the panel open, and survives a reload. A stored value naming a column with no switch cannot hide that column.
- Selecting a page size re-queries at that size, drops the active cursor, and survives a reload.
- Each control is reachable and operable from the keyboard, and the Storybook stories that cover them pass the `test-storybook` axe run.
<details>
<summary>Implementation details</summary>
Paths are relative to `ee/app/assets/javascripts/packages_and_registries/artifact_registry/` unless stated otherwise. Line numbers are as of `master` at the time of writing.
### Recent searches
- `repositories/list/repositories_toolbar.vue` (Modify) - prefix the `namespace` prop with the `gon` username, giving `<gon username>/<organization global ID>`, and add the AR `recentSearchesStorageKey`. When `gon` carries no username, keep the current namespace and omit only the storage key: `namespace` is a required prop, so dropping it warns, whereas the bar gates both its history list and its save path on the key alone. Use the organization global ID rather than the slug, because the slug is the S01 stub and real discovery would orphan every stored list.
- `ee/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js` (Modify) - register the AR key in the EE lookup table, which spreads the CE one.
- `repositories/list/repositories_list.stories.js` (Modify) - a story with a populated history.
- `ee/spec/frontend/packages_and_registries/artifact_registry/repositories/list/repositories_toolbar_spec.js` (Modify) - the composed key, the two-username and missing-username cases, and a re-applied selection reaching the page.
The history is accumulated content, not a preference, so it takes the `<username>/<purpose>` prefix that `super_sidebar/utils.js` and the work-item listboxes use. The sort key beside it stays bare, which is the monolith's split between the two kinds of client-side key.
### View options
- `repositories/list/view_options.vue` (Create) - copy the shape of `repositories/versions/view_options.vue`, which already implements the pattern: `GlDisclosureDropdown` with `category="tertiary"`, `icon="preferences"`, `no-caret`, `text-sr-only`, a hover tooltip naming the icon button, and `GlToggle` inside each `GlDisclosureDropdownItem`'s `#list-item` slot. Derive the switch list by filtering `REPOSITORIES_TABLE_FIELDS` against an optional-column list, so a switch cannot exist for a column the table declares no label for.
- `constants.js` (Modify) - `REPOSITORIES_OPTIONAL_COLUMNS` and `REPOSITORIES_COLUMNS_STORAGE_KEY`, beside `VERSION_LIST_OPTIONAL_COLUMNS` (`constants.js:408`) and `VERSION_LIST_COLUMNS_STORAGE_KEY` (`constants.js:413`).
- `repositories/list/repositories_list.vue` (Modify) - hold the hidden-column list, read it through `LocalStorageSync` beside the existing sort sync, and pass it to the table.
- `repositories/list/repositories_table.vue` (Modify) - filter its fields by the hidden list.
`REPOSITORIES_TABLE_FIELDS` (`constants.js:316`) declares `format`, `name`, `kind`, `downloadsCount`, `sizeBytes`, `lastUpdatedAt`, and the actions field. Four of them get a switch: `kind` (Type), `downloadsCount` (Downloads), `sizeBytes` (Size), and `lastUpdatedAt` (Last updated). `name` and the actions field are excluded under !251881's rule that the row-identity and row-actions columns get no switch, and `format` is excluded with them, because the design pairs the format icon with the name as the row's identity.
The storage key: !251881 scoped the version-list key to the format family and to nothing else, deliberately departing from that spec's "scoped per user" wording, on the grounds that a column preference is not sensitive and the monolith has no `LocalStorageSync` precedent for per-user keys. The repositories list has one table and one format-agnostic column set, so its key needs no segment at all: `artifact-registry-repositories-columns`, matching `REPOSITORIES_SORT_STORAGE_KEY` (`constants.js:298`) beside it.
Placement: the toolbar row, beside the format dropdown, rather than the header actions. The header hosts the S07-owned create entry, and `repositories_list.vue` renders the toolbar in the populated and zero-result states while hiding it in the empty and error states - which is exactly when a column preference has nothing to act on.
### Page-size selector
- `repositories/list/repositories_list.vue` (Modify) - render `GlCollapsibleListbox` beside `GlKeysetPagination`, point the smart query's `first` variable at the selection, and drop the active cursor when the selection changes. The existing route-query reset already covers the "the cursor cannot carry across a change to what is being paged" rule; a page-size change is the same case, because the cursor is opaque over the current page length.
- `constants.js` (Modify) - the option list, beside `GRAPHQL_PAGE_SIZE = 20` (`constants.js:261`), which stays the default.
Precedent: `ee/app/assets/javascripts/cd/components/overview_card.vue` renders a `GlCollapsibleListbox` page-size control from a `PAGE_SIZE_OPTIONS` constant (`ee/app/assets/javascripts/cd/constants.js:86`) and emits the selection upward.
Bounds on the option list: the monolith caps any connection at 100 (`default_max_page_size 100`, `app/graphql/gitlab_schema.rb:33`), and the AR list endpoint's `limit` parameter defaults to 20 and clamps values above 100 (`api/openapi/v1.yaml`, the `limit` query parameter). So 20 / 50 / 100 is the widest option list that no layer silently rewrites.
Persistence: a page size is a preference, not accumulated content, so it takes a bare static `LocalStorageSync` key with no user or organization segment, matching the sort key. Holding it in the route query instead would make a page link carry its size, at the cost of a second key the cursor reset has to reason about; the preference key is the smaller change and matches the `cd` precedent.
</details>
issue
GitLab AI Context
Project: gitlab-org/gitlab
Instance: https://gitlab.com
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/CONTRIBUTING.md — contribution guidelines
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/README.md — project overview and setup
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/AGENTS.md — AI agent instructions
- https://gitlab.com/gitlab-org/gitlab/-/raw/master/CLAUDE.md — Claude Code instructions
Repository: https://gitlab.com/gitlab-org/gitlab
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD