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:
2026-07-27 09:05:34 +02:00
parent 02603441c1
commit 038e90ecec
12 changed files with 563 additions and 35 deletions

View File

@ -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")

View File

@ -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 {