842 lines · 30.9 KB · Rust
RawDownload
use std::{collections::HashSet, fmt::Display, rc::Rc, sync::Arc}; use agent_client_protocol::schema::v1 as acp;use agent_servers::{AcpDebugMessage, AcpDebugMessageContent, AcpDebugMessageDirection};use agent_ui::agent_connection_store::AgentConnectionStatus;use agent_ui::{Agent, AgentConnectionStore, AgentPanel};use collections::HashMap;use gpui::{    App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, ListState,    SharedString, StyleRefinement, Subscription, Task, TextStyleRefinement, WeakEntity, Window,    actions, list, prelude::*,};use language::LanguageRegistry;use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};use project::{AgentId, Project};use settings::Settings;use theme_settings::ThemeSettings;use ui::{    ContextMenu, CopyButton, DropdownMenu, DropdownStyle, IconPosition, Tooltip, WithScrollbar,    prelude::*,};use util::ResultExt as _;use workspace::{    Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,}; actions!(dev, [OpenAcpLogs]); pub fn init(cx: &mut App) {    cx.observe_new(        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {            workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {                let connection_store = workspace                    .panel::<AgentPanel>(cx)                    .map(|panel| panel.read(cx).connection_store().clone());                let acp_tools = Box::new(cx.new(|cx| {                    AcpTools::new(                        workspace.weak_handle(),                        workspace.project().clone(),                        connection_store,                        cx,                    )                }));                workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);            });        },    )    .detach();} struct AcpTools {    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    focus_handle: FocusHandle,    expanded: HashSet<usize>,    watched_connections: HashMap<AgentId, WatchedConnection>,    selected_connection: Option<AgentId>,    connection_store: Option<Entity<AgentConnectionStore>>,    _workspace_subscription: Option<Subscription>,    _connection_store_subscription: Option<Subscription>,} struct WatchedConnection {    agent_id: AgentId,    connection: Rc<agent_servers::AcpConnection>,    messages: Vec<WatchedConnectionMessage>,    list_state: ListState,    incoming_request_methods: HashMap<acp::RequestId, Arc<str>>,    outgoing_request_methods: HashMap<acp::RequestId, Arc<str>>,    _task: Task<()>,} impl AcpTools {    fn new(        workspace: WeakEntity<Workspace>,        project: Entity<Project>,        connection_store: Option<Entity<AgentConnectionStore>>,        cx: &mut Context<Self>,    ) -> Self {        let workspace_subscription = workspace.upgrade().map(|workspace| {            cx.observe(&workspace, |this, _, cx| {                this.update_connection_store(cx);            })        });         let mut acp_tools = Self {            workspace,            project,            focus_handle: cx.focus_handle(),            expanded: HashSet::default(),            watched_connections: HashMap::default(),            selected_connection: None,            connection_store: None,            _workspace_subscription: workspace_subscription,            _connection_store_subscription: None,        };        acp_tools.set_connection_store(connection_store, cx);        acp_tools    }     fn set_connection_store(        &mut self,        connection_store: Option<Entity<AgentConnectionStore>>,        cx: &mut Context<Self>,    ) {        if self.connection_store == connection_store {            return;        }         self.connection_store = connection_store.clone();        self._connection_store_subscription = connection_store.as_ref().map(|connection_store| {            cx.observe(connection_store, |this, _, cx| {                this.refresh_connections(cx);            })        });        self.refresh_connections(cx);    }     fn update_connection_store(&mut self, cx: &mut Context<Self>) {        let connection_store = self.workspace.upgrade().and_then(|workspace| {            workspace                .read(cx)                .panel::<AgentPanel>(cx)                .map(|panel| panel.read(cx).connection_store().clone())        });        self.set_connection_store(connection_store, cx);    }     fn refresh_connections(&mut self, cx: &mut Context<Self>) {        let active_connections = self            .connection_store            .as_ref()            .map(|connection_store| connection_store.read(cx).active_acp_connections(cx))            .unwrap_or_default();         self.watched_connections            .retain(|agent_id, watched_connection| {                active_connections.iter().any(|active_connection| {                    active_connection.agent_id == *agent_id                        && Rc::ptr_eq(                            &active_connection.connection,                            &watched_connection.connection,                        )                })            });         for active_connection in active_connections {            if self                .watched_connections                .get(&active_connection.agent_id)                .is_some_and(|watched_connection| {                    Rc::ptr_eq(                        &active_connection.connection,                        &watched_connection.connection,                    )                })            {                continue;            }             let (backlog, messages_rx) = active_connection.connection.subscribe_debug_messages();            let agent_id = active_connection.agent_id.clone();            let task = cx.spawn({                let agent_id = agent_id.clone();                async move |this, cx| {                    while let Ok(message) = messages_rx.recv().await {                        this.update(cx, |this, cx| {                            this.push_stream_message(&agent_id, message, cx);                        })                        .log_err();                    }                }            });             let mut watched_connection = WatchedConnection {                agent_id: agent_id.clone(),                messages: Vec::new(),                list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),                connection: active_connection.connection.clone(),                incoming_request_methods: HashMap::default(),                outgoing_request_methods: HashMap::default(),                _task: task,            };             for message in backlog {                push_stream_message_for_connection(                    &mut watched_connection,                    &self.project,                    message,                    cx,                );            }             self.watched_connections                .insert(agent_id, watched_connection);        }         self.selected_connection = self            .selected_connection            .clone()            .filter(|agent_id| self.should_keep_selected_connection(agent_id, cx))            .or_else(|| self.watched_connections.keys().next().cloned());        self.expanded.clear();        cx.notify();    }     fn should_keep_selected_connection(&self, agent_id: &AgentId, cx: &App) -> bool {        self.watched_connections.contains_key(agent_id)            || self                .connection_store                .as_ref()                .is_some_and(|connection_store| {                    connection_store                        .read(cx)                        .connection_status(&Agent::from(agent_id.clone()), cx)                        != AgentConnectionStatus::Disconnected                })    }     fn select_connection(&mut self, agent_id: Option<AgentId>, cx: &mut Context<Self>) {        if self.selected_connection == agent_id {            return;        }         self.selected_connection = agent_id;        self.expanded.clear();        cx.notify();    }     fn restart_selected_connection(&mut self, cx: &mut Context<Self>) {        let Some(agent_id) = self.selected_connection.clone() else {            return;        };        let Some(workspace) = self.workspace.upgrade() else {            return;        };         workspace.update(cx, |workspace, cx| {            let Some(panel) = workspace.panel::<AgentPanel>(cx) else {                return;            };             let fs = workspace.app_state().fs.clone();            let (thread_store, connection_store) = {                let panel = panel.read(cx);                (                    panel.thread_store().clone(),                    panel.connection_store().clone(),                )            };            let agent = Agent::from(agent_id);            let server = agent.server(fs, thread_store);            connection_store.update(cx, |store, cx| {                store.restart_connection(agent, server, cx);            });        });    }     fn selected_connection_status(&self, cx: &App) -> Option<AgentConnectionStatus> {        let agent = Agent::from(self.selected_connection.clone()?);        Some(            self.connection_store                .as_ref()?                .read(cx)                .connection_status(&agent, cx),        )    }     fn selected_watched_connection(&self) -> Option<&WatchedConnection> {        let selected_connection = self.selected_connection.as_ref()?;        self.watched_connections.get(selected_connection)    }     fn selected_watched_connection_mut(&mut self) -> Option<&mut WatchedConnection> {        let selected_connection = self.selected_connection.clone()?;        self.watched_connections.get_mut(&selected_connection)    }     fn connection_menu_entries(&self) -> Vec<SharedString> {        let mut entries: Vec<_> = self            .watched_connections            .values()            .map(|connection| connection.agent_id.0.clone())            .collect();        entries.sort();        entries    }     fn selected_connection_label(&self) -> SharedString {        self.selected_connection            .as_ref()            .map(|agent_id| agent_id.0.clone())            .unwrap_or_else(|| SharedString::from("No connection selected"))    }     fn connection_menu(&self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {        let entries = self.connection_menu_entries();        let selected_connection = self.selected_connection.clone();        let acp_tools = cx.entity().downgrade();         ContextMenu::build(window, cx, move |mut menu, _window, _cx| {            if entries.is_empty() {                return menu.entry("No active connections", None, |_, _| {});            }             for entry in &entries {                let label = entry.clone();                let is_selected = selected_connection                    .as_ref()                    .is_some_and(|agent_id| agent_id.0.as_ref() == label.as_ref());                let acp_tools = acp_tools.clone();                menu = menu.toggleable_entry(                    label.clone(),                    is_selected,                    IconPosition::Start,                    None,                    move |_window, cx| {                        acp_tools                            .update(cx, |this, cx| {                                this.select_connection(Some(AgentId(label.clone())), cx);                            })                            .ok();                    },                );            }             menu        })    }     fn push_stream_message(        &mut self,        agent_id: &AgentId,        stream_message: AcpDebugMessage,        cx: &mut Context<Self>,    ) {        let Some(connection) = self.watched_connections.get_mut(agent_id) else {            return;        };        push_stream_message_for_connection(connection, &self.project, stream_message, cx);        cx.notify();    }     fn serialize_observed_messages(&self) -> Option<String> {        let connection = self.selected_watched_connection()?;         let messages: Vec<serde_json::Value> = connection            .messages            .iter()            .filter_map(|message| {                let params = match &message.params {                    Ok(Some(params)) => params.clone(),                    Ok(None) => serde_json::Value::Null,                    Err(err) => serde_json::to_value(err).ok()?,                };                Some(serde_json::json!({                    "_direction": match message.direction {                        AcpDebugMessageDirection::Incoming => "incoming",                        AcpDebugMessageDirection::Outgoing => "outgoing",                        AcpDebugMessageDirection::Stderr => "stderr",                    },                    "_type": message.message_type.to_string().to_lowercase(),                    "id": message.request_id,                    "method": message.name.to_string(),                    "params": params,                }))            })            .collect();         serde_json::to_string_pretty(&messages).ok()    }     fn clear_messages(&mut self, cx: &mut Context<Self>) {        if let Some(connection) = self.selected_watched_connection_mut() {            connection.messages.clear();            connection.list_state.reset(0);            connection.incoming_request_methods.clear();            connection.outgoing_request_methods.clear();            self.expanded.clear();            cx.notify();        }    }     fn render_message(        &mut self,        index: usize,        window: &mut Window,        cx: &mut Context<Self>,    ) -> AnyElement {        let Some(connection) = self.selected_watched_connection() else {            return Empty.into_any();        };         let Some(message) = connection.messages.get(index) else {            return Empty.into_any();        };         let base_size = TextSize::Editor.rems(cx);         let theme_settings = ThemeSettings::get_global(cx);        let text_style = window.text_style();         let colors = cx.theme().colors();        let expanded = self.expanded.contains(&index);         v_flex()            .id(index)            .group("message")            .font_buffer(cx)            .w_full()            .py_3()            .pl_4()            .pr_5()            .gap_2()            .items_start()            .text_size(base_size)            .border_color(colors.border)            .border_b_1()            .hover(|this| this.bg(colors.element_background.opacity(0.5)))            .child(                h_flex()                    .id(("acp-log-message-header", index))                    .w_full()                    .gap_2()                    .flex_shrink_0()                    .cursor_pointer()                    .on_click(cx.listener(move |this, _, _, cx| {                        if this.expanded.contains(&index) {                            this.expanded.remove(&index);                        } else {                            this.expanded.insert(index);                            let project = this.project.clone();                            let Some(connection) = this.selected_watched_connection_mut() else {                                return;                            };                            let Some(message) = connection.messages.get_mut(index) else {                                return;                            };                            message.expanded(project.read(cx).languages().clone(), cx);                            connection.list_state.scroll_to_reveal_item(index);                        }                        cx.notify()                    }))                    .child(match message.direction {                        AcpDebugMessageDirection::Incoming => Icon::new(IconName::ArrowDown)                            .color(Color::Error)                            .size(IconSize::Small),                        AcpDebugMessageDirection::Outgoing => Icon::new(IconName::ArrowUp)                            .color(Color::Success)                            .size(IconSize::Small),                        AcpDebugMessageDirection::Stderr => Icon::new(IconName::Warning)                            .color(Color::Warning)                            .size(IconSize::Small),                    })                    .child(                        Label::new(message.name.clone())                            .buffer_font(cx)                            .color(Color::Muted),                    )                    .child(div().flex_1())                    .child(                        div()                            .child(ui::Chip::new(message.message_type.to_string()))                            .visible_on_hover("message"),                    )                    .children(                        message                            .request_id                            .as_ref()                            .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),                    ),            )            // I'm aware using markdown is a hack. Trying to get something working for the demo.            // Will clean up soon!            .when_some(                if expanded {                    message.expanded_params_md.clone()                } else {                    message.collapsed_params_md.clone()                },                |this, params| {                    this.child(                        div().pl_6().w_full().child(                            MarkdownElement::new(                                params,                                MarkdownStyle {                                    base_text_style: text_style,                                    selection_background_color: colors.element_selection_background,                                    syntax: cx.theme().syntax().clone(),                                    code_block_overflow_x_scroll: true,                                    code_block: StyleRefinement {                                        text: TextStyleRefinement {                                            font_family: Some(                                                theme_settings.buffer_font.family.clone(),                                            ),                                            font_size: Some((base_size * 0.8).into()),                                            ..Default::default()                                        },                                        ..Default::default()                                    },

Lines 1–500 of 842.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t