@@ -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<Workspace>,
+) {
+ 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<gpui::Entity<Workspace>> = 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::<AgentPanel>(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();
+}