8097 lines · 323.1 KB · Rust
RawDownload
use acp_thread::ContentBlock;use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody};use editor::actions::OpenExcerpts; use crate::StartThreadIn;use gpui::{Corner, List};use language_model::{LanguageModelEffortLevel, Speed};use settings::update_settings_file;use ui::{ButtonLike, SplitButton, SplitButtonStyle, Tab};use workspace::SERIALIZATION_THROTTLE_TIME; use super::*; #[derive(Default)]struct ThreadFeedbackState {    feedback: Option<ThreadFeedback>,    comments_editor: Option<Entity<Editor>>,} impl ThreadFeedbackState {    pub fn submit(        &mut self,        thread: Entity<AcpThread>,        feedback: ThreadFeedback,        window: &mut Window,        cx: &mut App,    ) {        let Some(telemetry) = thread.read(cx).connection().telemetry() else {            return;        };         let project = thread.read(cx).project().read(cx);        let client = project.client();        let user_store = project.user_store();        let organization = user_store.read(cx).current_organization();         if self.feedback == Some(feedback) {            return;        }         self.feedback = Some(feedback);        match feedback {            ThreadFeedback::Positive => {                self.comments_editor = None;            }            ThreadFeedback::Negative => {                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));            }        }        let session_id = thread.read(cx).session_id().clone();        let parent_session_id = thread.read(cx).parent_session_id().cloned();        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();        let task = telemetry.thread_data(&session_id, cx);        let rating = match feedback {            ThreadFeedback::Positive => "positive",            ThreadFeedback::Negative => "negative",        };        cx.background_spawn(async move {            let thread = task.await?;             client                .cloud_client()                .submit_agent_feedback(SubmitAgentThreadFeedbackBody {                    organization_id: organization.map(|organization| organization.id.clone()),                    agent: agent_telemetry_id.to_string(),                    session_id: session_id.to_string(),                    parent_session_id: parent_session_id.map(|id| id.to_string()),                    rating: rating.to_string(),                    thread,                })                .await?;             anyhow::Ok(())        })        .detach_and_log_err(cx);    }     pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {        let Some(telemetry) = thread.read(cx).connection().telemetry() else {            return;        };         let Some(comments) = self            .comments_editor            .as_ref()            .map(|editor| editor.read(cx).text(cx))            .filter(|text| !text.trim().is_empty())        else {            return;        };         self.comments_editor.take();         let project = thread.read(cx).project().read(cx);        let client = project.client();        let user_store = project.user_store();        let organization = user_store.read(cx).current_organization();         let session_id = thread.read(cx).session_id().clone();        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();        let task = telemetry.thread_data(&session_id, cx);        cx.background_spawn(async move {            let thread = task.await?;             client                .cloud_client()                .submit_agent_feedback_comments(SubmitAgentThreadFeedbackCommentsBody {                    organization_id: organization.map(|organization| organization.id.clone()),                    agent: agent_telemetry_id.to_string(),                    session_id: session_id.to_string(),                    comments,                    thread,                })                .await?;             anyhow::Ok(())        })        .detach_and_log_err(cx);    }     pub fn clear(&mut self) {        *self = Self::default()    }     pub fn dismiss_comments(&mut self) {        self.comments_editor.take();    }     fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {        let buffer = cx.new(|cx| {            let empty_string = String::new();            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)        });         let editor = cx.new(|cx| {            let mut editor = Editor::new(                editor::EditorMode::AutoHeight {                    min_lines: 1,                    max_lines: Some(4),                },                buffer,                None,                window,                cx,            );            editor.set_placeholder_text(                "What went wrong? Share your feedback so we can improve.",                window,                cx,            );            editor        });         editor.read(cx).focus_handle(cx).focus(window, cx);        editor    }} pub enum AcpThreadViewEvent {    FirstSendRequested { content: Vec<acp::ContentBlock> },} impl EventEmitter<AcpThreadViewEvent> for ThreadView {} pub struct ThreadView {    pub id: acp::SessionId,    pub parent_id: Option<acp::SessionId>,    pub thread: Entity<AcpThread>,    pub(crate) conversation: Entity<super::Conversation>,    pub server_view: WeakEntity<ConversationView>,    pub agent_icon: IconName,    pub agent_icon_from_external_svg: Option<SharedString>,    pub agent_id: AgentId,    pub focus_handle: FocusHandle,    pub workspace: WeakEntity<Workspace>,    pub entry_view_state: Entity<EntryViewState>,    pub title_editor: Entity<Editor>,    pub config_options_view: Option<Entity<ConfigOptionsView>>,    pub mode_selector: Option<Entity<ModeSelector>>,    pub model_selector: Option<Entity<ModelSelectorPopover>>,    pub profile_selector: Option<Entity<ProfileSelector>>,    pub permission_dropdown_handle: PopoverMenuHandle<ContextMenu>,    pub thread_retry_status: Option<RetryStatus>,    pub(super) thread_error: Option<ThreadError>,    pub thread_error_markdown: Option<Entity<Markdown>>,    pub token_limit_callout_dismissed: bool,    pub last_token_limit_telemetry: Option<acp_thread::TokenUsageRatio>,    thread_feedback: ThreadFeedbackState,    pub list_state: ListState,    pub prompt_capabilities: Rc<RefCell<PromptCapabilities>>,    pub available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,    /// Tracks which tool calls have their content/output expanded.    /// Used for showing/hiding tool call results, terminal output, etc.    pub expanded_tool_calls: HashSet<agent_client_protocol::ToolCallId>,    pub expanded_tool_call_raw_inputs: HashSet<agent_client_protocol::ToolCallId>,    pub expanded_thinking_blocks: HashSet<(usize, usize)>,    auto_expanded_thinking_block: Option<(usize, usize)>,    pub subagent_scroll_handles: RefCell<HashMap<agent_client_protocol::SessionId, ScrollHandle>>,    pub edits_expanded: bool,    pub plan_expanded: bool,    pub queue_expanded: bool,    pub editor_expanded: bool,    pub should_be_following: bool,    pub editing_message: Option<usize>,    pub local_queued_messages: Vec<QueuedMessage>,    pub queued_message_editors: Vec<Entity<MessageEditor>>,    pub queued_message_editor_subscriptions: Vec<Subscription>,    pub last_synced_queue_length: usize,    pub turn_fields: TurnFields,    pub discarded_partial_edits: HashSet<agent_client_protocol::ToolCallId>,    pub is_loading_contents: bool,    pub new_server_version_available: Option<SharedString>,    pub resumed_without_history: bool,    pub _cancel_task: Option<Task<()>>,    _save_task: Option<Task<()>>,    _draft_resolve_task: Option<Task<()>>,    pub skip_queue_processing_count: usize,    pub user_interrupted_generation: bool,    pub can_fast_track_queue: bool,    pub hovered_edited_file_buttons: Option<usize>,    pub in_flight_prompt: Option<Vec<acp::ContentBlock>>,    pub _subscriptions: Vec<Subscription>,    pub message_editor: Entity<MessageEditor>,    pub add_context_menu_handle: PopoverMenuHandle<ContextMenu>,    pub thinking_effort_menu_handle: PopoverMenuHandle<ContextMenu>,    pub project: WeakEntity<Project>,    pub recent_history_entries: Vec<AgentSessionInfo>,    pub hovered_recent_history_item: Option<usize>,    pub show_external_source_prompt_warning: bool,    pub show_codex_windows_warning: bool,    pub history: Entity<ThreadHistory>,    pub _history_subscription: Subscription,}impl Focusable for ThreadView {    fn focus_handle(&self, cx: &App) -> FocusHandle {        if self.parent_id.is_some() {            self.focus_handle.clone()        } else {            self.active_editor(cx).focus_handle(cx)        }    }} #[derive(Default)]pub struct TurnFields {    pub _turn_timer_task: Option<Task<()>>,    pub last_turn_duration: Option<Duration>,    pub last_turn_tokens: Option<u64>,    pub turn_generation: usize,    pub turn_started_at: Option<Instant>,    pub turn_tokens: Option<u64>,} impl ThreadView {    pub(crate) fn new(        parent_id: Option<acp::SessionId>,        thread: Entity<AcpThread>,        conversation: Entity<super::Conversation>,        server_view: WeakEntity<ConversationView>,        agent_icon: IconName,        agent_icon_from_external_svg: Option<SharedString>,        agent_id: AgentId,        agent_display_name: SharedString,        workspace: WeakEntity<Workspace>,        entry_view_state: Entity<EntryViewState>,        config_options_view: Option<Entity<ConfigOptionsView>>,        mode_selector: Option<Entity<ModeSelector>>,        model_selector: Option<Entity<ModelSelectorPopover>>,        profile_selector: Option<Entity<ProfileSelector>>,        list_state: ListState,        prompt_capabilities: Rc<RefCell<PromptCapabilities>>,        available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,        resumed_without_history: bool,        project: WeakEntity<Project>,        thread_store: Option<Entity<ThreadStore>>,        history: Entity<ThreadHistory>,        prompt_store: Option<Entity<PromptStore>>,        initial_content: Option<AgentInitialContent>,        mut subscriptions: Vec<Subscription>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let id = thread.read(cx).session_id().clone();         let placeholder = placeholder_text(agent_display_name.as_ref(), false);         let history_subscription = cx.observe(&history, |this, history, cx| {            this.update_recent_history_from_cache(&history, cx);        });         let mut should_auto_submit = false;        let mut show_external_source_prompt_warning = false;         let message_editor = cx.new(|cx| {            let mut editor = MessageEditor::new(                workspace.clone(),                project.clone(),                thread_store,                history.downgrade(),                prompt_store,                prompt_capabilities.clone(),                available_commands.clone(),                agent_id.clone(),                &placeholder,                editor::EditorMode::AutoHeight {                    min_lines: AgentSettings::get_global(cx).message_editor_min_lines,                    max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),                },                window,                cx,            );            if let Some(content) = initial_content {                match content {                    AgentInitialContent::ThreadSummary { session_id, title } => {                        editor.insert_thread_summary(session_id, title, window, cx);                    }                    AgentInitialContent::ContentBlock {                        blocks,                        auto_submit,                    } => {                        should_auto_submit = auto_submit;                        editor.set_message(blocks, window, cx);                    }                    AgentInitialContent::FromExternalSource(prompt) => {                        show_external_source_prompt_warning = true;                        // SECURITY: Be explicit about not auto submitting prompt from external source.                        should_auto_submit = false;                        editor.set_message(                            vec![acp::ContentBlock::Text(acp::TextContent::new(                                prompt.into_string(),                            ))],                            window,                            cx,                        );                    }                }            } else if let Some(draft) = thread.read(cx).draft_prompt() {                editor.set_message(draft.to_vec(), window, cx);            }            editor        });         let show_codex_windows_warning = cfg!(windows)            && project.upgrade().is_some_and(|p| p.read(cx).is_local())            && agent_id.as_ref() == "Codex";         let title_editor = {            let can_edit = thread.update(cx, |thread, cx| thread.can_set_title(cx));            let editor = cx.new(|cx| {                let mut editor = Editor::single_line(window, cx);                editor.set_text(thread.read(cx).title(), window, cx);                editor.set_read_only(!can_edit);                editor            });            subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event));            editor        };         subscriptions.push(cx.subscribe_in(            &entry_view_state,            window,            Self::handle_entry_view_event,        ));         subscriptions.push(cx.subscribe_in(            &message_editor,            window,            Self::handle_message_editor_event,        ));         subscriptions.push(cx.observe(&message_editor, |this, editor, cx| {            let is_empty = editor.read(cx).text(cx).is_empty();            let draft_contents_task = if is_empty {                None            } else {                Some(editor.update(cx, |editor, cx| editor.draft_contents(cx)))            };            this._draft_resolve_task = Some(cx.spawn(async move |this, cx| {                let draft = if let Some(task) = draft_contents_task {                    let blocks = task.await.ok().filter(|b| !b.is_empty());                    blocks                } else {                    None                };                this.update(cx, |this, cx| {                    this.thread.update(cx, |thread, _cx| {                        thread.set_draft_prompt(draft);                    });                    this.schedule_save(cx);                })                .ok();            }));        }));         let recent_history_entries = history.read(cx).get_recent_sessions(3);         let mut this = Self {            id,            parent_id,            focus_handle: cx.focus_handle(),            thread,            conversation,            server_view,            agent_icon,            agent_icon_from_external_svg,            agent_id,            workspace,            entry_view_state,            title_editor,            config_options_view,            mode_selector,            model_selector,            profile_selector,            list_state,            prompt_capabilities,            available_commands,            resumed_without_history,            _subscriptions: subscriptions,            permission_dropdown_handle: PopoverMenuHandle::default(),            thread_retry_status: None,            thread_error: None,            thread_error_markdown: None,            token_limit_callout_dismissed: false,            last_token_limit_telemetry: None,            thread_feedback: Default::default(),            expanded_tool_calls: HashSet::default(),            expanded_tool_call_raw_inputs: HashSet::default(),            expanded_thinking_blocks: HashSet::default(),            auto_expanded_thinking_block: None,            subagent_scroll_handles: RefCell::new(HashMap::default()),            edits_expanded: false,            plan_expanded: false,            queue_expanded: true,            editor_expanded: false,            should_be_following: false,            editing_message: None,            local_queued_messages: Vec::new(),            queued_message_editors: Vec::new(),            queued_message_editor_subscriptions: Vec::new(),            last_synced_queue_length: 0,            turn_fields: TurnFields::default(),            discarded_partial_edits: HashSet::default(),            is_loading_contents: false,            new_server_version_available: None,            _cancel_task: None,            _save_task: None,            _draft_resolve_task: None,            skip_queue_processing_count: 0,            user_interrupted_generation: false,            can_fast_track_queue: false,            hovered_edited_file_buttons: None,            in_flight_prompt: None,            message_editor,            add_context_menu_handle: PopoverMenuHandle::default(),            thinking_effort_menu_handle: PopoverMenuHandle::default(),            project,            recent_history_entries,            hovered_recent_history_item: None,            show_external_source_prompt_warning,            history,            _history_subscription: history_subscription,            show_codex_windows_warning,        };        let list_state_for_scroll = this.list_state.clone();        let thread_view = cx.entity().downgrade();        this.list_state            .set_scroll_handler(move |_event, _window, cx| {                let list_state = list_state_for_scroll.clone();                let thread_view = thread_view.clone();                // N.B. We must defer because the scroll handler is called while the                // ListState's RefCell is mutably borrowed. Reading logical_scroll_top()                // directly would panic from a double borrow.                cx.defer(move |cx| {                    let scroll_top = list_state.logical_scroll_top();                    let _ = thread_view.update(cx, |this, cx| {                        if let Some(thread) = this.as_native_thread(cx) {                            thread.update(cx, |thread, _cx| {                                thread.set_ui_scroll_position(Some(scroll_top));                            });                        }                        this.schedule_save(cx);                    });                });            });         if should_auto_submit {            this.send(window, cx);        }        this    }     /// Schedule a throttled save of the thread state (draft prompt, scroll position, etc.).    /// Multiple calls within `SERIALIZATION_THROTTLE_TIME` are coalesced into a single save.    fn schedule_save(&mut self, cx: &mut Context<Self>) {        self._save_task = Some(cx.spawn(async move |this, cx| {            cx.background_executor()                .timer(SERIALIZATION_THROTTLE_TIME)                .await;            this.update(cx, |this, cx| {                if let Some(thread) = this.as_native_thread(cx) {

Lines 1–500 of 8097.