Add option to show pinned tabs in a separate row (#46573)

Yaroslav Yenkala and Matt Miller created

## Problem:

When working with many `pinned tabs`, they consume significant
horizontal space in the tab bar, leaving less room for unpinned (active
working) tabs, especially on small screens. This creates a poor user
experience as users must constantly scroll to find their working tabs,
or the tabs become too narrow to read file names.


![8_tabs](https://github.com/user-attachments/assets/087a0b4d-8f1c-42f0-851c-b774228f11dd)

##  Solution:

Added a new opt-in setting `tab_bar.show_pinned_tabs_in_separate_row`
that displays pinned and unpinned tabs in two separate rows:
  - Top row: Pinned tabs with navigation buttons and tab bar controls
  - Bottom row: Unpinned (working) tabs with full horizontal space
 
- when pinned and not pinned tabs we show 2 rows:  

<img width="1512" height="159" alt="2_rows"
src="https://github.com/user-attachments/assets/62a85fcc-8073-40d2-a8e4-270efd0d0f21"
/>

This is a well-established UX pattern used in many popular IDEs
including JetBrains products (IntelliJ IDEA, WebStorm, PyCharm,
RubyMine), Visual Studio, and Eclipse.

The two-row layout only appears when both pinned AND unpinned tabs
exist. If only one type is present, a single row is displayed:
  
- when only not pinned tabs we show one row:

<img width="1510" height="111" alt="when only not pinned"
src="https://github.com/user-attachments/assets/048df4fc-4b17-4ea2-9c5b-b4db91cdfc78"
/>

- when only pinned tabs we show one row:

<img width="1510" height="111" alt="when only pinned tabs"
src="https://github.com/user-attachments/assets/7ccf2517-8711-4114-815d-6cbb1e89d7e2"
/>

  **Important** - Default Behavior Preserved:

- The setting defaults to false - existing users will see no change
whatsoever
- Original tab bar logic is preserved in a dedicated
`render_single_row_tab_bar` method, while the new two-row layout lives
in `render_two_row_tab_bar`. Both methods share common components
(`configure_tab_bar_start`, `configure_tab_bar_end`,
`render_unpinned_tabs_container`, `render_tab_bar_drop_target`) ensuring
DRY code and consistent behavior.
- Purely opt-in - users must explicitly enable this feature via Settings
UI or settings.json
- No impact on default UX - Zed's default tab bar appearance and
behavior remains identical

This implementation ensures zero risk to the existing user experience
while providing an optional enhancement for users who prefer separated
tab rows.

  Configuration

  Via Settings UI:
  - Open Settings (Cmd+,) → Editor section → "Pinned Tabs Layout" toggle
  
  

![settings_ui](https://github.com/user-attachments/assets/59b9bb94-1c10-4c32-b942-13aa01943526)


  Via settings.json:
  
  ```json
  {
    "tab_bar": {
      "show_pinned_tabs_in_separate_row": true
    }
  }
  ```
  
<img width="468" height="67" alt="manual_settings"
src="https://github.com/user-attachments/assets/d21f6dde-7683-47b3-8dca-d190049d32fb"
/>

## Video of implemented feature:


https://github.com/user-attachments/assets/b1e52074-a5a1-4c4e-ad59-e55e2213509d

## Tested:

- Verify default behavior unchanged (single row with pinned + unpinned
tabs inline)
- Enable setting → verify two rows appear when both pinned and unpinned
tabs exist
  - Enable setting → verify single row when only pinned tabs exist
  - Enable setting → verify single row when only unpinned tabs exist
  - Verify drag & drop works in both layouts
  - Verify navigation buttons and tab bar buttons render correctly
  - Verify setting toggle works in Settings UI
  - Verify setting works via settings.json

  Tested on: MacBook Pro M4, macOS 26.2
  
   ## Release Notes:

- Added `tab_bar.show_pinned_tabs_in_separate_row` setting to display
pinned and unpinned tabs in separate rows, giving unpinned tabs full
horizontal space

---------

Co-authored-by: Matt Miller <mattrx@gmail.com>

Change summary

assets/settings/default.json                      |   4 
crates/settings/src/settings_content/workspace.rs |   5 
crates/settings/src/vscode_import.rs              |   1 
crates/settings_ui/src/page_data.rs               |  24 
crates/workspace/src/pane.rs                      | 453 ++++++++++++++--
crates/workspace/src/workspace_settings.rs        |   2 
6 files changed, 425 insertions(+), 64 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1179,6 +1179,10 @@
     "show_nav_history_buttons": true,
     // Whether or not to show the tab bar buttons.
     "show_tab_bar_buttons": true,
+    // Whether or not to show pinned tabs in a separate row.
+    // When enabled, pinned tabs appear in a top row and unpinned tabs in a bottom row.
+    // When disabled, all tabs appear in a single row (default behavior).
+    "show_pinned_tabs_in_separate_row": false,
   },
   // Settings related to the editor's tabs
   "tabs": {

crates/settings/src/settings_content/workspace.rs 🔗

@@ -413,6 +413,11 @@ pub struct TabBarSettingsContent {
     ///
     /// Default: true
     pub show_tab_bar_buttons: Option<bool>,
+    /// Whether or not to show pinned tabs in a separate row.
+    /// When enabled, pinned tabs appear in a top row and unpinned tabs in a bottom row.
+    ///
+    /// Default: false
+    pub show_pinned_tabs_in_separate_row: Option<bool>,
 }
 
 #[with_fallible_options]

crates/settings/src/vscode_import.rs 🔗

@@ -646,6 +646,7 @@ impl VsCodeSettings {
             show_tab_bar_buttons: self
                 .read_str("workbench.editor.editorActionsLocation")
                 .and_then(|str| if str == "hidden" { Some(false) } else { None }),
+            show_pinned_tabs_in_separate_row: None,
         })
     }
 

crates/settings_ui/src/page_data.rs 🔗

@@ -3405,7 +3405,7 @@ fn window_and_layout_page() -> SettingsPage {
         ]
     }
 
