937 lines · 34.7 KB · Rust
RawDownload
use std::{ops::Range, sync::Arc}; use acp_thread::{AcpThread, AgentThreadEntry, AssistantMessageChunk};use agent::ThreadStore;use agent_client_protocol::schema::v1 as acp;use agent_settings::AgentSettings;use collections::{HashMap, HashSet};use editor::{    Editor, EditorEvent, EditorMode, MinimapVisibility, RestoreOnlyUnstagedDiffHunkDelegate,    SizingBehavior,};use gpui::{    AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,    ScrollHandle, TextStyleRefinement, WeakEntity, Window,};use language::language_settings::SoftWrap;use project::{AgentId, Project, project_settings::DiagnosticSeverity};use rope::Point;use settings::{Settings as _, ThinkingBlockDisplay};use terminal_view::TerminalView;use theme_settings::ThemeSettings;use ui::{Context, TextSize};use workspace::Workspace; use crate::message_editor::{MessageEditor, MessageEditorEvent, SharedSessionCapabilities}; /// Maps an entry index through the removal of `removed` (a contiguous range of/// entries), returning `None` if the index referred to a removed entry.fn reindex_after_removal(index: usize, removed: &Range<usize>) -> Option<usize> {    if index < removed.start {        Some(index)    } else if index < removed.end {        None    } else {        Some(index - removed.len())    }} pub struct EntryViewState {    workspace: WeakEntity<Workspace>,    project: WeakEntity<Project>,    thread_store: Option<Entity<ThreadStore>>,    entries: Vec<Entry>,    session_capabilities: SharedSessionCapabilities,    agent_id: AgentId,    expanded_thinking_blocks: HashSet<(usize, usize)>,    auto_expanded_thinking_block: Option<(usize, usize)>,    user_toggled_thinking_blocks: HashSet<(usize, usize)>,    expanded_compactions: HashSet<usize>,    expanded_tool_calls: HashSet<acp::ToolCallId>,} impl EntryViewState {    pub fn new(        workspace: WeakEntity<Workspace>,        project: WeakEntity<Project>,        thread_store: Option<Entity<ThreadStore>>,        session_capabilities: SharedSessionCapabilities,        agent_id: AgentId,    ) -> Self {        Self {            workspace,            project,            thread_store,            entries: Vec::new(),            session_capabilities,            agent_id,            expanded_thinking_blocks: HashSet::default(),            auto_expanded_thinking_block: None,            user_toggled_thinking_blocks: HashSet::default(),            expanded_compactions: HashSet::default(),            expanded_tool_calls: HashSet::default(),        }    }     pub(crate) fn is_tool_call_expanded(&self, tool_call_id: &acp::ToolCallId) -> bool {        self.expanded_tool_calls.contains(tool_call_id)    }     pub(crate) fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId) {        self.expanded_tool_calls.insert(tool_call_id);    }     pub(crate) fn collapse_tool_call(&mut self, tool_call_id: &acp::ToolCallId) {        self.expanded_tool_calls.remove(tool_call_id);    }     pub(crate) fn toggle_tool_call_expansion(&mut self, tool_call_id: &acp::ToolCallId) {        if !self.expanded_tool_calls.remove(tool_call_id) {            self.expanded_tool_calls.insert(tool_call_id.clone());        }    }     pub(crate) fn is_compaction_expanded(&self, entry_ix: usize) -> bool {        self.expanded_compactions.contains(&entry_ix)    }     pub(crate) fn collapse_compaction(&mut self, entry_ix: usize) {        self.expanded_compactions.remove(&entry_ix);    }     pub(crate) fn toggle_compaction_expansion(&mut self, entry_ix: usize) {        if !self.expanded_compactions.remove(&entry_ix) {            self.expanded_compactions.insert(entry_ix);        }    }     pub(crate) fn clear_auto_expand_tracking(&mut self) {        self.auto_expanded_thinking_block = None;    }     pub(crate) fn is_auto_expanded_thinking_block(&self, key: (usize, usize)) -> bool {        self.auto_expanded_thinking_block == Some(key)    }     pub(crate) fn auto_expand_streaming_thought(&mut self, thread: &AcpThread, cx: &App) -> bool {        let thinking_display = AgentSettings::get_global(cx).thinking_display;         if !matches!(            thinking_display,            ThinkingBlockDisplay::Auto | ThinkingBlockDisplay::Preview        ) {            return false;        }         let last_ix = thread.entries().len().saturating_sub(1);        let key = match thread.entries().get(last_ix) {            Some(AgentThreadEntry::AssistantMessage(message)) => match message.chunks.last() {                Some(AssistantMessageChunk::Thought { .. }) => {                    Some((last_ix, message.chunks.len() - 1))                }                _ => None,            },            _ => None,        };         if let Some(key) = key {            if self.auto_expanded_thinking_block != Some(key) {                self.auto_expanded_thinking_block = Some(key);                self.expanded_thinking_blocks.insert(key);                return true;            }        } else if self.auto_expanded_thinking_block.is_some() {            if thinking_display == ThinkingBlockDisplay::Auto                && let Some(key) = self.auto_expanded_thinking_block                && !self.user_toggled_thinking_blocks.contains(&key)            {                self.expanded_thinking_blocks.remove(&key);            }            self.auto_expanded_thinking_block = None;            return true;        }         false    }     pub(crate) fn toggle_thinking_block_expansion(&mut self, key: (usize, usize), cx: &App) {        match AgentSettings::get_global(cx).thinking_display {            ThinkingBlockDisplay::Auto => {                let is_open = self.expanded_thinking_blocks.contains(&key)                    || self.user_toggled_thinking_blocks.contains(&key);                 if is_open {                    self.expanded_thinking_blocks.remove(&key);                    self.user_toggled_thinking_blocks.remove(&key);                } else {                    self.expanded_thinking_blocks.insert(key);                    self.user_toggled_thinking_blocks.insert(key);                }            }            ThinkingBlockDisplay::Preview => {                let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key);                let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);                 if is_user_expanded {                    self.user_toggled_thinking_blocks.remove(&key);                    self.expanded_thinking_blocks.remove(&key);                } else if is_in_expanded_set {                    self.user_toggled_thinking_blocks.insert(key);                } else {                    self.expanded_thinking_blocks.insert(key);                    self.user_toggled_thinking_blocks.insert(key);                }            }            ThinkingBlockDisplay::AlwaysExpanded => {                if self.user_toggled_thinking_blocks.contains(&key) {                    self.user_toggled_thinking_blocks.remove(&key);                } else {                    self.user_toggled_thinking_blocks.insert(key);                }            }            ThinkingBlockDisplay::AlwaysCollapsed => {                if self.user_toggled_thinking_blocks.contains(&key) {                    self.user_toggled_thinking_blocks.remove(&key);                    self.expanded_thinking_blocks.remove(&key);                } else {                    self.expanded_thinking_blocks.insert(key);                    self.user_toggled_thinking_blocks.insert(key);                }            }        }    }     pub(crate) fn thinking_block_state(&self, key: (usize, usize), cx: &App) -> (bool, bool) {        let is_user_toggled = self.user_toggled_thinking_blocks.contains(&key);        let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);         match AgentSettings::get_global(cx).thinking_display {            ThinkingBlockDisplay::Auto => {                let is_open = is_user_toggled || is_in_expanded_set;                (is_open, false)            }            ThinkingBlockDisplay::Preview => {                let is_open = is_user_toggled || is_in_expanded_set;                let is_constrained = is_in_expanded_set && !is_user_toggled;                (is_open, is_constrained)            }            ThinkingBlockDisplay::AlwaysExpanded => (!is_user_toggled, false),            ThinkingBlockDisplay::AlwaysCollapsed => (is_user_toggled, false),        }    }     pub fn entry(&self, index: usize) -> Option<&Entry> {        self.entries.get(index)    }     pub fn sync_entry(        &mut self,        index: usize,        thread: &Entity<AcpThread>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(thread_entry) = thread.read(cx).entries().get(index) else {            return;        };         match thread_entry {            AgentThreadEntry::UserMessage(message) => {                let can_rewind = thread.read(cx).supports_truncate(cx);                let has_client_id = message.client_id.is_some();                let is_subagent = thread.read(cx).parent_session_id().is_some();                let chunks = message.chunks.clone();                if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) {                    if !editor.focus_handle(cx).is_focused(window) {                        // Only update if we are not editing.                        // If we are, cancelling the edit will set the message to the newest content.                        editor.update(cx, |editor, cx| {                            editor.set_message(chunks, window, cx);                        });                    }                } else {                    let message_editor = cx.new(|cx| {                        let mut editor = MessageEditor::new(                            self.workspace.clone(),                            self.project.clone(),                            self.thread_store.clone(),                            self.session_capabilities.clone(),                            self.agent_id.clone(),                            "Edit message - @ to include context",                            editor::EditorMode::AutoHeight {                                min_lines: 1,                                max_lines: None,                            },                            window,                            cx,                        );                        if !can_rewind || !has_client_id || is_subagent {                            editor.set_read_only(true, cx);                        }                        editor.set_message(chunks, window, cx);                        editor                    });                    cx.subscribe(&message_editor, move |_, editor, event, cx| {                        cx.emit(EntryViewEvent {                            entry_index: index,                            view_event: ViewEvent::MessageEditorEvent(editor, event.clone()),                        })                    })                    .detach();                    self.set_entry(index, Entry::UserMessage(message_editor));                }            }            AgentThreadEntry::ToolCall(tool_call) => {                let id = tool_call.id.clone();                let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();                let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();                 let views = if let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) {                    &mut tool_call.content                } else {                    self.set_entry(                        index,                        Entry::ToolCall(ToolCallEntry {                            content: HashMap::default(),                            focus_handle: cx.focus_handle(),                        }),                    );                    let Some(Entry::ToolCall(tool_call)) = self.entries.get_mut(index) else {                        unreachable!()                    };                    &mut tool_call.content                };                 let is_tool_call_completed =                    matches!(tool_call.status, acp_thread::ToolCallStatus::Completed);                 for terminal in terminals {                    match views.entry(terminal.entity_id()) {                        collections::hash_map::Entry::Vacant(entry) => {                            let element = create_terminal(                                self.workspace.clone(),                                self.project.clone(),                                terminal.clone(),                                window,                                cx,                            )                            .into_any();                            cx.emit(EntryViewEvent {                                entry_index: index,                                view_event: ViewEvent::NewTerminal(id.clone()),                            });                            entry.insert(element);                        }                        collections::hash_map::Entry::Occupied(_entry) => {                            if is_tool_call_completed && terminal.read(cx).output().is_none() {                                cx.emit(EntryViewEvent {                                    entry_index: index,                                    view_event: ViewEvent::TerminalMovedToBackground(id.clone()),                                });                            }                        }                    }                }                 for diff in diffs {                    views.entry(diff.entity_id()).or_insert_with(|| {                        let editor = create_editor_diff(diff.clone(), window, cx);                        cx.subscribe(&editor, {                            let diff = diff.clone();                            let entry_index = index;                            move |_this, _editor, event: &EditorEvent, cx| {                                if let EditorEvent::OpenExcerptsRequested {                                    selections_by_buffer,                                    split,                                } = event                                {                                    let multibuffer = diff.read(cx).multibuffer();                                    if let Some((buffer_id, (ranges, _))) =                                        selections_by_buffer.iter().next()                                    {                                        if let Some(buffer) =                                            multibuffer.read(cx).buffer(*buffer_id)                                        {                                            if let Some(range) = ranges.first() {                                                let point =                                                    buffer.read(cx).offset_to_point(range.start.0);                                                if let Some(path) = diff.read(cx).file_path(cx) {                                                    cx.emit(EntryViewEvent {                                                        entry_index,                                                        view_event: ViewEvent::OpenDiffLocation {                                                            path,                                                            position: point,                                                            split: *split,                                                        },                                                    });                                                }                                            }                                        }                                    }                                }                            }                        })                        .detach();                        cx.emit(EntryViewEvent {                            entry_index: index,                            view_event: ViewEvent::NewDiff(id.clone()),                        });                        editor.into_any()                    });                }            }            AgentThreadEntry::Elicitation(_) => {                if !matches!(self.entries.get(index), Some(Entry::Elicitation { .. })) {                    self.set_entry(                        index,                        Entry::Elicitation {                            focus_handle: cx.focus_handle(),                        },                    );                }            }            AgentThreadEntry::AssistantMessage(message) => {                let entry = if let Some(Entry::AssistantMessage(entry)) =                    self.entries.get_mut(index)                {                    entry                } else {                    self.set_entry(                        index,                        Entry::AssistantMessage(AssistantMessageEntry {                            scroll_handles_by_chunk_index: HashMap::default(),                            focus_handle: cx.focus_handle(),                        }),                    );                    let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {                        unreachable!()                    };                    entry                };                entry.sync(message);            }            AgentThreadEntry::CompletedPlan(_) => {                if !matches!(self.entries.get(index), Some(Entry::CompletedPlan)) {                    self.set_entry(index, Entry::CompletedPlan);                }            }            AgentThreadEntry::ContextCompaction(_) => {                if !matches!(self.entries.get(index), Some(Entry::ContextCompaction)) {                    self.set_entry(index, Entry::ContextCompaction);                }            }        };    }     fn set_entry(&mut self, index: usize, entry: Entry) {        if index == self.entries.len() {            self.entries.push(entry);        } else {            self.entries[index] = entry;        }    }     pub fn remove(&mut self, range: Range<usize>) {        self.entries.drain(range.clone());         self.expanded_compactions = self            .expanded_compactions            .iter()            .filter_map(|&entry_ix| reindex_after_removal(entry_ix, &range))            .collect();        self.expanded_thinking_blocks = self            .expanded_thinking_blocks            .iter()            .filter_map(|&(entry_ix, chunk_ix)| {                reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix))            })            .collect();        self.user_toggled_thinking_blocks = self            .user_toggled_thinking_blocks            .iter()            .filter_map(|&(entry_ix, chunk_ix)| {                reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix))            })            .collect();        self.auto_expanded_thinking_block =            self.auto_expanded_thinking_block                .and_then(|(entry_ix, chunk_ix)| {                    reindex_after_removal(entry_ix, &range).map(|entry_ix| (entry_ix, chunk_ix))                });    }     pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {        for entry in self.entries.iter() {            match entry {                Entry::UserMessage { .. }                | Entry::AssistantMessage { .. }                | Entry::Elicitation { .. }                | Entry::CompletedPlan                | Entry::ContextCompaction => {}                Entry::ToolCall(ToolCallEntry { content, .. }) => {                    for view in content.values() {                        if let Ok(diff_editor) = view.clone().downcast::<Editor>() {                            diff_editor.update(cx, |diff_editor, cx| {                                diff_editor.set_text_style_refinement(                                    diff_editor_text_style_refinement(cx),                                );                                cx.notify();                            })                        }                    }                }            }        }    }} impl EventEmitter<EntryViewEvent> for EntryViewState {} pub struct EntryViewEvent {    pub entry_index: usize,    pub view_event: ViewEvent,} pub enum ViewEvent {    NewDiff(acp::ToolCallId),    NewTerminal(acp::ToolCallId),    TerminalMovedToBackground(acp::ToolCallId),    MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),    OpenDiffLocation {

Lines 1–500 of 937.