Restore query tabs on restart, and let Copy Path see them (#463) - #468
Merged
Merged
Conversation
Plan tabs came back after a restart. Query tabs did not, even when the query had been opened from a file and the path was sitting on the session control. GetTabFilePath knew exactly one tab shape: a DockPanel with a PlanViewerControl inside it. A query tab is a QuerySessionControl with no wrapper, and it has had a SourceFilePath of its own since #459. It was simply never asked, so SaveOpenPlans wrote nothing down and RestoreOpenPlans had nothing to bring back. The same blind spot hid Copy Path on the tab context menu, which is shown only when GetTabFilePath answers. It had never appeared on a query tab. With both kinds of file in one saved list, restore routes on extension through the existing OpenFileByExtension instead of assuming a plan. Handing a .sql file to LoadPlanFile produced an "XML is not valid" box where the user's query should have been. The setting is renamed open_plans -> open_tabs now that it holds both. A file written by the previous version is still read: the old key deserializes into a migration-only property that is merged into OpenTabs on load and then nulled, so it drops out of the file on the next save rather than taking a user's restored tabs with it. Scope: this restores query tabs that came from a file. A scratch tab that was never saved has no path and still does not come back; persisting unsaved buffers is #462. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx
|
Reviewed. No issues found.
New tests ( |
This was referenced Sep 1, 2026
rferraton
pushed a commit
to rferraton/PerformanceStudio
that referenced
this pull request
Oct 1, 2026
…rikdarlingdata#490) Session restore had two gaps, both from writing the list only at clean close (erikdarlingdata#468's original scope): any abnormal exit -- crash, task kill, an OS "shut down anyway" past the dirty-tab prompt -- restored zero tabs, and a file-backed plan or query detached into its own window was never written down at all, because SaveOpenPlans walked MainTabControl alone. Membership changes now write the list as they happen, debounced one second so a burst (restore, Close All) lands as one write. The trigger rides the erikdarlingdata#481 TabContentWatcher rather than the call sites, for exactly that commit's reason: a persist remembered at sixteen call sites gets forgotten at the seventeenth. The two changes the strip cannot show get explicit calls -- the detached register (a window closing changes no tab) and SaveQueryToPath (a scratch gaining its first file changes no membership). Detached windows join the collected set through a second register next to erikdarlingdata#473's: the prompt register is query-sessions-only because only an edit can be lost, while persistence needs every FILE-backed detached window, plans included. Detached entries append after the docked tabs; on the next start they come back as ordinary docked tabs, deliberately not re-detached windows. The crash-loop defense is kept and sharpened: RestoreOpenPlans clears and saves the empty list BEFORE the first open (it used to clear after the loop, which only defended against crashes after restore finished), and each file that opens successfully re-enters through the debounced writer, which restore flushes synchronously at its end. Net invariant: a file that crashes the app during load never persists -- it died before its own re-add -- while everything that opened does. A crash mid-restore still loses the tabs opened before it (their re-add was pending, the UI thread never flushed it); accepted, and said so in the code. OnClosed keeps its save as the final authoritative write, now with the debounce timer stopped first so no tick lands in a torn-down window. Under the test host no real timer is armed at all -- the suite shares one dispatcher, and a stray tick would write one test's tabs over another's staged state -- so tests drive the flush through a deterministic seam and assert the redirected settings file (erikdarlingdata#451/erikdarlingdata#487). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvAv72Pwb8czsjDWsCCk7n
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #463. Reported by @joshdbe in #458.
What was wrong
Close the app with a plan open and it comes back. Close it with a query open — a query you opened from a
.sqlfile, whose path the app has known since #459 — and it is gone.GetTabFilePathunderstood exactly one tab shape:A plan tab is a
DockPanelwrapping aPlanViewerControl. A query tab is aQuerySessionControlwith nothing around it, and it carries a perfectly goodSourceFilePath. It was never asked.SaveOpenPlansbuilds its list from this method, so it recorded nothing for query tabs andRestoreOpenPlanshad nothing to restore.Evidence, with only the
QuerySessionControlbranch removed and everything else in this PR left in place:An empty collection, with a query tab open that came from a file.
Copy Path, same cause
Copy Pathon the tab context menu is gated onGetTabFilePath(tab) != null, so it has never once appeared on a query tab. That is the same defect, not a second one, and it un-hides with the same three lines. It is also the half that is easy to fix by accident and never actually check, so there is a test that clicks the menu item and reads the path back off the clipboard rather than just assertingIsVisible.Restore has to route now
The saved list holds
.sqlpaths as well as plans, so restore goes through the existingOpenFileByExtensioninstead of assumingLoadPlanFile. Sending a query file toLoadPlanFilefails XML validation and puts up an error box where the user's query should have been — and at startup, before the main window is visible, that error box is worse than it sounds:That is the failure with the routing reverted and everything else in place.
ShowFileErrorhas a guard for exactly this;ShowError, whichLoadPlanFileuses, does not. Not fixed here — out of scope, and it lives in a file I am staying out of — but worth knowing it is there.The setting
open_plansis renamed toopen_tabsnow that it holds both kinds of file. An existing settings file is not silently emptied: the old key deserializes into a migration-only property that is merged intoOpenTabson load and then nulled. Nulls are not serialized, so the old key disappears from disk on the first ordinary save rather than lingering. If both keys somehow exist, the current one wins.SaveOpenPlansandRestoreOpenPlanskeep their names — renaming them means editingMainWindow.axaml.cs, which another PR is in the middle of.What this deliberately does not do
Copy Pathstays hidden on that tab until the next restart. Fixing that means recomputing on menu open, inside the tab-header construction another PR currently owns. Follow-up.Testing
335 passed / 0 failed / 2 skipped before, 343 / 0 / 2 after. Eight new tests, three of them proved red first by reverting one piece of the fix at a time:
QuerySessionControlbranch inGetTabFilePathAQueryOpenedFromAFileIsWrittenDownForTheNextSession,CopyPathIsOfferedOnAQueryTabAndCopiesTheFileOpenFileByExtensionback toLoadPlanFilein restoreAQueryFileComesBackAsAQueryTabOnTheNextStartTabsRecordedUnderTheOldSettingsKeyAreStillRestored,TheCurrentSettingsKeyWinsOverTheOldOneThe other three — a scratch tab records nothing,
Copy Pathstays hidden with no file, a plan still restores as a plan — pass either way by design. They pin the edges, they do not prove the fix.The migration was also checked against a real settings file rather than only in a unit test: an
appsettings.jsonwas hand-edited back to the pre-#463 shape — one plan path underopen_plans, noopen_tabs— and the suite run against it. Afterwards the file hasopen_tabsand noopen_plans, and the plan named only under the old key is at the head ofrecent_plans, which nothing butLoadPlanFileputs it there. The old list was read, opened, and retired.dotnet teststill reports "Zero tests ran" under SDK 10.0.302; that is the pre-existing runner integration problem, not this change. Counts above are from the test executable directly.🤖 Generated with Claude Code
https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx