use std::fmt::Write as _;use std::io::Write;use std::ops::Range;use std::sync::Arc;use std::time::Instant; use anyhow::{Context as _, Result, anyhow};use assistant_settings::AssistantSettings;use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};use chrono::{DateTime, Utc};use collections::{BTreeMap, HashMap};use feature_flags::{self, FeatureFlagAppExt};use futures::future::Shared;use futures::{FutureExt, StreamExt as _};use git::repository::DiffType;use gpui::{ AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,};use language_model::{ ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage,};use project::Project;use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};use prompt_store::PromptBuilder;use proto::Plan;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::Settings;use thiserror::Error;use util::{ResultExt as _, TryFutureExt as _, post_inc};use uuid::Uuid; use crate::context::{AssistantContext, ContextId, format_context_as_string};use crate::thread_store::{ SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext,};use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER}; #[derive(Debug, Clone, Copy)]pub enum RequestKind { Chat, /// Used when summarizing a thread. Summarize,} #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,)]pub struct ThreadId(Arc<str>); impl ThreadId { pub fn new() -> Self { Self(Uuid::new_v4().to_string().into()) }} impl std::fmt::Display for ThreadId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }} impl From<&str> for ThreadId { fn from(value: &str) -> Self { Self(value.into()) }} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]pub struct MessageId(pub(crate) usize); impl MessageId { fn post_inc(&mut self) -> Self { Self(post_inc(&mut self.0)) }} /// A message in a [`Thread`].#[derive(Debug, Clone)]pub struct Message { pub id: MessageId, pub role: Role, pub segments: Vec<MessageSegment>, pub context: String,} impl Message { /// Returns whether the message contains any meaningful text that should be displayed /// The model sometimes runs tool without producing any text or just a marker ([`USING_TOOL_MARKER`]) pub fn should_display_content(&self) -> bool { self.segments.iter().all(|segment| segment.should_display()) } pub fn push_thinking(&mut self, text: &str) { if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() { segment.push_str(text); } else { self.segments .push(MessageSegment::Thinking(text.to_string())); } } pub fn push_text(&mut self, text: &str) { if let Some(MessageSegment::Text(segment)) = self.segments.last_mut() { segment.push_str(text); } else { self.segments.push(MessageSegment::Text(text.to_string())); } } pub fn to_string(&self) -> String { let mut result = String::new(); if !self.context.is_empty() { result.push_str(&self.context); } for segment in &self.segments { match segment { MessageSegment::Text(text) => result.push_str(text), MessageSegment::Thinking(text) => { result.push_str("<think>"); result.push_str(text); result.push_str("</think>"); } } } result }} #[derive(Debug, Clone, PartialEq, Eq)]pub enum MessageSegment { Text(String), Thinking(String),} impl MessageSegment { pub fn text_mut(&mut self) -> &mut String { match self { Self::Text(text) => text, Self::Thinking(text) => text, } } pub fn should_display(&self) -> bool { // We add USING_TOOL_MARKER when making a request that includes tool uses // without non-whitespace text around them, and this can cause the model // to mimic the pattern, so we consider those segments not displayable. match self { Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER, Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER, } }} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct ProjectSnapshot { pub worktree_snapshots: Vec<WorktreeSnapshot>, pub unsaved_buffer_paths: Vec<String>, pub timestamp: DateTime<Utc>,} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct WorktreeSnapshot { pub worktree_path: String, pub git_state: Option<GitState>,} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct GitState { pub remote_url: Option<String>, pub head_sha: Option<String>, pub current_branch: Option<String>, pub diff: Option<String>,} #[derive(Clone)]pub struct ThreadCheckpoint { message_id: MessageId, git_checkpoint: GitStoreCheckpoint,} #[derive(Copy, Clone, Debug, PartialEq, Eq)]pub enum ThreadFeedback { Positive, Negative,} pub enum LastRestoreCheckpoint { Pending { message_id: MessageId, }, Error { message_id: MessageId, error: String, },} impl LastRestoreCheckpoint { pub fn message_id(&self) -> MessageId { match self { LastRestoreCheckpoint::Pending { message_id } => *message_id, LastRestoreCheckpoint::Error { message_id, .. } => *message_id, } }} #[derive(Clone, Debug, Default, Serialize, Deserialize)]pub enum DetailedSummaryState { #[default] NotGenerated, Generating { message_id: MessageId, }, Generated { text: SharedString, message_id: MessageId, },} #[derive(Default)]pub struct TotalTokenUsage { pub total: usize, pub max: usize, pub ratio: TokenUsageRatio,} #[derive(Debug, Default, PartialEq, Eq)]pub enum TokenUsageRatio { #[default] Normal, Warning, Exceeded,} /// A thread of conversation with the LLM.pub struct Thread { id: ThreadId, updated_at: DateTime<Utc>, summary: Option<SharedString>, pending_summary: Task<Option<()>>, detailed_summary_state: DetailedSummaryState, messages: Vec<Message>, next_message_id: MessageId, context: BTreeMap<ContextId, AssistantContext>, context_by_message: HashMap<MessageId, Vec<ContextId>>, project_context: SharedProjectContext, checkpoints_by_message: HashMap<MessageId, ThreadCheckpoint>, completion_count: usize, pending_completions: Vec<PendingCompletion>, project: Entity<Project>, prompt_builder: Arc<PromptBuilder>, tools: Entity<ToolWorkingSet>, tool_use: ToolUseState, action_log: Entity<ActionLog>, last_restore_checkpoint: Option<LastRestoreCheckpoint>, pending_checkpoint: Option<ThreadCheckpoint>, initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>, cumulative_token_usage: TokenUsage, exceeded_window_error: Option<ExceededWindowError>, feedback: Option<ThreadFeedback>, message_feedback: HashMap<MessageId, ThreadFeedback>, last_auto_capture_at: Option<Instant>,} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct ExceededWindowError { /// Model used when last message exceeded context window model_id: LanguageModelId, /// Token count including last message token_count: usize,} impl Thread { pub fn new( project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, system_prompt: SharedProjectContext, cx: &mut Context<Self>, ) -> Self { Self { id: ThreadId::new(), updated_at: Utc::now(), summary: None, pending_summary: Task::ready(None), detailed_summary_state: DetailedSummaryState::NotGenerated, messages: Vec::new(), next_message_id: MessageId(0), context: BTreeMap::default(), context_by_message: HashMap::default(), project_context: system_prompt, checkpoints_by_message: HashMap::default(), completion_count: 0, pending_completions: Vec::new(), project: project.clone(), prompt_builder, tools: tools.clone(), last_restore_checkpoint: None, pending_checkpoint: None, tool_use: ToolUseState::new(tools.clone()), action_log: cx.new(|_| ActionLog::new(project.clone())), initial_project_snapshot: { let project_snapshot = Self::project_snapshot(project, cx); cx.foreground_executor() .spawn(async move { Some(project_snapshot.await) }) .shared() }, cumulative_token_usage: TokenUsage::default(), exceeded_window_error: None, feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, } } pub fn deserialize( id: ThreadId, serialized: SerializedThread, project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, project_context: SharedProjectContext, cx: &mut Context<Self>, ) -> Self { let next_message_id = MessageId( serialized .messages .last() .map(|message| message.id.0 + 1) .unwrap_or(0), ); let tool_use = ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |_| true); Self { id, updated_at: serialized.updated_at, summary: Some(serialized.summary), pending_summary: Task::ready(None), detailed_summary_state: serialized.detailed_summary_state, messages: serialized .messages .into_iter() .map(|message| Message { id: message.id, role: message.role, segments: message .segments .into_iter() .map(|segment| match segment { SerializedMessageSegment::Text { text } => MessageSegment::Text(text), SerializedMessageSegment::Thinking { text } => { MessageSegment::Thinking(text) } }) .collect(), context: message.context, }) .collect(), next_message_id, context: BTreeMap::default(), context_by_message: HashMap::default(), project_context, checkpoints_by_message: HashMap::default(), completion_count: 0, pending_completions: Vec::new(), last_restore_checkpoint: None, pending_checkpoint: None, project: project.clone(), prompt_builder, tools, tool_use, action_log: cx.new(|_| ActionLog::new(project)), initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(), cumulative_token_usage: serialized.cumulative_token_usage, exceeded_window_error: None, feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, } } pub fn id(&self) -> &ThreadId { &self.id } pub fn is_empty(&self) -> bool { self.messages.is_empty() } pub fn updated_at(&self) -> DateTime<Utc> { self.updated_at } pub fn touch_updated_at(&mut self) { self.updated_at = Utc::now(); } pub fn summary(&self) -> Option<SharedString> { self.summary.clone() } pub fn project_context(&self) -> SharedProjectContext { self.project_context.clone() } pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread"); pub fn summary_or_default(&self) -> SharedString { self.summary.clone().unwrap_or(Self::DEFAULT_SUMMARY) } pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) { let Some(current_summary) = &self.summary else { // Don't allow setting summary until generated return; }; let mut new_summary = new_summary.into(); if new_summary.is_empty() { new_summary = Self::DEFAULT_SUMMARY; } if current_summary != &new_summary { self.summary = Some(new_summary); cx.emit(ThreadEvent::SummaryChanged); } } pub fn latest_detailed_summary_or_text(&self) -> SharedString { self.latest_detailed_summary() .unwrap_or_else(|| self.text().into()) } fn latest_detailed_summary(&self) -> Option<SharedString> { if let DetailedSummaryState::Generated { text, .. } = &self.detailed_summary_state { Some(text.clone()) } else { None } } pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } pub fn messages(&self) -> impl Iterator<Item = &Message> { self.messages.iter() } pub fn is_generating(&self) -> bool { !self.pending_completions.is_empty() || !self.all_tools_finished() } pub fn tools(&self) -> &Entity<ToolWorkingSet> { &self.tools } pub fn pending_tool(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> { self.tool_use .pending_tool_uses() .into_iter() .find(|tool_use| &tool_use.id == id) } pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = &PendingToolUse> { self.tool_use .pending_tool_uses() .into_iter() .filter(|tool_use| tool_use.status.needs_confirmation()) } pub fn has_pending_tool_uses(&self) -> bool { !self.tool_use.pending_tool_uses().is_empty() } pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> { self.checkpoints_by_message.get(&id).cloned() } pub fn restore_checkpoint( &mut self, checkpoint: ThreadCheckpoint, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.last_restore_checkpoint = Some(LastRestoreCheckpoint::Pending { message_id: checkpoint.message_id, }); cx.emit(ThreadEvent::CheckpointChanged); cx.notify(); Lines 1–500 of 2424. Load more
use std::fmt::Write as _; use std::io::Write; use std::ops::Range; use std::sync::Arc; use std::time::Instant; use anyhow::{Context as _, Result, anyhow}; use assistant_settings::AssistantSettings; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; use collections::{BTreeMap, HashMap}; use feature_flags::{self, FeatureFlagAppExt}; use futures::future::Shared; use futures::{FutureExt, StreamExt as _}; use git::repository::DiffType; use gpui::{ AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, }; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage, }; use project::Project; use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState}; use prompt_store::PromptBuilder; use proto::Plan; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use thiserror::Error; use util::{ResultExt as _, TryFutureExt as _, post_inc}; use uuid::Uuid; use crate::context::{AssistantContext, ContextId, format_context_as_string}; use crate::thread_store::{ SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext, }; use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER}; #[derive(Debug, Clone, Copy)] pub enum RequestKind { Chat, /// Used when summarizing a thread. Summarize, } #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, )] pub struct ThreadId(Arc<str>); impl ThreadId { pub fn new() -> Self { Self(Uuid::new_v4().to_string().into()) } } impl std::fmt::Display for ThreadId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl From<&str> for ThreadId { fn from(value: &str) -> Self { Self(value.into()) } } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] pub struct MessageId(pub(crate) usize); impl MessageId { fn post_inc(&mut self) -> Self { Self(post_inc(&mut self.0)) } } /// A message in a [`Thread`]. #[derive(Debug, Clone)] pub struct Message { pub id: MessageId, pub role: Role, pub segments: Vec<MessageSegment>, pub context: String, } impl Message { /// Returns whether the message contains any meaningful text that should be displayed /// The model sometimes runs tool without producing any text or just a marker ([`USING_TOOL_MARKER`]) pub fn should_display_content(&self) -> bool { self.segments.iter().all(|segment| segment.should_display()) } pub fn push_thinking(&mut self, text: &str) { if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() { segment.push_str(text); } else { self.segments .push(MessageSegment::Thinking(text.to_string())); } } pub fn push_text(&mut self, text: &str) { if let Some(MessageSegment::Text(segment)) = self.segments.last_mut() { segment.push_str(text); } else { self.segments.push(MessageSegment::Text(text.to_string())); } } pub fn to_string(&self) -> String { let mut result = String::new(); if !self.context.is_empty() { result.push_str(&self.context); } for segment in &self.segments { match segment { MessageSegment::Text(text) => result.push_str(text), MessageSegment::Thinking(text) => { result.push_str("<think>"); result.push_str(text); result.push_str("</think>"); } } } result } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum MessageSegment { Text(String), Thinking(String), } impl MessageSegment { pub fn text_mut(&mut self) -> &mut String { match self { Self::Text(text) => text, Self::Thinking(text) => text, } } pub fn should_display(&self) -> bool { // We add USING_TOOL_MARKER when making a request that includes tool uses // without non-whitespace text around them, and this can cause the model // to mimic the pattern, so we consider those segments not displayable. match self { Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER, Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectSnapshot { pub worktree_snapshots: Vec<WorktreeSnapshot>, pub unsaved_buffer_paths: Vec<String>, pub timestamp: DateTime<Utc>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorktreeSnapshot { pub worktree_path: String, pub git_state: Option<GitState>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitState { pub remote_url: Option<String>, pub head_sha: Option<String>, pub current_branch: Option<String>, pub diff: Option<String>, } #[derive(Clone)] pub struct ThreadCheckpoint { message_id: MessageId, git_checkpoint: GitStoreCheckpoint, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ThreadFeedback { Positive, Negative, } pub enum LastRestoreCheckpoint { Pending { message_id: MessageId, }, Error { message_id: MessageId, error: String, }, } impl LastRestoreCheckpoint { pub fn message_id(&self) -> MessageId { match self { LastRestoreCheckpoint::Pending { message_id } => *message_id, LastRestoreCheckpoint::Error { message_id, .. } => *message_id, } } } #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum DetailedSummaryState { #[default] NotGenerated, Generating { message_id: MessageId, }, Generated { text: SharedString, message_id: MessageId, }, } #[derive(Default)] pub struct TotalTokenUsage { pub total: usize, pub max: usize, pub ratio: TokenUsageRatio, } #[derive(Debug, Default, PartialEq, Eq)] pub enum TokenUsageRatio { #[default] Normal, Warning, Exceeded, } /// A thread of conversation with the LLM. pub struct Thread { id: ThreadId, updated_at: DateTime<Utc>, summary: Option<SharedString>, pending_summary: Task<Option<()>>, detailed_summary_state: DetailedSummaryState, messages: Vec<Message>, next_message_id: MessageId, context: BTreeMap<ContextId, AssistantContext>, context_by_message: HashMap<MessageId, Vec<ContextId>>, project_context: SharedProjectContext, checkpoints_by_message: HashMap<MessageId, ThreadCheckpoint>, completion_count: usize, pending_completions: Vec<PendingCompletion>, project: Entity<Project>, prompt_builder: Arc<PromptBuilder>, tools: Entity<ToolWorkingSet>, tool_use: ToolUseState, action_log: Entity<ActionLog>, last_restore_checkpoint: Option<LastRestoreCheckpoint>, pending_checkpoint: Option<ThreadCheckpoint>, initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>, cumulative_token_usage: TokenUsage, exceeded_window_error: Option<ExceededWindowError>, feedback: Option<ThreadFeedback>, message_feedback: HashMap<MessageId, ThreadFeedback>, last_auto_capture_at: Option<Instant>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExceededWindowError { /// Model used when last message exceeded context window model_id: LanguageModelId, /// Token count including last message token_count: usize, } impl Thread { pub fn new( project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, system_prompt: SharedProjectContext, cx: &mut Context<Self>, ) -> Self { Self { id: ThreadId::new(), updated_at: Utc::now(), summary: None, pending_summary: Task::ready(None), detailed_summary_state: DetailedSummaryState::NotGenerated, messages: Vec::new(), next_message_id: MessageId(0), context: BTreeMap::default(), context_by_message: HashMap::default(), project_context: system_prompt, checkpoints_by_message: HashMap::default(), completion_count: 0, pending_completions: Vec::new(), project: project.clone(), prompt_builder, tools: tools.clone(), last_restore_checkpoint: None, pending_checkpoint: None, tool_use: ToolUseState::new(tools.clone()), action_log: cx.new(|_| ActionLog::new(project.clone())), initial_project_snapshot: { let project_snapshot = Self::project_snapshot(project, cx); cx.foreground_executor() .spawn(async move { Some(project_snapshot.await) }) .shared() }, cumulative_token_usage: TokenUsage::default(), exceeded_window_error: None, feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, } } pub fn deserialize( id: ThreadId, serialized: SerializedThread, project: Entity<Project>, tools: Entity<ToolWorkingSet>, prompt_builder: Arc<PromptBuilder>, project_context: SharedProjectContext, cx: &mut Context<Self>, ) -> Self { let next_message_id = MessageId( serialized .messages .last() .map(|message| message.id.0 + 1) .unwrap_or(0), ); let tool_use = ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |_| true); Self { id, updated_at: serialized.updated_at, summary: Some(serialized.summary), pending_summary: Task::ready(None), detailed_summary_state: serialized.detailed_summary_state, messages: serialized .messages .into_iter() .map(|message| Message { id: message.id, role: message.role, segments: message .segments .into_iter() .map(|segment| match segment { SerializedMessageSegment::Text { text } => MessageSegment::Text(text), SerializedMessageSegment::Thinking { text } => { MessageSegment::Thinking(text) } }) .collect(), context: message.context, }) .collect(), next_message_id, context: BTreeMap::default(), context_by_message: HashMap::default(), project_context, checkpoints_by_message: HashMap::default(), completion_count: 0, pending_completions: Vec::new(), last_restore_checkpoint: None, pending_checkpoint: None, project: project.clone(), prompt_builder, tools, tool_use, action_log: cx.new(|_| ActionLog::new(project)), initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(), cumulative_token_usage: serialized.cumulative_token_usage, exceeded_window_error: None, feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, } } pub fn id(&self) -> &ThreadId { &self.id } pub fn is_empty(&self) -> bool { self.messages.is_empty() } pub fn updated_at(&self) -> DateTime<Utc> { self.updated_at } pub fn touch_updated_at(&mut self) { self.updated_at = Utc::now(); } pub fn summary(&self) -> Option<SharedString> { self.summary.clone() } pub fn project_context(&self) -> SharedProjectContext { self.project_context.clone() } pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread"); pub fn summary_or_default(&self) -> SharedString { self.summary.clone().unwrap_or(Self::DEFAULT_SUMMARY) } pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) { let Some(current_summary) = &self.summary else { // Don't allow setting summary until generated return; }; let mut new_summary = new_summary.into(); if new_summary.is_empty() { new_summary = Self::DEFAULT_SUMMARY; } if current_summary != &new_summary { self.summary = Some(new_summary); cx.emit(ThreadEvent::SummaryChanged); } } pub fn latest_detailed_summary_or_text(&self) -> SharedString { self.latest_detailed_summary() .unwrap_or_else(|| self.text().into()) } fn latest_detailed_summary(&self) -> Option<SharedString> { if let DetailedSummaryState::Generated { text, .. } = &self.detailed_summary_state { Some(text.clone()) } else { None } } pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } pub fn messages(&self) -> impl Iterator<Item = &Message> { self.messages.iter() } pub fn is_generating(&self) -> bool { !self.pending_completions.is_empty() || !self.all_tools_finished() } pub fn tools(&self) -> &Entity<ToolWorkingSet> { &self.tools } pub fn pending_tool(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> { self.tool_use .pending_tool_uses() .into_iter() .find(|tool_use| &tool_use.id == id) } pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = &PendingToolUse> { self.tool_use .pending_tool_uses() .into_iter() .filter(|tool_use| tool_use.status.needs_confirmation()) } pub fn has_pending_tool_uses(&self) -> bool { !self.tool_use.pending_tool_uses().is_empty() } pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> { self.checkpoints_by_message.get(&id).cloned() } pub fn restore_checkpoint( &mut self, checkpoint: ThreadCheckpoint, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.last_restore_checkpoint = Some(LastRestoreCheckpoint::Pending { message_id: checkpoint.message_id, }); cx.emit(ThreadEvent::CheckpointChanged); cx.notify();