first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+232 -323
View File
@@ -1,48 +1,41 @@
//! This module contains state management logic for pending context, where "pending context"
//! is defined as additional context to be attached to the next AI query.
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::ai::{
agent::{AnyFileContent, FileContext},
block_context::BlockContext,
};
use super::agent_view::{AgentViewController, AgentViewEntryOrigin, EnterAgentViewError};
use ai::project_context::model::ProjectContextModel;
use galaxy_core::features::FeatureFlag;
use galaxyui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity};
use parking_lot::FairMutex;
use crate::ai::agent::conversation::{AIConversationAutoexecuteMode, ConversationStatus};
use crate::{
ai::{
agent::todos::AIAgentTodoList,
agent::{
conversation::{AIConversation, AIConversationId},
AIAgentAttachment, AIAgentContext, ImageContext,
},
document::ai_document_model::AIDocumentId,
llms::{LLMPreferences, LLMPreferencesEvent},
outline::RepoOutlines,
},
terminal::{
event::{BlockCompletedEvent, BlockType},
model::{block::BlockId, session::Sessions},
model_events::{ModelEvent, ModelEventDispatcher},
TerminalModel,
},
workspaces::user_workspaces::UserWorkspaces,
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
use galaxyui::{
AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity, WeakModelHandle,
};
use super::{
block::DirectoryContext, history_model::BlocklistAIHistoryModel, BlocklistAIHistoryEvent,
use super::agent_view::{AgentViewEntryOrigin, EnterAgentViewError};
use super::block::DirectoryContext;
use super::{ConversationSelectionEvent, ConversationSelectionHandle};
use crate::ai::agent::conversation::{
AIConversation, AIConversationAutoexecuteMode, AIConversationId, ConversationStatus,
};
use crate::ai::agent::todos::AIAgentTodoList;
use crate::ai::agent::{
AIAgentAttachment, AIAgentContext, AnyFileContent, FileContext, ImageContext,
};
use crate::ai::block_context::BlockContext;
use crate::ai::document::ai_document_model::AIDocumentId;
use crate::ai::llms::{LLMPreferences, LLMPreferencesEvent};
use crate::ai::outline::RepoOutlines;
use crate::code_review::github_repo_model::GitHubRepoModel;
use crate::terminal::event::{BlockCompletedEvent, BlockType};
use crate::terminal::model::block::{BlockId, BlockMetadata};
use crate::terminal::model::session::Sessions;
use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
use crate::terminal::TerminalModel;
use crate::util::git::{PrInfo, RepositoryInfo};
use crate::workspaces::user_workspaces::UserWorkspaces;
/// A non-image file picked via the "attach file" button, stored until query submission.
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -80,36 +73,11 @@ impl PendingAttachment {
}
}
}
/// The state the pending query is in.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum PendingQueryState {
/// The next query will continue an existing conversation.
Existing { conversation_id: AIConversationId },
New {
/// Autoexecute override for the new conversation to be started.
autoexecute_override: AIConversationAutoexecuteMode,
},
}
impl Default for PendingQueryState {
fn default() -> Self {
Self::New {
autoexecute_override: AIConversationAutoexecuteMode::default(),
}
}
}
impl PendingQueryState {
pub fn targets_existing_conversation(&self) -> bool {
matches!(self, PendingQueryState::Existing { .. })
}
}
/// Model responsible for keeping track of session context to be attached to the next AI query.
pub struct BlocklistAIContextModel {
terminal_model: Arc<FairMutex<TerminalModel>>,
directory_context: DirectoryContext,
github_repo_model: Option<WeakModelHandle<GitHubRepoModel>>,
/// `BlockId`s corresponding to blocks to be included as context with the next AI query.
pending_context_block_ids: HashSet<BlockId>,
@@ -123,32 +91,19 @@ pub struct BlocklistAIContextModel {
/// Storage for diff hunk attachments that can be referenced in queries
pending_inline_diff_hunk_attachments: HashMap<String, AIAgentAttachment>,
/// The pending query could be new, which means it starts a new conversation, or follow-up, which means
/// it continues the selected conversation.
///
/// Note that this is intentionally decoupled from the active conversation in the HistoryModel.
/// The active conversation (the one that agent outputs are being streamed to) can be different from the
/// conversation we're following up in for the next query.
pending_query_state: PendingQueryState,
conversation_selection: ConversationSelectionHandle,
/// The ID of the terminal view this controller is associated with.
terminal_view_id: EntityId,
/// The ID of the terminal surface this model is associated with.
terminal_surface_id: EntityId,
/// AI document ID to be included as context with the next AI query.
/// When set, the document content will be attached as plain text context.
pending_document_id: Option<AIDocumentId>,
agent_view_controller: ModelHandle<AgentViewController>,
/// Block IDs of user-executed commands to be auto-attached as context.
/// When `AgentViewBlockContext` is enabled, completed user commands are tracked here
/// and automatically included as context with the next user query.
auto_attached_agent_view_user_block_ids: Vec<BlockId>,
/// When true, submitting a prompt while the agent is responding will queue it
/// instead of sending it immediately.
/// Persists across exchanges in the same conversation (like fast-forward).
queue_next_prompt_enabled: bool,
}
pub fn block_context_from_terminal_model(
@@ -185,136 +140,109 @@ pub fn block_context_from_terminal_model(
}
impl BlocklistAIContextModel {
/// Creates pending context state for a terminal surface.
pub fn new(
sessions: ModelHandle<Sessions>,
model_event_dispatcher: &ModelHandle<ModelEventDispatcher>,
terminal_model: Arc<FairMutex<TerminalModel>>,
terminal_view_id: EntityId,
agent_view_controller: ModelHandle<AgentViewController>,
terminal_surface_id: EntityId,
conversation_selection: ConversationSelectionHandle,
ctx: &mut ModelContext<Self>,
) -> Self {
ctx.subscribe_to_model(model_event_dispatcher, move |me, event, ctx| match event {
ModelEvent::BlockCompleted(BlockCompletedEvent {
block_type: BlockType::User(user_block_completed),
block_id,
..
}) => {
// If AgentViewBlockContext is enabled and we're in agent view, track user-executed
// blocks for auto-attachment as context.
if FeatureFlag::AgentViewBlockContext.is_enabled()
&& me.agent_view_controller.as_ref(ctx).is_fullscreen()
&& !user_block_completed.was_part_of_agent_interaction
{
me.auto_attached_agent_view_user_block_ids
.push(block_id.clone());
}
// If the block that finished was part of an agent interaction (i.e. LRC finishing),
// we should preserve input context.
if !FeatureFlag::AgentViewBlockContext.is_enabled()
&& !user_block_completed.was_part_of_agent_interaction
{
me.reset_context_to_default(ctx);
}
}
ModelEvent::BlockMetadataReceived(block_metadata_received) => {
let pwd = block_metadata_received
.block_metadata
.current_working_directory()
.map(|s| PathBuf::from(s.to_owned()));
let session_id = block_metadata_received.block_metadata.session_id();
if let Some(session_id) = session_id {
let active_session = sessions.as_ref(ctx).get(session_id);
if let Some(active_session) = active_session {
me.update_directory_context(
pwd.map(|p| p.to_string_lossy().to_string()),
active_session.home_dir().map(|sq| sq.to_owned()),
ctx,
);
}
}
}
_ => {}
});
ctx.subscribe_to_model(&BlocklistAIHistoryModel::handle(ctx), |me, event, ctx| {
if event
.terminal_view_id()
.is_some_and(|id| id != me.terminal_view_id)
{
return;
}
match event {
BlocklistAIHistoryEvent::ClearedConversationsInTerminalView { .. } => {
me.set_pending_query_state(PendingQueryState::default(), ctx);
if FeatureFlag::AgentView.is_enabled() {
me.agent_view_controller.update(ctx, |controller, ctx| {
controller.exit_agent_view(ctx);
});
}
}
BlocklistAIHistoryEvent::SplitConversation {
new_conversation_id,
ctx.subscribe_to_model(
model_event_dispatcher,
move |me, _, event, ctx| match event {
ModelEvent::BlockCompleted(BlockCompletedEvent {
block_type: BlockType::User(user_block_completed),
block_id,
..
} => {
me.set_pending_query_state_for_existing_conversation(
*new_conversation_id,
AgentViewEntryOrigin::AgentRequestedNewConversation,
ctx,
);
}) => {
// If AgentViewBlockContext is enabled and we're in agent view, track user-executed
// blocks for auto-attachment as context.
if FeatureFlag::AgentViewBlockContext.is_enabled()
&& me
.conversation_selection
.as_ref(ctx)
.is_conversation_fullscreen(ctx)
&& !user_block_completed.was_part_of_agent_interaction
{
me.auto_attached_agent_view_user_block_ids
.push(block_id.clone());
}
// If the block that finished was part of an agent interaction (i.e. LRC finishing),
// we should preserve input context.
if !FeatureFlag::AgentViewBlockContext.is_enabled()
&& !user_block_completed.was_part_of_agent_interaction
{
me.reset_context_to_default(ctx);
}
}
ModelEvent::BlockMetadataReceived(e) => {
me.apply_block_metadata_directory_context(&e.block_metadata, &sessions, ctx);
}
ModelEvent::BlockWorkingDirectoryUpdated(e) => {
me.apply_block_metadata_directory_context(&e.block_metadata, &sessions, ctx);
}
_ => {}
}
});
},
);
ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |me, event, ctx| {
ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |me, _, event, ctx| {
if let LLMPreferencesEvent::UpdatedActiveAgentModeLLM = event {
let llm_prefs = LLMPreferences::as_ref(ctx);
let vision_supported = llm_prefs.vision_supported(ctx, Some(me.terminal_view_id));
let vision_supported =
llm_prefs.vision_supported(ctx, Some(me.terminal_surface_id));
if !vision_supported {
me.clear_pending_images(ctx);
}
}
});
// Clear auto-attached blocks when exiting agent view or switching conversations
ctx.subscribe_to_model(&agent_view_controller, |me, event, _ctx| {
use super::agent_view::AgentViewControllerEvent;
match event {
AgentViewControllerEvent::ExitedAgentView { .. }
| AgentViewControllerEvent::EnteredAgentView { .. } => {
me.auto_attached_agent_view_user_block_ids.clear();
}
AgentViewControllerEvent::ExitConfirmed { .. } => {}
ctx.subscribe_to_model(&conversation_selection, |me, _, event, ctx| match event {
ConversationSelectionEvent::Changed => {
ctx.emit(BlocklistAIContextEvent::PendingQueryStateUpdated);
}
ConversationSelectionEvent::Activated { .. }
| ConversationSelectionEvent::Deactivated { .. } => {
me.auto_attached_agent_view_user_block_ids.clear();
}
});
// In sandboxed/autonomous mode (SDK mode with --sandboxed flag), automatically set
// conversations to RunToCompletion mode so they don't wait for user confirmation.
let pending_query_state =
if galaxy_core::execution_mode::AppExecutionMode::as_ref(ctx).is_sandboxed() {
PendingQueryState::New {
autoexecute_override: AIConversationAutoexecuteMode::RunToCompletion,
}
} else {
Default::default()
};
Self {
terminal_model,
directory_context: Default::default(),
github_repo_model: None,
pending_context_block_ids: HashSet::new(),
pending_context_selected_text: None,
pending_attachments: Default::default(),
pending_query_state,
terminal_view_id,
agent_view_controller,
conversation_selection,
terminal_surface_id,
pending_inline_diff_hunk_attachments: Default::default(),
pending_document_id: None,
auto_attached_agent_view_user_block_ids: Vec::new(),
}
}
/// Test-only constructor that skips production subscriptions and singleton lookups.
#[cfg(test)]
pub(crate) fn new_for_test(
terminal_model: Arc<FairMutex<TerminalModel>>,
terminal_surface_id: EntityId,
conversation_selection: ConversationSelectionHandle,
) -> Self {
Self {
terminal_model,
directory_context: Default::default(),
github_repo_model: None,
pending_context_block_ids: HashSet::new(),
pending_context_selected_text: None,
pending_attachments: Default::default(),
conversation_selection,
terminal_surface_id,
pending_inline_diff_hunk_attachments: Default::default(),
pending_document_id: None,
auto_attached_agent_view_user_block_ids: Vec::new(),
queue_next_prompt_enabled: false,
}
}
@@ -329,6 +257,12 @@ impl BlocklistAIContextModel {
self.auto_attached_agent_view_user_block_ids.clear();
}
/// Returns `true` if the next AI query has any context that should force the input to be
/// locked in AI mode (skipping NLD): a pending image or file attachment.
pub fn has_locking_attachment(&self) -> bool {
!self.pending_attachments.is_empty()
}
/// Returns the set `BlockId`s corresponding to blocks to be included as context with the next
/// query.
pub fn pending_context_block_ids(&self) -> &HashSet<BlockId> {
@@ -382,7 +316,14 @@ impl BlocklistAIContextModel {
/// Returns `AIAgentContext` for the blocks to be included in the current AI query.
/// If `is_user_query` is true, includes blocks, selected text, and images as context.
/// If false, excludes these user-specific contexts but includes everything else.
pub fn pending_context(&self, app: &AppContext, is_user_query: bool) -> Vec<AIAgentContext> {
pub fn pending_context(
&self,
app: &AppContext,
is_user_query: bool,
current_working_directory_location: Option<&LocalOrRemotePath>,
) -> Vec<AIAgentContext> {
// `pwd` is the shell-reported path used for directory context and local indexing.
// The location is passed separately because it preserves remote host identity for rules.
let pwd = self.current_pwd();
let is_pwd_indexed = if cfg!(feature = "agent_mode_evals") {
// In evals, we want to disable file outline based search. Full
@@ -395,15 +336,8 @@ impl BlocklistAIContextModel {
})
};
let project_rules = if let Some(pwd) = pwd.clone().and_then(|path| {
PathBuf::from_str(&path)
.ok()
.and_then(|s| s.canonicalize().ok())
}) {
ProjectContextModel::as_ref(app).find_applicable_rules(&pwd)
} else {
None
};
let project_rules = current_working_directory_location
.and_then(|pwd| ProjectContextModel::as_ref(app).find_applicable_rules(pwd));
let mut context = Vec::new();
@@ -429,17 +363,25 @@ impl BlocklistAIContextModel {
});
}
// Include repository info from the origin remote URL if available.
if let Some(repo_context) = self.repository_context(app) {
context.push(repo_context);
}
if let Some(pull_request_context) = self.pull_request_context(app) {
context.push(pull_request_context);
}
// Always include project rules if available
if let Some(rules) = project_rules {
context.push(AIAgentContext::ProjectRules {
root_path: rules.root_path.to_string_lossy().into(),
root_path: rules.root_path.display_path(),
active_rules: rules
.active_rules
.into_iter()
.map(|rule| {
let line_count = rule.content.lines().count();
FileContext {
file_name: rule.path.to_string_lossy().into(),
file_name: rule.path.display_path(),
content: AnyFileContent::StringContent(rule.content.clone()),
line_range: None,
last_modified: None,
@@ -477,13 +419,6 @@ impl BlocklistAIContextModel {
if let Some(selected_text) = &self.pending_context_selected_text {
context.push(AIAgentContext::SelectedText(selected_text.clone()));
}
// Add images from pending attachments
for attachment in &self.pending_attachments {
if let PendingAttachment::Image(image) = attachment {
context.push(AIAgentContext::Image(image.clone()));
}
}
}
context
@@ -512,6 +447,26 @@ impl BlocklistAIContextModel {
});
}
fn apply_block_metadata_directory_context(
&mut self,
block_metadata: &BlockMetadata,
sessions: &ModelHandle<Sessions>,
ctx: &mut ModelContext<Self>,
) {
let pwd = block_metadata
.current_working_directory()
.map(|s| PathBuf::from(s.to_owned()));
if let Some(session_id) = block_metadata.session_id() {
if let Some(active_session) = sessions.as_ref(ctx).get(session_id) {
self.update_directory_context(
pwd.map(|p| p.to_string_lossy().to_string()),
active_session.home_dir().map(|sq| sq.to_owned()),
ctx,
);
}
}
}
/// Set `requires_visual_resync` to `false` only if the pending context was modified as a result
/// of manual user selections. In such cases, a visual resync won't be required because the
/// pending context was synchronized to the manual selection.
@@ -680,10 +635,6 @@ impl BlocklistAIContextModel {
to_remove
}
pub fn pending_query_state(&self) -> &PendingQueryState {
&self.pending_query_state
}
/// Convenience function to set pending query state to continue an existing conversation by ID.
pub fn set_pending_query_state_for_existing_conversation(
&mut self,
@@ -691,14 +642,9 @@ impl BlocklistAIContextModel {
origin: AgentViewEntryOrigin,
ctx: &mut ModelContext<Self>,
) {
self.set_pending_query_state(PendingQueryState::Existing { conversation_id }, ctx);
if FeatureFlag::AgentView.is_enabled() {
if let Err(e) = self.agent_view_controller.update(ctx, |controller, ctx| {
controller.try_enter_agent_view(Some(conversation_id), origin, ctx)
}) {
log::error!("Failed to enter agent view for existing conversation: {e}");
}
}
self.conversation_selection.update(ctx, |selection, ctx| {
selection.select_existing_conversation(conversation_id, origin, ctx);
});
}
/// Sets the pending query state to the defaults for a *new* conversation (i.e. not a
@@ -708,40 +654,20 @@ impl BlocklistAIContextModel {
origin: AgentViewEntryOrigin,
ctx: &mut ModelContext<Self>,
) {
self.set_pending_query_state(PendingQueryState::default(), ctx);
if FeatureFlag::AgentView.is_enabled() {
if let Err(e) = self.agent_view_controller.update(ctx, |controller, ctx| {
controller.try_enter_agent_view(None, origin, ctx)
}) {
log::error!("Failed to enter agent view for new conversation: {e}");
}
}
self.conversation_selection.update(ctx, |selection, ctx| {
selection.select_new_conversation(origin, ctx);
});
}
/// Attempts to enter agent view for a new conversation and returns the conversation ID.
/// This should be used when a slash command needs to create a new conversation
/// and the AgentView feature flag is enabled.
///
/// Returns `Ok(conversation_id)` on success, or `Err` if entry is blocked.
pub fn try_enter_agent_view_for_new_conversation(
/// Starts and selects a new conversation, entering Agent View when this is a GUI selection.
pub(crate) fn try_start_new_conversation(
&mut self,
origin: AgentViewEntryOrigin,
ctx: &mut ModelContext<Self>,
) -> Result<AIConversationId, EnterAgentViewError> {
let conversation_id = self.agent_view_controller.update(ctx, |controller, ctx| {
controller.try_enter_agent_view(None, origin, ctx)
})?;
self.set_pending_query_state(PendingQueryState::default(), ctx);
Ok(conversation_id)
}
/// Sets the value of `pending_query_state`, emitting an event if it changed.
fn set_pending_query_state(&mut self, state: PendingQueryState, ctx: &mut ModelContext<Self>) {
if self.pending_query_state != state {
self.pending_query_state = state;
ctx.emit(BlocklistAIContextEvent::PendingQueryStateUpdated);
}
self.conversation_selection.update(ctx, |selection, ctx| {
selection.try_start_new_conversation(origin, ctx)
})
}
/// Returns `true` if a new conversation may be created.
@@ -763,28 +689,15 @@ impl BlocklistAIContextModel {
/// Returns the conversation ID the pending query is following up for, if any.
/// None if the pending query should start a new conversation.
pub fn selected_conversation_id(&self, ctx: &AppContext) -> Option<AIConversationId> {
if FeatureFlag::AgentView.is_enabled() {
return self
.agent_view_controller
.as_ref(ctx)
.agent_view_state()
.active_conversation_id();
}
match self.pending_query_state {
PendingQueryState::Existing {
conversation_id, ..
} => Some(conversation_id),
PendingQueryState::New { .. } => None,
}
self.conversation_selection
.as_ref(ctx)
.selected_conversation_id(ctx)
}
pub fn selected_conversation<'a>(&self, ctx: &'a AppContext) -> Option<&'a AIConversation> {
self.selected_conversation_id(ctx)
.as_ref()
.and_then(|conversation_id| {
BlocklistAIHistoryModel::as_ref(ctx).conversation(conversation_id)
})
self.conversation_selection
.as_ref(ctx)
.selected_conversation(ctx)
}
pub fn selected_conversation_todolist<'a>(
@@ -807,81 +720,24 @@ impl BlocklistAIContextModel {
&self,
ctx: &AppContext,
) -> AIConversationAutoexecuteMode {
match &self.pending_query_state {
PendingQueryState::New {
autoexecute_override,
} => *autoexecute_override,
PendingQueryState::Existing {
conversation_id, ..
} => BlocklistAIHistoryModel::as_ref(ctx)
.conversation(conversation_id)
.map(|conversation| conversation.autoexecute_override())
.unwrap_or_default(),
}
}
pub fn is_queue_next_prompt_enabled(&self) -> bool {
self.queue_next_prompt_enabled
}
pub fn toggle_queue_next_prompt(&mut self, ctx: &mut ModelContext<Self>) {
self.queue_next_prompt_enabled = !self.queue_next_prompt_enabled;
ctx.emit(BlocklistAIContextEvent::QueueNextPromptToggled);
self.conversation_selection
.as_ref(ctx)
.pending_query_autoexecute_override(ctx)
}
pub fn toggle_pending_query_autoexecute(&mut self, ctx: &mut ModelContext<Self>) {
// When AgentView is enabled, the autoexecution toggle should apply to the active agent view
// conversation -- even when starting a new conversation, the agent view always has a conversation
// ID.
if FeatureFlag::AgentView.is_enabled() {
if let Some(conversation_id) = self
.agent_view_controller
.as_ref(ctx)
.agent_view_state()
.active_conversation_id()
{
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.toggle_autoexecute_override(
&conversation_id,
self.terminal_view_id,
ctx,
);
});
}
return;
}
match &mut self.pending_query_state {
PendingQueryState::New {
autoexecute_override,
} => {
*autoexecute_override = if *autoexecute_override
== AIConversationAutoexecuteMode::RespectUserSettings
{
AIConversationAutoexecuteMode::RunToCompletion
} else {
AIConversationAutoexecuteMode::RespectUserSettings
};
ctx.emit(BlocklistAIContextEvent::PendingQueryStateUpdated);
}
PendingQueryState::Existing {
conversation_id, ..
} => {
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
history.toggle_autoexecute_override(
conversation_id,
self.terminal_view_id,
ctx,
);
});
}
}
self.conversation_selection.update(ctx, |selection, ctx| {
selection.toggle_pending_query_autoexecute(ctx);
});
}
/// Returns true if the pending query targets an existing conversation
/// (as opposed to starting a new one).
pub fn is_targeting_existing_conversation(&self) -> bool {
self.pending_query_state.targets_existing_conversation()
pub fn is_targeting_existing_conversation(&self, ctx: &AppContext) -> bool {
self.conversation_selection
.as_ref(ctx)
.selected_conversation_id(ctx)
.is_some()
}
/// Returns the status of the selected conversation for purposes of rendering the input hint
@@ -961,6 +817,39 @@ impl BlocklistAIContextModel {
}
}
pub fn set_github_repo_model(&mut self, handle: Option<WeakModelHandle<GitHubRepoModel>>) {
self.github_repo_model = handle;
}
/// Builds an `AIAgentContext::Repository` from cached git remote metadata, if available.
fn repository_context(&self, app: &AppContext) -> Option<AIAgentContext> {
let handle = self.github_repo_model.as_ref()?.upgrade(app)?;
let repository_info = handle.as_ref(app).repository_info(app)?;
Some(Self::repository_context_from_repository_info(
repository_info,
))
}
fn repository_context_from_repository_info(repository_info: &RepositoryInfo) -> AIAgentContext {
AIAgentContext::Repository {
name: repository_info.name.clone(),
owner: repository_info.owner.clone(),
}
}
fn pull_request_context(&self, app: &AppContext) -> Option<AIAgentContext> {
let handle = self.github_repo_model.as_ref()?.upgrade(app)?;
let pr_info = handle.as_ref(app).pr_info(app)?;
Self::pull_request_context_from_pr_info(pr_info)
}
fn pull_request_context_from_pr_info(pr_info: &PrInfo) -> Option<AIAgentContext> {
Some(AIAgentContext::PullRequest {
number: i32::try_from(pr_info.number).ok()?,
state: pr_info.state.clone(),
draft: pr_info.draft,
base_branch: pr_info.base_branch.clone(),
})
}
/// Clears all pending attachments.
pub fn clear_pending_attachments(&mut self, ctx: &mut ModelContext<Self>) {
if !self.pending_attachments.is_empty() {
@@ -972,6 +861,23 @@ impl BlocklistAIContextModel {
}
self.pending_attachments.clear();
}
/// Drains all pending attachments, returning them, and emits the same update event as
/// [`Self::clear_pending_attachments`] so the input's attachment chips disappear. Used to
/// move staged attachments onto a queued prompt row at enqueue time.
pub fn take_pending_attachments(
&mut self,
ctx: &mut ModelContext<Self>,
) -> Vec<PendingAttachment> {
if !self.pending_attachments.is_empty() {
ctx.emit(BlocklistAIContextEvent::UpdatedPendingContext {
previous_block_ids: self.pending_context_block_ids.clone(),
requires_block_resync: false,
requires_text_resync: false,
});
}
std::mem::take(&mut self.pending_attachments)
}
}
pub enum BlocklistAIContextEvent {
@@ -985,9 +891,12 @@ pub enum BlocklistAIContextEvent {
},
/// Emitted whenever the value changes.
PendingQueryStateUpdated,
QueueNextPromptToggled,
}
impl Entity for BlocklistAIContextModel {
type Event = BlocklistAIContextEvent;
}
#[cfg(test)]
#[path = "context_model_tests.rs"]
mod tests;