| @@ -14,8 +14,8 @@ use super::Workspace; |
| 14 | 14 | use crate::models::{Catalog, ProbeStatus}; |
| 15 | 15 | use crate::orchestrator::{self, CAPABILITIES, DELEGATE, TASK_CANCEL, TASK_STATUS, ToolRequest}; |
| 16 | 16 | use crate::session::{ |
| 17 | | - ChatItem, NewSession, Reasoning, Session, SessionEvent, SessionStatus, SubagentMember, SubagentParent, TaskStatus, |
| 18 | | - brief_input, to_millis, |
| 17 | + ChatItem, Delegation, NewSession, Reasoning, Session, SessionEvent, SessionStatus, SubagentMember, SubagentParent, |
| 18 | + TaskStatus, brief_input, to_millis, |
| 19 | 19 | }; |
| 20 | 20 | use crate::settings; |
| 21 | 21 | |
| @@ -469,3 +469,85 @@ fn capabilities(thread: &Session, cx: &App) -> Value { |
| 469 | 469 | "tools": [DELEGATE, TASK_STATUS, TASK_CANCEL, CAPABILITIES], |
| 470 | 470 | }) |
| 471 | 471 | } |
| 472 | + |
| 473 | + /// The thread a `delegate_task` call started, from its result: the reply's |
| 474 | + /// `childThreadId`, however the agent quoted it. |
| 475 | + fn delegated_thread(result: &str) -> Option<String> { |
| 476 | + let (_, rest) = result.split_once("childThreadId")?; |
| 477 | + let id: String = rest |
| 478 | + .trim_start_matches(|c: char| matches!(c, '"' | '\\' | ':') || c.is_whitespace()) |
| 479 | + .chars() |
| 480 | + .take_while(|c| c.is_ascii_alphanumeric() || *c == '-') |
| 481 | + .collect(); |
| 482 | + (!id.is_empty()).then_some(id) |
| 483 | + } |
| 484 | + |
| 485 | + impl Workspace { |
| 486 | + /// Marks subagents' threads again when a build that didn't know them |
| 487 | + /// saved them as ordinary threads, from the `delegate_task` calls that |
| 488 | + /// started them, and gives their parents back their rows. |
| 489 | + pub(super) fn recover_delegations(&mut self, cx: &mut Context<Self>) { |
| 490 | + let sessions: Vec<Entity<Session>> = self |
| 491 | + .projects |
| 492 | + .iter() |
| 493 | + .filter(|project| project.environment.is_none()) |
| 494 | + .flat_map(|project| project.sessions.clone()) |
| 495 | + .collect(); |
| 496 | + let mut started = Vec::new(); |
| 497 | + for parent in &sessions { |
| 498 | + for (ix, item) in parent.read(cx).items.iter().enumerate() { |
| 499 | + if let ChatItem::Tool(tool) = item |
| 500 | + && tool.is_delegation() |
| 501 | + && let Some(child) = tool.result.as_deref().and_then(delegated_thread) |
| 502 | + { |
| 503 | + started.push((parent.clone(), ix, child)); |
| 504 | + } |
| 505 | + } |
| 506 | + } |
| 507 | + for (parent, ix, child_id) in started { |
| 508 | + let Some(child) = sessions.iter().find(|session| session.read(cx).id == child_id) else { continue }; |
| 509 | + if child.read(cx).delegation.is_some() || child == &parent { |
| 510 | + continue; |
| 511 | + } |
| 512 | + let parent_id = parent.read(cx).id.clone(); |
| 513 | + child.update(cx, |child, _| { |
| 514 | + child.delegation = Some(Delegation { parent: parent_id, delivered: true, cancelled: false }); |
| 515 | + }); |
| 516 | + let state = child.read(cx); |
| 517 | + let status = TaskState::of(state).panel_status(); |
| 518 | + let member = SubagentMember { |
| 519 | + id: child_id.clone(), |
| 520 | + label: state.title.to_string(), |
| 521 | + status: status.key().to_string(), |
| 522 | + started: to_millis(state.created), |
| 523 | + finished: (status != TaskStatus::Running).then(|| to_millis(state.updated)), |
| 524 | + tokens: state.context_tokens, |
| 525 | + summary: state.last_answer().as_deref().and_then(last_line), |
| 526 | + ..Default::default() |
| 527 | + }; |
| 528 | + parent.update(cx, |parent, _| { |
| 529 | + if let Some(ChatItem::Tool(tool)) = parent.items.get_mut(ix) { |
| 530 | + let progress = tool.subagent.get_or_insert_with(Default::default); |
| 531 | + if !progress.members.iter().any(|member| member.id == child_id) { |
| 532 | + progress.members.push(member); |
| 533 | + } |
| 534 | + } |
| 535 | + }); |
| 536 | + self.dirty.insert(child.entity_id()); |
| 537 | + self.dirty.insert(parent.entity_id()); |
| 538 | + } |
| 539 | + } |
| 540 | + } |
| 541 | + |
| 542 | + #[cfg(test)] |
| 543 | + mod tests { |
| 544 | + use super::delegated_thread; |
| 545 | + |
| 546 | + #[test] |
| 547 | + fn finds_the_child_thread_however_the_result_is_quoted() { |
| 548 | + let id = Some("1a127-118828-4".to_string()); |
| 549 | + assert_eq!(delegated_thread("{\n \"taskId\": \"1a127-118828-4\",\n \"childThreadId\": \"1a127-118828-4\"\n}"), id); |
| 550 | + assert_eq!(delegated_thread(r#"[{"type":"text","text":"{\"childThreadId\":\"1a127-118828-4\"}"}]"#), id); |
| 551 | + assert_eq!(delegated_thread("The task failed to start."), None); |
| 552 | + } |
| 553 | + } |