Save and restore tab groups in launch configurations - #14624
Conversation
Launch configs stored windows, tabs, panes, titles and colors but not tab groups, so a saved workspace reopened with every tab flat. The projection `From<WindowSnapshot> for WindowTemplate` never read `snapshot.tab_groups`, and WindowTemplate had nowhere to put them -- session restore already round-trips groups through sqlite, so only this path lost them. WindowTemplate gains `tab_groups`, TabTemplate gains a `group` index into it. Both are skipped when empty, so configs saved from ungrouped windows serialize exactly as before. Runtime TabGroupIds are not stored: they are per-session UUIDs, and a config can be reopened into a workspace that already holds groups, so open_launch_config_window mints fresh ids and resolves membership through the index. Membership rides on each tab's own group_id rather than its position, because the save path drops tabs it cannot represent and renumbers the survivors -- the snapshot index and the template index diverge exactly then. A group whose only member was dropped does not survive either, so a config never restores an empty group the user can neither see nor remove. Restore mirrors the session-restore path's feature gating: groups only under GroupedTabs, pinned state only under PinnedTabs.
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR extends launch-config serialization and restoration so saved launch configs can carry tab group metadata and per-tab group references, with unit coverage for the save-side projection.
Concerns
- Launch config restore accepts split group memberships from hand-edited YAML; the rest of the tab-group code assumes each group is a contiguous run.
- For this user-facing restore behavior, please include screenshots or a screen recording demonstrating grouped launch config restore end to end. The PR description explicitly says the restore half was not exercised in a running app with
GroupedTabsenabled.
Verdict
Found: 0 critical, 2 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| .map_or(SelectedTabColor::Unset, SelectedTabColor::Color); | ||
| // An out-of-range index (hand-edited YAML) leaves the tab | ||
| // ungrouped rather than failing the whole window. | ||
| self.tabs[start_index + tab_index].group_id = tab_template |
There was a problem hiding this comment.
group indices directly, so a hand-edited config can assign the same group to non-contiguous tabs. The tab-group code assumes each group is a contiguous run; normalize or drop split memberships before setting group_id to avoid corrupting group operations.
The tab bar collapses each contiguous run of same-group tabs into one group container (tab_bar_slots), pushing a fresh Group slot whenever the run breaks. Restore accepted membership straight from the config, so a hand-edited YAML reading group 0, ungrouped, group 0 would draw two containers sharing one id -- a state no other code path can reach. resolve_group_memberships now honors the first run of each group and returns later stragglers ungrouped, alongside the existing out-of-range handling. Reordering the tabs would also restore the invariant, but silently moving tabs the config explicitly ordered is the more surprising of the two. Pulling the resolution out of open_launch_config_window's closure also makes it reachable from unit tests, which needed a ViewContext before.
|
Thanks — the first concern was real, and I had missed it. Pushed Split group membershipsConfirmed against Restore now keeps each group to its first contiguous run and returns later stragglers ungrouped. I considered reordering the tabs instead — it also restores the invariant — but silently moving tabs a config explicitly ordered seemed the more surprising of the two; happy to switch if you disagree. The resolution moved out of
Visual evidenceI want to give you a straight answer rather than a promise I cannot keep: I do not think an external contributor can produce this recording, because
So the restore half is unreachable in my build, which is exactly why the PR description said it was unverified rather than quietly claiming otherwise. What I can offer instead, and would rather do: an integration test in Tell me which you'd prefer:
Happy with any of the three; I just can't do the recording unaided. |
Fixes #13898
Why
Saving a launch config from a window with grouped tabs and reopening it gives you flat tabs — the groups are gone.
impl From<WindowSnapshot> for WindowTemplatereadssnapshot.tabsandsnapshot.active_tab_indexand never looks atsnapshot.tab_groups, andWindowTemplatehad nowhere to put them.Worth noting because it narrows the fix: this is a gap in one path, not a missing capability. Session restore already round-trips groups end to end —
persistence/sqlite.rswrites them (:1012-1038) and reads them back (:2538-2703), covered bytest_sqlite_round_trips_tab_groups. So the data model was already there; only the launch-config projection ignored it.What changed
Schema (
launch_config.rs) —WindowTemplategainstab_groups: Vec<TabGroupTemplate>andTabTemplategainsgroup: Option<usize>, an index into that list.Both fields are
skip_serializing_if+default, so existing configs are unaffected — a window with no groups serializes byte-for-byte as before. There is a test asserting neither key appears in that case.Runtime
TabGroupIds are deliberately not serialized. They are UUIDs minted per session, a config can be opened repeatedly, and it can be opened into a workspace that already holds groups — reusing saved ids would collide. The index indirection sidesteps that.Restore (
workspace/view.rs) —open_launch_config_windowcreates the groups first (fresh ids), then assigns each tab'sgroup_idthrough the index. This mirrors the session-restore path deliberately, including both feature gates: groups are only created underFeatureFlag::GroupedTabs, andpinnedis only honored underFeatureFlag::PinnedTabs.The part that needed care
The save path filters out tabs that cannot be represented (notebook panes) and renumbers the survivors via
num_valid_tabs. So group membership cannot be derived from tab position — the snapshot index and the template index diverge exactly when a tab drops out. Membership is instead carried on each tab's owngroup_idand resolved against the surviving group list.Two consequences, both locked by tests:
groupindices shift with them.test_config_from_snapshot_remaps_groups_around_unsaveable_tabsis the one that guards this, and I checked it actually bites rather than trusting the green run: dropping the "keep only groups that still have a member" filter turns it red withand restoring the filter returns the suite to 14 passed.
An out-of-range
groupindex (hand-edited YAML) leaves that tab ungrouped rather than failing the window — same posture as the session-restore path, which drops a tab's group reference when the group itself did not restore.Tests
Three new tests in
launch_config_tests.rs:test_config_from_snapshot_preserves_tab_groups— name/color carried, membership correct, ungrouped tabs stay ungroupedtest_config_from_snapshot_remaps_groups_around_unsaveable_tabs— the re-indexing case abovetest_config_from_snapshot_omits_tab_groups_when_there_are_none— backward compatibility of the serialized formThe four existing
WindowTemplate/TabTemplateliterals incrates/integration/src/test/launch_configs.rswere updated for the new fields; no behavior change there.Known validation limitation
cargo fmt -- --checkis clean,cargo check -p warp --all-targetsis clean, andcargo test -p warp --lib launch_configreports 14 passed / 0 failed.That covers the save half. I have not exercised the restore half through a running app on a machine with
GroupedTabsenabled — the flag gating and the id-minting onopen_launch_config_windoware verified by reading against the session-restore path they mirror, not by a manual round trip in the UI. If you would like that confirmed before merge, tell me which flag configuration to run and I will capture it.