Skip to content

Fix duplicate dock band pins#46637

Open
niels9001 wants to merge 2 commits intomainfrom
niels9001/deduping-bands
Open

Fix duplicate dock band pins#46637
niels9001 wants to merge 2 commits intomainfrom
niels9001/deduping-bands

Conversation

@niels9001
Copy link
Copy Markdown
Collaborator

@niels9001 niels9001 commented Mar 30, 2026

Summary of the Pull Request

Summary

Dock bands could be pinned multiple times, resulting in duplicate entries in settings and duplicate UI items in the dock.

Root Cause

CommandProviderWrapper.PinDockBand had a TOCTOU (time-of-check-time-of-use) race: the duplicate check read a settings snapshot before entering the UpdateSettings CAS loop. If concurrent or retried calls overlapped, the transform lambda would blindly .Add() without re-checking, producing duplicate entries in DockSettings.StartBands / CenterBands / EndBands.

Additionally, DockViewModel.SetupBands rendered every entry from settings without deduplication, so any duplicates already persisted in settings.json were faithfully displayed.

TeamsExtension didn't set an explicit id
The TeamsExtension was missing a best practice: the dock band's MeetingControlPage had no explicit Id, so CmdPal generated an unstable hash-based ID from the band's title and subtitle. Setting a stable Id makes the band more resilient against CmdPal bugs and ensures pin entries in settings always match the band across title renames or extension updates.

Changes

File Change
CommandProviderWrapper.cs Moved the duplicate guard inside the UpdateSettings lambda so it is re-evaluated on every CAS retry against the latest snapshot
CommandProviderWrapper.cs Added a dedup check in the internal PinDockBand(TopLevelViewModel) before appending to DockBandItems
DockViewModel.cs Added a HashSet in SetupBands to skip duplicate (ProviderId, CommandId) entries at render time

Validation

  • Build succeeds (exit code 0)
  • Verified duplicate entries in settings.json are no longer rendered after the SetupBands dedup
  • Verified the CAS-lambda guard prevents new duplicates from being written

PR Checklist

  • Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
  • Tests: Added/updated and all pass
  • Localization: All end-user-facing strings can be localized
  • Dev docs: Added/updated
  • New binaries: Added on the required places
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

Validation Steps Performed

@niels9001 niels9001 requested a review from zadjii-msft March 30, 2026 19:06
@jiripolasek
Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@michaeljolley michaeljolley added the Product-Command Palette Refers to the Command Palette utility label Apr 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an issue in CmdPal Dock where the same dock band could be pinned multiple times (persisting duplicates in settings and rendering duplicate UI items) by making pinning idempotent under concurrent updates and adding UI-time deduplication.

Changes:

  • Moved the “already pinned” guard into the ISettingsService.UpdateSettings CAS transform to eliminate the TOCTOU window during retries.
  • Added an in-memory dedup guard when appending to CommandProviderWrapper.DockBandItems.
  • Added render-time deduplication in DockViewModel.SetupBands to skip duplicate (ProviderId, CommandId) entries from settings.

Reviewed changes

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

File Description
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Dock/DockViewModel.cs Deduplicates dock band settings entries at render time to avoid duplicate UI items.
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs Makes dock-band pinning safe under CAS retries and avoids duplicate in-memory band entries.

Comment on lines +88 to +95
HashSet<string> seen = new(StringComparer.Ordinal);
foreach (var band in bands)
{
var commandId = band.CommandId;

// Skip duplicate entries that share the same provider + command id
var key = $"{band.ProviderId}\0{commandId}";
if (!seen.Add(key))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Building a composite string key here allocates on every iteration of SetupBands. Since this can run on every settings change / DockBands collection change, consider avoiding the per-item string allocation (e.g., use a HashSet of tuples or a custom comparer over (ProviderId, CommandId)).

Suggested change
HashSet<string> seen = new(StringComparer.Ordinal);
foreach (var band in bands)
{
var commandId = band.CommandId;
// Skip duplicate entries that share the same provider + command id
var key = $"{band.ProviderId}\0{commandId}";
if (!seen.Add(key))
HashSet<(string ProviderId, string CommandId)> seen = new();
foreach (var band in bands)
{
var commandId = band.CommandId;
// Skip duplicate entries that share the same provider + command id
if (!seen.Add((band.ProviderId, commandId)))

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +98
if (!seen.Add(key))
{
Logger.LogWarning($"Skipping duplicate dock band entry {commandId} for provider {band.ProviderId}");
continue;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Logging a warning for each duplicate entry inside this loop can spam logs if settings.json already contains duplicates (and SetupBands runs repeatedly). Consider downgrading to Debug/Info or aggregating (log once with a count) to keep this path quieter.

Copilot uses AI. Check for mistakes.
Comment on lines 586 to +594
var bands = this.DockBandItems.ToList();

// Prevent duplicate entries in the in-memory band list
if (bands.Any(b => b.Id == bandVm.Id))
{
Logger.LogDebug($"Dock band '{bandVm.Id}' already exists in DockBandItems; skipping.");
return;
}

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This method materializes DockBandItems into a List before checking for duplicates. If the band is already present, the ToList allocation is wasted; consider checking DockBandItems first and only allocating a List when you actually need to append.

Suggested change
var bands = this.DockBandItems.ToList();
// Prevent duplicate entries in the in-memory band list
if (bands.Any(b => b.Id == bandVm.Id))
{
Logger.LogDebug($"Dock band '{bandVm.Id}' already exists in DockBandItems; skipping.");
return;
}
var existingBands = this.DockBandItems;
// Prevent duplicate entries in the in-memory band list
if (existingBands.Any(b => b.Id == bandVm.Id))
{
Logger.LogDebug($"Dock band '{bandVm.Id}' already exists in DockBandItems; skipping.");
return;
}
var bands = existingBands.ToList();

Copilot uses AI. Check for mistakes.
Comment on lines +519 to +525
if (dockSettings.StartBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId) ||
dockSettings.CenterBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId) ||
dockSettings.EndBands.Any(b => b.CommandId == commandId && b.ProviderId == this.ProviderId))
{
Logger.LogDebug($"Dock band '{commandId}' from provider '{this.ProviderId}' is already pinned; skipping.");
return s;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Because this CAS lambda can now return the original settings when the band is already pinned, the overall PinDockBand call becomes a no-op in that case — but the method still raises CommandsChanged afterwards (outside this hunk). This can trigger unnecessary reload work/UI churn; consider adding a fast-path pre-check before UpdateSettings (while keeping this in-lambda guard) or otherwise only raising CommandsChanged when a pin was actually added.

Copilot uses AI. Check for mistakes.
@niels9001 niels9001 requested a review from michaeljolley April 3, 2026 09:24
@niels9001 niels9001 requested a review from jiripolasek April 3, 2026 09:24
// retrieve the new items.
this.CommandsChanged?.Invoke(this, args);

internal void PinDockBand(TopLevelViewModel bandVm)
Copy link
Copy Markdown
Collaborator

@jiripolasek jiripolasek Apr 3, 2026

Choose a reason for hiding this comment

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

This is called only from an unused TopLevelCommandManager.PinDockBand (also unused in v0.98.1)

ShowSubtitles = showSubtitles,
};

settingsService.UpdateSettings(
Copy link
Copy Markdown
Collaborator

@jiripolasek jiripolasek Apr 3, 2026

Choose a reason for hiding this comment

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

This is not in 0.98.* line (added in 46451) (I mentioned this in relation of merging this to stable as a hotfix 0.98.2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

0.98.2 Product-Command Palette Refers to the Command Palette utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CmdPal Dock: duplicate bands

4 participants