diff --git a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml
index d0b241c..1841365 100644
--- a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml
+++ b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml
@@ -100,8 +100,17 @@
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
index 16f4a37..d6fc363 100644
--- a/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
+++ b/src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
@@ -209,6 +209,62 @@ private void LoadHighlightedPlan_Click(object? sender, RoutedEventArgs e)
PlansSelected?.Invoke(this, new List { row.Plan });
}
+ // ── Context menu ────────────────────────────────────────────────────────
+
+ private void ContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e)
+ {
+ var row = ResultsGrid.SelectedItem as QueryStoreRow;
+ var hasRow = row != null;
+
+ CopyQueryIdItem.IsEnabled = hasRow;
+ CopyPlanIdItem.IsEnabled = hasRow;
+ CopyQueryHashItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.QueryHash);
+ CopyPlanHashItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.QueryPlanHash);
+ CopyModuleItem.IsEnabled = hasRow && !string.IsNullOrEmpty(row!.ModuleName);
+ CopyQueryTextItem.IsEnabled = hasRow;
+ CopyRowItem.IsEnabled = hasRow;
+
+ // Wire click handlers (clear first to avoid stacking)
+ CopyQueryIdItem.Click -= CopyMenuItem_Click;
+ CopyPlanIdItem.Click -= CopyMenuItem_Click;
+ CopyQueryHashItem.Click -= CopyMenuItem_Click;
+ CopyPlanHashItem.Click -= CopyMenuItem_Click;
+ CopyModuleItem.Click -= CopyMenuItem_Click;
+ CopyQueryTextItem.Click -= CopyMenuItem_Click;
+ CopyRowItem.Click -= CopyMenuItem_Click;
+
+ if (!hasRow) return;
+
+ CopyQueryIdItem.Tag = row!.QueryId.ToString();
+ CopyPlanIdItem.Tag = row.PlanId.ToString();
+ CopyQueryHashItem.Tag = row.QueryHash;
+ CopyPlanHashItem.Tag = row.QueryPlanHash;
+ CopyModuleItem.Tag = row.ModuleName;
+ CopyQueryTextItem.Tag = row.FullQueryText;
+ CopyRowItem.Tag = $"{row.QueryId}\t{row.PlanId}\t{row.QueryHash}\t{row.QueryPlanHash}\t{row.ModuleName}\t{row.LastExecutedLocal}\t{row.ExecsDisplay}\t{row.TotalCpuDisplay}\t{row.AvgCpuDisplay}\t{row.TotalDurDisplay}\t{row.AvgDurDisplay}\t{row.TotalReadsDisplay}\t{row.AvgReadsDisplay}\t{row.TotalWritesDisplay}\t{row.AvgWritesDisplay}\t{row.TotalPhysReadsDisplay}\t{row.AvgPhysReadsDisplay}\t{row.TotalMemDisplay}\t{row.AvgMemDisplay}\t{row.FullQueryText}";
+
+ CopyQueryIdItem.Click += CopyMenuItem_Click;
+ CopyPlanIdItem.Click += CopyMenuItem_Click;
+ CopyQueryHashItem.Click += CopyMenuItem_Click;
+ CopyPlanHashItem.Click += CopyMenuItem_Click;
+ CopyModuleItem.Click += CopyMenuItem_Click;
+ CopyQueryTextItem.Click += CopyMenuItem_Click;
+ CopyRowItem.Click += CopyMenuItem_Click;
+ }
+
+ private async void CopyMenuItem_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is MenuItem item && item.Tag is string text)
+ await SetClipboardTextAsync(text);
+ }
+
+ private async System.Threading.Tasks.Task SetClipboardTextAsync(string text)
+ {
+ var topLevel = Avalonia.Controls.TopLevel.GetTopLevel(this);
+ if (topLevel?.Clipboard != null)
+ await topLevel.Clipboard.SetTextAsync(text);
+ }
+
// ── Column filter infrastructure ───────────────────────────────────────
private static readonly Dictionary> TextAccessors = new()