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. Load more
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) {