Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 250 additions & 7 deletions app/src/ai/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2189,7 +2189,13 @@ pub struct MCPServer {
}

/// Contains context that may be attached to a user query.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
///
/// Serialization derives the externally tagged form for the named variants,
/// with `Block` serialized untagged as a bare [`BlockContext`] object.
/// Deserialization is hand-written below and must accept exactly those
/// forms; it avoids the generic buffering machinery that serde generates for
/// enums that mix tagged and untagged variants.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum AIAgentContext {
Directory {
pwd: Option<String>,
Expand Down Expand Up @@ -2246,19 +2252,14 @@ pub enum AIAgentContext {
/// Information about the GitHub pull request associated with the current branch.
PullRequest {
/// The pull request number.
#[serde(default, deserialize_with = "deserialize_pull_request_number")]
number: i32,
/// The pull request state (for example, `OPEN`, `MERGED`, or `CLOSED`).
#[serde(default)]
state: String,
/// Whether the pull request is marked as draft.
#[serde(default)]
draft: bool,
/// The pull request's base branch.
#[serde(default)]
base_branch: String,
/// The full URL of the pull request (e.g. "https://github.com/owner/repo/pull/123").
#[serde(default)]
url: String,
},

Expand All @@ -2272,6 +2273,131 @@ pub enum AIAgentContext {
Block(Box<BlockContext>),
}

/// The tagged variants of [`AIAgentContext`], used by its hand-written
/// `Deserialize` implementation. The variant and field shapes must stay
/// identical to [`AIAgentContext`] so the serialized forms match.
#[derive(Deserialize)]
enum AIAgentContextTagged {
Directory {
pwd: Option<String>,
home_dir: Option<String>,
are_file_symbols_indexed: bool,
},
SelectedText(String),
ExecutionEnvironment(WarpAiExecutionContext),
CurrentTime {
current_time: DateTime<Local>,
},
Image(ImageContext),
Codebase {
path: String,
name: String,
},
ProjectRules {
root_path: String,
active_rules: Vec<FileContext>,
additional_rule_paths: Vec<String>,
},
File(FileContext),
Git {
head: String,
branch: Option<String>,
},
Repository {
name: String,
owner: Option<String>,
host: Option<String>,
},
PullRequest {
#[serde(default, deserialize_with = "deserialize_pull_request_number")]
number: i32,
#[serde(default)]
state: String,
#[serde(default)]
draft: bool,
#[serde(default)]
base_branch: String,
#[serde(default)]
url: String,
},
Skills {
skills: Vec<SkillDescriptor>,
},
}

impl From<AIAgentContextTagged> for AIAgentContext {
fn from(tagged: AIAgentContextTagged) -> Self {
match tagged {
AIAgentContextTagged::Directory {
pwd,
home_dir,
are_file_symbols_indexed,
} => AIAgentContext::Directory {
pwd,
home_dir,
are_file_symbols_indexed,
},
AIAgentContextTagged::SelectedText(text) => AIAgentContext::SelectedText(text),
AIAgentContextTagged::ExecutionEnvironment(context) => {
AIAgentContext::ExecutionEnvironment(context)
}
AIAgentContextTagged::CurrentTime { current_time } => {
AIAgentContext::CurrentTime { current_time }
}
AIAgentContextTagged::Image(image) => AIAgentContext::Image(image),
AIAgentContextTagged::Codebase { path, name } => {
AIAgentContext::Codebase { path, name }
}
AIAgentContextTagged::ProjectRules {
root_path,
active_rules,
additional_rule_paths,
} => AIAgentContext::ProjectRules {
root_path,
active_rules,
additional_rule_paths,
},
AIAgentContextTagged::File(file) => AIAgentContext::File(file),
AIAgentContextTagged::Git { head, branch } => AIAgentContext::Git { head, branch },
AIAgentContextTagged::Repository { name, owner, host } => {
AIAgentContext::Repository { name, owner, host }
}
AIAgentContextTagged::PullRequest {
number,
state,
draft,
base_branch,
url,
} => AIAgentContext::PullRequest {
number,
state,
draft,
base_branch,
url,
},
AIAgentContextTagged::Skills { skills } => AIAgentContext::Skills { skills },
}
}
}

impl<'de> Deserialize<'de> for AIAgentContext {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// Buffer the input into one JSON value, then try the tagged variants
// and fall back to the untagged Block variant. This matches the
// behavior of serde's derive for mixed tagged and untagged enums.
let value = serde_json::Value::deserialize(deserializer)?;
match AIAgentContextTagged::deserialize(&value) {
Ok(tagged) => Ok(tagged.into()),
Err(tagged_error) => BlockContext::deserialize(&value)
.map(|block| AIAgentContext::Block(Box::new(block)))
.map_err(|_| serde::de::Error::custom(tagged_error)),
}
}
}

fn deserialize_pull_request_number<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down Expand Up @@ -2339,7 +2465,12 @@ pub enum DocumentContentAttachmentSource {
PlanEdited,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
/// Serialization derives the externally tagged form for the named variants,
/// with `Block` serialized untagged as a bare [`BlockContext`] object.
/// Deserialization is hand-written below and must accept exactly those
/// forms; it avoids the generic buffering machinery that serde generates for
/// enums that mix tagged and untagged variants.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum AIAgentAttachment {
PlainText(String),
DocumentContent {
Expand Down Expand Up @@ -2384,6 +2515,118 @@ pub enum AIAgentAttachment {
Block(BlockContext),
}

/// The tagged variants of [`AIAgentAttachment`], used by its hand-written
/// `Deserialize` implementation. The variant and field shapes must stay
/// identical to [`AIAgentAttachment`] so the serialized forms match.
#[derive(Deserialize)]
enum AIAgentAttachmentTagged {
PlainText(String),
DocumentContent {
document_id: String,
content: String,
source: DocumentContentAttachmentSource,
line_range: Option<Range<LineCount>>,
},
DriveObject {
uid: String,
payload: Option<DriveObjectPayload>,
},
DiffHunk {
file_path: String,
line_range: Range<LineCount>,
diff_content: String,
lines_added: u32,
lines_removed: u32,
current: Option<CurrentHead>,
base: DiffBase,
},
DiffSet {
file_diffs: HashMap<String, Vec<DiffSetHunk>>,
current: Option<CurrentHead>,
base: DiffBase,
},
FilePathReference {
file_id: String,
file_name: String,
file_path: String,
},
}

impl From<AIAgentAttachmentTagged> for AIAgentAttachment {
fn from(tagged: AIAgentAttachmentTagged) -> Self {
match tagged {
AIAgentAttachmentTagged::PlainText(text) => AIAgentAttachment::PlainText(text),
AIAgentAttachmentTagged::DocumentContent {
document_id,
content,
source,
line_range,
} => AIAgentAttachment::DocumentContent {
document_id,
content,
source,
line_range,
},
AIAgentAttachmentTagged::DriveObject { uid, payload } => {
AIAgentAttachment::DriveObject { uid, payload }
}
AIAgentAttachmentTagged::DiffHunk {
file_path,
line_range,
diff_content,
lines_added,
lines_removed,
current,
base,
} => AIAgentAttachment::DiffHunk {
file_path,
line_range,
diff_content,
lines_added,
lines_removed,
current,
base,
},
AIAgentAttachmentTagged::DiffSet {
file_diffs,
current,
base,
} => AIAgentAttachment::DiffSet {
file_diffs,
current,
base,
},
AIAgentAttachmentTagged::FilePathReference {
file_id,
file_name,
file_path,
} => AIAgentAttachment::FilePathReference {
file_id,
file_name,
file_path,
},
}
}
}

impl<'de> Deserialize<'de> for AIAgentAttachment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// Buffer the input into one JSON value, then try the tagged variants
// and fall back to the untagged Block variant. This matches the
// behavior of serde's derive for mixed tagged and untagged enums.
let value = serde_json::Value::deserialize(deserializer)?;
match AIAgentAttachmentTagged::deserialize(&value) {
Ok(tagged) => Ok(tagged.into()),
Err(tagged_error) => BlockContext::deserialize(&value)
.map(AIAgentAttachment::Block)
.map_err(|_| serde::de::Error::custom(tagged_error)),
}
}
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CurrentHead {
BranchName(String),
Expand Down
Loading
Loading