-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Migrate conversation list to AgentConversationEntry #10197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| #[allow(dead_code)] | ||
| pub mod entry; | ||
|
|
||
| pub use entry::AgentConversationEntry; | ||
| pub use entry::{ | ||
| AgentConversationEntry, AgentConversationEntryId, AgentConversationNavigationSubject, | ||
| AgentConversationProvenance, | ||
| }; | ||
|
|
||
| use crate::ai::active_agent_views_model::ActiveAgentViewsModel; | ||
| use crate::ai::agent::api::ServerConversationToken; | ||
|
|
@@ -250,20 +253,29 @@ enum LinkPreference { | |
|
|
||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub enum AgentRunDisplayStatus { | ||
| /// Raw task-service lifecycle states. `from_task` only returns `TaskInProgress` while the | ||
| /// task still has an active execution, or when there is no shadowed local conversation to | ||
| /// provide a more granular status. | ||
| TaskQueued, | ||
| TaskPending, | ||
| TaskClaimed, | ||
| TaskInProgress, | ||
| TaskSucceeded, | ||
| TaskFailed, | ||
| TaskError, | ||
| TaskBlocked { blocked_action: String }, | ||
| TaskBlocked { | ||
| blocked_action: String, | ||
| }, | ||
| TaskCancelled, | ||
| TaskUnknown, | ||
| /// Conversation-derived lifecycle states, used for interactive conversations and for | ||
| /// in-progress ambient tasks after they can be resolved to their shadowed local conversation. | ||
| ConversationInProgress, | ||
| ConversationSucceeded, | ||
| ConversationError, | ||
| ConversationBlocked { blocked_action: String }, | ||
| ConversationBlocked { | ||
| blocked_action: String, | ||
| }, | ||
| ConversationCancelled, | ||
| } | ||
|
|
||
|
|
@@ -346,6 +358,32 @@ impl AgentRunDisplayStatus { | |
| } | ||
| } | ||
|
|
||
| pub fn to_conversation_status(&self) -> ConversationStatus { | ||
| match self { | ||
| AgentRunDisplayStatus::TaskQueued | ||
| | AgentRunDisplayStatus::TaskPending | ||
| | AgentRunDisplayStatus::TaskClaimed | ||
| | AgentRunDisplayStatus::TaskInProgress | ||
| | AgentRunDisplayStatus::ConversationInProgress => ConversationStatus::InProgress, | ||
| AgentRunDisplayStatus::TaskSucceeded | AgentRunDisplayStatus::ConversationSucceeded => { | ||
| ConversationStatus::Success | ||
| } | ||
| AgentRunDisplayStatus::TaskFailed | ||
| | AgentRunDisplayStatus::TaskError | ||
| | AgentRunDisplayStatus::TaskUnknown | ||
| | AgentRunDisplayStatus::ConversationError => ConversationStatus::Error, | ||
| AgentRunDisplayStatus::TaskBlocked { blocked_action } | ||
| | AgentRunDisplayStatus::ConversationBlocked { blocked_action } => { | ||
| ConversationStatus::Blocked { | ||
| blocked_action: blocked_action.clone(), | ||
| } | ||
| } | ||
| AgentRunDisplayStatus::TaskCancelled | AgentRunDisplayStatus::ConversationCancelled => { | ||
| ConversationStatus::Cancelled | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn is_cancellable(&self) -> bool { | ||
| self.is_working() | ||
| } | ||
|
|
@@ -557,13 +595,9 @@ impl ConversationOrTask<'_> { | |
| /// Returns the session ID for tasks, if we have one. | ||
| pub fn session_id(&self) -> Option<SessionId> { | ||
| match self { | ||
| ConversationOrTask::Task(task) => task.session_id.as_deref().and_then(|s| { | ||
| let session_id = s.parse::<SessionId>(); | ||
| if let Err(ref e) = session_id { | ||
| log::warn!("Failed to parse shared session ID: {e}"); | ||
| } | ||
| session_id.ok() | ||
| }), | ||
| ConversationOrTask::Task(task) => { | ||
| task.session_id.as_deref().and_then(entry::parse_session_id) | ||
| } | ||
| ConversationOrTask::Conversation(_) => None, | ||
| } | ||
| } | ||
|
|
@@ -1487,6 +1521,184 @@ impl AgentConversationsModel { | |
| .collect() | ||
| } | ||
|
|
||
| pub fn get_entry_by_id( | ||
| &self, | ||
| id: &AgentConversationEntryId, | ||
| app: &AppContext, | ||
| ) -> Option<AgentConversationEntry> { | ||
| let history_model = BlocklistAIHistoryModel::as_ref(app); | ||
| match id { | ||
| AgentConversationEntryId::AmbientRun(task_id) => self | ||
| .tasks | ||
| .get(task_id) | ||
| .map(|task| entry::entry_for_task(task, history_model, app)), | ||
| AgentConversationEntryId::Conversation(conversation_id) => self | ||
| .conversations | ||
| .get(conversation_id) | ||
| .map(|metadata| entry::entry_for_conversation(metadata, history_model, app)) | ||
| .or_else(|| { | ||
| history_model | ||
| .get_conversation_metadata(conversation_id) | ||
| .map(|metadata| { | ||
| let nav_data = | ||
| ConversationNavigationData::from_historical_conversation_metadata( | ||
| metadata, | ||
| ); | ||
| entry::entry_for_historical_metadata( | ||
| metadata, | ||
| nav_data, | ||
| history_model, | ||
| app, | ||
| ) | ||
| }) | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| pub fn resolve_open_action( | ||
| subject: AgentConversationNavigationSubject, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it might be worth a comment on the difference between
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It took me a sec to grok the distinction in both cases |
||
| restore_layout: Option<RestoreConversationLayout>, | ||
| app: &AppContext, | ||
| ) -> Option<WorkspaceAction> { | ||
| let model = Self::as_ref(app); | ||
| match subject { | ||
| AgentConversationNavigationSubject::Entry(id) => model | ||
| .get_entry_by_id(&id, app) | ||
| .and_then(|entry| model.resolve_entry_open_action(&entry, restore_layout, app)), | ||
| AgentConversationNavigationSubject::ServerToken(server_token) => model | ||
| .entry_for_server_token(&server_token, app) | ||
| .and_then(|entry| model.resolve_entry_open_action(&entry, restore_layout, app)) | ||
| .or_else(|| { | ||
| Some(WorkspaceAction::OpenConversationTranscriptViewer { | ||
| ambient_agent_task_id: model.task_id_for_server_token(&server_token), | ||
| conversation_id: server_token, | ||
| }) | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| fn resolve_entry_open_action( | ||
| &self, | ||
| entry: &AgentConversationEntry, | ||
| restore_layout: Option<RestoreConversationLayout>, | ||
| app: &AppContext, | ||
| ) -> Option<WorkspaceAction> { | ||
| let active_views_model = ActiveAgentViewsModel::as_ref(app); | ||
|
|
||
| if let Some(task_id) = entry.identity.ambient_agent_task_id { | ||
| if let Some(terminal_view_id) = | ||
| active_views_model.get_terminal_view_id_for_ambient_task(task_id) | ||
| { | ||
| return Some(WorkspaceAction::FocusTerminalViewInWorkspace { terminal_view_id }); | ||
| } | ||
| } | ||
|
|
||
| if let Some(conversation_id) = entry.identity.local_conversation_id { | ||
| if active_views_model.is_conversation_open(conversation_id, app) { | ||
| if let Some(nav_data) = self | ||
| .conversations | ||
| .get(&conversation_id) | ||
| .map(|metadata| &metadata.nav_data) | ||
| { | ||
| return Some(WorkspaceAction::RestoreOrNavigateToConversation { | ||
| conversation_id, | ||
| window_id: nav_data.window_id, | ||
| pane_view_locator: nav_data.pane_view_locator, | ||
| terminal_view_id: nav_data.terminal_view_id, | ||
| restore_layout, | ||
| }); | ||
| } | ||
|
|
||
| if let Some(terminal_view_id) = | ||
| active_views_model.get_terminal_view_id_for_conversation(conversation_id, app) | ||
| { | ||
| return Some(WorkspaceAction::FocusTerminalViewInWorkspace { | ||
| terminal_view_id, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if let Some(task_id) = entry.identity.ambient_agent_task_id { | ||
| if let Some(session_id) = self | ||
| .tasks | ||
| .get(&task_id) | ||
| .and_then(AmbientAgentTask::active_execution_session_id) | ||
| .and_then(entry::parse_session_id) | ||
| { | ||
| return Some(WorkspaceAction::OpenAmbientAgentSession { | ||
| session_id, | ||
| task_id, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| if let Some(conversation_id) = entry.identity.local_conversation_id { | ||
| let nav_data = self | ||
| .conversations | ||
| .get(&conversation_id) | ||
| .map(|metadata| &metadata.nav_data); | ||
| if entry.backing.has_loaded_conversation | ||
| || entry.backing.has_local_persisted_data | ||
| || nav_data.is_some() | ||
| { | ||
| return Some(WorkspaceAction::RestoreOrNavigateToConversation { | ||
| conversation_id, | ||
| window_id: nav_data.and_then(|nav_data| nav_data.window_id), | ||
| pane_view_locator: None, | ||
| terminal_view_id: nav_data.and_then(|nav_data| nav_data.terminal_view_id), | ||
| restore_layout, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| entry | ||
| .identity | ||
| .server_conversation_token | ||
| .as_ref() | ||
| .map(|token| WorkspaceAction::OpenConversationTranscriptViewer { | ||
| conversation_id: token.clone(), | ||
| ambient_agent_task_id: entry.identity.ambient_agent_task_id, | ||
| }) | ||
| } | ||
|
|
||
| fn entry_for_server_token( | ||
| &self, | ||
| server_token: &ServerConversationToken, | ||
| app: &AppContext, | ||
| ) -> Option<AgentConversationEntry> { | ||
| let history_model = BlocklistAIHistoryModel::as_ref(app); | ||
| if let Some(task) = self.tasks.values().find(|task| { | ||
| task.conversation_id() | ||
| .is_some_and(|conversation_id| conversation_id == server_token.as_str()) | ||
| }) { | ||
| return Some(entry::entry_for_task(task, history_model, app)); | ||
| } | ||
|
|
||
| let conversation_id = history_model.find_conversation_id_by_server_token(server_token)?; | ||
| if let Some(task) = self.tasks.values().find(|task| { | ||
| entry::conversation_id_shadowed_by_task(task, history_model) == Some(conversation_id) | ||
| }) { | ||
| return Some(entry::entry_for_task(task, history_model, app)); | ||
| } | ||
|
|
||
| self.get_entry_by_id( | ||
| &AgentConversationEntryId::Conversation(conversation_id), | ||
| app, | ||
| ) | ||
| } | ||
|
|
||
| fn task_id_for_server_token( | ||
| &self, | ||
| server_token: &ServerConversationToken, | ||
| ) -> Option<AmbientAgentTaskId> { | ||
| self.tasks.values().find_map(|task| { | ||
| task.conversation_id() | ||
| .is_some_and(|conversation_id| conversation_id == server_token.as_str()) | ||
| .then_some(task.task_id) | ||
| }) | ||
| } | ||
|
|
||
| fn handle_history_event( | ||
| &mut self, | ||
| event: &BlocklistAIHistoryEvent, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a difference between task in progress and conversation in progress? I know the source is different, but wondering if at the abstraction layer they should be the same thing (and then same question for the other overlapping statuses)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added some comments to distinguihs