diff --git a/src/PlanViewer.App/MainWindow.axaml b/src/PlanViewer.App/MainWindow.axaml
index 68372c0..40e7c87 100644
--- a/src/PlanViewer.App/MainWindow.axaml
+++ b/src/PlanViewer.App/MainWindow.axaml
@@ -21,6 +21,8 @@
+
+
diff --git a/src/PlanViewer.App/MainWindow.axaml.cs b/src/PlanViewer.App/MainWindow.axaml.cs
index d01fd56..dc61e5f 100644
--- a/src/PlanViewer.App/MainWindow.axaml.cs
+++ b/src/PlanViewer.App/MainWindow.axaml.cs
@@ -35,17 +35,22 @@ public partial class MainWindow : Window
private McpHostService? _mcpHost;
private CancellationTokenSource? _mcpCts;
private int _queryCounter;
+ private AppSettings _appSettings;
public MainWindow()
{
_credentialService = CredentialServiceFactory.Create();
_connectionStore = new ConnectionStore();
+ _appSettings = AppSettingsService.Load();
// Listen for file paths from other instances (e.g. SSMS extension)
StartPipeServer();
InitializeComponent();
+ // Build the Recent Plans submenu from saved state
+ RebuildRecentPlansMenu();
+
// Wire up drag-and-drop
AddHandler(DragDrop.DropEvent, OnDrop);
AddHandler(DragDrop.DragOverEvent, OnDragOver);
@@ -88,7 +93,7 @@ public MainWindow()
}
}, RoutingStrategies.Tunnel);
- // Accept command-line argument or open a default query editor
+ // Accept command-line argument or restore previously open plans
var args = Environment.GetCommandLineArgs();
if (args.Length > 1 && File.Exists(args[1]))
{
@@ -96,8 +101,8 @@ public MainWindow()
}
else
{
- // Open with a query editor so toolbar buttons are visible on startup
- NewQuery_Click(this, new RoutedEventArgs());
+ // Restore plans that were open in the previous session
+ RestoreOpenPlans();
}
// Start MCP server if enabled in settings
@@ -162,6 +167,9 @@ private void StartMcpServer()
protected override async void OnClosed(EventArgs e)
{
+ // Save the list of currently open file-based plans for session restore
+ SaveOpenPlans();
+
_pipeCts.Cancel();
if (_mcpHost != null && _mcpCts != null)
@@ -360,6 +368,9 @@ private void LoadPlanFile(string filePath)
MainTabControl.Items.Add(tab);
MainTabControl.SelectedItem = tab;
UpdateEmptyOverlay();
+
+ // Track in recent plans list and persist
+ TrackRecentPlan(filePath);
}
catch (Exception ex)
{
@@ -1175,6 +1186,137 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer)
}
}
+ // ── Recent Plans & Session Restore ────────────────────────────────────
+
+ ///
+ /// Adds a file path to the recent plans list, saves settings, and rebuilds the menu.
+ ///
+ private void TrackRecentPlan(string filePath)
+ {
+ AppSettingsService.AddRecentPlan(_appSettings, filePath);
+ AppSettingsService.Save(_appSettings);
+ RebuildRecentPlansMenu();
+ }
+
+ ///
+ /// Rebuilds the Recent Plans submenu from the current settings.
+ /// Shows a disabled "(empty)" item when the list is empty, plus a Clear Recent separator.
+ ///
+ private void RebuildRecentPlansMenu()
+ {
+ RecentPlansMenu.Items.Clear();
+
+ if (_appSettings.RecentPlans.Count == 0)
+ {
+ var emptyItem = new MenuItem
+ {
+ Header = "(empty)",
+ IsEnabled = false
+ };
+ RecentPlansMenu.Items.Add(emptyItem);
+ return;
+ }
+
+ foreach (var path in _appSettings.RecentPlans)
+ {
+ var fileName = Path.GetFileName(path);
+ var directory = Path.GetDirectoryName(path) ?? "";
+
+ // Show "filename — directory" so the user can distinguish same-named files
+ var displayText = string.IsNullOrEmpty(directory)
+ ? fileName
+ : $"{fileName} — {directory}";
+
+ var item = new MenuItem
+ {
+ Header = displayText,
+ Tag = path
+ };
+
+ item.Click += RecentPlanItem_Click;
+ RecentPlansMenu.Items.Add(item);
+ }
+
+ RecentPlansMenu.Items.Add(new Separator());
+
+ var clearItem = new MenuItem { Header = "Clear Recent Plans" };
+ clearItem.Click += ClearRecentPlans_Click;
+ RecentPlansMenu.Items.Add(clearItem);
+ }
+
+ private void RecentPlanItem_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuItem item || item.Tag is not string path)
+ return;
+
+ if (!File.Exists(path))
+ {
+ // File was moved or deleted — remove from the list and notify the user
+ AppSettingsService.RemoveRecentPlan(_appSettings, path);
+ AppSettingsService.Save(_appSettings);
+ RebuildRecentPlansMenu();
+
+ ShowError($"The file no longer exists and has been removed from recent plans:\n\n{path}");
+ return;
+ }
+
+ LoadPlanFile(path);
+ }
+
+ private void ClearRecentPlans_Click(object? sender, RoutedEventArgs e)
+ {
+ _appSettings.RecentPlans.Clear();
+ AppSettingsService.Save(_appSettings);
+ RebuildRecentPlansMenu();
+ }
+
+ ///
+ /// Saves the file paths of all currently open file-based plan tabs.
+ ///
+ private void SaveOpenPlans()
+ {
+ _appSettings.OpenPlans.Clear();
+
+ foreach (var item in MainTabControl.Items)
+ {
+ if (item is not TabItem tab) continue;
+
+ var path = GetTabFilePath(tab);
+ if (!string.IsNullOrEmpty(path))
+ _appSettings.OpenPlans.Add(path);
+ }
+
+ AppSettingsService.Save(_appSettings);
+ }
+
+ ///
+ /// Restores plan tabs from the previous session. Skips files that no longer exist.
+ /// Falls back to a new query tab if nothing was restored.
+ ///
+ private void RestoreOpenPlans()
+ {
+ var restored = false;
+
+ foreach (var path in _appSettings.OpenPlans)
+ {
+ if (File.Exists(path))
+ {
+ LoadPlanFile(path);
+ restored = true;
+ }
+ }
+
+ // Clear the open plans list now that we've restored
+ _appSettings.OpenPlans.Clear();
+ AppSettingsService.Save(_appSettings);
+
+ if (!restored)
+ {
+ // Nothing to restore — open a fresh query editor like before
+ NewQuery_Click(this, new RoutedEventArgs());
+ }
+ }
+
private void ShowError(string message)
{
var dialog = new Window
diff --git a/src/PlanViewer.App/Services/AppSettingsService.cs b/src/PlanViewer.App/Services/AppSettingsService.cs
new file mode 100644
index 0000000..e8faf34
--- /dev/null
+++ b/src/PlanViewer.App/Services/AppSettingsService.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PlanViewer.App.Services;
+
+///
+/// Persists recent plans and open session state to a JSON file in the app's local data directory.
+///
+internal sealed class AppSettingsService
+{
+ private const int MaxRecentPlans = 10;
+ private static readonly string SettingsDir;
+ private static readonly string SettingsPath;
+
+ static AppSettingsService()
+ {
+ SettingsDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "PerformanceStudio");
+ SettingsPath = Path.Combine(SettingsDir, "appsettings.json");
+ }
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
+ ///
+ /// Loads settings from disk. Returns default settings if the file is missing or corrupt.
+ ///
+ public static AppSettings Load()
+ {
+ try
+ {
+ if (!File.Exists(SettingsPath))
+ return new AppSettings();
+
+ var json = File.ReadAllText(SettingsPath);
+ var settings = JsonSerializer.Deserialize(json, JsonOptions);
+ return settings ?? new AppSettings();
+ }
+ catch
+ {
+ return new AppSettings();
+ }
+ }
+
+ ///
+ /// Saves settings to disk. Silently ignores write failures.
+ ///
+ public static void Save(AppSettings settings)
+ {
+ try
+ {
+ Directory.CreateDirectory(SettingsDir);
+ var json = JsonSerializer.Serialize(settings, JsonOptions);
+ File.WriteAllText(SettingsPath, json);
+ }
+ catch
+ {
+ // Best-effort persistence — don't crash the app
+ }
+ }
+
+ ///
+ /// Adds a file path to the recent plans list (most recent first).
+ /// Deduplicates by full path (case-insensitive on Windows).
+ ///
+ public static void AddRecentPlan(AppSettings settings, string filePath)
+ {
+ var fullPath = Path.GetFullPath(filePath);
+
+ // Remove any existing entry for this path
+ settings.RecentPlans.RemoveAll(p =>
+ string.Equals(p, fullPath, StringComparison.OrdinalIgnoreCase));
+
+ // Insert at the front
+ settings.RecentPlans.Insert(0, fullPath);
+
+ // Trim to max size
+ if (settings.RecentPlans.Count > MaxRecentPlans)
+ settings.RecentPlans.RemoveRange(MaxRecentPlans, settings.RecentPlans.Count - MaxRecentPlans);
+ }
+
+ ///
+ /// Removes a specific path from the recent plans list.
+ ///
+ public static void RemoveRecentPlan(AppSettings settings, string filePath)
+ {
+ settings.RecentPlans.RemoveAll(p =>
+ string.Equals(p, filePath, StringComparison.OrdinalIgnoreCase));
+ }
+}
+
+///
+/// Serializable settings model for the application.
+///
+internal sealed class AppSettings
+{
+ ///
+ /// Most recently opened plan file paths, newest first. Max 10.
+ ///
+ [JsonPropertyName("recent_plans")]
+ public List RecentPlans { get; set; } = new();
+
+ ///
+ /// File paths that were open when the app last closed — restored on next launch.
+ ///
+ [JsonPropertyName("open_plans")]
+ public List OpenPlans { get; set; } = new();
+}