-    fn tab_bar_section() -> [SettingsPageItem; 8] {
+    fn tab_bar_section() -> [SettingsPageItem; 9] {
         [
             SettingsPageItem::SectionHeader("Tab Bar"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -3524,6 +3524,28 @@ fn window_and_layout_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Pinned Tabs Layout",
+                description: "Show pinned tabs in a separate row above unpinned tabs.",
+                field: Box::new(SettingField {
+                    json_path: Some("tab_bar.show_pinned_tabs_in_separate_row"),
+                    pick: |settings_content| {
+                        settings_content
+                            .tab_bar
+                            .as_ref()?
+                            .show_pinned_tabs_in_separate_row
+                            .as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .tab_bar
+                            .get_or_insert_default()
+                            .show_pinned_tabs_in_separate_row = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
         ]
     }
 

crates/workspace/src/pane.rs 🔗

@@ -2765,7 +2765,7 @@ impl Pane {
             .on_drop(
                 cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
                     this.drag_split_direction = None;
-                    this.handle_tab_drop(dragged_tab, ix, window, cx)
+                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
                 }),
             )
             .on_drop(
@@ -3276,6 +3276,7 @@ impl Pane {
             .zip(tab_details(&self.items, window, cx))
             .map(|((ix, item), detail)| {
                 self.render_tab(ix, &**item, detail, &focus_handle, window, cx)
+                    .into_any_element()
             })
             .collect::<Vec<_>>();
         let tab_count = tab_items.len();
@@ -3320,7 +3321,51 @@ impl Pane {
                 .flatten()
                 .unwrap_or(false);
 
-        TabBar::new("tab_bar")
+        let tab_bar_settings = TabBarSettings::get_global(cx);
+        let use_separate_rows = tab_bar_settings.show_pinned_tabs_in_separate_row;
+
+        if use_separate_rows && !pinned_tabs.is_empty() && !unpinned_tabs.is_empty() {
+            self.render_two_row_tab_bar(
+                pinned_tabs,
+                unpinned_tabs,
+                tab_count,
+                navigate_backward,
+                navigate_forward,
+                open_aside_left,
+                open_aside_right,
+                render_aside_toggle_left,
+                render_aside_toggle_right,
+                window,
+                cx,
+            )
+        } else {
+            self.render_single_row_tab_bar(
+                pinned_tabs,
+                unpinned_tabs,
+                tab_count,
+                navigate_backward,
+                navigate_forward,
+                open_aside_left,
+                open_aside_right,
+                render_aside_toggle_left,
+                render_aside_toggle_right,
+                window,
+                cx,
+            )
+        }
+    }
+
+    fn configure_tab_bar_start(
+        &mut self,
+        tab_bar: TabBar,
+        navigate_backward: IconButton,
+        navigate_forward: IconButton,
+        open_aside_left: Option<AnyElement>,
+        render_aside_toggle_left: bool,
+        window: &mut Window,
+        cx: &mut Context<Pane>,
+    ) -> TabBar {
+        tab_bar
             .map(|tab_bar| {
                 if let Some(open_aside_left) = open_aside_left
                     && render_aside_toggle_left
@@ -3349,6 +3394,48 @@ impl Pane {
                     tab_bar
                 }
             })
+    }
+
+    fn configure_tab_bar_end(
+        tab_bar: TabBar,
+        open_aside_right: Option<AnyElement>,
+        render_aside_toggle_right: bool,
+    ) -> TabBar {
+        tab_bar.map(|tab_bar| {
+            if let Some(open_aside_right) = open_aside_right
+                && render_aside_toggle_right
+            {
+                tab_bar.end_child(open_aside_right)
+            } else {
+                tab_bar
+            }
+        })
+    }
+
+    fn render_single_row_tab_bar(
+        &mut self,
+        pinned_tabs: Vec<AnyElement>,
+        unpinned_tabs: Vec<AnyElement>,
+        tab_count: usize,
+        navigate_backward: IconButton,
+        navigate_forward: IconButton,
+        open_aside_left: Option<AnyElement>,
+        open_aside_right: Option<AnyElement>,
+        render_aside_toggle_left: bool,
+        render_aside_toggle_right: bool,
+        window: &mut Window,
+        cx: &mut Context<Pane>,
+    ) -> AnyElement {
+        let tab_bar = self
+            .configure_tab_bar_start(
+                TabBar::new("tab_bar"),
+                navigate_backward,
+                navigate_forward,
+                open_aside_left,
+                render_aside_toggle_left,
+                window,
+                cx,
+            )
             .children(pinned_tabs.len().ne(&0).then(|| {
                 let max_scroll = self.tab_bar_scroll_handle.max_offset().width;
                 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
@@ -3365,75 +3452,129 @@ impl Pane {
                             .border_color(cx.theme().colors().border)
                     })
             }))
+            .child(self.render_unpinned_tabs_container(unpinned_tabs, tab_count, cx));
+        Self::configure_tab_bar_end(tab_bar, open_aside_right, render_aside_toggle_right)
+            .into_any_element()
+    }
+
+    fn render_two_row_tab_bar(
+        &mut self,
+        pinned_tabs: Vec<AnyElement>,
+        unpinned_tabs: Vec<AnyElement>,
+        tab_count: usize,
+        navigate_backward: IconButton,
+        navigate_forward: IconButton,
+        open_aside_left: Option<AnyElement>,
+        open_aside_right: Option<AnyElement>,
+        render_aside_toggle_left: bool,
+        render_aside_toggle_right: bool,
+        window: &mut Window,
+        cx: &mut Context<Pane>,
+    ) -> AnyElement {
+        let pinned_tab_bar = self
+            .configure_tab_bar_start(
+                TabBar::new("pinned_tab_bar"),
+                navigate_backward,
+                navigate_forward,
+                open_aside_left,
+                render_aside_toggle_left,
+                window,
+                cx,
+            )
             .child(
                 h_flex()
-                    .id("unpinned tabs")
+                    .id("pinned_tabs_row")
+                    .debug_selector(|| "pinned_tabs_row".into())
                     .overflow_x_scroll()
                     .w_full()
-                    .track_scroll(&self.tab_bar_scroll_handle)
-                    .on_scroll_wheel(cx.listener(|this, _, _, _| {
-                        this.suppress_scroll = true;
-                    }))
-                    .children(unpinned_tabs)
-                    .child(
-                        div()
-                            .id("tab_bar_drop_target")
-                            .min_w_6()
-                            // HACK: This empty child is currently necessary to force the drop target to appear
-                            // despite us setting a min width above.
-                            .child("")
-                            // HACK: h_full doesn't occupy the complete height, using fixed height instead
-                            .h(Tab::container_height(cx))
-                            .flex_grow()
-                            .drag_over::<DraggedTab>(|bar, _, _, cx| {
-                                bar.bg(cx.theme().colors().drop_target_background)
-                            })
-                            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
-                                bar.bg(cx.theme().colors().drop_target_background)
-                            })
-                            .on_drop(cx.listener(
-                                move |this, dragged_tab: &DraggedTab, window, cx| {
-                                    this.drag_split_direction = None;
-                                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
-                                },
-                            ))
-                            .on_drop(cx.listener(
-                                move |this, selection: &DraggedSelection, window, cx| {
-                                    this.drag_split_direction = None;
-                                    this.handle_project_entry_drop(
-                                        &selection.active_selection.entry_id,
-                                        Some(tab_count),
-                                        window,
-                                        cx,
-                                    )
-                                },
-                            ))
-                            .on_drop(cx.listener(move |this, paths, window, cx| {
-                                this.drag_split_direction = None;
-                                this.handle_external_paths_drop(paths, window, cx)
-                            }))
-                            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
-                                if event.click_count() == 2 {
-                                    window.dispatch_action(
-                                        this.double_click_dispatch_action.boxed_clone(),
-                                        cx,
-                                    );
-                                }
-                            })),
-                    ),
+                    .children(pinned_tabs),
+            );
+        let pinned_tab_bar = Self::configure_tab_bar_end(
+            pinned_tab_bar,
+            open_aside_right,
+            render_aside_toggle_right,
+        );
+
+        v_flex()
+            .w_full()
+            .flex_none()
+            .child(pinned_tab_bar)
+            .child(
+                TabBar::new("unpinned_tab_bar").child(self.render_unpinned_tabs_container(
+                    unpinned_tabs,
+                    tab_count,
+                    cx,
+                )),
             )
-            .map(|tab_bar| {
-                if let Some(open_aside_right) = open_aside_right
-                    && render_aside_toggle_right
-                {
-                    tab_bar.end_child(open_aside_right)
-                } else {
-                    tab_bar
-                }
-            })
             .into_any_element()
     }
 
