feat: livrable ticket #91 — notification fin BackgroundTask enrichie
- Backend: enrichissement HeadlessRendezvous avec requester/target/conversationId - Frontend: toast 'Requester -> Target completed/...' explicite - Clic ouvrant viewer de conversation pour visualiser l'échange
This commit is contained in:
@ -127,6 +127,22 @@ fn resolve_background_cwd(
|
||||
ProjectPath::new(path).map_err(|_| AppError::Invalid("invalid background task cwd".to_owned()))
|
||||
}
|
||||
|
||||
fn rendezvous_context_for_task(task: &BackgroundTask) -> Option<domain::RendezvousContext> {
|
||||
match &task.kind {
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
..
|
||||
} => Some(domain::RendezvousContext {
|
||||
requester_agent_id: *requester_agent_id,
|
||||
target_agent_id: *target_agent_id,
|
||||
conversation_id: *conversation_id,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_path_no_parent(path: &Path) -> Result<PathBuf, AppError> {
|
||||
let mut out = PathBuf::new();
|
||||
for component in path.components() {
|
||||
@ -847,22 +863,26 @@ impl OrchestratorService {
|
||||
if task.is_terminal() {
|
||||
return Ok(());
|
||||
}
|
||||
let rendezvous = rendezvous_context_for_task(&task);
|
||||
let event = match &result {
|
||||
BackgroundTaskResult::Success { .. } => DomainEvent::BackgroundTaskCompleted {
|
||||
project_id: project.id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
},
|
||||
BackgroundTaskResult::Failure { .. } => DomainEvent::BackgroundTaskFailed {
|
||||
project_id: project.id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
},
|
||||
BackgroundTaskResult::Cancelled { .. } | BackgroundTaskResult::Expired { .. } => {
|
||||
DomainEvent::BackgroundTaskCancelled {
|
||||
project_id: project.id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous,
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2967,6 +2987,52 @@ mod tests {
|
||||
assert_eq!(submit.delay_ms, Some(CODEX_SUBMIT_DELAY_MS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendezvous_context_is_extracted_from_headless_background_task_kind() {
|
||||
let requester = aid(1);
|
||||
let target = aid(2);
|
||||
let conversation_id = domain::ConversationId::from_uuid(uuid::Uuid::from_u128(3));
|
||||
let task = BackgroundTask::new(
|
||||
TaskId::from_uuid(uuid::Uuid::from_u128(4)),
|
||||
domain::ProjectId::from_uuid(uuid::Uuid::from_u128(5)),
|
||||
target,
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id: Some(requester),
|
||||
target_agent_id: target,
|
||||
ticket_id: TicketId::from_uuid(uuid::Uuid::from_u128(6)),
|
||||
conversation_id,
|
||||
},
|
||||
BackgroundTaskWakePolicy::RecordOnly,
|
||||
100,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let context = rendezvous_context_for_task(&task).expect("rendezvous context");
|
||||
|
||||
assert_eq!(context.requester_agent_id, Some(requester));
|
||||
assert_eq!(context.target_agent_id, target);
|
||||
assert_eq!(context.conversation_id, conversation_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendezvous_context_is_absent_for_command_background_task() {
|
||||
let task = BackgroundTask::new(
|
||||
TaskId::from_uuid(uuid::Uuid::from_u128(7)),
|
||||
domain::ProjectId::from_uuid(uuid::Uuid::from_u128(8)),
|
||||
aid(9),
|
||||
BackgroundTaskKind::Command {
|
||||
label: "cargo test".to_owned(),
|
||||
},
|
||||
BackgroundTaskWakePolicy::RecordOnly,
|
||||
100,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendezvous_context_for_task(&task), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_profile_submit_delay_is_preserved() {
|
||||
let p = profile(3, "OpenAI Codex CLI", "codex")
|
||||
|
||||
@ -231,6 +231,12 @@ pub struct AgentBackgroundTaskState {
|
||||
pub stdout_tail: Option<String>,
|
||||
/// Bounded stderr tail.
|
||||
pub stderr_tail: Option<String>,
|
||||
/// Agent that requested a headless rendezvous, when this task is one.
|
||||
pub requester_agent_id: Option<AgentId>,
|
||||
/// Target agent for a headless rendezvous, when this task is one.
|
||||
pub target_agent_id: Option<AgentId>,
|
||||
/// Conversation opened by a headless rendezvous, when this task is one.
|
||||
pub conversation_id: Option<domain::ConversationId>,
|
||||
/// Creation timestamp, epoch milliseconds.
|
||||
pub created_at_ms: u64,
|
||||
/// Last update timestamp, epoch milliseconds.
|
||||
@ -592,6 +598,8 @@ impl GetProjectWorkState {
|
||||
impl From<BackgroundTask> for AgentBackgroundTaskState {
|
||||
fn from(task: BackgroundTask) -> Self {
|
||||
let (exit_code, summary, stdout_tail, stderr_tail) = flatten_background_result(&task);
|
||||
let (requester_agent_id, target_agent_id, conversation_id) =
|
||||
flatten_background_rendezvous_context(&task.kind);
|
||||
Self {
|
||||
task_id: task.id,
|
||||
kind: BackgroundTaskKindLabel::from(&task.kind),
|
||||
@ -600,12 +608,37 @@ impl From<BackgroundTask> for AgentBackgroundTaskState {
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_background_rendezvous_context(
|
||||
kind: &BackgroundTaskKind,
|
||||
) -> (
|
||||
Option<AgentId>,
|
||||
Option<AgentId>,
|
||||
Option<domain::ConversationId>,
|
||||
) {
|
||||
match kind {
|
||||
BackgroundTaskKind::HeadlessRendezvous {
|
||||
requester_agent_id,
|
||||
target_agent_id,
|
||||
conversation_id,
|
||||
..
|
||||
} => (
|
||||
*requester_agent_id,
|
||||
Some(*target_agent_id),
|
||||
Some(*conversation_id),
|
||||
),
|
||||
_ => (None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&BackgroundTaskKind> for BackgroundTaskKindLabel {
|
||||
fn from(kind: &BackgroundTaskKind) -> Self {
|
||||
match kind {
|
||||
|
||||
Reference in New Issue
Block a user