Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Link the GitHub issue this PR addresses. Before opening this PR, please confirm:
<!--
How did you test this change? What automated tests did you add? If you didn't add any new tests, what's your justification for not adding any?

Manual testing is required for changes that can be manually tested, and almost all changes can be manually tested. If your change can be manually tested, please include screenshots or a screen recording that show it working end to end.
Manual testing is required for changes that can be manually tested, and almost all changes can be manually tested. If your change can be manually tested, please include screenshots or a screen recording that show it working end to end.

You can run the app locally using `./script/run` - see WARP.md for more details on how to get set up.
You can run the app locally using `./script/run` - see WARP.md for more details on how to get set up.
-->

- [ ] I have manually tested my changes locally with `./script/run`
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ cargo run # build and run Warp
Tests are required for most code changes:

### Manual Testing
Manual testing is required for changes that can be manually tested, and almost all changes can be manually tested. If your change can be manually tested, please include screenshots or a screen recording that show it working end to end in the PR description.
Manual testing is required for changes that can be manually tested, and almost all changes can be manually tested. If your change can be manually tested, please include screenshots or a screen recording that show it working end to end in the PR description.

You can run the app locally using `./script/run` - see [WARP.md](WARP.md) for more details on how to get set up.

Expand Down
2 changes: 2 additions & 0 deletions app/src/ai/agent/api/convert_conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub fn convert_conversation_data_to_ai_conversation(
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: None,
Expand All @@ -97,6 +98,7 @@ pub fn convert_conversation_data_to_ai_conversation(
artifacts_json: serde_json::to_string(&metadata.artifacts).ok(),
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
// TODO: Populate run_id from server metadata once it is exposed
Expand Down
33 changes: 33 additions & 0 deletions app/src/ai/agent/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::{collections::HashMap, fmt::Display};
use warp_cli::agent::Harness;

use super::task_store::TaskStore;
use uuid::Uuid;
Expand Down Expand Up @@ -217,6 +218,8 @@ pub struct AIConversation {
parent_agent_id: Option<String>,
/// The display name for this agent (e.g. "Agent 1"), assigned by the orchestrator.
agent_name: Option<String>,
Comment thread
kjankov marked this conversation as resolved.
/// Harness metadata associated with this child agent in orchestration flows.
orchestration_harness_type: Option<String>,
/// The local conversation ID of the parent that spawned this child, if any.
parent_conversation_id: Option<AIConversationId>,
/// True when this conversation is a placeholder for a child agent executing
Expand Down Expand Up @@ -282,6 +285,7 @@ impl AIConversation {
artifacts: Vec::new(),
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
last_event_sequence: None,
Expand Down Expand Up @@ -364,6 +368,7 @@ impl AIConversation {
artifacts,
parent_agent_id,
agent_name,
orchestration_harness_type,
parent_conversation_id,
is_remote_child,
run_id,
Expand All @@ -388,6 +393,7 @@ impl AIConversation {
.unwrap_or_default();
let parent_agent_id = data.parent_agent_id;
let agent_name = data.agent_name;
let orchestration_harness_type = data.orchestration_harness_type;
let parent_conversation_id = data
.parent_conversation_id
.and_then(|id| AIConversationId::try_from(id).ok());
Expand All @@ -410,6 +416,7 @@ impl AIConversation {
artifacts,
parent_agent_id,
agent_name,
orchestration_harness_type,
parent_conversation_id,
is_remote_child,
run_id,
Expand All @@ -426,6 +433,7 @@ impl AIConversation {
None,
None,
None,
None,
false,
None,
AIConversationAutoexecuteMode::default(),
Expand Down Expand Up @@ -470,6 +478,7 @@ impl AIConversation {
artifacts,
parent_agent_id,
agent_name,
orchestration_harness_type,
parent_conversation_id,
is_remote_child,
last_event_sequence,
Expand Down Expand Up @@ -808,6 +817,24 @@ impl AIConversation {
pub fn set_agent_name(&mut self, name: String) {
self.agent_name = Some(name);
}
pub fn orchestration_harness_type(&self) -> Option<&str> {
self.orchestration_harness_type.as_deref()
}

pub fn orchestration_harness(&self) -> Option<Harness> {
self.orchestration_harness_type
.as_deref()
.map(parse_orchestration_harness_type)
.or_else(|| {
self.server_metadata
.as_ref()
.map(|metadata| Harness::from(metadata.harness))
})
}

pub fn set_orchestration_harness(&mut self, harness: Harness) {
self.orchestration_harness_type = Some(harness.config_name().to_string());
}

pub fn parent_conversation_id(&self) -> Option<AIConversationId> {
self.parent_conversation_id
Expand Down Expand Up @@ -2909,6 +2936,7 @@ impl AIConversation {
artifacts_json,
parent_agent_id: self.parent_agent_id.clone(),
agent_name: self.agent_name.clone(),
orchestration_harness_type: self.orchestration_harness_type.clone(),
parent_conversation_id: self.parent_conversation_id.map(|id| id.to_string()),
is_remote_child: self.is_remote_child,
run_id: self.task_id.map(|id| id.to_string()),
Expand Down Expand Up @@ -3407,6 +3435,11 @@ impl AIConversation {
}
}

fn parse_orchestration_harness_type(value: &str) -> Harness {
Harness::from_config_name(value)
.or_else(|| Harness::parse_orchestration_harness(value))
.unwrap_or(Harness::Unknown)
}
pub(super) fn update_todo_list_from_todo_op(
todo_lists: &mut Vec<AIAgentTodoList>,
op: api::message::update_todos::Operation,
Expand Down
12 changes: 12 additions & 0 deletions app/src/ai/agent_conversations_model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ fn test_display_status_uses_matching_conversation_for_in_progress_task() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -309,6 +310,7 @@ fn test_display_status_uses_active_execution_over_previous_conversation_status()
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -370,6 +372,7 @@ fn test_display_status_updates_when_blocked_conversation_resumes() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -447,6 +450,7 @@ fn test_display_status_terminal_task_state_overrides_matching_conversation() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -500,6 +504,7 @@ fn test_status_filter_uses_display_status_for_task_backed_conversations() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -789,6 +794,7 @@ fn test_get_entries_merges_task_and_local_conversation_by_run_id() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -841,6 +847,7 @@ fn test_get_entries_merges_task_and_local_conversation_by_server_token() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: None,
Expand Down Expand Up @@ -1008,6 +1015,7 @@ fn test_resolve_open_action_falls_back_to_local_conversation_for_invalid_session
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -1297,6 +1305,7 @@ fn test_server_token_assignment_updates_copy_link_resolution() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: None,
Expand Down Expand Up @@ -1391,6 +1400,7 @@ fn test_resolve_copy_link_uses_attached_synced_conversation_for_task_without_tok
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -1715,6 +1725,7 @@ fn test_get_entries_prefers_task_when_task_id_matches_conversation_run_id() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: Some(task_id.clone()),
Expand Down Expand Up @@ -1773,6 +1784,7 @@ fn test_get_entries_prefers_task_when_server_token_matches() {
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
run_id: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fn execute_returns_error_when_child_startup_is_blocked_before_initialization() {
terminal_view_id,
"Agent 1".to_string(),
parent_conversation_id,
None,
ctx,
)
});
Expand Down Expand Up @@ -154,6 +155,7 @@ fn execute_returns_detailed_error_when_child_startup_fails_before_initialization
terminal_view_id,
"Agent 1".to_string(),
parent_conversation_id,
None,
ctx,
)
});
Expand Down Expand Up @@ -410,6 +412,7 @@ fn parallel_pendings_each_resolve_independently_via_recorded_child_id() {
terminal_view_id,
"Agent A".to_string(),
parent_conversation_id,
None,
ctx,
)
});
Expand All @@ -418,6 +421,7 @@ fn parallel_pendings_each_resolve_independently_via_recorded_child_id() {
terminal_view_id,
"Agent B".to_string(),
parent_conversation_id,
None,
ctx,
)
});
Expand Down
1 change: 1 addition & 0 deletions app/src/ai/blocklist/agent_view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod controller;
mod ephemeral_message_model;
mod inline_agent_view_header;
// TODO: Move orchestration_conversation_links module import elsewhere.
pub(crate) mod orchestration_avatar;
pub(crate) mod orchestration_conversation_links;
pub mod orchestration_pill_bar;
pub mod shortcuts;
Expand Down
41 changes: 41 additions & 0 deletions app/src/ai/blocklist/agent_view/orchestration_avatar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use warpui::elements::Element;
use warpui::{AppContext, SingletonEntity};

use crate::ai::blocklist::agent_view::orchestration_pill_bar::{
render_agent_avatar_disc, render_orchestrator_avatar_disc,
};
use crate::appearance::Appearance;

const TRANSCRIPT_AVATAR_SCALE: f32 = 1.25;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum OrchestrationAvatar {
Orchestrator,
Agent { display_name: String },
}

impl OrchestrationAvatar {
pub(crate) fn agent(display_name: String) -> Self {
Self::Agent { display_name }
}

pub(crate) fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let size = app.font_cache().line_height(
appearance.monospace_font_size(),
appearance.line_height_ratio(),
) * TRANSCRIPT_AVATAR_SCALE;

match self {
Self::Orchestrator => render_orchestrator_avatar_disc(size, theme, appearance),
Self::Agent { display_name } => {
render_agent_avatar_disc(display_name, size, theme, appearance)
}
}
}
}

#[cfg(test)]
#[path = "orchestration_avatar_tests.rs"]
mod tests;
11 changes: 11 additions & 0 deletions app/src/ai/blocklist/agent_view/orchestration_avatar_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use super::OrchestrationAvatar;

#[test]
fn agent_avatar_identity_is_display_name_based_for_pill_consistency() {
assert_eq!(
OrchestrationAvatar::agent("Agent 1".to_string()),
OrchestrationAvatar::Agent {
display_name: "Agent 1".to_string(),
}
);
}
Loading
Loading