diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2561e7b0433c795e492d67534962fc6622d92125..d40d00f60e2baffb809fc39f973f2a113b8427db 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -34,8 +34,8 @@ use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore}; use crate::{ AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, - OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu, - ToggleOptionsMenu, + OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, + ShowThreadMetadata, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, conversation_view::{AcpThreadViewEvent, ThreadView}, ui::EndTrialUpsell, @@ -50,10 +50,11 @@ use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore}; use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Context as _, Result}; +use chrono::{DateTime, Utc}; use client::UserStore; use cloud_api_types::Plan; use collections::HashMap; -use editor::Editor; +use editor::{Editor, MultiBuffer}; use extension::ExtensionEvents; use extension_host::ExtensionStore; use fs::Fs; @@ -311,6 +312,24 @@ pub fn init(cx: &mut App) { }); } }) + .register_action(|workspace, _: &ShowThreadMetadata, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.show_thread_metadata(&ShowThreadMetadata, window, cx); + }); + } + }) + .register_action(|workspace, _: &ShowAllSidebarThreadMetadata, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.show_all_sidebar_thread_metadata( + &ShowAllSidebarThreadMetadata, + window, + cx, + ); + }); + } + }) .register_action(|workspace, action: &ReviewBranchDiff, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; @@ -588,6 +607,46 @@ fn build_conflicted_files_resolution_prompt( content } +fn format_timestamp_human(dt: &DateTime) -> String { + let now = Utc::now(); + let duration = now.signed_duration_since(*dt); + + let relative = if duration.num_seconds() < 0 { + "in the future".to_string() + } else if duration.num_seconds() < 60 { + let seconds = duration.num_seconds(); + format!("{seconds} seconds ago") + } else if duration.num_minutes() < 60 { + let minutes = duration.num_minutes(); + format!("{minutes} minutes ago") + } else if duration.num_hours() < 24 { + let hours = duration.num_hours(); + format!("{hours} hours ago") + } else { + let days = duration.num_days(); + format!("{days} days ago") + }; + + format!("{} ({})", dt.to_rfc3339(), relative) +} + +/// Used for `dev: show thread metadata` action +fn thread_metadata_to_debug_json( + metadata: &crate::thread_metadata_store::ThreadMetadata, +) -> serde_json::Value { + serde_json::json!({ + "thread_id": metadata.thread_id, + "session_id": metadata.session_id.as_ref().map(|s| s.0.to_string()), + "agent_id": metadata.agent_id.0.to_string(), + "title": metadata.title.as_ref().map(|t| t.to_string()), + "updated_at": format_timestamp_human(&metadata.updated_at), + "created_at": metadata.created_at.as_ref().map(format_timestamp_human), + "interacted_at": metadata.interacted_at.as_ref().map(format_timestamp_human), + "worktree_paths": format!("{:?}", metadata.worktree_paths), + "archived": metadata.archived, + }) +} + pub(crate) struct AgentThread { conversation_view: Entity, } @@ -1873,6 +1932,108 @@ impl AgentPanel { .detach_and_log_err(cx); } + fn show_thread_metadata( + &mut self, + _: &ShowThreadMetadata, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread_id) = self.active_thread_id(cx) else { + Self::show_deferred_toast(&self.workspace, "No active thread", cx); + return; + }; + + let Some(store) = ThreadMetadataStore::try_global(cx) else { + Self::show_deferred_toast(&self.workspace, "Thread metadata store not available", cx); + return; + }; + + let Some(metadata) = store.read(cx).entry(thread_id).cloned() else { + Self::show_deferred_toast(&self.workspace, "No metadata found for active thread", cx); + return; + }; + + let json = thread_metadata_to_debug_json(&metadata); + let text = serde_json::to_string_pretty(&json).unwrap_or_default(); + let title = format!("Thread Metadata: {}", metadata.display_title()); + + self.open_json_buffer(title, text, window, cx); + } + + fn show_all_sidebar_thread_metadata( + &mut self, + _: &ShowAllSidebarThreadMetadata, + window: &mut Window, + cx: &mut Context, + ) { + let Some(store) = ThreadMetadataStore::try_global(cx) else { + Self::show_deferred_toast(&self.workspace, "Thread metadata store not available", cx); + return; + }; + + let entries: Vec = store + .read(cx) + .entries() + .filter(|t| !t.archived) + .map(thread_metadata_to_debug_json) + .collect(); + + let json = serde_json::Value::Array(entries); + let text = serde_json::to_string_pretty(&json).unwrap_or_default(); + + self.open_json_buffer("All Sidebar Thread Metadata".to_string(), text, window, cx); + } + + fn open_json_buffer( + &self, + title: String, + text: String, + window: &mut Window, + cx: &mut Context, + ) { + let json_language = self.language_registry.language_for_name("JSON"); + let project = self.project.clone(); + let workspace = self.workspace.clone(); + + window + .spawn(cx, async move |cx| { + let json_language = json_language.await.ok(); + + let buffer = project + .update(cx, |project, cx| { + project.create_buffer(json_language, false, cx) + }) + .await?; + + buffer.update(cx, |buffer, cx| { + buffer.set_text(text, cx); + buffer.set_capability(language::Capability::ReadWrite, cx); + }); + + workspace.update_in(cx, |workspace, window, cx| { + let buffer = + cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.clone())); + + workspace.add_item_to_active_pane( + Box::new(cx.new(|cx| { + let mut editor = + Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); + editor.set_breadcrumb_header(title); + editor.disable_mouse_wheel_zoom(); + editor + })), + None, + true, + window, + cx, + ); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + fn handle_agent_configuration_event( &mut self, _entity: &Entity, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 955a0a81be62ae2abe0d6ea84d61c3188c177f77..fca8bd6e7ba68c8b754b3306147a27829ec52c51 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -200,6 +200,16 @@ actions!( ] ); +actions!( + dev, + [ + /// Shows metadata for the currently active thread. + ShowThreadMetadata, + /// Shows metadata for all threads in the sidebar. + ShowAllSidebarThreadMetadata, + ] +); + /// Action to authorize a tool call with a specific permission option. /// This is used by the permission granularity dropdown to authorize tool calls. #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]