@@ -251,32 +251,6 @@ fn save_thread_metadata(
cx.run_until_parked();
}
-fn save_thread_metadata_with_main_paths(
- session_id: &str,
- title: &str,
- folder_paths: PathList,
- main_worktree_paths: PathList,
- cx: &mut TestAppContext,
-) {
- let session_id = acp::SessionId::new(Arc::from(session_id));
- let title = SharedString::from(title.to_string());
- let updated_at = chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap();
- let metadata = ThreadMetadata {
- session_id,
- agent_id: agent::ZED_AGENT_ID.clone(),
- title,
- updated_at,
- created_at: None,
- folder_paths,
- main_worktree_paths,
- archived: false,
- };
- cx.update(|cx| {
- ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save_manually(metadata, cx));
- });
- cx.run_until_parked();
-}
-
fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
sidebar.update_in(cx, |_, window, cx| {
cx.focus_self(window);
@@ -348,11 +322,6 @@ fn visible_entries_as_strings(
} else {
""
};
- let is_active = sidebar
- .active_entry
- .as_ref()
- .is_some_and(|active| active.matches_entry(entry));
- let active_indicator = if is_active { " (active)" } else { "" };
match entry {
ListEntry::ProjectHeader {
label,
@@ -369,7 +338,7 @@ fn visible_entries_as_strings(
}
ListEntry::Thread(thread) => {
let title = thread.metadata.title.as_ref();
- let live = if thread.is_live { " *" } else { "" };
+ let active = if thread.is_live { " *" } else { "" };
let status_str = match thread.status {
AgentThreadStatus::Running => " (running)",
AgentThreadStatus::Error => " (error)",
@@ -385,7 +354,7 @@ fn visible_entries_as_strings(
""
};
let worktree = format_linked_worktree_chips(&thread.worktrees);
- format!(" {title}{worktree}{live}{status_str}{notified}{active_indicator}{selected}")
+ format!(" {title}{worktree}{active}{status_str}{notified}{selected}")
}
ListEntry::ViewMore {
is_fully_expanded, ..
@@ -405,7 +374,7 @@ fn visible_entries_as_strings(
if workspace.is_some() {
format!(" [+ New Thread{}]{}", worktree, selected)
} else {
- format!(" [~ Draft{}]{}{}", worktree, active_indicator, selected)
+ format!(" [~ Draft{}]{}", worktree, selected)
}
}
}
@@ -574,10 +543,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]"]
);
}
@@ -613,7 +579,6 @@ async fn test_single_workspace_with_saved_threads(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix crash in project panel",
" Add inline diff view",
@@ -644,11 +609,7 @@ async fn test_workspace_lifecycle(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Thread A1",
- ]
+ vec!["v [project-a]", " Thread A1"]
);
// Add a second workspace
@@ -659,11 +620,7 @@ async fn test_workspace_lifecycle(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Thread A1",
- ]
+ vec!["v [project-a]", " Thread A1",]
);
}
@@ -682,7 +639,6 @@ async fn test_view_more_pagination(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Thread 12",
" Thread 11",
@@ -793,11 +749,7 @@ async fn test_collapse_and_expand_group(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Thread 1",
- ]
+ vec!["v [my-project]", " Thread 1"]
);
// Collapse
@@ -808,10 +760,7 @@ async fn test_collapse_and_expand_group(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "> [my-project]",
- ]
+ vec!["> [my-project]"]
);
// Expand
@@ -822,11 +771,7 @@ async fn test_collapse_and_expand_group(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Thread 1",
- ]
+ vec!["v [my-project]", " Thread 1"]
);
}
@@ -996,7 +941,6 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [expanded-project]",
" Completed thread",
" Running thread * (running) <== selected",
@@ -1160,14 +1104,10 @@ async fn test_keyboard_confirm_on_project_header_toggles_collapse(cx: &mut TestA
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Thread 1",
- ]
+ vec!["v [my-project]", " Thread 1"]
);
- // Focus the sidebar and select the header
+ // Focus the sidebar and select the header (index 0)
focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(0);
@@ -1179,10 +1119,7 @@ async fn test_keyboard_confirm_on_project_header_toggles_collapse(cx: &mut TestA
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "> [my-project] <== selected",
- ]
+ vec!["> [my-project] <== selected"]
);
// Confirm again expands the group
@@ -1191,11 +1128,7 @@ async fn test_keyboard_confirm_on_project_header_toggles_collapse(cx: &mut TestA
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project] <== selected",
- " Thread 1",
- ]
+ vec!["v [my-project] <== selected", " Thread 1",]
);
}
@@ -1246,11 +1179,7 @@ async fn test_keyboard_expand_and_collapse_selected_entry(cx: &mut TestAppContex
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Thread 1",
- ]
+ vec!["v [my-project]", " Thread 1"]
);
// Focus sidebar and manually select the header (index 0). Press left to collapse.
@@ -1264,10 +1193,7 @@ async fn test_keyboard_expand_and_collapse_selected_entry(cx: &mut TestAppContex
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "> [my-project] <== selected",
- ]
+ vec!["> [my-project] <== selected"]
);
// Press right to expand
@@ -1276,11 +1202,7 @@ async fn test_keyboard_expand_and_collapse_selected_entry(cx: &mut TestAppContex
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project] <== selected",
- " Thread 1",
- ]
+ vec!["v [my-project] <== selected", " Thread 1",]
);
// Press right again on already-expanded header moves selection down
@@ -1307,11 +1229,7 @@ async fn test_keyboard_collapse_from_child_selects_parent(cx: &mut TestAppContex
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Thread 1 <== selected",
- ]
+ vec!["v [my-project]", " Thread 1 <== selected",]
);
// Pressing left on a child collapses the parent group and selects it
@@ -1321,10 +1239,7 @@ async fn test_keyboard_collapse_from_child_selects_parent(cx: &mut TestAppContex
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "> [my-project] <== selected",
- ]
+ vec!["> [my-project] <== selected"]
);
}
@@ -1338,10 +1253,7 @@ async fn test_keyboard_navigation_on_empty_list(cx: &mut TestAppContext) {
// An empty project has only the header.
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [empty-project]",
- ]
+ vec!["v [empty-project]"]
);
// Focus sidebar — focus_in does not set a selection
@@ -1473,12 +1385,7 @@ async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) {
entries[1..].sort();
assert_eq!(
entries,
- vec![
- //
- "v [my-project]",
- " Hello * (active)",
- " Hello * (running)",
- ]
+ vec!["v [my-project]", " Hello *", " Hello * (running)",]
);
}
@@ -1571,11 +1478,7 @@ async fn test_background_thread_completion_triggers_notification(cx: &mut TestAp
// Thread A is still running; no notification yet.
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Hello * (running) (active)",
- ]
+ vec!["v [project-a]", " Hello * (running)",]
);
// Complete thread A's turn (transition Running → Completed).
@@ -1585,11 +1488,7 @@ async fn test_background_thread_completion_triggers_notification(cx: &mut TestAp
// The completed background thread shows a notification indicator.
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Hello * (!) (active)",
- ]
+ vec!["v [project-a]", " Hello * (!)",]
);
}
@@ -1629,7 +1528,6 @@ async fn test_search_narrows_visible_threads_to_matches(cx: &mut TestAppContext)
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix crash in project panel",
" Add inline diff view",
@@ -1642,11 +1540,7 @@ async fn test_search_narrows_visible_threads_to_matches(cx: &mut TestAppContext)
type_in_search(&sidebar, "diff", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Add inline diff view <== selected",
- ]
+ vec!["v [my-project]", " Add inline diff view <== selected",]
);
// User changes query to something with no matches — list is empty.
@@ -1681,7 +1575,6 @@ async fn test_search_matches_regardless_of_case(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix Crash In Project Panel <== selected",
]
@@ -1692,7 +1585,6 @@ async fn test_search_matches_regardless_of_case(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix Crash In Project Panel <== selected",
]
@@ -1723,12 +1615,7 @@ async fn test_escape_clears_search_and_restores_full_list(cx: &mut TestAppContex
// Confirm the full list is showing.
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Alpha thread",
- " Beta thread",
- ]
+ vec!["v [my-project]", " Alpha thread", " Beta thread",]
);
// User types a search query to filter down.
@@ -1736,11 +1623,7 @@ async fn test_escape_clears_search_and_restores_full_list(cx: &mut TestAppContex
type_in_search(&sidebar, "alpha", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Alpha thread <== selected",
- ]
+ vec!["v [my-project]", " Alpha thread <== selected",]
);
// User presses Escape — filter clears, full list is restored.
@@ -1750,7 +1633,6 @@ async fn test_escape_clears_search_and_restores_full_list(cx: &mut TestAppContex
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Alpha thread <== selected",
" Beta thread",
@@ -1807,7 +1689,6 @@ async fn test_search_only_shows_workspace_headers_with_matches(cx: &mut TestAppC
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [project-a]",
" Fix bug in sidebar",
" Add tests for editor",
@@ -1818,11 +1699,7 @@ async fn test_search_only_shows_workspace_headers_with_matches(cx: &mut TestAppC
type_in_search(&sidebar, "sidebar", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Fix bug in sidebar <== selected",
- ]
+ vec!["v [project-a]", " Fix bug in sidebar <== selected",]
);
// "typo" only matches in the second workspace — the first header disappears.
@@ -1838,7 +1715,6 @@ async fn test_search_only_shows_workspace_headers_with_matches(cx: &mut TestAppC
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [project-a]",
" Fix bug in sidebar <== selected",
" Add tests for editor",
@@ -1898,7 +1774,6 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [alpha-project]",
" Fix bug in sidebar <== selected",
" Add tests for editor",
@@ -1910,11 +1785,7 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
type_in_search(&sidebar, "sidebar", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [alpha-project]",
- " Fix bug in sidebar <== selected",
- ]
+ vec!["v [alpha-project]", " Fix bug in sidebar <== selected",]
);
// "alpha sidebar" matches the workspace name "alpha-project" (fuzzy: a-l-p-h-a-s-i-d-e-b-a-r
@@ -1924,11 +1795,7 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
type_in_search(&sidebar, "fix", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [alpha-project]",
- " Fix bug in sidebar <== selected",
- ]
+ vec!["v [alpha-project]", " Fix bug in sidebar <== selected",]
);
// A query that matches a workspace name AND a thread in that same workspace.
@@ -1937,7 +1804,6 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [alpha-project]",
" Fix bug in sidebar <== selected",
" Add tests for editor",
@@ -1951,7 +1817,6 @@ async fn test_search_matches_workspace_name(cx: &mut TestAppContext) {
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [alpha-project]",
" Fix bug in sidebar <== selected",
" Add tests for editor",
@@ -2001,11 +1866,7 @@ async fn test_search_finds_threads_hidden_behind_view_more(cx: &mut TestAppConte
let filtered = visible_entries_as_strings(&sidebar, cx);
assert_eq!(
filtered,
- vec![
- //
- "v [my-project]",
- " Hidden gem thread <== selected",
- ]
+ vec!["v [my-project]", " Hidden gem thread <== selected",]
);
assert!(
!filtered.iter().any(|e| e.contains("View More")),
@@ -2041,21 +1902,14 @@ async fn test_search_finds_threads_inside_collapsed_groups(cx: &mut TestAppConte
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "> [my-project] <== selected",
- ]
+ vec!["> [my-project] <== selected"]
);
// User types a search — the thread appears even though its group is collapsed.
type_in_search(&sidebar, "important", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "> [my-project]",
- " Important thread <== selected",
- ]
+ vec!["> [my-project]", " Important thread <== selected",]
);
}
@@ -2089,7 +1943,6 @@ async fn test_search_then_keyboard_navigate_and_confirm(cx: &mut TestAppContext)
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix crash in panel <== selected",
" Fix lint warnings",
@@ -2102,7 +1955,6 @@ async fn test_search_then_keyboard_navigate_and_confirm(cx: &mut TestAppContext)
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix crash in panel",
" Fix lint warnings <== selected",
@@ -2114,7 +1966,6 @@ async fn test_search_then_keyboard_navigate_and_confirm(cx: &mut TestAppContext)
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
- //
"v [my-project]",
" Fix crash in panel <== selected",
" Fix lint warnings",
@@ -2155,11 +2006,7 @@ async fn test_confirm_on_historical_thread_activates_workspace(cx: &mut TestAppC
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Historical Thread",
- ]
+ vec!["v [my-project]", " Historical Thread",]
);
// Switch to workspace 1 so we can verify the confirm switches back.
@@ -2220,12 +2067,7 @@ async fn test_click_clears_selection_and_focus_in_restores_it(cx: &mut TestAppCo
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Thread A",
- " Thread B",
- ]
+ vec!["v [my-project]", " Thread A", " Thread B",]
);
// Keyboard confirm preserves selection.
@@ -2277,11 +2119,7 @@ async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext)
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Hello * (active)",
- ]
+ vec!["v [my-project]", " Hello *"]
);
// Simulate the agent generating a title. The notification chain is:
@@ -2303,11 +2141,7 @@ async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext)
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [my-project]",
- " Friendly Greeting with AI * (active)",
- ]
+ vec!["v [my-project]", " Friendly Greeting with AI *"]
);
}
@@ -2449,825 +2283,186 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
sidebar.read_with(cx, |sidebar, _cx| {
assert_active_thread(
sidebar,
- &session_id_a,
- "Switching workspace should seed focused_thread from the new active panel",
- );
- assert!(
- has_thread_entry(sidebar, &session_id_a),
- "The seeded thread should be present in the entries"
- );
- });
-
- let connection_b2 = StubAgentConnection::new();
- connection_b2.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
- acp::ContentChunk::new(DEFAULT_THREAD_TITLE.into()),
- )]);
- open_thread_with_connection(&panel_b, connection_b2, cx);
- send_message(&panel_b, cx);
- let session_id_b2 = active_session_id(&panel_b, cx);
- save_test_thread_metadata(&session_id_b2, &project_b, cx).await;
- cx.run_until_parked();
-
- // Panel B is not the active workspace's panel (workspace A is
- // active), so opening a thread there should not change focused_thread.
- // This prevents running threads in background workspaces from causing
- // the selection highlight to jump around.
- sidebar.read_with(cx, |sidebar, _cx| {
- assert_active_thread(
- sidebar,
- &session_id_a,
- "Opening a thread in a non-active panel should not change focused_thread",
- );
- });
-
- workspace_b.update_in(cx, |workspace, window, cx| {
- workspace.focus_handle(cx).focus(window, cx);
- });
- cx.run_until_parked();
-
- sidebar.read_with(cx, |sidebar, _cx| {
- assert_active_thread(
- sidebar,
- &session_id_a,
- "Defocusing the sidebar should not change focused_thread",
- );
- });
-
- // Switching workspaces via the multi_workspace (simulates clicking
- // a workspace header) should clear focused_thread.
- multi_workspace.update_in(cx, |mw, window, cx| {
- let workspace = mw.workspaces().find(|w| *w == &workspace_b).cloned();
- if let Some(workspace) = workspace {
- mw.activate(workspace, window, cx);
- }
- });
- cx.run_until_parked();
-
- sidebar.read_with(cx, |sidebar, _cx| {
- assert_active_thread(
- sidebar,
- &session_id_b2,
- "Switching workspace should seed focused_thread from the new active panel",
- );
- assert!(
- has_thread_entry(sidebar, &session_id_b2),
- "The seeded thread should be present in the entries"
- );
- });
-
- // ── 8. Focusing the agent panel thread keeps focused_thread ────
- // Workspace B still has session_id_b2 loaded in the agent panel.
- // Clicking into the thread (simulated by focusing its view) should
- // keep focused_thread since it was already seeded on workspace switch.
- panel_b.update_in(cx, |panel, window, cx| {
- if let Some(thread_view) = panel.active_conversation_view() {
- thread_view.read(cx).focus_handle(cx).focus(window, cx);
- }
- });
- cx.run_until_parked();
-
- sidebar.read_with(cx, |sidebar, _cx| {
- assert_active_thread(
- sidebar,
- &session_id_b2,
- "Focusing the agent panel thread should set focused_thread",
- );
- assert!(
- has_thread_entry(sidebar, &session_id_b2),
- "The focused thread should be present in the entries"
- );
- });
-}
-
-#[gpui::test]
-async fn test_new_thread_button_works_after_adding_folder(cx: &mut TestAppContext) {
- let project = init_test_project_with_agent_panel("/project-a", cx).await;
- let fs = cx.update(|cx| <dyn fs::Fs>::global(cx));
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
-
- // Start a thread and send a message so it has history.
- let connection = StubAgentConnection::new();
- connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
- acp::ContentChunk::new("Done".into()),
- )]);
- open_thread_with_connection(&panel, connection, cx);
- send_message(&panel, cx);
- let session_id = active_session_id(&panel, cx);
- save_test_thread_metadata(&session_id, &project, cx).await;
- cx.run_until_parked();
-
- // Verify the thread appears in the sidebar.
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Hello * (active)",
- ]
- );
-
- // The "New Thread" button should NOT be in "active/draft" state
- // because the panel has a thread with messages.
- sidebar.read_with(cx, |sidebar, _cx| {
- assert!(
- matches!(&sidebar.active_entry, Some(ActiveEntry::Thread { .. })),
- "Panel has a thread with messages, so active_entry should be Thread, got {:?}",
- sidebar.active_entry,
- );
- });
-
- // Now add a second folder to the workspace, changing the path_list.
- fs.as_fake()
- .insert_tree("/project-b", serde_json::json!({ "src": {} }))
- .await;
- project
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/project-b", true, cx)
- })
- .await
- .expect("should add worktree");
- cx.run_until_parked();
-
- // The workspace path_list is now [project-a, project-b]. The active
- // thread's metadata was re-saved with the new paths by the agent panel's
- // project subscription. The old [project-a] key is replaced by the new
- // key since no other workspace claims it.
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Hello * (active)",
- ]
- );
-
- // The "New Thread" button must still be clickable (not stuck in
- // "active/draft" state). Verify that `active_thread_is_draft` is
- // false — the panel still has the old thread with messages.
- sidebar.read_with(cx, |sidebar, _cx| {
- assert!(
- matches!(&sidebar.active_entry, Some(ActiveEntry::Thread { .. })),
- "After adding a folder the panel still has a thread with messages, \
- so active_entry should be Thread, got {:?}",
- sidebar.active_entry,
- );
- });
-
- // Actually click "New Thread" by calling create_new_thread and
- // verify a new draft is created.
- let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
- sidebar.update_in(cx, |sidebar, window, cx| {
- sidebar.create_new_thread(&workspace, window, cx);
- });
- cx.run_until_parked();
-
- // After creating a new thread, the panel should now be in draft
- // state (no messages on the new thread).
- sidebar.read_with(cx, |sidebar, _cx| {
- assert_active_draft(
- sidebar,
- &workspace,
- "After creating a new thread active_entry should be Draft",
- );
- });
-}
-
-#[gpui::test]
-async fn test_worktree_add_and_remove_migrates_threads(cx: &mut TestAppContext) {
- // When a worktree is added to a project, the project group key changes
- // and all historical threads should be migrated to the new key. Removing
- // the worktree should migrate them back.
- let (_fs, project) = init_multi_project_test(&["/project-a", "/project-b"], cx).await;
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Save two threads against the initial project group [/project-a].
- save_n_test_threads(2, &project, cx).await;
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Thread 2",
- " Thread 1",
- ]
- );
-
- // Verify the metadata store has threads under the old key.
- let old_key_paths = PathList::new(&[PathBuf::from("/project-a")]);
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 2,
- "should have 2 threads under old key before add"
- );
- });
-
- // Add a second worktree to the same project.
- project
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/project-b", true, cx)
- })
- .await
- .expect("should add worktree");
- cx.run_until_parked();
-
- // The project group key should now be [/project-a, /project-b].
- let new_key_paths = PathList::new(&[PathBuf::from("/project-a"), PathBuf::from("/project-b")]);
-
- // Verify multi-workspace state: exactly one project group key, the new one.
- multi_workspace.read_with(cx, |mw, _cx| {
- let keys: Vec<_> = mw.project_group_keys().cloned().collect();
- assert_eq!(
- keys.len(),
- 1,
- "should have exactly 1 project group key after add"
- );
- assert_eq!(
- keys[0].path_list(),
- &new_key_paths,
- "the key should be the new combined path list"
- );
- });
-
- // Verify threads were migrated to the new key.
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 0,
- "should have 0 threads under old key after migration"
- );
- assert_eq!(
- store.entries_for_main_worktree_path(&new_key_paths).count(),
- 2,
- "should have 2 threads under new key after migration"
- );
- });
-
- // Sidebar should show threads under the new header.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread 2",
- " Thread 1",
- ]
- );
-
- // Now remove the second worktree.
- let worktree_id = project.read_with(cx, |project, cx| {
- project
- .visible_worktrees(cx)
- .find(|wt| wt.read(cx).abs_path().as_ref() == Path::new("/project-b"))
- .map(|wt| wt.read(cx).id())
- .expect("should find project-b worktree")
- });
- project.update(cx, |project, cx| {
- project.remove_worktree(worktree_id, cx);
- });
- cx.run_until_parked();
-
- // The key should revert to [/project-a].
- multi_workspace.read_with(cx, |mw, _cx| {
- let keys: Vec<_> = mw.project_group_keys().cloned().collect();
- assert_eq!(
- keys.len(),
- 1,
- "should have exactly 1 project group key after remove"
- );
- assert_eq!(
- keys[0].path_list(),
- &old_key_paths,
- "the key should revert to the original path list"
- );
- });
-
- // Threads should be migrated back to the old key.
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&new_key_paths).count(),
- 0,
- "should have 0 threads under new key after revert"
- );
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 2,
- "should have 2 threads under old key after revert"
- );
- });
-
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a]",
- " Thread 2",
- " Thread 1",
- ]
- );
-}
-
-#[gpui::test]
-async fn test_worktree_add_key_collision_removes_duplicate_workspace(cx: &mut TestAppContext) {
- // When a worktree is added to workspace A and the resulting key matches
- // an existing workspace B's key (and B has the same root paths), B
- // should be removed as a true duplicate.
- let (fs, project_a) = init_multi_project_test(&["/project-a", "/project-b"], cx).await;
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Save a thread against workspace A [/project-a].
- save_named_thread_metadata("thread-a", "Thread A", &project_a, cx).await;
-
- // Create workspace B with both worktrees [/project-a, /project-b].
- let project_b = project::Project::test(
- fs.clone() as Arc<dyn Fs>,
- ["/project-a".as_ref(), "/project-b".as_ref()],
- cx,
- )
- .await;
- let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_b.clone(), window, cx)
- });
- cx.run_until_parked();
-
- // Switch back to workspace A so it's the active workspace when the collision happens.
- let workspace_a =
- multi_workspace.read_with(cx, |mw, _| mw.workspaces().next().unwrap().clone());
- multi_workspace.update_in(cx, |mw, window, cx| {
- mw.activate(workspace_a, window, cx);
- });
- cx.run_until_parked();
-
- // Save a thread against workspace B [/project-a, /project-b].
- save_named_thread_metadata("thread-b", "Thread B", &project_b, cx).await;
-
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- // Both project groups should be visible.
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread B",
- "v [project-a]",
- " Thread A",
- ]
- );
-
- let workspace_b_id = workspace_b.entity_id();
-
- // Now add /project-b to workspace A's project, causing a key collision.
- project_a
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/project-b", true, cx)
- })
- .await
- .expect("should add worktree");
- cx.run_until_parked();
-
- // Workspace B should have been removed (true duplicate — same root paths).
- multi_workspace.read_with(cx, |mw, _cx| {
- let workspace_ids: Vec<_> = mw.workspaces().map(|ws| ws.entity_id()).collect();
- assert!(
- !workspace_ids.contains(&workspace_b_id),
- "workspace B should have been removed after key collision"
- );
- });
-
- // There should be exactly one project group key now.
- let combined_paths = PathList::new(&[PathBuf::from("/project-a"), PathBuf::from("/project-b")]);
- multi_workspace.read_with(cx, |mw, _cx| {
- let keys: Vec<_> = mw.project_group_keys().cloned().collect();
- assert_eq!(
- keys.len(),
- 1,
- "should have exactly 1 project group key after collision"
- );
- assert_eq!(
- keys[0].path_list(),
- &combined_paths,
- "the remaining key should be the combined paths"
- );
- });
-
- // Both threads should be visible under the merged group.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " Thread A",
- " Thread B",
- ]
- );
-}
-
-#[gpui::test]
-async fn test_worktree_collision_keeps_active_workspace(cx: &mut TestAppContext) {
- // When workspace A adds a folder that makes it collide with workspace B,
- // and B is the *active* workspace, A (the incoming one) should be
- // dropped so the user stays on B. A linked worktree sibling of A
- // should migrate into B's group.
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- // Set up /project-a with a linked worktree.
- fs.insert_tree(
- "/project-a",
- serde_json::json!({
- ".git": {
- "worktrees": {
- "feature": {
- "commondir": "../../",
- "HEAD": "ref: refs/heads/feature",
- },
- },
- },
- "src": {},
- }),
- )
- .await;
- fs.insert_tree(
- "/wt-feature",
- serde_json::json!({
- ".git": "gitdir: /project-a/.git/worktrees/feature",
- "src": {},
- }),
- )
- .await;
- fs.add_linked_worktree_for_repo(
- Path::new("/project-a/.git"),
- false,
- git::repository::Worktree {
- path: PathBuf::from("/wt-feature"),
- ref_name: Some("refs/heads/feature".into()),
- sha: "aaa".into(),
- is_main: false,
- },
- )
- .await;
- fs.insert_tree("/project-b", serde_json::json!({ ".git": {}, "src": {} }))
- .await;
- cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
-
- let project_a = project::Project::test(fs.clone(), ["/project-a".as_ref()], cx).await;
- project_a.update(cx, |p, cx| p.git_scans_complete(cx)).await;
-
- // Linked worktree sibling of A.
- let project_wt = project::Project::test(fs.clone(), ["/wt-feature".as_ref()], cx).await;
- project_wt
- .update(cx, |p, cx| p.git_scans_complete(cx))
- .await;
-
- // Workspace B has both folders already.
- let project_b = project::Project::test(
- fs.clone() as Arc<dyn Fs>,
- ["/project-a".as_ref(), "/project-b".as_ref()],
- cx,
- )
- .await;
-
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
-
- // Add agent panels to all workspaces.
- let workspace_a_entity = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
- add_agent_panel(&workspace_a_entity, cx);
-
- // Add the linked worktree workspace (sibling of A).
- let workspace_wt = multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_wt.clone(), window, cx)
- });
- add_agent_panel(&workspace_wt, cx);
- cx.run_until_parked();
-
- // Add workspace B (will become active).
- let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(project_b.clone(), window, cx)
- });
- add_agent_panel(&workspace_b, cx);
- cx.run_until_parked();
-
- // Save threads in each group.
- save_named_thread_metadata("thread-a", "Thread A", &project_a, cx).await;
- save_thread_metadata_with_main_paths(
- "thread-wt",
- "Worktree Thread",
- PathList::new(&[PathBuf::from("/wt-feature")]),
- PathList::new(&[PathBuf::from("/project-a")]),
- cx,
- );
- save_named_thread_metadata("thread-b", "Thread B", &project_b, cx).await;
-
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- // B is active, A and wt-feature are in one group, B in another.
- assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspace().entity_id()),
- workspace_b.entity_id(),
- "workspace B should be active"
- );
- multi_workspace.read_with(cx, |mw, _cx| {
- assert_eq!(mw.project_group_keys().count(), 2, "should have 2 groups");
- assert_eq!(mw.workspaces().count(), 3, "should have 3 workspaces");
- });
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " [~ Draft] (active)",
- " Thread B",
- "v [project-a]",
- " Thread A",
- " Worktree Thread {wt-feature}",
- ]
- );
-
- let workspace_a = multi_workspace.read_with(cx, |mw, _| {
- mw.workspaces()
- .find(|ws| {
- ws.entity_id() != workspace_b.entity_id()
- && ws.entity_id() != workspace_wt.entity_id()
- })
- .unwrap()
- .clone()
- });
-
- // Add /project-b to workspace A's project, causing a collision with B.
- project_a
- .update(cx, |project, cx| {
- project.find_or_create_worktree("/project-b", true, cx)
- })
- .await
- .expect("should add worktree");
- cx.run_until_parked();
-
- // Workspace A (the incoming duplicate) should have been dropped.
- multi_workspace.read_with(cx, |mw, _cx| {
- let workspace_ids: Vec<_> = mw.workspaces().map(|ws| ws.entity_id()).collect();
- assert!(
- !workspace_ids.contains(&workspace_a.entity_id()),
- "workspace A should have been dropped"
- );
- });
-
- // The active workspace should still be B.
- assert_eq!(
- multi_workspace.read_with(cx, |mw, _| mw.workspace().entity_id()),
- workspace_b.entity_id(),
- "workspace B should still be active"
- );
-
- // The linked worktree sibling should have migrated into B's group
- // (it got the folder add and now shares the same key).
- multi_workspace.read_with(cx, |mw, _cx| {
- let workspace_ids: Vec<_> = mw.workspaces().map(|ws| ws.entity_id()).collect();
- assert!(
- workspace_ids.contains(&workspace_wt.entity_id()),
- "linked worktree workspace should still exist"
- );
- assert_eq!(
- mw.project_group_keys().count(),
- 1,
- "should have 1 group after merge"
- );
- assert_eq!(
- mw.workspaces().count(),
- 2,
- "should have 2 workspaces (B + linked worktree)"
- );
- });
-
- // The linked worktree workspace should have gotten the new folder.
- let wt_worktree_count =
- project_wt.read_with(cx, |project, cx| project.visible_worktrees(cx).count());
- assert_eq!(
- wt_worktree_count, 2,
- "linked worktree project should have gotten /project-b"
- );
-
- // After: everything merged under one group. Thread A migrated,
- // worktree thread shows its chip, B's thread and draft remain.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
- cx.run_until_parked();
-
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project-a, project-b]",
- " [~ Draft] (active)",
- " [+ New Thread {project-a:wt-feature}]",
- " Thread A",
- " Worktree Thread {project-a:wt-feature}",
- " Thread B",
- ]
- );
-}
-
-#[gpui::test]
-async fn test_worktree_add_syncs_linked_worktree_sibling(cx: &mut TestAppContext) {
- // When a worktree is added to the main workspace, a linked worktree
- // sibling (different root paths, same project group key) should also
- // get the new folder added to its project.
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- fs.insert_tree(
- "/project",
- serde_json::json!({
- ".git": {
- "worktrees": {
- "feature": {
- "commondir": "../../",
- "HEAD": "ref: refs/heads/feature",
- },
- },
- },
- "src": {},
- }),
- )
- .await;
-
- fs.insert_tree(
- "/wt-feature",
- serde_json::json!({
- ".git": "gitdir: /project/.git/worktrees/feature",
- "src": {},
- }),
- )
- .await;
-
- fs.add_linked_worktree_for_repo(
- Path::new("/project/.git"),
- false,
- git::repository::Worktree {
- path: PathBuf::from("/wt-feature"),
- ref_name: Some("refs/heads/feature".into()),
- sha: "aaa".into(),
- is_main: false,
- },
- )
- .await;
-
- // Create a second independent project to add as a folder later.
- fs.insert_tree(
- "/other-project",
- serde_json::json!({ ".git": {}, "src": {} }),
- )
- .await;
-
- cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
+ &session_id_a,
+ "Switching workspace should seed focused_thread from the new active panel",
+ );
+ assert!(
+ has_thread_entry(sidebar, &session_id_a),
+ "The seeded thread should be present in the entries"
+ );
+ });
- let main_project = project::Project::test(fs.clone(), ["/project".as_ref()], cx).await;
- let worktree_project = project::Project::test(fs.clone(), ["/wt-feature".as_ref()], cx).await;
+ let connection_b2 = StubAgentConnection::new();
+ connection_b2.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
+ acp::ContentChunk::new(DEFAULT_THREAD_TITLE.into()),
+ )]);
+ open_thread_with_connection(&panel_b, connection_b2, cx);
+ send_message(&panel_b, cx);
+ let session_id_b2 = active_session_id(&panel_b, cx);
+ save_test_thread_metadata(&session_id_b2, &project_b, cx).await;
+ cx.run_until_parked();
- main_project
- .update(cx, |p, cx| p.git_scans_complete(cx))
- .await;
- worktree_project
- .update(cx, |p, cx| p.git_scans_complete(cx))
- .await;
+ // Panel B is not the active workspace's panel (workspace A is
+ // active), so opening a thread there should not change focused_thread.
+ // This prevents running threads in background workspaces from causing
+ // the selection highlight to jump around.
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert_active_thread(
+ sidebar,
+ &session_id_a,
+ "Opening a thread in a non-active panel should not change focused_thread",
+ );
+ });
- let (multi_workspace, cx) =
- cx.add_window_view(|window, cx| MultiWorkspace::test_new(main_project.clone(), window, cx));
- let sidebar = setup_sidebar(&multi_workspace, cx);
+ workspace_b.update_in(cx, |workspace, window, cx| {
+ workspace.focus_handle(cx).focus(window, cx);
+ });
+ cx.run_until_parked();
- // Add agent panel to the main workspace.
- let main_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
- add_agent_panel(&main_workspace, cx);
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert_active_thread(
+ sidebar,
+ &session_id_a,
+ "Defocusing the sidebar should not change focused_thread",
+ );
+ });
- // Open the linked worktree as a separate workspace.
- let wt_workspace = multi_workspace.update_in(cx, |mw, window, cx| {
- mw.test_add_workspace(worktree_project.clone(), window, cx)
+ // Switching workspaces via the multi_workspace (simulates clicking
+ // a workspace header) should clear focused_thread.
+ multi_workspace.update_in(cx, |mw, window, cx| {
+ let workspace = mw.workspaces().find(|w| *w == &workspace_b).cloned();
+ if let Some(workspace) = workspace {
+ mw.activate(workspace, window, cx);
+ }
});
- add_agent_panel(&wt_workspace, cx);
cx.run_until_parked();
- // Both workspaces should share the same project group key [/project].
- multi_workspace.read_with(cx, |mw, _cx| {
- assert_eq!(
- mw.project_group_keys().count(),
- 1,
- "should have 1 project group key before add"
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert_active_thread(
+ sidebar,
+ &session_id_b2,
+ "Switching workspace should seed focused_thread from the new active panel",
+ );
+ assert!(
+ has_thread_entry(sidebar, &session_id_b2),
+ "The seeded thread should be present in the entries"
);
- assert_eq!(mw.workspaces().count(), 2, "should have 2 workspaces");
});
- // Save threads against each workspace.
- save_named_thread_metadata("main-thread", "Main Thread", &main_project, cx).await;
- save_named_thread_metadata("wt-thread", "Worktree Thread", &worktree_project, cx).await;
+ // ── 8. Focusing the agent panel thread keeps focused_thread ────
+ // Workspace B still has session_id_b2 loaded in the agent panel.
+ // Clicking into the thread (simulated by focusing its view) should
+ // keep focused_thread since it was already seeded on workspace switch.
+ panel_b.update_in(cx, |panel, window, cx| {
+ if let Some(thread_view) = panel.active_conversation_view() {
+ thread_view.read(cx).focus_handle(cx).focus(window, cx);
+ }
+ });
+ cx.run_until_parked();
- // Verify both threads are under the old key [/project].
- let old_key_paths = PathList::new(&[PathBuf::from("/project")]);
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 2,
- "should have 2 threads under old key before add"
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert_active_thread(
+ sidebar,
+ &session_id_b2,
+ "Focusing the agent panel thread should set focused_thread",
+ );
+ assert!(
+ has_thread_entry(sidebar, &session_id_b2),
+ "The focused thread should be present in the entries"
);
});
+}
+
+#[gpui::test]
+async fn test_new_thread_button_works_after_adding_folder(cx: &mut TestAppContext) {
+ let project = init_test_project_with_agent_panel("/project-a", cx).await;
+ let fs = cx.update(|cx| <dyn fs::Fs>::global(cx));
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
+ // Start a thread and send a message so it has history.
+ let connection = StubAgentConnection::new();
+ connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
+ acp::ContentChunk::new("Done".into()),
+ )]);
+ open_thread_with_connection(&panel, connection, cx);
+ send_message(&panel, cx);
+ let session_id = active_session_id(&panel, cx);
+ save_test_thread_metadata(&session_id, &project, cx).await;
cx.run_until_parked();
+ // Verify the thread appears in the sidebar.
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [project]",
- " [~ Draft {wt-feature}] (active)",
- " Worktree Thread {wt-feature}",
- " Main Thread",
- ]
+ vec!["v [project-a]", " Hello *",]
);
- // Add /other-project as a folder to the main workspace.
- main_project
+ // The "New Thread" button should NOT be in "active/draft" state
+ // because the panel has a thread with messages.
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert!(
+ matches!(&sidebar.active_entry, Some(ActiveEntry::Thread { .. })),
+ "Panel has a thread with messages, so active_entry should be Thread, got {:?}",
+ sidebar.active_entry,
+ );
+ });
+
+ // Now add a second folder to the workspace, changing the path_list.
+ fs.as_fake()
+ .insert_tree("/project-b", serde_json::json!({ "src": {} }))
+ .await;
+ project
.update(cx, |project, cx| {
- project.find_or_create_worktree("/other-project", true, cx)
+ project.find_or_create_worktree("/project-b", true, cx)
})
.await
.expect("should add worktree");
cx.run_until_parked();
- // The linked worktree workspace should have gotten the new folder too.
- let wt_worktree_count =
- worktree_project.read_with(cx, |project, cx| project.visible_worktrees(cx).count());
+ // The workspace path_list is now [project-a, project-b]. The active
+ // thread's metadata was re-saved with the new paths by the agent panel's
+ // project subscription, so it stays visible under the updated group.
+ // The old [project-a] group persists in the sidebar (empty) because
+ // project_group_keys is append-only.
assert_eq!(
- wt_worktree_count, 2,
- "linked worktree project should have gotten the new folder"
+ visible_entries_as_strings(&sidebar, cx),
+ vec![
+ "v [project-a, project-b]", //
+ " Hello *",
+ "v [project-a]",
+ ]
);
- // Both workspaces should still exist under one key.
- multi_workspace.read_with(cx, |mw, _cx| {
- assert_eq!(mw.workspaces().count(), 2, "both workspaces should survive");
- assert_eq!(
- mw.project_group_keys().count(),
- 1,
- "should still have 1 project group key"
+ // The "New Thread" button must still be clickable (not stuck in
+ // "active/draft" state). Verify that `active_thread_is_draft` is
+ // false — the panel still has the old thread with messages.
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert!(
+ matches!(&sidebar.active_entry, Some(ActiveEntry::Thread { .. })),
+ "After adding a folder the panel still has a thread with messages, \
+ so active_entry should be Thread, got {:?}",
+ sidebar.active_entry,
);
});
- // Threads should have been migrated to the new key.
- let new_key_paths =
- PathList::new(&[PathBuf::from("/other-project"), PathBuf::from("/project")]);
- cx.update(|_window, cx| {
- let store = ThreadMetadataStore::global(cx).read(cx);
- assert_eq!(
- store.entries_for_main_worktree_path(&old_key_paths).count(),
- 0,
- "should have 0 threads under old key after migration"
- );
- assert_eq!(
- store.entries_for_main_worktree_path(&new_key_paths).count(),
- 2,
- "should have 2 threads under new key after migration"
- );
+ // Actually click "New Thread" by calling create_new_thread and
+ // verify a new draft is created.
+ let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
+ sidebar.update_in(cx, |sidebar, window, cx| {
+ sidebar.create_new_thread(&workspace, window, cx);
});
-
- // Both threads should still be visible in the sidebar.
- sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx));
cx.run_until_parked();
- assert_eq!(
- visible_entries_as_strings(&sidebar, cx),
- vec![
- //
- "v [other-project, project]",
- " [~ Draft {project:wt-feature}] (active)",
- " Worktree Thread {project:wt-feature}",
- " Main Thread",
- ]
- );
+ // After creating a new thread, the panel should now be in draft
+ // state (no messages on the new thread).
+ sidebar.read_with(cx, |sidebar, _cx| {
+ assert_active_draft(
+ sidebar,
+ &workspace,
+ "After creating a new thread active_entry should be Draft",
+ );
+ });
}
#[gpui::test]