From 902e891cea1c16fa1304e6cb00a06c16afc7b9fe Mon Sep 17 00:00:00 2001 From: Michael Atwood <70112949+doowta@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:31:32 -0500 Subject: [PATCH] Agent: activate search in its sidebar row Keep the expanded header clear and place Search below New Task. Replace the row with the focused field in the same box, reveal it for the shortcut, and return to the row when an empty search ends. Clicking away drops the input focus while retaining a nonempty filter. Co-Authored-By: Claude Opus 5.5 --- apps/maple-agent/app/src/keymap.rs | 2 +- apps/maple-agent/app/src/ui/chat/commands.rs | 11 + apps/maple-agent/app/src/ui/chat/composer.rs | 8 + .../maple-agent/app/src/ui/chat/navigation.rs | 2 + apps/maple-agent/app/src/ui/chat/sidebar.rs | 257 ++++++++++++------ .../app/src/ui/chat/sidebar_computers.rs | 6 +- apps/maple-agent/app/src/ui/chat/tests.rs | 189 ++++++++++++- 7 files changed, 385 insertions(+), 90 deletions(-) diff --git a/apps/maple-agent/app/src/keymap.rs b/apps/maple-agent/app/src/keymap.rs index 3735ce503..466a73e8a 100644 --- a/apps/maple-agent/app/src/keymap.rs +++ b/apps/maple-agent/app/src/keymap.rs @@ -379,7 +379,7 @@ fn catalog_for(os: HostOs) -> Vec { "Focus task search", ShortcutCategory::Chat, Some("Chat"), - "secondary-k", + "secondary-f", SlotAction::FocusSearch, ), slot( diff --git a/apps/maple-agent/app/src/ui/chat/commands.rs b/apps/maple-agent/app/src/ui/chat/commands.rs index 903740d85..7ed177ae7 100644 --- a/apps/maple-agent/app/src/ui/chat/commands.rs +++ b/apps/maple-agent/app/src/ui/chat/commands.rs @@ -65,6 +65,17 @@ impl ChatScreen { } ChatCommand::ChooseProject => self.toggle_root_menu(cx), ChatCommand::CopySelection => self.copy_selected_text(cx), + ChatCommand::Escape + if self.sidebar.read(cx).search_input_focused(window, cx) + && self + .sidebar + .update(cx, |sidebar, cx| sidebar.hide_empty_search(cx)) => + { + if let Some(composer) = &self.composer { + use gpui::Focusable; + window.focus(&composer.read(cx).focus_handle(cx), cx); + } + } ChatCommand::Escape if self.application_vim_enabled => { self.application_escape(window, cx) } diff --git a/apps/maple-agent/app/src/ui/chat/composer.rs b/apps/maple-agent/app/src/ui/chat/composer.rs index 2c52ee0bf..eee36e85d 100644 --- a/apps/maple-agent/app/src/ui/chat/composer.rs +++ b/apps/maple-agent/app/src/ui/chat/composer.rs @@ -157,6 +157,14 @@ impl ChatScreen { ChatCommand::NewTask, 4, ), + ( + "header-search", + "search", + "Search", + &super::FocusSearch, + ChatCommand::FocusSearch, + 5, + ), ] .into_iter() .filter(|(_, _, _, _, command, _)| { diff --git a/apps/maple-agent/app/src/ui/chat/navigation.rs b/apps/maple-agent/app/src/ui/chat/navigation.rs index bf3030f27..f8dda9b7d 100644 --- a/apps/maple-agent/app/src/ui/chat/navigation.rs +++ b/apps/maple-agent/app/src/ui/chat/navigation.rs @@ -31,6 +31,7 @@ pub(super) enum ChatRegion { pub(super) enum SidebarTarget { Computer(super::computers::Navigate), NewTask, + Search, Projects, Task(String), Archived, @@ -560,6 +561,7 @@ impl ChatScreen { match self.application_vim.region { ChatRegion::Sidebar => match self.sidebar.read(cx).vim_selected().cloned() { Some(SidebarTarget::NewTask) => self.new_session(cx), + Some(SidebarTarget::Search) => self.focus_sidebar_search(window, cx), Some(SidebarTarget::Projects) => self.sidebar.update(cx, |sidebar, cx| { let open = sidebar.switcher_menu_open(); sidebar.set_switcher_menu_open(!open, cx); diff --git a/apps/maple-agent/app/src/ui/chat/sidebar.rs b/apps/maple-agent/app/src/ui/chat/sidebar.rs index 4990bc75b..5dcdf178f 100644 --- a/apps/maple-agent/app/src/ui/chat/sidebar.rs +++ b/apps/maple-agent/app/src/ui/chat/sidebar.rs @@ -80,6 +80,8 @@ use crate::ui::theme; use crate::ui::titlebar; use crate::ui::widgets; +const SEARCH_ROW_RADIUS: gpui::Pixels = px(6.); + /// Geometry shared by the expanded wordmark row and the collapsed mark. pub(super) const WORDMARK_LEFT: gpui::Pixels = px(16.); pub(super) const WORDMARK_HEIGHT: gpui::Pixels = px(16.); @@ -245,6 +247,7 @@ pub(super) enum SidebarEntry { ComputerMore(usize), LoadMore(PageKind), NewTask, + Search, /// The project switcher row above the sections. ProjectsHeader, /// A section label, shown only when the section has rows. @@ -436,6 +439,8 @@ pub(super) struct Sidebar { // Search and rename. filter: String, search_input: Entity, + search_visible: bool, + search_focus_out: Option, rename: Option, rename_input: Option>, rename_focus_pending: bool, @@ -591,6 +596,7 @@ impl Sidebar { || self.project_filter.is_some(); self.search_input .update(cx, |input, cx| input.set_text(&state.search, cx)); + self.search_visible = !state.search.is_empty(); self.filter = filter; self.archived_expanded = state.archived_expanded; // A project filter belongs to the previous execution computer. @@ -671,6 +677,8 @@ impl Sidebar { menu_trust: None, filter: String::new(), search_input, + search_visible: false, + search_focus_out: None, rename: None, rename_input: None, rename_focus_pending: false, @@ -1190,11 +1198,37 @@ impl Sidebar { } pub(super) fn focus_search(&mut self, window: &mut Window, cx: &mut Context) { + self.search_visible = true; + self.reveal_row(1); + self.list.remeasure_items(1..2); + let handle = self.search_input.read(cx).focus_handle(cx); + handle.focus(window, cx); + self.search_focus_out = Some(cx.on_focus_out(&handle, window, |_this, _, window, cx| { + cx.defer_in(window, |this, window, cx| { + if !this.search_input.read(cx).menu_open() && !this.search_input_focused(window, cx) + { + this.hide_empty_search(cx); + } + }); + })); + cx.notify(); + } + + pub(super) fn search_input_focused(&self, window: &Window, cx: &gpui::App) -> bool { self.search_input .read(cx) .focus_handle(cx) - .focus(window, cx); + .is_focused(window) + } + + pub(super) fn hide_empty_search(&mut self, cx: &mut Context) -> bool { + if !self.search_visible || !self.search_input.read(cx).text_ref().is_empty() { + return false; + } + self.search_visible = false; + self.list.remeasure_items(1..2); cx.notify(); + true } /// Rebuild the sidebar sections: pinned tasks first, then the active @@ -1309,7 +1343,11 @@ impl Sidebar { /// span that changed, so the scroll position and the measured /// heights of untouched rows survive every rebuild. fn rebuild_entries(&mut self) { - let mut entries = vec![SidebarEntry::NewTask, SidebarEntry::ProjectsHeader]; + let mut entries = vec![ + SidebarEntry::NewTask, + SidebarEntry::Search, + SidebarEntry::ProjectsHeader, + ]; let tasks = |entries: &mut Vec, section: SidebarSection, rows: &[usize], @@ -2101,6 +2139,7 @@ impl Sidebar { | SidebarEntry::Computer(_) | SidebarEntry::ComputerMore(_)) => self.computer_target(entry), SidebarEntry::NewTask => Some(SidebarTarget::NewTask), + SidebarEntry::Search => Some(SidebarTarget::Search), SidebarEntry::ProjectsHeader => Some(SidebarTarget::Projects), SidebarEntry::SectionLabel(_) | SidebarEntry::Empty(_) => None, SidebarEntry::LoadMore(kind) => { @@ -2251,6 +2290,70 @@ impl Sidebar { // ---- Render ------------------------------------------------------------ + fn render_search(&self, cx: &mut Context) -> AnyElement { + let active = !self.filter.is_empty(); + div() + .debug_selector(|| "sidebar-search".to_string()) + // Most of the sidebar takes no focus, so a press anywhere else + // must end the search itself: drop the cursor, and return to + // the Search row when nothing was typed. + .on_mouse_down_out(cx.listener(|this, _event, window, cx| { + if this.search_input.read(cx).menu_open() { + return; + } + if this.search_input_focused(window, cx) { + window.blur(); + } + this.hide_empty_search(cx); + })) + .flex() + .items_center() + .gap_2() + .w_full() + .my(px(1.)) + .mb_3() + .px_2() + .h_8() + .rounded(SEARCH_ROW_RADIUS) + .bg(gpui::rgb(if active { + theme::bg_sidebar_row_selected() + } else { + theme::bg_sidebar_row_hover() + })) + .text_sm() + .child(icon("search", px(16.), theme::text_secondary())) + .child(div().flex_1().min_w_0().child(self.search_input.clone())) + .when(active, |row| { + row.child( + div() + .id("search-clear") + .debug_selector(|| "search-clear".to_string()) + .size_5() + .flex() + .items_center() + .justify_center() + .rounded(theme::RADIUS_SM) + .hover(|style| { + style + .bg(gpui::rgb(theme::bg_sidebar_row_selected())) + .cursor_pointer() + }) + .tooltip(widgets::tooltip_for_action( + "Clear search", + &super::ChatEscape, + )) + .on_click(cx.listener(|this, _event, window, cx| { + this.clear_search(cx); + if !this.search_input_focused(window, cx) { + this.hide_empty_search(cx); + } + })) + .child(icon("x", px(12.), theme::text_secondary())), + ) + }) + .into_any_element() + } + /// One row of the sidebar list. fn render_entry( &mut self, @@ -2268,40 +2371,68 @@ impl Sidebar { | SidebarEntry::ComputerMore(_)), ) => self.render_computer_entry(entry, self.vim_selects_row(ix), cx), Some(SidebarEntry::LoadMore(kind)) => self.render_load_more(kind, cx), - Some(SidebarEntry::NewTask) => div() - .id("new-task") - .role(gpui::Role::Button) - .aria_label("New task") - .w_full() - .mb_3() - .px_4() - .py_1p5() - .rounded(theme::RADIUS_SM) - .flex() - .items_center() - .gap_2() - .text_sm() - .text_color(gpui::rgb(theme::accent())) - .hover(|style| { - style - .bg(gpui::rgb(theme::bg_sidebar_row_hover())) - .cursor_pointer() - }) - .active(|style| style.bg(gpui::rgb(theme::bg_sidebar_row_selected()))) - .tooltip(widgets::tooltip_for_action( - "Start a new task", - &super::NewTask, - )) - .on_click(move |_event, window, cx| { - if let Some(chat) = chat.upgrade() { - chat.update(cx, |chat, cx| { - chat.execute_command(ChatCommand::NewTask, window, cx); - }); - } - }) - .child(icon("square-pen", px(16.), theme::accent())) - .child("New Task") - .into_any_element(), + Some(SidebarEntry::Search) if self.search_visible => self.render_search(cx), + Some(entry @ (SidebarEntry::NewTask | SidebarEntry::Search)) => { + let search = entry == SidebarEntry::Search; + let (id, name, label, command, action, color): (_, _, _, _, &dyn gpui::Action, _) = + if search { + ( + "sidebar-search-row", + "search", + "Search", + ChatCommand::FocusSearch, + &super::FocusSearch, + theme::text_secondary(), + ) + } else { + ( + "new-task", + "square-pen", + "New Task", + ChatCommand::NewTask, + &super::NewTask, + theme::accent(), + ) + }; + div() + .id(id) + .debug_selector(move || id.to_string()) + .role(gpui::Role::Button) + .aria_label(label) + .w_full() + .my(px(1.)) + .when(search, |row| { + row.mb_3() + .tab_index(2) + .focus_visible(widgets::focus_ring) + .on_key_down(ChatScreen::sidebar_button_key_down) + }) + .px_2() + .h_8() + .rounded(SEARCH_ROW_RADIUS) + .flex() + .items_center() + .gap_2() + .text_sm() + .text_color(gpui::rgb(color)) + .hover(|style| { + style + .bg(gpui::rgb(theme::bg_sidebar_row_hover())) + .cursor_pointer() + }) + .active(|style| style.bg(gpui::rgb(theme::bg_sidebar_row_selected()))) + .tooltip(widgets::tooltip_for_action(label, action)) + .on_click(move |_event, window, cx| { + if let Some(chat) = chat.upgrade() { + chat.update(cx, |chat, cx| { + chat.execute_command(command, window, cx); + }); + } + }) + .child(icon(name, px(16.), color)) + .child(label) + .into_any_element() + } Some(SidebarEntry::ProjectsHeader) => self.render_projects_header(window, cx), Some(SidebarEntry::Task(task)) => { let application_selected = self.vim_selects_row(ix); @@ -2449,6 +2580,14 @@ impl Sidebar { } None => div().into_any_element(), }; + let entry = if matches!( + self.entries.get(ix), + Some(SidebarEntry::NewTask | SidebarEntry::Search) + ) { + div().w_full().px_2().child(entry).into_any_element() + } else { + entry + }; if !self.application_vim_enabled { return entry; } @@ -2983,7 +3122,6 @@ impl Render for Sidebar { sidebar.update(cx, |sidebar, cx| sidebar.render_entry(ix, window, cx)) }) .size_full(); - let active = !self.filter.is_empty(); div() .size_full() .flex() @@ -3013,51 +3151,6 @@ impl Render for Sidebar { ), ), )) - .child( - div() - .flex() - .items_center() - .gap_2() - .mx_4() - .mt_3() - .px_2() - .py_1() - .rounded(theme::RADIUS_SM) - .bg(gpui::rgb(theme::bg_sidebar_pill())) - .border_1() - .border_color(gpui::rgb(if active { - theme::accent() - } else { - theme::border_subtle() - })) - .text_sm() - .child(icon("search", px(14.), theme::text_muted())) - .child(div().flex_1().min_w_0().child(self.search_input.clone())) - .when(active, |row| { - row.child( - div() - .id("search-clear") - .size_5() - .flex() - .items_center() - .justify_center() - .rounded(theme::RADIUS_SM) - .hover(|style| { - style - .bg(gpui::rgb(theme::bg_sidebar_row_selected())) - .cursor_pointer() - }) - .tooltip(widgets::tooltip_for_action( - "Clear search", - &super::ChatEscape, - )) - .on_click(cx.listener(|this, _event, _window, cx| { - this.clear_search(cx); - })) - .child(icon("x", px(12.), theme::text_secondary())), - ) - }), - ) .child( div() .id("session-list") diff --git a/apps/maple-agent/app/src/ui/chat/sidebar_computers.rs b/apps/maple-agent/app/src/ui/chat/sidebar_computers.rs index eeaead3f9..26e7c5cb0 100644 --- a/apps/maple-agent/app/src/ui/chat/sidebar_computers.rs +++ b/apps/maple-agent/app/src/ui/chat/sidebar_computers.rs @@ -833,7 +833,11 @@ impl Sidebar { self.other_tasks.clear(); self.group_pages.more.clear(); self.computer_archived_count = 0; - let mut entries = vec![SidebarEntry::NewTask, SidebarEntry::ProjectsHeader]; + let mut entries = vec![ + SidebarEntry::NewTask, + SidebarEntry::Search, + SidebarEntry::ProjectsHeader, + ]; let mut archived_entries = Vec::new(); // These small clones let rendering append row projections without // borrowing the per-host paging cache for the whole rebuild. diff --git a/apps/maple-agent/app/src/ui/chat/tests.rs b/apps/maple-agent/app/src/ui/chat/tests.rs index e4e486fed..973fc9cb6 100644 --- a/apps/maple-agent/app/src/ui/chat/tests.rs +++ b/apps/maple-agent/app/src/ui/chat/tests.rs @@ -5981,7 +5981,6 @@ mod state_tests { ); assert_eq!(toggle.left(), SIDEBAR_RAIL_WIDTH + SIDEBAR_COLLAPSED_INSET); assert!(toggle.right() < new_task.left()); - assert_eq!(new_task.right() + px(25.), title.left()); assert!(title.right() < project.left()); assert_eq!(toggle.center().y, title.center().y); @@ -6059,6 +6058,184 @@ mod state_tests { assert!(cx.update(|_, app| chat.read(app).sidebar_collapsed)); } + #[gpui::test] + fn sidebar_search_button_and_shortcut(cx: &mut TestAppContext) { + let (chat, cx) = chat_window(cx, |this, cx| { + this.sessions = (0..80) + .map(|index| summary(&format!("s{index}"), "Task")) + .collect(); + this.sync_sidebar(cx); + }); + let focus = cx.update(|_, app| chat.read(app).sidebar.read(app).search_focus_handle(app)); + let composer = cx.update(|_, app| chat.read(app).composer.clone().unwrap()); + let composer_focus = cx.update(|_, app| composer.read(app).focus_handle(app)); + for (trigger, collapsed) in [ + ("sidebar-search-row", false), + ("header-search", true), + ("shortcut", false), + ("shortcut", true), + ] { + chat.update(cx, |this, cx| this.set_sidebar_collapsed(collapsed, cx)); + cx.run_until_parked(); + if trigger == "shortcut" { + chat.update(cx, |this, cx| { + this.sidebar.update(cx, |sidebar, cx| { + sidebar.list().scroll_to_reveal_item(70); + cx.notify(); + }) + }); + cx.update(|window, _| window.refresh()); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search-row").is_none()); + cx.simulate_keystrokes(if cfg!(target_os = "macos") { + "cmd-f" + } else { + "ctrl-f" + }); + cx.run_until_parked(); + } else { + press(cx, trigger); + } + assert!(!cx.update(|_, app| chat.read(app).sidebar_collapsed)); + assert!(cx.debug_bounds("sidebar-search").is_some()); + assert!(cx.update(|window, _| focus.is_focused(window))); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search-row").is_some()); + assert!(!cx.update(|window, _| focus.is_focused(window))); + assert!(cx.update(|window, _| composer_focus.is_focused(window))); + } + } + + #[gpui::test] + fn sidebar_search_empty_escape_with_application_vim(cx: &mut TestAppContext) { + use crate::ui::chat::navigation::{ChatRegion, SidebarTarget}; + let (chat, cx) = chat_window(cx, |this, cx| { + this.set_application_vim_enabled(true, cx); + this.application_vim.region = ChatRegion::Sidebar; + this.sidebar.update(cx, |sidebar, cx| { + sidebar.vim_select(SidebarTarget::Search, cx) + }); + }); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_some()); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_none()); + assert!(cx.update(|window, app| { + chat.read(app) + .application_focus + .as_ref() + .unwrap() + .is_focused(window) + })); + } + + #[gpui::test] + fn sidebar_search_survives_its_fields_menu(cx: &mut TestAppContext) { + let (chat, cx) = chat_window(cx, |this, cx| { + this.sessions = vec![summary("s1", "Alpha"), summary("s2", "Beta")]; + this.sync_sidebar(cx); + }); + let input = cx.update(|_, app| chat.read(app).sidebar.read(app).inputs()[0].clone()); + let focus = cx.update(|_, app| input.read(app).focus_handle(app)); + for text in ["", "alpha"] { + press(cx, "sidebar-search-row"); + cx.update(|_, app| { + app.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string())); + }); + let field = cx.debug_bounds("sidebar-search").expect("search field"); + cx.simulate_mouse_down( + field.center(), + gpui::MouseButton::Right, + gpui::Modifiers::default(), + ); + cx.simulate_mouse_up( + field.center(), + gpui::MouseButton::Right, + gpui::Modifiers::default(), + ); + cx.run_until_parked(); + assert!(input.read_with(cx, |input, _| input.menu_open())); + assert!(!cx.update(|window, _| focus.is_focused(window))); + assert!(cx.debug_bounds("sidebar-search").is_some()); + press(cx, "text-input-paste"); + assert!(!input.read_with(cx, |input, _| input.menu_open())); + assert_eq!(input.read_with(cx, |input, _| input.text()), text); + assert!(cx.debug_bounds("sidebar-search").is_some()); + assert!(cx.update(|window, _| focus.is_focused(window))); + assert_eq!( + chat.update(cx, |this, cx| this.sidebar_active(cx)), + if text.is_empty() { vec![0, 1] } else { vec![0] } + ); + cx.simulate_click(gpui::point(px(40.), px(700.)), gpui::Modifiers::default()); + cx.run_until_parked(); + assert!(!cx.update(|window, _| focus.is_focused(window))); + assert_eq!( + cx.debug_bounds("sidebar-search").is_some(), + !text.is_empty() + ); + } + press(cx, "search-clear"); + assert!(cx.debug_bounds("sidebar-search").is_none()); + assert_eq!( + chat.update(cx, |this, cx| this.sidebar_active(cx)), + vec![0, 1] + ); + } + + #[gpui::test] + fn sidebar_search_button_keyboard(cx: &mut TestAppContext) { + let (chat, cx) = chat_window(cx, |_, _| {}); + let composer = cx.update(|_, app| chat.read(app).composer.clone().unwrap()); + let composer_focus = cx.update(|_, app| composer.read(app).focus_handle(app)); + let search_focus = + cx.update(|_, app| chat.read(app).sidebar.read(app).search_focus_handle(app)); + for collapsed in [false, true] { + for key in ["enter", "space"] { + chat.update(cx, |this, cx| this.set_sidebar_collapsed(collapsed, cx)); + composer.update(cx, |input, cx| input.set_text("Unsent draft", cx)); + cx.update(|window, app| window.focus(&composer_focus, app)); + cx.run_until_parked(); + cx.simulate_keystrokes(if collapsed { "tab tab tab tab" } else { "tab" }); + cx.simulate_keystrokes(key); + assert_eq!(composer.update(cx, |input, _| input.text()), "Unsent draft"); + cx.update(|window, app| { + window.dispatch_event( + gpui::PlatformInput::KeyUp(gpui::KeyUpEvent { + keystroke: gpui::Keystroke::parse(key).unwrap(), + }), + app, + ); + }); + cx.run_until_parked(); + assert!(!cx.update(|_, app| chat.read(app).sidebar_collapsed)); + assert!(cx.update(|window, _| search_focus.is_focused(window))); + cx.simulate_input("filter"); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_some()); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_none()); + } + } + press(cx, "sidebar-search-row"); + cx.simulate_input("filter"); + cx.update(|window, app| window.focus(&composer_focus, app)); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_some()); + cx.update(|window, app| window.focus(&search_focus, app)); + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_some()); + cx.update(|window, app| window.focus(&composer_focus, app)); + cx.run_until_parked(); + assert!(cx.debug_bounds("sidebar-search").is_none()); + assert!(cx.debug_bounds("sidebar-search-row").is_some()); + } + /// The header's New task button starts the same draft as the sidebar. #[gpui::test] fn collapsed_sidebar_header_starts_a_draft(cx: &mut TestAppContext) { @@ -7518,7 +7695,7 @@ mod state_tests { this.sync_sidebar(cx); // The screen fixture selects s1: one step moves to s2, and // back stops at the top. - assert_eq!(this.task_step_target(1, cx), Some((4, local("s2")))); + assert_eq!(this.task_step_target(1, cx), Some((5, local("s2")))); assert_eq!( this.task_step_target(-1, cx), None, @@ -7526,13 +7703,13 @@ mod state_tests { ); this.selected_session = Some("s2".to_string()); this.sync_sidebar_selection(cx); - assert_eq!(this.task_step_target(1, cx), Some((5, local("s3")))); - assert_eq!(this.task_step_target(-1, cx), Some((3, local("s1")))); + assert_eq!(this.task_step_target(1, cx), Some((6, local("s3")))); + assert_eq!(this.task_step_target(-1, cx), Some((4, local("s1")))); // With nothing selected, each direction enters from its end. this.selected_session = None; this.sync_sidebar_selection(cx); - assert_eq!(this.task_step_target(1, cx), Some((3, local("s1")))); - assert_eq!(this.task_step_target(-1, cx), Some((5, local("s3")))); + assert_eq!(this.task_step_target(1, cx), Some((4, local("s1")))); + assert_eq!(this.task_step_target(-1, cx), Some((6, local("s3")))); }); }