microsoft / microsoft/PowerToys

CmdPal: list selection jumps to first row after in-place RaiseItemsChanged

Open
#48,895 1 comment 0 reactions 1 assignee Claimed by @jiripolasek View on GitHub
CmdPal-API Product-Command Palette
Dominant language
C
Stars
139k
Forks
8.6k
PR merge metrics
PR metrics pending

Description

## Summary

After an in-place command on a list page (extension returns `KeepOpen`, no navigation), keyboard selection often jumps to the **first row** instead of staying on the row the user was on. This affects **all invocation paths** — Ctrl+K context menu, future hover actions, shortcuts — not a single extension.

## Repro

1. Open CmdPal and navigate to any extension list page that reloads after an in-place action (e.g. toggle favorite/pin, undo, etc.).
2. Arrow-key to a row that is **not** the first item.
3. Run an in-place action that triggers `RaiseItemsChanged()` (e.g. Favorite via **Ctrl+K** or a context-menu invokable that returns `KeepOpen`).
4. **Expected:** selection stays on the same logical row (keyboard continuity).
5. **Actual:** highlight jumps to the first selectable row (often feels like it “jumped to Settings” or another top item depending on sort order).

## Why this looks intentional but isn’t (on sub-pages)

CmdPal already has two refresh modes:

- **Full refresh** (`RaiseItemsChanged()` default) — on the **home** page, resetting to the first item after a full extension-list refresh is intentional.
- **Incremental / preserve selection** (`RaiseItemsChanged(ListViewModel.IncrementalRefresh)` / `-2`) — used by `MainListPage` with an explicit `// preserve selection` comment.

`ListItemsView.TrySetSelectionAfterUpdate` also tries to restore `_stickySelectedItem` when `forceFirstItem` is false — clearly aimed at keeping keyboard position on soft updates.

The gap: sticky restore only matches **VM object identity**. After `FetchItems`, extensions often produce new `ListItem` instances → new `ListItemViewModel`s → `items.Contains(_stickySelectedItem)` fails → fallback selects `items[firstUsefulIndex]`.

Extension SDK docs only mention `RaiseItemsChanged()`; `IncrementalRefresh` is not exposed on `ListPage` in the toolkit, so most extensions cannot opt into preserve-selection without referencing UI internals.

## Suggested fix (host-side)

In `Microsoft.CmdPal.UI/ExtViews/ListItemsView.xaml.cs`, extend sticky selection restore in `TrySetSelectionAfterUpdate`:

1. Track `_stickySelectedItemCommandId` (from `ListItemViewModel.Command.Id`) whenever selection changes.
2. When VM reference restore fails after a list refresh, find the new row by matching `Command.Id` (ordinal).
3. Restore that item, call `SetListSelected(true)`, update `_stickySelectedItem`.
4. Keep existing `forceFirstItem` behavior unchanged for home-page full refreshes.

No extension-specific logic required; benefits any provider that reloads in place.

## Optional follow-up

- Expose `RaiseItemsChangedPreserveSelection()` (or document `IncrementalRefresh`) in the extension SDK so authors can opt in explicitly.
- Add a note in extension docs that in-place mutations should prefer preserve-selection refresh when item VMs may be recreated.

## Related

- Keyboard/a11y: losing list position is especially bad for keyboard-only users (arrow → act → arrow again).
- Not specific to list hover actions; observed via Ctrl+K and in-place invokables generally.

## Environment

- CmdPal / PowerToys `main` (also reproduced while testing list hover actions)
- Windows 11

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.