diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 37dc2a86a8e4225ecc5bef3315714900240c13fb..87852910a255ed03c105f9e922f21818964429fa 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1909,6 +1909,14 @@ impl AgentPanel { } } + pub fn workspace_id(&self) -> Option { + self.workspace_id + } + + pub fn background_threads(&self) -> &HashMap> { + &self.background_threads + } + pub fn active_conversation_view(&self) -> Option<&Entity> { match &self.active_view { ActiveView::AgentThread { conversation_view } => Some(conversation_view), diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 281a140931318be206de615e06ab78a5e499669d..d80e1f19c709ed5c865779752da46087398212df 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -68,6 +68,14 @@ gpui::actions!( ] ); +gpui::actions!( + dev, + [ + /// Dumps multi-workspace state (projects, worktrees, active threads) into a new buffer. + DumpWorkspaceInfo, + ] +); + const DEFAULT_WIDTH: Pixels = px(300.0); const MIN_WIDTH: Pixels = px(200.0); const MAX_WIDTH: Pixels = px(800.0); @@ -3448,3 +3456,197 @@ fn all_thread_infos_for_workspace( Some(threads).into_iter().flatten() } + +pub fn dump_workspace_info( + workspace: &mut Workspace, + _: &DumpWorkspaceInfo, + window: &mut gpui::Window, + cx: &mut gpui::Context, +) { + use std::fmt::Write; + + let mut output = String::new(); + let this_entity = cx.entity(); + + let multi_workspace = workspace.multi_workspace().and_then(|weak| weak.upgrade()); + let workspaces: Vec> = match &multi_workspace { + Some(mw) => mw.read(cx).workspaces().to_vec(), + None => vec![this_entity.clone()], + }; + let active_index = multi_workspace + .as_ref() + .map(|mw| mw.read(cx).active_workspace_index()); + + writeln!(output, "MultiWorkspace: {} workspace(s)", workspaces.len()).ok(); + if let Some(index) = active_index { + writeln!(output, "Active workspace index: {index}").ok(); + } + writeln!(output).ok(); + + for (index, ws) in workspaces.iter().enumerate() { + let is_active = active_index == Some(index); + writeln!( + output, + "--- Workspace {index}{} ---", + if is_active { " (active)" } else { "" } + ) + .ok(); + + // The action handler is already inside an update on `this_entity`, + // so we must avoid a nested read/update on that same entity. + if *ws == this_entity { + dump_single_workspace(workspace, &mut output, cx); + } else { + ws.read_with(cx, |ws, cx| { + dump_single_workspace(ws, &mut output, cx); + }); + } + } + + let project = workspace.project().clone(); + cx.spawn_in(window, async move |_this, cx| { + let buffer = project + .update(cx, |project, cx| project.create_buffer(None, false, cx)) + .await?; + + buffer.update(cx, |buffer, cx| { + buffer.set_text(output, cx); + }); + + let buffer = cx.new(|cx| { + editor::MultiBuffer::singleton(buffer, cx).with_title("Workspace Info".into()) + }); + + _this.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane( + Box::new(cx.new(|cx| { + let mut editor = + editor::Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); + editor.set_read_only(true); + editor.set_should_serialize(false, cx); + editor.set_breadcrumb_header("Workspace Info".into()); + editor + })), + None, + true, + window, + cx, + ); + }) + }) + .detach_and_log_err(cx); +} + +fn dump_single_workspace(workspace: &Workspace, output: &mut String, cx: &gpui::App) { + use std::fmt::Write; + + let workspace_db_id = workspace.database_id(); + match workspace_db_id { + Some(id) => writeln!(output, "Workspace DB ID: {id:?}").ok(), + None => writeln!(output, "Workspace DB ID: (none)").ok(), + }; + + let project = workspace.project().read(cx); + + let repos: Vec<_> = project + .repositories(cx) + .values() + .map(|repo| repo.read(cx).snapshot()) + .collect(); + + writeln!(output, "Worktrees:").ok(); + for worktree in project.worktrees(cx) { + let worktree = worktree.read(cx); + let abs_path = worktree.abs_path(); + let visible = worktree.is_visible(); + + let repo_info = repos + .iter() + .find(|snapshot| abs_path.starts_with(&*snapshot.work_directory_abs_path)); + + let is_linked = repo_info.map(|s| s.is_linked_worktree()).unwrap_or(false); + let original_repo_path = repo_info.map(|s| &s.original_repo_abs_path); + let branch = repo_info.and_then(|s| s.branch.as_ref().map(|b| b.ref_name.clone())); + + write!(output, " - {}", abs_path.display()).ok(); + if !visible { + write!(output, " (hidden)").ok(); + } + if let Some(branch) = &branch { + write!(output, " [branch: {branch}]").ok(); + } + if is_linked { + if let Some(original) = original_repo_path { + write!(output, " [linked worktree -> {}]", original.display()).ok(); + } else { + write!(output, " [linked worktree]").ok(); + } + } + writeln!(output).ok(); + } + + if let Some(panel) = workspace.panel::(cx) { + let panel = panel.read(cx); + + let panel_workspace_id = panel.workspace_id(); + if panel_workspace_id != workspace_db_id { + writeln!( + output, + " \u{26a0} workspace ID mismatch! panel has {panel_workspace_id:?}, workspace has {workspace_db_id:?}" + ) + .ok(); + } + + if let Some(thread) = panel.active_agent_thread(cx) { + let thread = thread.read(cx); + let title = thread.title().unwrap_or_else(|| "(untitled)".into()); + let session_id = thread.session_id(); + let status = match thread.status() { + ThreadStatus::Idle => "idle", + ThreadStatus::Generating => "generating", + }; + let entry_count = thread.entries().len(); + write!(output, "Active thread: {title} (session: {session_id})").ok(); + write!(output, " [{status}, {entry_count} entries").ok(); + if thread.is_waiting_for_confirmation() { + write!(output, ", awaiting confirmation").ok(); + } + writeln!(output, "]").ok(); + } else { + writeln!(output, "Active thread: (none)").ok(); + } + + let background_threads = panel.background_threads(); + if !background_threads.is_empty() { + writeln!( + output, + "Background threads ({}): ", + background_threads.len() + ) + .ok(); + for (session_id, conversation_view) in background_threads { + if let Some(thread_view) = conversation_view.read(cx).root_thread(cx) { + let thread = thread_view.read(cx).thread.read(cx); + let title = thread.title().unwrap_or_else(|| "(untitled)".into()); + let status = match thread.status() { + ThreadStatus::Idle => "idle", + ThreadStatus::Generating => "generating", + }; + let entry_count = thread.entries().len(); + write!(output, " - {title} (session: {session_id})").ok(); + write!(output, " [{status}, {entry_count} entries").ok(); + if thread.is_waiting_for_confirmation() { + write!(output, ", awaiting confirmation").ok(); + } + writeln!(output, "]").ok(); + } else { + writeln!(output, " - (not connected) (session: {session_id})").ok(); + } + } + } + } else { + writeln!(output, "Agent panel: not loaded").ok(); + } + + writeln!(output).ok(); +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bf7f4dd2d4dab50d4478515ec9b0a328a7730e5a..8f7e812a427593ca73aef6a7034481a28b42af9f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1202,6 +1202,8 @@ fn register_actions( } }); } + + workspace.register_action(sidebar::dump_workspace_info); } fn initialize_pane(