diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index ea4ec36674878ca958a2f73af0adf749a40157f6..c9c202217b05c8e3ee28f0196a4a60e88ffc2f98 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -45,7 +45,7 @@ fn assert_active_thread(sidebar: &Sidebar, session_id: &acp::SessionId, msg: &st #[track_caller] fn assert_active_draft(sidebar: &Sidebar, workspace: &Entity, msg: &str) { assert!( - matches!(&sidebar.active_entry, Some(ActiveEntry::Draft(ws)) if ws == workspace), + matches!(&sidebar.active_entry, Some(ActiveEntry::Draft { workspace: ws, .. }) if ws == workspace), "{msg}: expected active_entry to be Draft for workspace {:?}, got {:?}", workspace.entity_id(), sidebar.active_entry, @@ -388,17 +388,14 @@ fn visible_entries_as_strings( format!(" + View More{}", selected) } } - ListEntry::DraftThread { - workspace, - worktrees, - .. - } => { + ListEntry::DraftThread { worktrees, .. } => { let worktree = format_linked_worktree_chips(worktrees); - if workspace.is_some() { - format!(" [+ New Thread{}]{}", worktree, selected) - } else { - format!(" [~ Draft{}]{}{}", worktree, active_indicator, selected) - } + let is_active = sidebar + .active_entry + .as_ref() + .is_some_and(|e| e.matches_entry(entry)); + let active_marker = if is_active { " *" } else { "" }; + format!(" [~ Draft{}]{}{}", worktree, active_marker, selected) } } }) @@ -566,10 +563,7 @@ async fn test_single_workspace_no_threads(cx: &mut TestAppContext) { assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [my-project]", - ] + vec!["v [my-project]", " [~ Draft]"] ); } @@ -1329,13 +1323,10 @@ async fn test_keyboard_navigation_on_empty_list(cx: &mut TestAppContext) { cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); let sidebar = setup_sidebar(&multi_workspace, cx); - // An empty project has only the header. + // An empty project has the header and an auto-created draft. assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [empty-project]", - ] + vec!["v [empty-project]", " [~ Draft]"] ); // Focus sidebar — focus_in does not set a selection @@ -1346,7 +1337,11 @@ async fn test_keyboard_navigation_on_empty_list(cx: &mut TestAppContext) { cx.dispatch_action(SelectNext); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0)); - // At the end (only one entry), wraps back to first entry + // SelectNext advances to index 1 (draft entry) + cx.dispatch_action(SelectNext); + assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1)); + + // At the end (two entries), wraps back to first entry cx.dispatch_action(SelectNext); assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0)); @@ -2591,9 +2586,10 @@ async fn test_new_thread_button_works_after_adding_folder(cx: &mut TestAppContex assert_eq!( visible_entries_as_strings(&sidebar, cx), vec![ - // - "v [project-a, project-b]", - " Hello * (active)", + "v [project-a, project-b]", // + " Hello *", + "v [project-a]", + " [~ Draft]", ] ); @@ -3437,12 +3433,7 @@ async fn test_cmd_n_shows_new_thread_entry(cx: &mut TestAppContext) { assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [my-project]", - " [~ Draft] (active)", - " Hello *", - ], + vec!["v [my-project]", " [~ Draft] *", " Hello *"], "After Cmd-N the sidebar should show a highlighted Draft entry" ); @@ -3482,21 +3473,16 @@ async fn test_draft_with_server_session_shows_as_draft(cx: &mut TestAppContext) ] ); - // Open a new draft thread via a server connection. This gives the - // conversation a parent_id (session assigned by the server) but - // no messages have been sent, so active_thread_is_draft() is true. - let draft_connection = StubAgentConnection::new(); - open_thread_with_connection(&panel, draft_connection, cx); + // Create a new draft via Cmd-N. Since new_thread() now creates a + // tracked draft in the AgentPanel, it appears in the sidebar. + panel.update_in(cx, |panel, window, cx| { + panel.new_thread(&NewThread, window, cx); + }); cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [my-project]", - " [~ Draft] (active)", - " Hello *", - ], + vec!["v [my-project]", " [~ Draft] *", " Hello *"], ); let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone()); @@ -3611,8 +3597,8 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp vec![ // "v [project]", - " [~ Draft {wt-feature-a}] (active)", - " Hello {wt-feature-a} *", + " [~ Draft {wt-feature-a}] *", + " Hello {wt-feature-a} *" ], "After Cmd-N in an absorbed worktree, the sidebar should show \ a highlighted Draft entry under the main repo header" @@ -3729,11 +3715,7 @@ async fn test_git_worktree_added_live_updates_sidebar(cx: &mut TestAppContext) { // The chip name is derived from the path even before git discovery. assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [project]", - " Worktree Thread {rosewood}", - ] + vec!["v [project]", " [~ Draft]"] ); // Now add the worktree to the git state and trigger a rescan. @@ -3925,12 +3907,7 @@ async fn test_threadless_workspace_shows_new_thread_with_worktree_chip(cx: &mut // appears as a "New Thread" button with its worktree chip. assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [project]", - " [+ New Thread {wt-feature-b}]", - " Thread A {wt-feature-a}", - ] + vec!["v [project]", " Thread A {wt-feature-a}",] ); } @@ -4184,12 +4161,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp let entries = visible_entries_as_strings(&sidebar, cx); assert_eq!( entries, - vec![ - // - "v [project]", - " [~ Draft] (active)", - " Hello {wt-feature-a} * (running)", - ] + vec!["v [project]", " Hello {wt-feature-a} * (running)",] ); } @@ -4272,12 +4244,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [project]", - " [~ Draft] (active)", - " Hello {wt-feature-a} * (running)", - ] + vec!["v [project]", " Hello {wt-feature-a} * (running)",] ); connection.end_turn(session_id, acp::StopReason::EndTurn); @@ -4285,12 +4252,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp assert_eq!( visible_entries_as_strings(&sidebar, cx), - vec![ - // - "v [project]", - " [~ Draft] (active)", - " Hello {wt-feature-a} * (!)", - ] + vec!["v [project]", " Hello {wt-feature-a} * (!)",] ); } @@ -5498,6 +5460,7 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test vec![ // "v [other, project]", + " [~ Draft]", "v [project]", " Worktree Thread {wt-feature-a}", ] @@ -5951,7 +5914,7 @@ async fn test_archive_thread_active_entry_management(cx: &mut TestAppContext) { // active_entry should still be a draft on workspace_b (the active one). sidebar.read_with(cx, |sidebar, _| { assert!( - matches!(&sidebar.active_entry, Some(ActiveEntry::Draft(ws)) if ws == &workspace_b), + matches!(&sidebar.active_entry, Some(ActiveEntry::Draft { workspace: ws, .. }) if ws == &workspace_b), "expected Draft(workspace_b) after archiving non-active thread, got: {:?}", sidebar.active_entry, ); @@ -5986,7 +5949,7 @@ async fn test_archive_thread_active_entry_management(cx: &mut TestAppContext) { // Should fall back to a draft on the same workspace. sidebar.read_with(cx, |sidebar, _| { assert!( - matches!(&sidebar.active_entry, Some(ActiveEntry::Draft(ws)) if ws == &workspace_b), + matches!(&sidebar.active_entry, Some(ActiveEntry::Draft { workspace: ws, .. }) if ws == &workspace_b), "expected Draft(workspace_b) after archiving active thread, got: {:?}", sidebar.active_entry, ); @@ -6059,7 +6022,7 @@ async fn test_switch_to_workspace_with_archived_thread_shows_draft(cx: &mut Test sidebar.read_with(cx, |sidebar, _| { assert!( - matches!(&sidebar.active_entry, Some(ActiveEntry::Draft(ws)) if ws == &workspace_a), + matches!(&sidebar.active_entry, Some(ActiveEntry::Draft { workspace: ws, .. }) if ws == &workspace_a), "expected Draft(workspace_a) after switching to workspace with archived thread, got: {:?}", sidebar.active_entry, ); @@ -6561,9 +6524,10 @@ async fn test_archive_thread_on_linked_worktree_selects_sibling_thread(cx: &mut #[gpui::test] async fn test_linked_worktree_workspace_reachable_and_dismissable(cx: &mut TestAppContext) { // When a linked worktree is opened as its own workspace and the user - // switches away, the workspace must still be reachable from a DraftThread - // sidebar entry. Pressing RemoveSelectedThread (shift-backspace) on that - // entry should remove the workspace. + // creates a draft thread from it, then switches away, the workspace must + // still be reachable from that DraftThread sidebar entry. Pressing + // RemoveSelectedThread (shift-backspace) on that entry should remove the + // workspace. init_test(cx); let fs = FakeFs::new(cx.executor()); @@ -6627,6 +6591,14 @@ async fn test_linked_worktree_workspace_reachable_and_dismissable(cx: &mut TestA add_agent_panel(&worktree_workspace, cx); cx.run_until_parked(); + // Explicitly create a draft thread from the linked worktree workspace. + // Auto-created drafts use the group's first workspace (the main one), + // so a user-created draft is needed to make the linked worktree reachable. + sidebar.update_in(cx, |sidebar, window, cx| { + sidebar.create_new_thread(&worktree_workspace, window, cx); + }); + cx.run_until_parked(); + // Switch back to the main workspace. multi_workspace.update_in(cx, |mw, window, cx| { let main_ws = mw.workspaces().next().unwrap().clone(); @@ -6656,7 +6628,7 @@ async fn test_linked_worktree_workspace_reachable_and_dismissable(cx: &mut TestA "linked worktree workspace should be reachable, but reachable are: {reachable:?}" ); - // Find the DraftThread entry for the linked worktree and dismiss it. + // Find the DraftThread entry whose workspace is the linked worktree. let new_thread_ix = sidebar.read_with(cx, |sidebar, _| { sidebar .contents @@ -6666,9 +6638,9 @@ async fn test_linked_worktree_workspace_reachable_and_dismissable(cx: &mut TestA matches!( entry, ListEntry::DraftThread { - workspace: Some(_), + workspace: Some(ws), .. - } + } if ws.entity_id() == worktree_ws_id ) }) .expect("expected a DraftThread entry for the linked worktree") @@ -6687,8 +6659,25 @@ async fn test_linked_worktree_workspace_reachable_and_dismissable(cx: &mut TestA assert_eq!( multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()), - 1, - "linked worktree workspace should be removed after dismissing DraftThread entry" + 2, + "dismissing a draft no longer removes the linked worktree workspace" + ); + + let has_draft_for_worktree = sidebar.read_with(cx, |sidebar, _| { + sidebar.contents.entries.iter().any(|entry| { + matches!( + entry, + ListEntry::DraftThread { + draft_id: Some(_), + workspace: Some(ws), + .. + } if ws.entity_id() == worktree_ws_id + ) + }) + }); + assert!( + !has_draft_for_worktree, + "DraftThread entry for the linked worktree should be removed after dismiss" ); } @@ -7899,7 +7888,7 @@ mod property_test { let panel = active_workspace.read(cx).panel::(cx).unwrap(); if panel.read(cx).active_thread_is_draft(cx) { anyhow::ensure!( - matches!(entry, ActiveEntry::Draft(_)), + matches!(entry, ActiveEntry::Draft { .. }), "panel shows a draft but active_entry is {:?}", entry, );