Skip to content

Commit e75bf80

Browse files
kjankovoz-agent
andauthored
Update orchestration message transcript UI (#10285)
## Description - Updates successful orchestration send/receive messages to render as transcript-style rows with the agent avatar beside the message content instead of boxed status cards. - Uses the orchestrator avatar for the lead/parent agent, existing harness/provider logos for known child agents, and distinct fallback child avatars for Oz/unknown child agents. - Persists child harness metadata so orchestration avatars can be restored consistently. ## Linked Issue - Linear: [QUALITY-594 Send message/received message UI](https://linear.app/warpdotdev/issue/QUALITY-594/send-messagereceived-message-ui) - [ ] The linked issue is labeled `ready-to-spec` or `ready-to-implement`. - [ ] Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes). ## Screenshots / Videos Not included. ## Testing - `cargo fmt` - `cargo fmt --check` - `git --no-pager diff --check` - `cargo test -p warp orchestration --lib` - `cargo test -p warp hidden_child --lib` - `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings` ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode - Conversation: https://app.warp.dev/conversation/1b48f020-21b0-4b7b-a0e5-7274863bab36 - Plan: https://app.warp.dev/drive/notebook/R5oEvNd2TWKKGqNoBVs9zE Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent c2565f5 commit e75bf80

27 files changed

Lines changed: 721 additions & 197 deletions

.github/pull_request_template.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ Link the GitHub issue this PR addresses. Before opening this PR, please confirm:
1212
<!--
1313
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?
1414
15-
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.
15+
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.
1616
17-
You can run the app locally using `./script/run` - see WARP.md for more details on how to get set up.
17+
You can run the app locally using `./script/run` - see WARP.md for more details on how to get set up.
1818
-->
1919

2020
- [ ] I have manually tested my changes locally with `./script/run`

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ cargo run # build and run Warp
141141
Tests are required for most code changes:
142142

143143
### Manual Testing
144-
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.
144+
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.
145145

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

app/src/ai/agent/api/convert_conversation.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub fn convert_conversation_data_to_ai_conversation(
8181
artifacts_json: None,
8282
parent_agent_id: None,
8383
agent_name: None,
84+
orchestration_harness_type: None,
8485
parent_conversation_id: None,
8586
is_remote_child: false,
8687
run_id: None,
@@ -97,6 +98,7 @@ pub fn convert_conversation_data_to_ai_conversation(
9798
artifacts_json: serde_json::to_string(&metadata.artifacts).ok(),
9899
parent_agent_id: None,
99100
agent_name: None,
101+
orchestration_harness_type: None,
100102
parent_conversation_id: None,
101103
is_remote_child: false,
102104
// TODO: Populate run_id from server metadata once it is exposed

app/src/ai/agent/conversation.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use itertools::Itertools as _;
2323
use serde::{Deserialize, Serialize};
2424
use std::collections::HashSet;
2525
use std::{collections::HashMap, fmt::Display};
26+
use warp_cli::agent::Harness;
2627

2728
use super::task_store::TaskStore;
2829
use uuid::Uuid;
@@ -217,6 +218,8 @@ pub struct AIConversation {
217218
parent_agent_id: Option<String>,
218219
/// The display name for this agent (e.g. "Agent 1"), assigned by the orchestrator.
219220
agent_name: Option<String>,
221+
/// Harness metadata associated with this child agent in orchestration flows.
222+
orchestration_harness_type: Option<String>,
220223
/// The local conversation ID of the parent that spawned this child, if any.
221224
parent_conversation_id: Option<AIConversationId>,
222225
/// True when this conversation is a placeholder for a child agent executing
@@ -282,6 +285,7 @@ impl AIConversation {
282285
artifacts: Vec::new(),
283286
parent_agent_id: None,
284287
agent_name: None,
288+
orchestration_harness_type: None,
285289
parent_conversation_id: None,
286290
is_remote_child: false,
287291
last_event_sequence: None,
@@ -364,6 +368,7 @@ impl AIConversation {
364368
artifacts,
365369
parent_agent_id,
366370
agent_name,
371+
orchestration_harness_type,
367372
parent_conversation_id,
368373
is_remote_child,
369374
run_id,
@@ -388,6 +393,7 @@ impl AIConversation {
388393
.unwrap_or_default();
389394
let parent_agent_id = data.parent_agent_id;
390395
let agent_name = data.agent_name;
396+
let orchestration_harness_type = data.orchestration_harness_type;
391397
let parent_conversation_id = data
392398
.parent_conversation_id
393399
.and_then(|id| AIConversationId::try_from(id).ok());
@@ -410,6 +416,7 @@ impl AIConversation {
410416
artifacts,
411417
parent_agent_id,
412418
agent_name,
419+
orchestration_harness_type,
413420
parent_conversation_id,
414421
is_remote_child,
415422
run_id,
@@ -426,6 +433,7 @@ impl AIConversation {
426433
None,
427434
None,
428435
None,
436+
None,
429437
false,
430438
None,
431439
AIConversationAutoexecuteMode::default(),
@@ -470,6 +478,7 @@ impl AIConversation {
470478
artifacts,
471479
parent_agent_id,
472480
agent_name,
481+
orchestration_harness_type,
473482
parent_conversation_id,
474483
is_remote_child,
475484
last_event_sequence,
@@ -808,6 +817,24 @@ impl AIConversation {
808817
pub fn set_agent_name(&mut self, name: String) {
809818
self.agent_name = Some(name);
810819
}
820+
pub fn orchestration_harness_type(&self) -> Option<&str> {
821+
self.orchestration_harness_type.as_deref()
822+
}
823+
824+
pub fn orchestration_harness(&self) -> Option<Harness> {
825+
self.orchestration_harness_type
826+
.as_deref()
827+
.map(parse_orchestration_harness_type)
828+
.or_else(|| {
829+
self.server_metadata
830+
.as_ref()
831+
.map(|metadata| Harness::from(metadata.harness))
832+
})
833+
}
834+
835+
pub fn set_orchestration_harness(&mut self, harness: Harness) {
836+
self.orchestration_harness_type = Some(harness.config_name().to_string());
837+
}
811838

812839
pub fn parent_conversation_id(&self) -> Option<AIConversationId> {
813840
self.parent_conversation_id
@@ -2909,6 +2936,7 @@ impl AIConversation {
29092936
artifacts_json,
29102937
parent_agent_id: self.parent_agent_id.clone(),
29112938
agent_name: self.agent_name.clone(),
2939+
orchestration_harness_type: self.orchestration_harness_type.clone(),
29122940
parent_conversation_id: self.parent_conversation_id.map(|id| id.to_string()),
29132941
is_remote_child: self.is_remote_child,
29142942
run_id: self.task_id.map(|id| id.to_string()),
@@ -3407,6 +3435,11 @@ impl AIConversation {
34073435
}
34083436
}
34093437

3438+
fn parse_orchestration_harness_type(value: &str) -> Harness {
3439+
Harness::from_config_name(value)
3440+
.or_else(|| Harness::parse_orchestration_harness(value))
3441+
.unwrap_or(Harness::Unknown)
3442+
}
34103443
pub(super) fn update_todo_list_from_todo_op(
34113444
todo_lists: &mut Vec<AIAgentTodoList>,
34123445
op: api::message::update_todos::Operation,

app/src/ai/agent_conversations_model_tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ fn test_display_status_uses_matching_conversation_for_in_progress_task() {
255255
artifacts_json: None,
256256
parent_agent_id: None,
257257
agent_name: None,
258+
orchestration_harness_type: None,
258259
parent_conversation_id: None,
259260
is_remote_child: false,
260261
run_id: Some(task_id.clone()),
@@ -309,6 +310,7 @@ fn test_display_status_uses_active_execution_over_previous_conversation_status()
309310
artifacts_json: None,
310311
parent_agent_id: None,
311312
agent_name: None,
313+
orchestration_harness_type: None,
312314
parent_conversation_id: None,
313315
is_remote_child: false,
314316
run_id: Some(task_id.clone()),
@@ -370,6 +372,7 @@ fn test_display_status_updates_when_blocked_conversation_resumes() {
370372
artifacts_json: None,
371373
parent_agent_id: None,
372374
agent_name: None,
375+
orchestration_harness_type: None,
373376
parent_conversation_id: None,
374377
is_remote_child: false,
375378
run_id: Some(task_id.clone()),
@@ -447,6 +450,7 @@ fn test_display_status_terminal_task_state_overrides_matching_conversation() {
447450
artifacts_json: None,
448451
parent_agent_id: None,
449452
agent_name: None,
453+
orchestration_harness_type: None,
450454
parent_conversation_id: None,
451455
is_remote_child: false,
452456
run_id: Some(task_id.clone()),
@@ -500,6 +504,7 @@ fn test_status_filter_uses_display_status_for_task_backed_conversations() {
500504
artifacts_json: None,
501505
parent_agent_id: None,
502506
agent_name: None,
507+
orchestration_harness_type: None,
503508
parent_conversation_id: None,
504509
is_remote_child: false,
505510
run_id: Some(task_id.clone()),
@@ -789,6 +794,7 @@ fn test_get_entries_merges_task_and_local_conversation_by_run_id() {
789794
artifacts_json: None,
790795
parent_agent_id: None,
791796
agent_name: None,
797+
orchestration_harness_type: None,
792798
parent_conversation_id: None,
793799
is_remote_child: false,
794800
run_id: Some(task_id.clone()),
@@ -841,6 +847,7 @@ fn test_get_entries_merges_task_and_local_conversation_by_server_token() {
841847
artifacts_json: None,
842848
parent_agent_id: None,
843849
agent_name: None,
850+
orchestration_harness_type: None,
844851
parent_conversation_id: None,
845852
is_remote_child: false,
846853
run_id: None,
@@ -1008,6 +1015,7 @@ fn test_resolve_open_action_falls_back_to_local_conversation_for_invalid_session
10081015
artifacts_json: None,
10091016
parent_agent_id: None,
10101017
agent_name: None,
1018+
orchestration_harness_type: None,
10111019
parent_conversation_id: None,
10121020
is_remote_child: false,
10131021
run_id: Some(task_id.clone()),
@@ -1297,6 +1305,7 @@ fn test_server_token_assignment_updates_copy_link_resolution() {
12971305
artifacts_json: None,
12981306
parent_agent_id: None,
12991307
agent_name: None,
1308+
orchestration_harness_type: None,
13001309
parent_conversation_id: None,
13011310
is_remote_child: false,
13021311
run_id: None,
@@ -1391,6 +1400,7 @@ fn test_resolve_copy_link_uses_attached_synced_conversation_for_task_without_tok
13911400
artifacts_json: None,
13921401
parent_agent_id: None,
13931402
agent_name: None,
1403+
orchestration_harness_type: None,
13941404
parent_conversation_id: None,
13951405
is_remote_child: false,
13961406
run_id: Some(task_id.clone()),
@@ -1715,6 +1725,7 @@ fn test_get_entries_prefers_task_when_task_id_matches_conversation_run_id() {
17151725
artifacts_json: None,
17161726
parent_agent_id: None,
17171727
agent_name: None,
1728+
orchestration_harness_type: None,
17181729
parent_conversation_id: None,
17191730
is_remote_child: false,
17201731
run_id: Some(task_id.clone()),
@@ -1773,6 +1784,7 @@ fn test_get_entries_prefers_task_when_server_token_matches() {
17731784
artifacts_json: None,
17741785
parent_agent_id: None,
17751786
agent_name: None,
1787+
orchestration_harness_type: None,
17761788
parent_conversation_id: None,
17771789
is_remote_child: false,
17781790
run_id: None,

app/src/ai/blocklist/action_model/execute/start_agent_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ fn execute_returns_error_when_child_startup_is_blocked_before_initialization() {
6666
terminal_view_id,
6767
"Agent 1".to_string(),
6868
parent_conversation_id,
69+
None,
6970
ctx,
7071
)
7172
});
@@ -154,6 +155,7 @@ fn execute_returns_detailed_error_when_child_startup_fails_before_initialization
154155
terminal_view_id,
155156
"Agent 1".to_string(),
156157
parent_conversation_id,
158+
None,
157159
ctx,
158160
)
159161
});
@@ -410,6 +412,7 @@ fn parallel_pendings_each_resolve_independently_via_recorded_child_id() {
410412
terminal_view_id,
411413
"Agent A".to_string(),
412414
parent_conversation_id,
415+
None,
413416
ctx,
414417
)
415418
});
@@ -418,6 +421,7 @@ fn parallel_pendings_each_resolve_independently_via_recorded_child_id() {
418421
terminal_view_id,
419422
"Agent B".to_string(),
420423
parent_conversation_id,
424+
None,
421425
ctx,
422426
)
423427
});

app/src/ai/blocklist/agent_view/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod controller;
66
mod ephemeral_message_model;
77
mod inline_agent_view_header;
88
// TODO: Move orchestration_conversation_links module import elsewhere.
9+
pub(crate) mod orchestration_avatar;
910
pub(crate) mod orchestration_conversation_links;
1011
pub mod orchestration_pill_bar;
1112
pub mod shortcuts;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use warpui::elements::Element;
2+
use warpui::{AppContext, SingletonEntity};
3+
4+
use crate::ai::blocklist::agent_view::orchestration_pill_bar::{
5+
render_agent_avatar_disc, render_orchestrator_avatar_disc,
6+
};
7+
use crate::appearance::Appearance;
8+
9+
const TRANSCRIPT_AVATAR_SCALE: f32 = 1.25;
10+
11+
#[derive(Clone, Debug, Eq, PartialEq)]
12+
pub(crate) enum OrchestrationAvatar {
13+
Orchestrator,
14+
Agent { display_name: String },
15+
}
16+
17+
impl OrchestrationAvatar {
18+
pub(crate) fn agent(display_name: String) -> Self {
19+
Self::Agent { display_name }
20+
}
21+
22+
pub(crate) fn render(&self, app: &AppContext) -> Box<dyn Element> {
23+
let appearance = Appearance::as_ref(app);
24+
let theme = appearance.theme();
25+
let size = app.font_cache().line_height(
26+
appearance.monospace_font_size(),
27+
appearance.line_height_ratio(),
28+
) * TRANSCRIPT_AVATAR_SCALE;
29+
30+
match self {
31+
Self::Orchestrator => render_orchestrator_avatar_disc(size, theme, appearance),
32+
Self::Agent { display_name } => {
33+
render_agent_avatar_disc(display_name, size, theme, appearance)
34+
}
35+
}
36+
}
37+
}
38+
39+
#[cfg(test)]
40+
#[path = "orchestration_avatar_tests.rs"]
41+
mod tests;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use super::OrchestrationAvatar;
2+
3+
#[test]
4+
fn agent_avatar_identity_is_display_name_based_for_pill_consistency() {
5+
assert_eq!(
6+
OrchestrationAvatar::agent("Agent 1".to_string()),
7+
OrchestrationAvatar::Agent {
8+
display_name: "Agent 1".to_string(),
9+
}
10+
);
11+
}

0 commit comments

Comments
 (0)