assistant: Tweak tab bar layout (#15901)

Piotr Osiewicz created

- Add "New Context" button next to the hamburger
- Add "History"
- Allow Pane tab rendering callback to return items for both left and
right side of the tab bar.


![image](https://github.com/user-attachments/assets/d5aa599d-c9e4-4f26-ad66-ffc290c53c29)


Release Notes:

- N/A

Change summary

assets/icons/text-search.svg               |  1 
crates/assistant/src/assistant_panel.rs    | 30 ++++++++++++++++++++++-
crates/terminal_view/src/terminal_panel.rs |  7 +++--
crates/ui/src/components/icon.rs           |  2 +
crates/workspace/src/pane.rs               | 23 ++++++++++--------
5 files changed, 48 insertions(+), 15 deletions(-)

Detailed changes

assets/icons/text-search.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-search"><path d="M21 6H3"/><path d="M10 12H3"/><path d="M10 18H3"/><circle cx="17" cy="15" r="3"/><path d="m21 19-1.9-1.9"/></svg>

crates/assistant/src/assistant_panel.rs 🔗

@@ -362,8 +362,32 @@ impl AssistantPanel {
             pane.display_nav_history_buttons(None);
             pane.set_should_display_tab_bar(|_| true);
             pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
-                h_flex()
+                let focus_handle = pane.focus_handle(cx);
+                let left_children = IconButton::new("history", IconName::TextSearch)
+                    .on_click(
+                        cx.listener(|_, _, cx| cx.dispatch_action(DeployHistory.boxed_clone())),
+                    )
+                    .tooltip(move |cx| {
+                        cx.new_view(|cx| {
+                            let keybind =
+                                KeyBinding::for_action_in(&DeployHistory, &focus_handle, cx);
+                            Tooltip::new("History").key_binding(keybind)
+                        })
+                        .into()
+                    })
+                    .selected(
+                        pane.active_item()
+                            .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
+                    );
+                let right_children = h_flex()
                     .gap(Spacing::Small.rems(cx))
+                    .child(
+                        IconButton::new("new-context", IconName::Plus)
+                            .on_click(
+                                cx.listener(|_, _, cx| cx.dispatch_action(NewFile.boxed_clone())),
+                            )
+                            .tooltip(|cx| Tooltip::for_action("New Context", &NewFile, cx)),
+                    )
                     .child(
                         IconButton::new("menu", IconName::Menu)
                             .icon_size(IconSize::Small)
@@ -392,7 +416,9 @@ impl AssistantPanel {
                         el.child(Pane::render_menu_overlay(new_item_menu))
                     })
                     .into_any_element()
-                    .into()
+                    .into();
+
+                (Some(left_children.into_any_element()), right_children)
             });
             pane.toolbar().update(cx, |toolbar, cx| {
                 toolbar.add_item(context_editor_toolbar.clone(), cx);

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -174,9 +174,9 @@ impl TerminalPanel {
         self.pane.update(cx, |pane, cx| {
             pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
                 if !pane.has_focus(cx) {
-                    return None;
+                    return (None, None);
                 }
-                h_flex()
+                let right_children = h_flex()
                     .gap_2()
                     .children(additional_buttons.clone())
                     .child(
@@ -232,7 +232,8 @@ impl TerminalPanel {
                             })
                     })
                     .into_any_element()
-                    .into()
+                    .into();
+                (None, right_children)
             });
         });
     }

crates/ui/src/components/icon.rs 🔗

@@ -253,6 +253,7 @@ pub enum IconName {
     Tab,
     Terminal,
     TextCursor,
+    TextSearch,
     Trash,
     TriangleRight,
     Update,
@@ -414,6 +415,7 @@ impl IconName {
             IconName::Tab => "icons/tab.svg",
             IconName::Terminal => "icons/terminal.svg",
             IconName::TextCursor => "icons/text-cursor.svg",
+            IconName::TextSearch => "icons/text-search.svg",
             IconName::Trash => "icons/trash.svg",
             IconName::TriangleRight => "icons/triangle_right.svg",
             IconName::Update => "icons/update.svg",

crates/workspace/src/pane.rs 🔗

@@ -237,7 +237,8 @@ pub struct Pane {
         Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
     can_split: bool,
     should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
-    render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> Option<AnyElement>>,
+    render_tab_bar_buttons:
+        Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
     _subscriptions: Vec<Subscription>,
     tab_bar_scroll_handle: ScrollHandle,
     /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
@@ -357,11 +358,11 @@ impl Pane {
             should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
             render_tab_bar_buttons: Rc::new(move |pane, cx| {
                 if !pane.has_focus(cx) {
-                    return None;
+                    return (None, None);
                 }
                 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
                 // `end_slot`, but due to needing a view here that isn't possible.
-                h_flex()
+                let right_children = h_flex()
                     // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
                     .gap(Spacing::Small.rems(cx))
                     .child(
@@ -441,7 +442,8 @@ impl Pane {
                         el.child(Self::render_menu_overlay(split_item_menu))
                     })
                     .into_any_element()
-                    .into()
+                    .into();
+                (None, right_children)
             }),
             display_nav_history_buttons: Some(
                 TabBarSettings::get_global(cx).show_nav_history_buttons,
@@ -586,7 +588,8 @@ impl Pane {
 
     pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
     where
-        F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> Option<AnyElement>,
+        F: 'static
+            + Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>),
     {
         self.render_tab_bar_buttons = Rc::new(render);
         cx.notify();
@@ -1892,11 +1895,11 @@ impl Pane {
             )
             .map(|tab_bar| {
                 let render_tab_buttons = self.render_tab_bar_buttons.clone();
-                if let Some(buttons) = render_tab_buttons(self, cx) {
-                    tab_bar.end_child(buttons)
-                } else {
-                    tab_bar
-                }
+                let (left_children, right_children) = render_tab_buttons(self, cx);
+
+                tab_bar
+                    .start_children(left_children)
+                    .end_children(right_children)
             })
             .children(
                 self.items