+    fn render_unpinned_tabs_container(
+        &mut self,
+        unpinned_tabs: Vec<AnyElement>,
+        tab_count: usize,
+        cx: &mut Context<Pane>,
+    ) -> impl IntoElement {
+        h_flex()
+            .id("unpinned tabs")
+            .overflow_x_scroll()
+            .w_full()
+            .track_scroll(&self.tab_bar_scroll_handle)
+            .on_scroll_wheel(cx.listener(|this, _, _, _| {
+                this.suppress_scroll = true;
+            }))
+            .children(unpinned_tabs)
+            .child(self.render_tab_bar_drop_target(tab_count, cx))
+    }
+
+    fn render_tab_bar_drop_target(
+        &self,
+        tab_count: usize,
+        cx: &mut Context<Pane>,
+    ) -> impl IntoElement {
+        div()
+            .id("tab_bar_drop_target")
+            .min_w_6()
+            // HACK: This empty child is currently necessary to force the drop target to appear
+            // despite us setting a min width above.
+            .child("")
+            // HACK: h_full doesn't occupy the complete height, using fixed height instead
+            .h(Tab::container_height(cx))
+            .flex_grow()
+            .drag_over::<DraggedTab>(|bar, _, _, cx| {
+                bar.bg(cx.theme().colors().drop_target_background)
+            })
+            .drag_over::<DraggedSelection>(|bar, _, _, cx| {
+                bar.bg(cx.theme().colors().drop_target_background)
+            })
+            .on_drop(
+                cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| {
+                    this.drag_split_direction = None;
+                    this.handle_tab_drop(dragged_tab, this.items.len(), window, cx)
+                }),
+            )
+            .on_drop(
+                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
+                    this.drag_split_direction = None;
+                    this.handle_project_entry_drop(
+                        &selection.active_selection.entry_id,
+                        Some(tab_count),
+                        window,
+                        cx,
+                    )
+                }),
+            )
+            .on_drop(cx.listener(move |this, paths, window, cx| {
+                this.drag_split_direction = None;
+                this.handle_external_paths_drop(paths, window, cx)
+            }))
+            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
+                if event.click_count() == 2 {
+                    window.dispatch_action(this.double_click_dispatch_action.boxed_clone(), cx);
+                }
+            }))
+    }
+
     pub fn render_menu_overlay(menu: &Entity<ContextMenu>) -> Div {
         div().absolute().bottom_0().right_0().size_0().child(
             deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1),
@@ -5007,6 +5148,181 @@ mod tests {
         assert_item_labels(&pane, ["A", "B*", "C"], cx);
     }
 
+    #[gpui::test]
+    async fn test_separate_pinned_row_disabled_by_default(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        let item_a = add_labeled_item(&pane, "A", false, cx);
+        add_labeled_item(&pane, "B", false, cx);
+        add_labeled_item(&pane, "C", false, cx);
+
+        // Pin one tab
+        pane.update_in(cx, |pane, window, cx| {
+            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
+            pane.pin_tab_at(ix, window, cx);
+        });
+        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
+
+        // Verify setting is disabled by default
+        let is_separate_row_enabled = pane.read_with(cx, |_, cx| {
+            TabBarSettings::get_global(cx).show_pinned_tabs_in_separate_row
+        });
+        assert!(
+            !is_separate_row_enabled,
+            "Separate pinned row should be disabled by default"
+        );
+
+        // Verify pinned_tabs_row element does NOT exist (single row layout)
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_none(),
+            "pinned_tabs_row should not exist when setting is disabled"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_separate_pinned_row_two_rows_when_both_tab_types_exist(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        // Enable separate row setting
+        set_pinned_tabs_separate_row(cx, true);
+
+        let item_a = add_labeled_item(&pane, "A", false, cx);
+        add_labeled_item(&pane, "B", false, cx);
+        add_labeled_item(&pane, "C", false, cx);
+
+        // Pin one tab - now we have both pinned and unpinned tabs
+        pane.update_in(cx, |pane, window, cx| {
+            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
+            pane.pin_tab_at(ix, window, cx);
+        });
+        assert_item_labels(&pane, ["A!", "B", "C*"], cx);
+
+        // Verify pinned_tabs_row element exists (two row layout)
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_some(),
+            "pinned_tabs_row should exist when setting is enabled and both tab types exist"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_separate_pinned_row_single_row_when_only_pinned_tabs(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        // Enable separate row setting
+        set_pinned_tabs_separate_row(cx, true);
+
+        let item_a = add_labeled_item(&pane, "A", false, cx);
+        let item_b = add_labeled_item(&pane, "B", false, cx);
+
+        // Pin all tabs - only pinned tabs exist
+        pane.update_in(cx, |pane, window, cx| {
+            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
+            pane.pin_tab_at(ix, window, cx);
+            let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
+            pane.pin_tab_at(ix, window, cx);
+        });
+        assert_item_labels(&pane, ["A!", "B*!"], cx);
+
+        // Verify pinned_tabs_row does NOT exist (single row layout for pinned-only)
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_none(),
+            "pinned_tabs_row should not exist when only pinned tabs exist (uses single row)"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_separate_pinned_row_single_row_when_only_unpinned_tabs(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        // Enable separate row setting
+        set_pinned_tabs_separate_row(cx, true);
+
+        // Add only unpinned tabs
+        add_labeled_item(&pane, "A", false, cx);
+        add_labeled_item(&pane, "B", false, cx);
+        add_labeled_item(&pane, "C", false, cx);
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+        // Verify pinned_tabs_row does NOT exist (single row layout for unpinned-only)
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_none(),
+            "pinned_tabs_row should not exist when only unpinned tabs exist (uses single row)"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_separate_pinned_row_toggles_between_layouts(cx: &mut TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        let item_a = add_labeled_item(&pane, "A", false, cx);
+        add_labeled_item(&pane, "B", false, cx);
+
+        // Pin one tab
+        pane.update_in(cx, |pane, window, cx| {
+            let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
+            pane.pin_tab_at(ix, window, cx);
+        });
+
+        // Initially disabled - single row
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_none(),
+            "Should be single row when disabled"
+        );
+
+        // Enable - two rows
+        set_pinned_tabs_separate_row(cx, true);
+        cx.run_until_parked();
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_some(),
+            "Should be two rows when enabled"
+        );
+
+        // Disable again - back to single row
+        set_pinned_tabs_separate_row(cx, false);
+        cx.run_until_parked();
+        let pinned_row_bounds = cx.debug_bounds("pinned_tabs_row");
+        assert!(
+            pinned_row_bounds.is_none(),
+            "Should be single row when disabled again"
+        );
+    }
+
     #[gpui::test]
     async fn test_pinning_active_tab_without_position_change_maintains_focus(
         cx: &mut TestAppContext,
@@ -7295,6 +7611,17 @@ mod tests {
         });
     }
 
+    fn set_pinned_tabs_separate_row(cx: &mut TestAppContext, enabled: bool) {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings(cx, |settings| {
+                settings
+                    .tab_bar
+                    .get_or_insert_default()
+                    .show_pinned_tabs_in_separate_row = Some(enabled);
+            });
+        });
+    }
+
     fn add_labeled_item(
         pane: &Entity<Pane>,
         label: &str,

crates/workspace/src/workspace_settings.rs 🔗

@@ -60,6 +60,7 @@ pub struct TabBarSettings {
     pub show: bool,
     pub show_nav_history_buttons: bool,
     pub show_tab_bar_buttons: bool,
+    pub show_pinned_tabs_in_separate_row: bool,
 }
 
 impl Settings for WorkspaceSettings {
@@ -121,6 +122,7 @@ impl Settings for TabBarSettings {
             show: tab_bar.show.unwrap(),
             show_nav_history_buttons: tab_bar.show_nav_history_buttons.unwrap(),
             show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(),
+            show_pinned_tabs_in_separate_row: tab_bar.show_pinned_tabs_in_separate_row.unwrap(),
         }
     }
 }