From b48d5b874f1c0a338ffa2ea1ce0ce717720b0e2b Mon Sep 17 00:00:00 2001
From: coso
Date: Sun, 22 Mar 2026 00:50:45 +0800
Subject: [PATCH] fix: include full v0.93.0 release changes
---
src-tauri/crates/agent/src/session_store.rs | 21 +
.../crates/agent/src/subagent_profiles.rs | 20 +-
src-tauri/src/commands/aster_agent_cmd/dto.rs | 4 +
src-tauri/src/commands/aster_agent_cmd/mod.rs | 6 +
.../aster_agent_cmd/prompt_context.rs | 200 ++++++---
.../run_metadata/request_metadata.rs | 6 +
.../commands/aster_agent_cmd/runtime_turn.rs | 400 +++++++++++++++++-
.../aster_agent_cmd/subagent_runtime.rs | 20 +-
.../src/commands/aster_agent_cmd/tests.rs | 192 ++++++++-
.../tool_runtime/subagent_tools.rs | 14 +
.../agent/chat/AgentChatWorkspace.tsx | 57 ++-
.../components/TeamWorkbenchSummaryPanel.tsx | 2 +-
.../components/TeamWorkspaceBoard.test.tsx | 109 +++++
.../chat/components/TeamWorkspaceBoard.tsx | 319 +++++++++++---
.../hooks/useRuntimeTeamFormation.test.tsx | 127 ++++--
.../chat/hooks/useRuntimeTeamFormation.ts | 126 +++---
.../chat/utils/harnessRequestMetadata.test.ts | 58 +++
.../chat/utils/harnessRequestMetadata.ts | 55 ++-
src/lib/api/agentRuntime.ts | 6 +
19 files changed, 1507 insertions(+), 235 deletions(-)
diff --git a/src-tauri/crates/agent/src/session_store.rs b/src-tauri/crates/agent/src/session_store.rs
index 114012cf7..202360569 100644
--- a/src-tauri/crates/agent/src/session_store.rs
+++ b/src-tauri/crates/agent/src/session_store.rs
@@ -99,6 +99,10 @@ pub struct ChildSubagentSession {
#[serde(skip_serializing_if = "Option::is_none")]
pub created_from_turn_id: Option,
#[serde(skip_serializing_if = "Option::is_none")]
+ pub blueprint_role_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub blueprint_role_label: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub profile_id: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_name: Option,
@@ -135,6 +139,10 @@ pub struct SubagentParentContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub created_from_turn_id: Option,
#[serde(skip_serializing_if = "Option::is_none")]
+ pub blueprint_role_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub blueprint_role_label: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub profile_id: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_name: Option,
@@ -379,6 +387,8 @@ fn build_child_subagent_session_summary(
role_hint: normalize_optional_text(metadata.role_hint),
origin_tool: normalize_optional_text(Some(metadata.origin_tool)),
created_from_turn_id: normalize_optional_text(metadata.created_from_turn_id),
+ blueprint_role_id: customization.blueprint_role_id,
+ blueprint_role_label: customization.blueprint_role_label,
profile_id: customization.profile_id,
profile_name: customization.profile_name,
role_key: customization.role_key,
@@ -441,6 +451,8 @@ fn build_subagent_parent_context(
task_summary: normalize_optional_nonempty_body(metadata.task_summary),
origin_tool: normalize_optional_text(Some(metadata.origin_tool)),
created_from_turn_id: normalize_optional_text(metadata.created_from_turn_id),
+ blueprint_role_id: customization.blueprint_role_id,
+ blueprint_role_label: customization.blueprint_role_label,
profile_id: customization.profile_id,
profile_name: customization.profile_name,
role_key: customization.role_key,
@@ -1281,6 +1293,8 @@ mod tests {
Some("turn-9"),
);
session.extension_data = SubagentCustomizationState {
+ blueprint_role_id: Some("runtime-explorer".to_string()),
+ blueprint_role_label: Some("分析".to_string()),
profile_id: Some("code-explorer".to_string()),
profile_name: Some("代码分析员".to_string()),
role_key: Some("explorer".to_string()),
@@ -1303,6 +1317,11 @@ mod tests {
let summary = build_child_subagent_session_summary(None, session)
.expect("child summary should exist");
+ assert_eq!(
+ summary.blueprint_role_id.as_deref(),
+ Some("runtime-explorer")
+ );
+ assert_eq!(summary.blueprint_role_label.as_deref(), Some("分析"));
assert_eq!(summary.profile_id.as_deref(), Some("code-explorer"));
assert_eq!(summary.profile_name.as_deref(), Some("代码分析员"));
assert_eq!(summary.role_key.as_deref(), Some("explorer"));
@@ -1439,6 +1458,8 @@ mod tests {
role_hint: Some("explorer".to_string()),
origin_tool: Some("spawn_agent".to_string()),
created_from_turn_id: Some("turn-1".to_string()),
+ blueprint_role_id: None,
+ blueprint_role_label: None,
profile_id: None,
profile_name: None,
role_key: None,
diff --git a/src-tauri/crates/agent/src/subagent_profiles.rs b/src-tauri/crates/agent/src/subagent_profiles.rs
index d5bc155a0..87d2b011d 100644
--- a/src-tauri/crates/agent/src/subagent_profiles.rs
+++ b/src-tauri/crates/agent/src/subagent_profiles.rs
@@ -43,6 +43,10 @@ pub struct TeamPresetSummary {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq)]
pub struct SubagentCustomizationState {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub blueprint_role_id: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub blueprint_role_label: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -89,7 +93,9 @@ impl SubagentCustomizationState {
}
pub fn is_empty(&self) -> bool {
- self.profile_id.is_none()
+ self.blueprint_role_id.is_none()
+ && self.blueprint_role_label.is_none()
+ && self.profile_id.is_none()
&& self.profile_name.is_none()
&& self.role_key.is_none()
&& self.team_preset_id.is_none()
@@ -369,6 +375,18 @@ pub fn build_subagent_customization_prompt(
builtin_team_preset_label_by_id(team_preset_id).unwrap_or(team_preset_id);
header_lines.push(format!("- 团队预设:{preset_label} ({team_preset_id})"));
}
+ if let Some(blueprint_role_label) = customization.blueprint_role_label.as_deref() {
+ let blueprint_role_id_suffix = customization
+ .blueprint_role_id
+ .as_deref()
+ .map(|role_id| format!(" ({role_id})"))
+ .unwrap_or_default();
+ header_lines.push(format!(
+ "- 蓝图角色:{blueprint_role_label}{blueprint_role_id_suffix}"
+ ));
+ } else if let Some(blueprint_role_id) = customization.blueprint_role_id.as_deref() {
+ header_lines.push(format!("- 蓝图角色 ID:{blueprint_role_id}"));
+ }
if let Some(profile_name) = customization.profile_name.as_deref() {
let profile_id_suffix = customization
.profile_id
diff --git a/src-tauri/src/commands/aster_agent_cmd/dto.rs b/src-tauri/src/commands/aster_agent_cmd/dto.rs
index df23d7b85..e5184e005 100644
--- a/src-tauri/src/commands/aster_agent_cmd/dto.rs
+++ b/src-tauri/src/commands/aster_agent_cmd/dto.rs
@@ -250,6 +250,10 @@ pub struct AgentRuntimeSpawnSubagentRequest {
pub reasoning_effort: Option,
#[serde(default, alias = "forkContext")]
pub fork_context: bool,
+ #[serde(default, alias = "blueprintRoleId")]
+ pub blueprint_role_id: Option,
+ #[serde(default, alias = "blueprintRoleLabel")]
+ pub blueprint_role_label: Option,
#[serde(default, alias = "profileId")]
pub profile_id: Option,
#[serde(default, alias = "profileName")]
diff --git a/src-tauri/src/commands/aster_agent_cmd/mod.rs b/src-tauri/src/commands/aster_agent_cmd/mod.rs
index 890a21d5a..a61cbbd5e 100644
--- a/src-tauri/src/commands/aster_agent_cmd/mod.rs
+++ b/src-tauri/src/commands/aster_agent_cmd/mod.rs
@@ -333,6 +333,12 @@ use run_metadata::{
resolve_social_run_artifact_descriptor,
};
pub(crate) use runtime_turn::{build_queued_turn_task, build_runtime_queue_executor};
+#[cfg(test)]
+pub(crate) use runtime_turn::{
+ build_runtime_prepared_team_spawn_message, parse_runtime_prepared_team_roles,
+ plan_runtime_prepared_team_actions, RuntimePreparedTeamAction, RuntimePreparedTeamRole,
+ RuntimePreparedTeamSessionCandidate,
+};
pub(crate) use session_runtime::{
delete_runtime_session_internal, persist_session_provider_routing,
resolve_session_provider_selector,
diff --git a/src-tauri/src/commands/aster_agent_cmd/prompt_context.rs b/src-tauri/src/commands/aster_agent_cmd/prompt_context.rs
index dc16c010c..22dcd2d6d 100644
--- a/src-tauri/src/commands/aster_agent_cmd/prompt_context.rs
+++ b/src-tauri/src/commands/aster_agent_cmd/prompt_context.rs
@@ -173,6 +173,76 @@ pub(crate) fn merge_system_prompt_with_elicitation_context(
}
}
+fn render_team_roles(role_items: &[serde_json::Value]) -> Vec {
+ role_items
+ .iter()
+ .filter_map(|value| {
+ let object = value.as_object()?;
+ let label = object
+ .get("label")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())?;
+ let summary = object
+ .get("summary")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .unwrap_or("负责当前分工。");
+ let role_id_suffix = object
+ .get("id")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(|value| format!(" / id: {value}"))
+ .unwrap_or_default();
+ let profile_suffix = object
+ .get("profile_id")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(|value| format!(" / profile: {value}"))
+ .unwrap_or_default();
+ let role_key_suffix = object
+ .get("role_key")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(|value| format!(" / roleKey: {value}"))
+ .unwrap_or_default();
+ let skill_suffix = object
+ .get("skill_ids")
+ .and_then(serde_json::Value::as_array)
+ .map(|items| {
+ items
+ .iter()
+ .filter_map(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .collect::>()
+ })
+ .filter(|items| !items.is_empty())
+ .map(|items| format!(" / skills: {}", items.join(", ")))
+ .unwrap_or_default();
+
+ Some(format!(
+ " - {label}:{summary}{role_id_suffix}{profile_suffix}{role_key_suffix}{skill_suffix}"
+ ))
+ })
+ .collect()
+}
+
+fn describe_turn_team_reason(reason: &str) -> &'static str {
+ match reason {
+ "runtime_team_prepared" => "GUI 已完成本轮 Team 预编队",
+ "runtime_team_generation_failed" => "GUI 尝试预编队失败,当前回合降级为单 Agent",
+ "subagent_disabled" => "当前回合未开启 Team 模式",
+ "turn_purpose_override" => "当前回合属于特定目的任务,不走 Team 预编队",
+ "single_agent_direct" => "当前回合判断为直接单 Agent 执行更合适",
+ _ => "GUI 已记录本轮 Team 判定",
+ }
+}
+
pub(crate) fn build_team_preference_system_prompt(
request_metadata: Option<&serde_json::Value>,
) -> Option {
@@ -201,6 +271,16 @@ pub(crate) fn build_team_preference_system_prompt(
request_metadata,
&["selected_team_roles", "selectedTeamRoles"],
);
+ let turn_team_decision = extract_harness_string(
+ request_metadata,
+ &["turn_team_decision", "turnTeamDecision"],
+ );
+ let turn_team_reason =
+ extract_harness_string(request_metadata, &["turn_team_reason", "turnTeamReason"]);
+ let turn_team_blueprint = extract_harness_nested_object(
+ request_metadata,
+ &["turn_team_blueprint", "turnTeamBlueprint"],
+ );
if !subagent_mode_enabled {
return None;
@@ -241,68 +321,84 @@ pub(crate) fn build_team_preference_system_prompt(
}
if let Some(role_items) = selected_team_roles {
- let rendered_roles = role_items
- .iter()
- .filter_map(|value| {
- let object = value.as_object()?;
- let label = object
- .get("label")
- .and_then(serde_json::Value::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())?;
- let summary = object
- .get("summary")
- .and_then(serde_json::Value::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .unwrap_or("负责当前分工。");
- let profile_suffix = object
- .get("profile_id")
- .and_then(serde_json::Value::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(|value| format!(" / profile: {value}"))
- .unwrap_or_default();
- let role_key_suffix = object
- .get("role_key")
- .and_then(serde_json::Value::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(|value| format!(" / roleKey: {value}"))
- .unwrap_or_default();
- let skill_suffix = object
- .get("skill_ids")
- .and_then(serde_json::Value::as_array)
- .map(|items| {
- items
- .iter()
- .filter_map(serde_json::Value::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .collect::>()
- })
- .filter(|items| !items.is_empty())
- .map(|items| format!(" / skills: {}", items.join(", ")))
- .unwrap_or_default();
-
- Some(format!(
- " - {label}:{summary}{profile_suffix}{role_key_suffix}{skill_suffix}"
- ))
- })
- .collect::>();
-
+ let rendered_roles = render_team_roles(role_items);
if !rendered_roles.is_empty() {
lines.push("- 当前 Team 角色参考:".to_string());
lines.extend(rendered_roles);
lines.push(
- "- 如果你决定调用 spawn_agent,请优先把上述 profile / roleKey / skillIds 映射到对应结构化字段,保持 GUI Team 画布与实际分工一致。"
+ "- 如果你决定调用 spawn_agent,请优先把上述 profile / roleKey / skillIds 映射到对应结构化字段;若该角色带有 id,优先同步写入 blueprintRoleId / blueprintRoleLabel,保持 GUI Team 画布与实际分工一致。"
.to_string(),
);
}
}
+ match turn_team_decision.as_deref() {
+ Some("team_prepared") => {
+ lines.push(
+ "- 当前回合已经在 GUI 发送前完成 Team 预编队;这是一份本轮执行契约,不是事后建议。"
+ .to_string(),
+ );
+ if let Some(reason) = turn_team_reason.as_deref() {
+ lines.push(format!(
+ "- GUI 判定:{}。",
+ describe_turn_team_reason(reason)
+ ));
+ }
+
+ if let Some(blueprint) = turn_team_blueprint {
+ let blueprint_label = blueprint
+ .get("label")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty());
+ let blueprint_description = blueprint
+ .get("description")
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty());
+ let rendered_roles = blueprint
+ .get("roles")
+ .and_then(serde_json::Value::as_array)
+ .map(|items| render_team_roles(items))
+ .unwrap_or_default();
+
+ if let Some(label) = blueprint_label {
+ lines.push(format!("- 本轮 Team 蓝图:{label}。"));
+ }
+ if let Some(description) = blueprint_description {
+ lines.push(format!("- 本轮蓝图说明:{description}"));
+ }
+ if !rendered_roles.is_empty() {
+ lines.push("- 本轮 Team 角色分工:".to_string());
+ lines.extend(rendered_roles);
+ }
+ }
+
+ lines.push(
+ "- 如果你决定调用 spawn_agent / send_input,应在本轮开局阶段优先按上述蓝图启动角色,并把蓝图里的 id / label 映射到 blueprintRoleId / blueprintRoleLabel,让各角色承担自己的输出,不要等主 agent 整轮完成后再补做 team。"
+ .to_string(),
+ );
+ }
+ Some("single_agent") => {
+ lines.push(
+ "- 当前回合未在 GUI 中预编队 Team,本轮默认按单 Agent 直接推进。".to_string(),
+ );
+ if let Some(reason) = turn_team_reason.as_deref() {
+ lines.push(format!(
+ "- GUI 判定:{}。",
+ describe_turn_team_reason(reason)
+ ));
+ }
+ lines.push(
+ "- 除非执行中出现明确的拆分必要性,否则不要为了形式化 team 而推迟主任务。"
+ .to_string(),
+ );
+ }
+ _ => {}
+ }
+
lines.push(
- "- spawn_agent 支持这些结构化字段:teamPresetId、profileId、profileName、roleKey、skillIds、skillDirectories、theme、systemOverlay、outputContract。"
+ "- spawn_agent 支持这些结构化字段:blueprintRoleId、blueprintRoleLabel、teamPresetId、profileId、profileName、roleKey、skillIds、skillDirectories、theme、systemOverlay、outputContract。"
.to_string(),
);
lines.push(
diff --git a/src-tauri/src/commands/aster_agent_cmd/run_metadata/request_metadata.rs b/src-tauri/src/commands/aster_agent_cmd/run_metadata/request_metadata.rs
index ae68342a1..13b3a4b0a 100644
--- a/src-tauri/src/commands/aster_agent_cmd/run_metadata/request_metadata.rs
+++ b/src-tauri/src/commands/aster_agent_cmd/run_metadata/request_metadata.rs
@@ -93,6 +93,12 @@ pub(in crate::commands::aster_agent_cmd) fn extend_map_with_harness_fields(
("selectedTeamSummary", "selected_team_summary"),
("selected_team_roles", "selected_team_roles"),
("selectedTeamRoles", "selected_team_roles"),
+ ("turn_team_decision", "turn_team_decision"),
+ ("turnTeamDecision", "turn_team_decision"),
+ ("turn_team_reason", "turn_team_reason"),
+ ("turnTeamReason", "turn_team_reason"),
+ ("turn_team_blueprint", "turn_team_blueprint"),
+ ("turnTeamBlueprint", "turn_team_blueprint"),
("browser_requirement", "browser_requirement"),
("browserRequirement", "browser_requirement"),
("browser_requirement_reason", "browser_requirement_reason"),
diff --git a/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs b/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs
index 711edb8f1..02e7bfac7 100644
--- a/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs
+++ b/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs
@@ -1,5 +1,352 @@
use super::*;
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct RuntimePreparedTeamRole {
+ pub(crate) id: String,
+ pub(crate) label: String,
+ pub(crate) summary: Option,
+ pub(crate) profile_id: Option,
+ pub(crate) role_key: Option,
+ pub(crate) skill_ids: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct RuntimePreparedTeamSessionCandidate {
+ pub(crate) blueprint_role_id: String,
+ pub(crate) session_id: String,
+ pub(crate) status_kind: SubagentRuntimeStatusKind,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum RuntimePreparedTeamAction {
+ Spawn(RuntimePreparedTeamRole),
+ Resume {
+ role: RuntimePreparedTeamRole,
+ session_id: String,
+ },
+}
+
+fn normalize_runtime_team_role_string(value: Option<&serde_json::Value>) -> Option {
+ normalize_optional_text(
+ value
+ .and_then(serde_json::Value::as_str)
+ .map(ToString::to_string),
+ )
+}
+
+fn normalize_runtime_team_role_id_fragment(value: &str) -> Option {
+ let normalized = value.split_whitespace().collect::>().join("-");
+ let normalized = normalized.trim_matches('-').to_string();
+ if normalized.is_empty() {
+ None
+ } else {
+ Some(normalized)
+ }
+}
+
+fn normalize_runtime_team_skill_ids(value: Option<&serde_json::Value>) -> Vec {
+ let Some(items) = value.and_then(serde_json::Value::as_array) else {
+ return Vec::new();
+ };
+
+ let mut seen = HashSet::new();
+ let mut skill_ids = Vec::new();
+ for item in items {
+ let Some(skill_id) = normalize_runtime_team_role_string(Some(item)) else {
+ continue;
+ };
+ if seen.insert(skill_id.clone()) {
+ skill_ids.push(skill_id);
+ }
+ }
+
+ skill_ids
+}
+
+fn parse_runtime_prepared_team_role(
+ role_value: &serde_json::Value,
+ index: usize,
+) -> Option {
+ let role = role_value.as_object()?;
+ let profile_id = normalize_runtime_team_role_string(role.get("profile_id"))
+ .or_else(|| normalize_runtime_team_role_string(role.get("profileId")));
+ let role_key = normalize_runtime_team_role_string(role.get("role_key"))
+ .or_else(|| normalize_runtime_team_role_string(role.get("roleKey")));
+ let label = normalize_runtime_team_role_string(role.get("label"))
+ .or_else(|| profile_id.clone())
+ .or_else(|| role_key.clone())
+ .unwrap_or_else(|| format!("角色 {}", index + 1));
+ let id = normalize_runtime_team_role_string(role.get("id"))
+ .or_else(|| {
+ profile_id
+ .as_deref()
+ .and_then(normalize_runtime_team_role_id_fragment)
+ .map(|fragment| format!("profile-{fragment}"))
+ })
+ .or_else(|| {
+ role_key
+ .as_deref()
+ .and_then(normalize_runtime_team_role_id_fragment)
+ .map(|fragment| format!("role-{fragment}"))
+ })
+ .or_else(|| {
+ normalize_runtime_team_role_id_fragment(&label)
+ .map(|fragment| format!("lane-{fragment}"))
+ })
+ .unwrap_or_else(|| format!("runtime-team-role-{}", index + 1));
+
+ Some(RuntimePreparedTeamRole {
+ id,
+ label,
+ summary: normalize_runtime_team_role_string(role.get("summary")),
+ profile_id,
+ role_key,
+ skill_ids: normalize_runtime_team_skill_ids(
+ role.get("skill_ids").or_else(|| role.get("skillIds")),
+ ),
+ })
+}
+
+pub(crate) fn parse_runtime_prepared_team_roles(
+ request_metadata: Option<&serde_json::Value>,
+) -> Vec {
+ if extract_harness_string(
+ request_metadata,
+ &["turn_team_decision", "turnTeamDecision"],
+ )
+ .as_deref()
+ != Some("team_prepared")
+ {
+ return Vec::new();
+ }
+
+ let Some(blueprint) = extract_harness_nested_object(
+ request_metadata,
+ &["turn_team_blueprint", "turnTeamBlueprint"],
+ ) else {
+ return Vec::new();
+ };
+ let Some(role_values) = blueprint.get("roles").and_then(serde_json::Value::as_array) else {
+ return Vec::new();
+ };
+
+ let mut seen = HashSet::new();
+ let mut roles = Vec::new();
+ for (index, role_value) in role_values.iter().enumerate() {
+ let Some(role) = parse_runtime_prepared_team_role(role_value, index) else {
+ continue;
+ };
+ if seen.insert(role.id.clone()) {
+ roles.push(role);
+ }
+ }
+
+ roles
+}
+
+fn runtime_prepared_team_session_rank(status_kind: SubagentRuntimeStatusKind) -> u8 {
+ match status_kind {
+ SubagentRuntimeStatusKind::Running => 7,
+ SubagentRuntimeStatusKind::Queued => 6,
+ SubagentRuntimeStatusKind::Idle => 5,
+ SubagentRuntimeStatusKind::Completed => 4,
+ SubagentRuntimeStatusKind::Failed => 3,
+ SubagentRuntimeStatusKind::Aborted => 2,
+ SubagentRuntimeStatusKind::Closed => 1,
+ SubagentRuntimeStatusKind::NotFound => 0,
+ }
+}
+
+pub(crate) fn plan_runtime_prepared_team_actions(
+ roles: &[RuntimePreparedTeamRole],
+ existing_candidates: &[RuntimePreparedTeamSessionCandidate],
+) -> Vec {
+ let mut existing_by_role_id: HashMap<&str, &RuntimePreparedTeamSessionCandidate> =
+ HashMap::new();
+
+ for candidate in existing_candidates {
+ let replace = existing_by_role_id
+ .get(candidate.blueprint_role_id.as_str())
+ .map(|current| {
+ runtime_prepared_team_session_rank(candidate.status_kind)
+ > runtime_prepared_team_session_rank(current.status_kind)
+ })
+ .unwrap_or(true);
+ if replace {
+ existing_by_role_id.insert(candidate.blueprint_role_id.as_str(), candidate);
+ }
+ }
+
+ roles
+ .iter()
+ .filter_map(|role| match existing_by_role_id.get(role.id.as_str()) {
+ Some(candidate) if candidate.status_kind == SubagentRuntimeStatusKind::Closed => {
+ Some(RuntimePreparedTeamAction::Resume {
+ role: role.clone(),
+ session_id: candidate.session_id.clone(),
+ })
+ }
+ Some(candidate) if candidate.status_kind != SubagentRuntimeStatusKind::NotFound => None,
+ _ => Some(RuntimePreparedTeamAction::Spawn(role.clone())),
+ })
+ .collect()
+}
+
+pub(crate) fn build_runtime_prepared_team_spawn_message(
+ role: &RuntimePreparedTeamRole,
+ user_message: &str,
+) -> String {
+ let mut sections = vec![format!("你是本轮 Team 中的「{}」角色。", role.label)];
+ if let Some(summary) = role.summary.as_deref() {
+ sections.push(format!("你的职责:{summary}"));
+ }
+ sections.push(
+ "请直接在当前子会话输出你的执行过程与结果,不要把具体产出留给父会话代写。".to_string(),
+ );
+ sections.push(format!("当前用户任务:\n{}", user_message.trim()));
+ sections.push("只处理当前角色范围内的工作;如果依赖其他角色,请明确指出协作边界。".to_string());
+ sections.join("\n\n")
+}
+
+async fn collect_runtime_prepared_team_candidates(
+ parent_session_id: &str,
+) -> Result, String> {
+ let child_sessions = list_subagent_child_sessions(parent_session_id)
+ .await
+ .map_err(|error| format!("读取 runtime team child sessions 失败: {error}"))?;
+ let mut candidates = Vec::new();
+
+ for child_session in child_sessions {
+ let Some(customization) = SubagentCustomizationState::from_session(&child_session) else {
+ continue;
+ };
+ let Some(blueprint_role_id) = normalize_optional_text(customization.blueprint_role_id)
+ else {
+ continue;
+ };
+ let status_kind = load_subagent_runtime_status(&child_session.id)
+ .await
+ .map(|status| status.kind)
+ .unwrap_or(SubagentRuntimeStatusKind::NotFound);
+ candidates.push(RuntimePreparedTeamSessionCandidate {
+ blueprint_role_id,
+ session_id: child_session.id,
+ status_kind,
+ });
+ }
+
+ Ok(candidates)
+}
+
+async fn maybe_prepare_runtime_team_sessions(
+ app: &AppHandle,
+ state: &AsterAgentState,
+ db: &DbConnection,
+ api_key_provider_service: &ApiKeyProviderServiceState,
+ logs: &LogState,
+ config_manager: &GlobalConfigManagerState,
+ mcp_manager: &McpManagerState,
+ automation_state: &AutomationServiceState,
+ request: &AsterChatRequest,
+) -> Result<(), String> {
+ let roles = parse_runtime_prepared_team_roles(request.metadata.as_ref());
+ if roles.is_empty() {
+ return Ok(());
+ }
+
+ let existing_candidates = collect_runtime_prepared_team_candidates(&request.session_id).await?;
+ let actions = plan_runtime_prepared_team_actions(&roles, &existing_candidates);
+ if actions.is_empty() {
+ tracing::info!(
+ "[AsterAgent][RuntimeTeam] session={} 所有 blueprint 角色已就绪,无需新增预拉起",
+ request.session_id
+ );
+ return Ok(());
+ }
+
+ tracing::info!(
+ "[AsterAgent][RuntimeTeam] session={} 开始按 blueprint 预拉起角色: total_roles={}, pending_actions={}",
+ request.session_id,
+ roles.len(),
+ actions.len()
+ );
+
+ let runtime = SubagentControlRuntime::new(
+ app.clone(),
+ state,
+ db,
+ api_key_provider_service,
+ logs,
+ config_manager,
+ mcp_manager,
+ automation_state,
+ );
+
+ for action in actions {
+ match action {
+ RuntimePreparedTeamAction::Spawn(role) => {
+ tracing::info!(
+ "[AsterAgent][RuntimeTeam] session={} 自动预拉起角色: role_id={}, label={}",
+ request.session_id,
+ role.id,
+ role.label
+ );
+ agent_runtime_spawn_subagent_internal(
+ &runtime,
+ AgentRuntimeSpawnSubagentRequest {
+ parent_session_id: request.session_id.clone(),
+ message: build_runtime_prepared_team_spawn_message(&role, &request.message),
+ agent_type: Some(role.label.clone()),
+ model: None,
+ reasoning_effort: None,
+ fork_context: false,
+ blueprint_role_id: Some(role.id.clone()),
+ blueprint_role_label: Some(role.label.clone()),
+ profile_id: role.profile_id.clone(),
+ profile_name: None,
+ role_key: role.role_key.clone(),
+ skill_ids: role.skill_ids.clone(),
+ skill_directories: Vec::new(),
+ team_preset_id: None,
+ theme: None,
+ system_overlay: None,
+ output_contract: None,
+ },
+ )
+ .await
+ .map_err(|error| {
+ format!(
+ "自动预拉起 team 角色失败: role_id={}, label={}, error={error}",
+ role.id, role.label
+ )
+ })?;
+ }
+ RuntimePreparedTeamAction::Resume { role, session_id } => {
+ tracing::info!(
+ "[AsterAgent][RuntimeTeam] session={} 恢复已关闭角色 lane: role_id={}, label={}, child_session={}",
+ request.session_id,
+ role.id,
+ role.label,
+ session_id
+ );
+ agent_runtime_resume_subagent_internal(
+ &runtime,
+ AgentRuntimeResumeSubagentRequest { id: session_id.clone() },
+ )
+ .await
+ .map_err(|error| {
+ format!(
+ "恢复已关闭 team 角色失败: role_id={}, label={}, child_session={}, error={error}",
+ role.id, role.label, session_id
+ )
+ })?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
async fn execute_aster_chat_request(
app: &AppHandle,
state: &AsterAgentState,
@@ -604,21 +951,50 @@ async fn execute_aster_chat_request(
session_config_builder.build()
};
+ let runtime_turn_initialized = {
+ let guard = agent_arc.read().await;
+ let agent = guard.as_ref().ok_or("Agent not initialized")?;
+ match agent
+ .ensure_runtime_turn_initialized(
+ &runtime_status_session_config,
+ Some(request.message.clone()),
+ )
+ .await
+ {
+ Ok(_) => true,
+ Err(error) => {
+ tracing::warn!(
+ "[AsterAgent] 初始化 runtime turn 失败,后续降级继续: {}",
+ error
+ );
+ false
+ }
+ }
+ };
+
+ if runtime_turn_initialized {
+ maybe_prepare_runtime_team_sessions(
+ app,
+ state,
+ db,
+ api_key_provider_service,
+ logs,
+ config_manager,
+ mcp_manager,
+ automation_state,
+ &request,
+ )
+ .await?;
+ } else {
+ tracing::warn!(
+ "[AsterAgent][RuntimeTeam] 跳过本轮 team 预拉起,因为父会话 runtime turn 尚未就绪: session={}",
+ session_id
+ );
+ }
+
// 获取 Agent Arc 并保持 guard 在整个流处理期间存活
let guard = agent_arc.read().await;
let agent = guard.as_ref().ok_or("Agent not initialized")?;
- if let Err(error) = agent
- .ensure_runtime_turn_initialized(
- &runtime_status_session_config,
- Some(request.message.clone()),
- )
- .await
- {
- tracing::warn!(
- "[AsterAgent] 初始化 runtime turn 失败,后续降级继续: {}",
- error
- );
- }
let (initial_runtime_status, decided_runtime_status) = build_turn_runtime_statuses(
&request,
diff --git a/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs b/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs
index 899f72f27..365cfd22d 100644
--- a/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs
+++ b/src-tauri/src/commands/aster_agent_cmd/subagent_runtime.rs
@@ -153,9 +153,11 @@ fn normalize_optional_vec(values: &[String]) -> Vec {
fn build_subagent_session_name(
message: &str,
agent_type: Option<&str>,
+ blueprint_role_label: Option<&str>,
profile_name: Option<&str>,
) -> String {
normalize_optional_text(agent_type.map(ToString::to_string))
+ .or_else(|| normalize_optional_text(blueprint_role_label.map(ToString::to_string)))
.or_else(|| normalize_optional_text(profile_name.map(ToString::to_string)))
.or_else(|| build_subagent_task_summary(message))
.unwrap_or_else(|| "子代理".to_string())
@@ -166,6 +168,7 @@ fn resolve_subagent_role_hint(
customization: Option<&SubagentCustomizationState>,
) -> Option {
normalize_optional_text(request.agent_type.clone())
+ .or_else(|| customization.and_then(|state| state.blueprint_role_label.clone()))
.or_else(|| customization.and_then(|state| state.profile_name.clone()))
.or_else(|| customization.and_then(|state| state.role_key.clone()))
}
@@ -210,6 +213,8 @@ fn build_local_subagent_skill_payload(
pub(crate) fn build_subagent_customization_state(
request: &AgentRuntimeSpawnSubagentRequest,
) -> Result
{!hasRealTeamGraph && runtimeTeamState ? (
{
});
expect(container.textContent).toContain("Team Workspace");
+ expect(container.textContent).toContain("角色执行画布");
+ expect(container.textContent).toContain(
+ "主对话只保留调度记录,每个角色在自己的泳道中输出过程与结果。",
+ );
expect(container.textContent).toContain("研究员");
expect(container.textContent).toContain("执行器");
expect(container.textContent).toContain("2 位成员已加入");
@@ -313,6 +317,20 @@ describe("TeamWorkspaceBoard", () => {
expect(container.textContent).toContain("命令输出");
expect(container.textContent).toContain("已对 3 个来源完成去重校验。");
+ const researcherLane = container.querySelector(
+ '[data-testid="team-workspace-member-lane-child-1"]',
+ );
+ const executorLane = container.querySelector(
+ '[data-testid="team-workspace-member-lane-child-2"]',
+ );
+ expect(researcherLane?.textContent).toContain("角色输出");
+ expect(researcherLane?.textContent).toContain(
+ "回复:已完成竞品摘要与数据来源梳理。",
+ );
+ expect(executorLane?.textContent).toContain(
+ "计划:先整理落地步骤,再生成第一版实施清单。",
+ );
+
const openButton = Array.from(container.querySelectorAll("button")).find(
(element) => element.textContent?.includes("查看对话"),
);
@@ -325,6 +343,82 @@ describe("TeamWorkspaceBoard", () => {
expect(onOpenSubagentSession).toHaveBeenCalledWith("child-1");
});
+ it("真实成员存在时应优先按蓝图角色顺序排列泳道", async () => {
+ const container = await renderBoard({
+ childSubagentSessions: [
+ {
+ id: "child-executor",
+ name: "执行成员",
+ created_at: 1_710_000_010,
+ updated_at: 1_710_000_120,
+ session_type: "sub_agent",
+ runtime_status: "queued",
+ latest_turn_status: "queued",
+ task_summary: "负责提交修复。",
+ role_hint: "executor",
+ blueprint_role_id: "runtime-executor",
+ blueprint_role_label: "执行",
+ profile_id: "code-executor",
+ role_key: "executor",
+ },
+ {
+ id: "child-explorer",
+ name: "分析成员",
+ created_at: 1_710_000_000,
+ updated_at: 1_710_000_100,
+ session_type: "sub_agent",
+ runtime_status: "running",
+ latest_turn_status: "running",
+ task_summary: "负责定位问题。",
+ role_hint: "explorer",
+ blueprint_role_id: "runtime-explorer",
+ blueprint_role_label: "分析",
+ profile_id: "code-explorer",
+ role_key: "explorer",
+ },
+ ],
+ runtimeTeamState: {
+ requestId: "runtime-formed-ordered",
+ status: "formed",
+ label: "修复 Team",
+ summary: "分析、执行协同推进。",
+ members: [
+ {
+ id: "runtime-explorer",
+ label: "分析",
+ summary: "先定位问题。",
+ roleKey: "explorer",
+ profileId: "code-explorer",
+ skillIds: ["repo-exploration"],
+ status: "planned",
+ },
+ {
+ id: "runtime-executor",
+ label: "执行",
+ summary: "再提交修复。",
+ roleKey: "executor",
+ profileId: "code-executor",
+ skillIds: ["bounded-implementation"],
+ status: "planned",
+ },
+ ],
+ blueprint: null,
+ updatedAt: Date.now(),
+ },
+ });
+
+ const laneIds = Array.from(
+ container.querySelectorAll('[data-testid^="team-workspace-member-lane-"]'),
+ ).map((element) => element.getAttribute("data-testid"));
+
+ expect(laneIds).toEqual([
+ "team-workspace-member-lane-child-explorer",
+ "team-workspace-member-lane-child-executor",
+ ]);
+ expect(container.textContent).toContain("蓝图 · 分析");
+ expect(container.textContent).toContain("蓝图 · 执行");
+ });
+
it("子线程视角应展示父会话、最近过程并支持切换 sibling", async () => {
const onOpenSubagentSession = vi.fn();
const onReturnToParentSession = vi.fn();
@@ -420,6 +514,7 @@ describe("TeamWorkspaceBoard", () => {
});
expect(container.textContent).toContain("主线程总览");
+ expect(container.textContent).toContain("角色执行画布");
expect(container.textContent).toContain("实现代理");
expect(container.textContent).toContain("检索代理");
expect(container.textContent).toContain("队列 1");
@@ -436,6 +531,20 @@ describe("TeamWorkspaceBoard", () => {
"工具 browser_snapshot:页面已刷新为最新状态并生成差异截图。",
);
+ const currentLane = container.querySelector(
+ '[data-testid="team-workspace-member-lane-child-current"]',
+ );
+ const siblingLane = container.querySelector(
+ '[data-testid="team-workspace-member-lane-child-sibling-1"]',
+ );
+ expect(currentLane?.textContent).toContain("角色输出");
+ expect(currentLane?.textContent).toContain(
+ "推理:先检查 team runtime 的控制面状态,再决定是否等待。",
+ );
+ expect(siblingLane?.textContent).toContain(
+ "工具 browser_snapshot:页面已刷新为最新状态并生成差异截图。",
+ );
+
const returnButton = Array.from(container.querySelectorAll("button")).find(
(element) => element.textContent?.includes("返回父会话"),
);
diff --git a/src/components/agent/chat/components/TeamWorkspaceBoard.tsx b/src/components/agent/chat/components/TeamWorkspaceBoard.tsx
index 9454796f5..83f08573b 100644
--- a/src/components/agent/chat/components/TeamWorkspaceBoard.tsx
+++ b/src/components/agent/chat/components/TeamWorkspaceBoard.tsx
@@ -100,6 +100,8 @@ interface TeamSessionCard {
model?: string;
originTool?: string;
createdFromTurnId?: string;
+ blueprintRoleId?: string;
+ blueprintRoleLabel?: string;
profileId?: string;
profileName?: string;
roleKey?: string;
@@ -588,6 +590,8 @@ function buildCurrentChildSession(
sessionType: "sub_agent",
originTool: subagentParentContext.origin_tool,
createdFromTurnId: subagentParentContext.created_from_turn_id,
+ blueprintRoleId: subagentParentContext.blueprint_role_id,
+ blueprintRoleLabel: subagentParentContext.blueprint_role_label,
profileId: subagentParentContext.profile_id,
profileName: subagentParentContext.profile_name,
roleKey: subagentParentContext.role_key,
@@ -640,6 +644,136 @@ function dedupeSessions(
return result;
}
+function normalizeComparableText(value?: string | null): string {
+ return value?.trim().toLocaleLowerCase() || "";
+}
+
+function resolveSessionBlueprintRoleId(
+ session: TeamSessionCard,
+ runtimeRoles: Array<{
+ id: string;
+ label?: string | null;
+ profileId?: string;
+ roleKey?: string;
+ }>,
+ usedRoleIds: Set
,
+): string | null {
+ const explicitRoleId = session.blueprintRoleId?.trim();
+ if (
+ explicitRoleId &&
+ !usedRoleIds.has(explicitRoleId) &&
+ runtimeRoles.some((role) => role.id === explicitRoleId)
+ ) {
+ return explicitRoleId;
+ }
+
+ const sessionBlueprintRoleLabel = normalizeComparableText(
+ session.blueprintRoleLabel,
+ );
+ const sessionRoleKey = normalizeComparableText(
+ session.roleKey || session.roleHint,
+ );
+ const sessionProfileId = normalizeComparableText(session.profileId);
+ const sessionName = normalizeComparableText(session.name);
+
+ const candidates = runtimeRoles
+ .filter((role) => !usedRoleIds.has(role.id))
+ .map((role) => {
+ let score = 0;
+ if (
+ sessionBlueprintRoleLabel &&
+ normalizeComparableText(role.label) === sessionBlueprintRoleLabel
+ ) {
+ score += 8;
+ }
+ if (sessionRoleKey && normalizeComparableText(role.roleKey) === sessionRoleKey) {
+ score += 4;
+ }
+ if (
+ sessionProfileId &&
+ normalizeComparableText(role.profileId) === sessionProfileId
+ ) {
+ score += 3;
+ }
+ if (sessionName && normalizeComparableText(role.label) === sessionName) {
+ score += 2;
+ }
+ return {
+ roleId: role.id,
+ score,
+ };
+ })
+ .filter((candidate) => candidate.score > 0)
+ .sort((left, right) => right.score - left.score);
+
+ if (candidates.length === 0) {
+ return null;
+ }
+ if (
+ candidates.length > 1 &&
+ candidates[0]?.score === candidates[1]?.score
+ ) {
+ return null;
+ }
+
+ return candidates[0]?.roleId ?? null;
+}
+
+function orderSessionsByRuntimeRoles(
+ sessions: TeamSessionCard[],
+ runtimeTeamState?: TeamWorkspaceRuntimeFormationState | null,
+): TeamSessionCard[] {
+ if (sessions.length <= 1 || !runtimeTeamState) {
+ return sessions;
+ }
+
+ const runtimeRoles = (
+ runtimeTeamState.members.length > 0
+ ? runtimeTeamState.members
+ : runtimeTeamState.blueprint?.roles ?? []
+ ).map((role) => ({
+ id: role.id,
+ label: role.label,
+ profileId: role.profileId,
+ roleKey: role.roleKey,
+ }));
+
+ if (runtimeRoles.length === 0) {
+ return sessions;
+ }
+
+ const roleOrder = new Map(runtimeRoles.map((role, index) => [role.id, index]));
+ const usedRoleIds = new Set();
+
+ return [...sessions]
+ .map((session, index) => {
+ const matchedRoleId = resolveSessionBlueprintRoleId(
+ session,
+ runtimeRoles,
+ usedRoleIds,
+ );
+ if (matchedRoleId) {
+ usedRoleIds.add(matchedRoleId);
+ }
+ return {
+ session,
+ index,
+ matchedRoleId,
+ roleOrder:
+ matchedRoleId !== null
+ ? (roleOrder.get(matchedRoleId) ?? Number.MAX_SAFE_INTEGER)
+ : Number.MAX_SAFE_INTEGER,
+ };
+ })
+ .sort((left, right) => {
+ if (left.roleOrder !== right.roleOrder) {
+ return left.roleOrder - right.roleOrder;
+ }
+ return left.index - right.index;
+ })
+ .map((entry) => entry.session);
+}
+
function buildBoardHeadline(params: {
hasRealTeamGraph: boolean;
isChildSession: boolean;
@@ -730,6 +864,39 @@ function buildRuntimeFormationEmptyDetail(
}
}
+function buildSessionLaneEmptyState(params: {
+ session?: TeamSessionCard | null;
+ previewState?: SessionActivityPreviewState | null;
+}) {
+ const { session, previewState } = params;
+
+ if (previewState?.status === "error") {
+ return previewState.errorMessage?.trim() || "同步角色输出失败";
+ }
+
+ if (previewState?.status === "loading") {
+ return "正在同步该角色的最新输出...";
+ }
+
+ if (session?.runtimeStatus === "queued") {
+ return "已进入队列,等待 orchestrator 拉起执行。";
+ }
+
+ if (session?.runtimeStatus === "running") {
+ return "角色已开始执行,最新过程会持续刷新到这里。";
+ }
+
+ if (session?.runtimeStatus === "completed") {
+ return "该角色已完成当前任务,等待 orchestrator 汇总或继续交接。";
+ }
+
+ if (session?.runtimeStatus === "failed" || session?.runtimeStatus === "aborted") {
+ return "该角色执行失败,可在下方焦点区查看细节并决定是否恢复。";
+ }
+
+ return "该角色暂未产出可展示的正文内容。";
+}
+
export function TeamWorkspaceBoard({
className,
embedded = false,
@@ -833,6 +1000,8 @@ export function TeamWorkspaceBoard({
model: session.model,
originTool: session.origin_tool,
createdFromTurnId: session.created_from_turn_id,
+ blueprintRoleId: session.blueprint_role_id,
+ blueprintRoleLabel: session.blueprint_role_label,
profileId: session.profile_id,
profileName: session.profile_name,
roleKey: session.role_key,
@@ -1159,6 +1328,16 @@ export function TeamWorkspaceBoard({
);
};
+ const memberCanvasSessions = useMemo(
+ () =>
+ orderSessionsByRuntimeRoles(
+ isChildSession
+ ? dedupeSessions([currentChildSession, ...visibleSessions])
+ : visibleSessions,
+ runtimeTeamState,
+ ),
+ [currentChildSession, isChildSession, runtimeTeamState, visibleSessions],
+ );
const railSessions = useMemo(
() =>
dedupeSessions(
@@ -1184,7 +1363,7 @@ export function TeamWorkspaceBoard({
useEffect(() => {
const defaultSelectedId = isChildSession
? currentSessionId ?? railSessions[0]?.id ?? null
- : visibleSessions[0]?.id ?? railSessions[0]?.id ?? null;
+ : memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null;
if (!selectedSessionId) {
setSelectedSessionId(defaultSelectedId);
@@ -1199,17 +1378,17 @@ export function TeamWorkspaceBoard({
if (
!isChildSession &&
selectedSessionId === orchestratorSession?.id &&
- visibleSessions.length > 0
+ memberCanvasSessions.length > 0
) {
- setSelectedSessionId(visibleSessions[0]?.id ?? defaultSelectedId);
+ setSelectedSessionId(memberCanvasSessions[0]?.id ?? defaultSelectedId);
}
}, [
currentSessionId,
isChildSession,
+ memberCanvasSessions,
orchestratorSession?.id,
railSessions,
selectedSessionId,
- visibleSessions,
]);
const selectedSession = useMemo(
@@ -1728,6 +1907,18 @@ export function TeamWorkspaceBoard({
},
[onSendSubagentInput, selectedSession, selectedSessionInputMessage],
);
+ const memberCanvasTitle = "角色执行画布";
+ const memberCanvasSubtitle = hasRealTeamGraph
+ ? isChildSession
+ ? "当前成员与同组角色在各自泳道中输出过程与结果,主对话仅保留调度记录。"
+ : `${visibleSessions.length} 位成员已加入,主对话只保留调度记录,每个角色在自己的泳道中输出过程与结果。`
+ : runtimeTeamState?.status === "forming"
+ ? "正在准备本轮角色泳道,真实成员接入后会在这里独立输出。"
+ : runtimeTeamState?.status === "formed"
+ ? "本轮编队已完成,等待真实角色接入后在各自泳道中开始执行。"
+ : runtimeTeamState?.status === "failed"
+ ? "本轮编队失败,暂时无法生成角色泳道。"
+ : "等待成员加入后,这里会按角色展开各自的输出泳道。";
if (
!subagentParentContext &&
@@ -1825,6 +2016,9 @@ export function TeamWorkspaceBoard({
const selectedPresetOption = getTeamPresetOption(selectedSession?.teamPresetId);
const selectedSkills = selectedSession?.skills ?? [];
const selectedMetadata = [
+ selectedSession?.blueprintRoleLabel
+ ? `蓝图角色 ${selectedSession.blueprintRoleLabel}`
+ : null,
selectedSession?.sessionType
? resolveSessionTypeLabel(selectedSession.sessionType)
: null,
@@ -1884,20 +2078,6 @@ export function TeamWorkspaceBoard({
const timelineEntryClassName = embedded
? "border-l-2 border-slate-200 pl-3"
: "rounded-[14px] border border-slate-200 bg-white p-3";
- const railTitle = isChildSession ? "同组成员轨道" : "团队成员轨道";
- const railSubtitle = isChildSession
- ? siblingCount > 0
- ? `当前成员与 ${siblingCount} 位同组成员协作`
- : "当前只有一位成员"
- : hasRealTeamGraph
- ? `${visibleSessions.length} 位成员已加入`
- : runtimeTeamState?.status === "forming"
- ? "正在准备本轮成员"
- : runtimeTeamState?.status === "formed"
- ? `${runtimeMembers.length} 个成员待启动`
- : runtimeTeamState?.status === "failed"
- ? "Team 准备失败"
- : "等待成员加入";
return (
- {railTitle}
+ {memberCanvasTitle}
- {railSubtitle}
+ {memberCanvasSubtitle}
{selectedSession ? (
@@ -2141,17 +2321,17 @@ export function TeamWorkspaceBoard({
) : null}
- {railSessions.length > 0 ? (
+ {memberCanvasSessions.length > 0 ? (
- {railSessions.map((session) => {
+ {memberCanvasSessions.map((session) => {
const meta = resolveStatusMeta(session.runtimeStatus);
const selected = selectedSession?.id === session.id;
const runtimeDetail = buildRuntimeDetailSummary(session);
@@ -2160,10 +2340,17 @@ export function TeamWorkspaceBoard({
sessionActivityPreviewById[session.id]?.entries,
ACTIVITY_TIMELINE_ENTRY_LIMIT,
);
- const cardActivityPreview = buildCardActivityPreview(
- buildActivityPreviewFromEntry(mergedCardEntries[0]) ??
- sessionActivityPreviewById[session.id]?.preview,
- );
+ const previewState = sessionActivityPreviewById[session.id] ?? null;
+ const cardActivityPreview = buildActivityPreviewFromEntry(
+ mergedCardEntries[0],
+ )
+ ?? previewState?.preview
+ ?? null;
+ const laneEntries = mergedCardEntries.slice(0, 3);
+ const laneEmptyState = buildSessionLaneEmptyState({
+ session,
+ previewState,
+ });
return (