diff --git a/cmd/gateway_consumer.go b/cmd/gateway_consumer.go index 0de0526a..9576a7dd 100644 --- a/cmd/gateway_consumer.go +++ b/cmd/gateway_consumer.go @@ -136,11 +136,6 @@ func autoSetFollowup(ctx context.Context, teamStore store.TeamStore, agentStore if err != nil || team == nil || team.LeadAgentID != ag.ID { return // only lead agent triggers auto-set } - // Followup is a v2 feature. - if !isConsumerTeamV2(team) { - return - } - // Skip auto-followup when lead is waiting for teammates (not user). if hasMember, _ := teamStore.HasActiveMemberTasks(ctx, team.ID, ag.ID); hasMember { slog.Debug("auto-followup: skipping, active member tasks exist", "team_id", team.ID) @@ -159,9 +154,6 @@ func autoSetFollowup(ctx context.Context, teamStore store.TeamStore, agentStore } } -// isConsumerTeamV2 delegates to tools.IsTeamV2 for version checking. -var isConsumerTeamV2 = tools.IsTeamV2 - // parseFollowupSettings extracts followup interval and max reminders from team settings. func parseFollowupSettings(team *store.TeamData) (time.Duration, int) { const ( diff --git a/cmd/gateway_consumer_handlers.go b/cmd/gateway_consumer_handlers.go index 310e668a..2c355fe9 100644 --- a/cmd/gateway_consumer_handlers.go +++ b/cmd/gateway_consumer_handlers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "path/filepath" "strings" "sync" "time" @@ -330,7 +331,7 @@ func handleTeammateMessage( teamID, _ := uuid.Parse(inMeta["team_id"]) if teamTaskID != uuid.Nil && teamStore != nil { cachedTeam, _ = teamStore.GetTeam(ctx, teamID) - if cachedTeam != nil && isConsumerTeamV2(cachedTeam) { + if cachedTeam != nil { // Check current task status — agent may have already updated it via tool. currentTask, taskErr := teamStore.GetTask(ctx, teamTaskID) alreadyTerminal := taskErr == nil && currentTask != nil && @@ -432,6 +433,33 @@ func handleTeammateMessage( announceMedia = outcome.Result.Media } + // Append member comments & attachments so leader sees them in the announce. + if taskIDStr := inMeta["team_task_id"]; taskIDStr != "" && teamStore != nil { + if taskUUID, err := uuid.Parse(taskIDStr); err == nil { + if comments, err := teamStore.ListRecentTaskComments(ctx, taskUUID, 5); err == nil && len(comments) > 0 { + var parts []string + for _, c := range comments { + author := c.AgentKey + if author == "" { + author = "system" + } + text := c.Content + if len([]rune(text)) > 500 { + text = string([]rune(text)[:500]) + "..." + } + parts = append(parts, fmt.Sprintf("- [%s]: %s", author, text)) + } + announceContent += "\n\n[Member notes]\n" + strings.Join(parts, "\n") + } + if attachments, err := teamStore.ListTaskAttachments(ctx, taskUUID); err == nil && len(attachments) > 0 { + announceContent += "\n\n[Attached files in team workspace]" + for _, a := range attachments { + announceContent += "\n- " + filepath.Base(a.Path) + } + } + } + } + // Announce result (or failure) to lead agent via announce queue. // Queue merges concurrent completions into a single batched announce. if origChatID == "" { @@ -483,30 +511,9 @@ func handleTeammateMessage( parentRootSpanID, _ = uuid.Parse(sid) } - // Enrich announce content with member comments on the task (if any). - if taskIDStr := inMeta["team_task_id"]; taskIDStr != "" && teamStore != nil { - if tid, err := uuid.Parse(taskIDStr); err == nil { - if comments, err := teamStore.ListTaskComments(ctx, tid); err == nil && len(comments) > 0 { - // Include last 5 comments, each truncated to 200 chars. - start := 0 - if len(comments) > 5 { - start = len(comments) - 5 - } - var commentLines []string - for _, c := range comments[start:] { - author := c.AgentKey - if author == "" && c.UserID != "" { - author = "user:" + c.UserID - } - body := c.Content - if len(body) > 200 { - body = body[:200] + "..." - } - commentLines = append(commentLines, fmt.Sprintf("- [%s]: %s", author, body)) - } - announceContent += "\n\n--- Member comments ---\n" + strings.Join(commentLines, "\n") - } - } + // Cap announce content to prevent context blowup for the leader agent. + if len([]rune(announceContent)) > 50_000 { + announceContent = string([]rune(announceContent)[:50_000]) + "\n[truncated]" } // Enqueue result. If we become the processor, run the announce loop. diff --git a/internal/agent/resolver.go b/internal/agent/resolver.go index a6e0a03f..5ab32bf3 100644 --- a/internal/agent/resolver.go +++ b/internal/agent/resolver.go @@ -162,7 +162,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc { hasTeam = true contextFiles = append(contextFiles, bootstrap.ContextFile{ Path: bootstrap.TeamFile, - Content: buildTeamMD(team, members, ag.ID, tools.IsTeamV2(team)), + Content: buildTeamMD(team, members, ag.ID), }) // Detect lead role for tool policy for _, m := range members { diff --git a/internal/agent/resolver_helpers.go b/internal/agent/resolver_helpers.go index b3fabf02..5c78868b 100644 --- a/internal/agent/resolver_helpers.go +++ b/internal/agent/resolver_helpers.go @@ -14,8 +14,7 @@ import ( // buildTeamMD generates compact TEAM.md content for an agent that is part of a team. // Kept minimal — tool descriptions already live in tool Parameters()/Description(). -// isV2 controls whether advanced sections (orchestration, followup, review) are rendered. -func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uuid.UUID, isV2 bool) string { +func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uuid.UUID) string { var sb strings.Builder sb.WriteString("# Team: " + team.Name + "\n") if team.Description != "" { @@ -77,48 +76,37 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu // Workflow guidance — version-aware to match backend behavior. sb.WriteString("\n## Workflow\n\n") if selfRole == store.TeamRoleLead { - if isV2 { - sb.WriteString("Delegate work to team members using `team_tasks` with `assignee`.\n\n") - sb.WriteString("```\nteam_tasks(action=\"create\", subject=\"...\", description=\"...\", assignee=\"agent-key\")\n```\n\n") - sb.WriteString("The system auto-dispatches to the assigned member and auto-completes when done.\n") - sb.WriteString("Do NOT use `spawn` for team delegation — `spawn` is only for self-clone subagent work.\n\n") - sb.WriteString("Rules:\n") - sb.WriteString("- Always specify `assignee` — match member expertise from the list above\n") - sb.WriteString("- **Check task board first** — call `team_tasks(action=\"search\", query=\"\")` to find similar tasks before creating. This uses semantic search and saves tokens vs listing all. The system blocks creation if you skip this step\n") - sb.WriteString("- **Create ALL tasks upfront** in one batch, then announce — then STOP. Do NOT create one task, wait for it to finish, then create the next\n") - sb.WriteString("- Delegation is NOT completion — do NOT say \"done\"/\"xong\"/\"finished\" after delegating. Only report completion when ALL task results have been delivered\n") - sb.WriteString("- Results arrive automatically — do NOT present partial results\n") - sb.WriteString("- **Prefer delegation** — delegate tasks to members, do NOT do the work yourself. If you need research, create a research task. If you need code, create a coding task. Only use tools like web_search yourself for quick clarifications needed to plan tasks (e.g., checking a member's capability), NOT for producing deliverables\n") - sb.WriteString("- **Do NOT block on completed tasks** — pass completed task's result in the description instead of using blocked_by\n") + sb.WriteString("Delegate work to team members using `team_tasks` with `assignee`.\n\n") + sb.WriteString("```\nteam_tasks(action=\"create\", subject=\"...\", description=\"...\", assignee=\"agent-key\")\n```\n\n") + sb.WriteString("The system auto-dispatches to the assigned member and auto-completes when done.\n") + sb.WriteString("Do NOT use `spawn` for team delegation — `spawn` is only for self-clone subagent work.\n\n") + sb.WriteString("Rules:\n") + sb.WriteString("- Always specify `assignee` — match member expertise from the list above\n") + sb.WriteString("- **Check task board first** — call `team_tasks(action=\"search\", query=\"\")` to find similar tasks before creating. This uses semantic search and saves tokens vs listing all. The system blocks creation if you skip this step\n") + sb.WriteString("- **Create ALL tasks upfront** in one batch, then announce — then STOP. Do NOT create one task, wait for it to finish, then create the next\n") + sb.WriteString("- Delegation is NOT completion — do NOT say \"done\"/\"xong\"/\"finished\" after delegating. Only report completion when ALL task results have been delivered\n") + sb.WriteString("- Results arrive automatically — do NOT present partial results\n") + sb.WriteString("- **Prefer delegation** — delegate tasks to members, do NOT do the work yourself. If you need research, create a research task. If you need code, create a coding task. Only use tools like web_search yourself for quick clarifications needed to plan tasks (e.g., checking a member's capability), NOT for producing deliverables\n") + sb.WriteString("- **Do NOT block on completed tasks** — pass completed task's result in the description instead of using blocked_by\n") - sb.WriteString("\n## Task Planning\n\n") - sb.WriteString("**CRITICAL: Create the full task graph in ONE batch.** Do NOT create→wait→create sequentially.\n\n") - sb.WriteString("Each task = ONE deliverable. Complex requests need 3+ tasks.\n\n") - sb.WriteString("1. Identify ALL distinct deliverables (research, writing, design, code...)\n") - sb.WriteString("2. Create independent tasks FIRST → get their UUIDs from the response\n") - sb.WriteString("3. THEN create dependent tasks with `blocked_by=[UUID]` — the system auto-dispatches when blockers complete\n") - sb.WriteString(" `blocked_by` only accepts real UUIDs returned by previous create calls. Never use placeholders.\n\n") - sb.WriteString("Same member → sequential (higher priority first). Different members → parallel.\n\n") - sb.WriteString("**Anti-pattern (WRONG):** create task A → wait for A to finish → create task B → wait...\n") - sb.WriteString("**Correct pattern:** create A → create B → create C(blocked_by=[A.id, B.id]) → announce → STOP. You must create tasks first to get their UUIDs, then use those UUIDs in blocked_by of dependent tasks.\n\n") - sb.WriteString("**Example:** User: \"research X and make infographic\"\n") - sb.WriteString("→ WRONG: web_search(X) yourself → summarize → delegate only infographic\n") - sb.WriteString("→ RIGHT: task 1 \"Research X\" (assignee=researcher) → task 2 \"Create infographic about X\" (assignee=artist, blocked_by=[task1.id])\n") + sb.WriteString("\n## Task Planning\n\n") + sb.WriteString("**CRITICAL: Create the full task graph in ONE batch.** Do NOT create→wait→create sequentially.\n\n") + sb.WriteString("Each task = ONE deliverable. Complex requests need 3+ tasks.\n\n") + sb.WriteString("1. Identify ALL distinct deliverables (research, writing, design, code...)\n") + sb.WriteString("2. Create independent tasks FIRST → get their UUIDs from the response\n") + sb.WriteString("3. THEN create dependent tasks with `blocked_by=[UUID]` — the system auto-dispatches when blockers complete\n") + sb.WriteString(" `blocked_by` only accepts real UUIDs returned by previous create calls. Never use placeholders.\n\n") + sb.WriteString("Same member → sequential (higher priority first). Different members → parallel.\n\n") + sb.WriteString("**Anti-pattern (WRONG):** create task A → wait for A to finish → create task B → wait...\n") + sb.WriteString("**Correct pattern:** create A → create B → create C(blocked_by=[A.id, B.id]) → announce → STOP. You must create tasks first to get their UUIDs, then use those UUIDs in blocked_by of dependent tasks.\n\n") + sb.WriteString("**Example:** User: \"research X and make infographic\"\n") + sb.WriteString("→ WRONG: web_search(X) yourself → summarize → delegate only infographic\n") + sb.WriteString("→ RIGHT: task 1 \"Research X\" (assignee=researcher) → task 2 \"Create infographic about X\" (assignee=artist, blocked_by=[task1.id])\n") - sb.WriteString("\n## Follow-up Reminders\n\n") - sb.WriteString("When you need user input/decision: create+claim task, then `ask_user` with text=. ONLY use when you have a question for the user — NOT for waiting on teammates or status updates.\n") - sb.WriteString("IMPORTANT: Present the question directly to the user in your response. `ask_user` only sets up periodic REMINDERS in case they don't reply — it does NOT present the question for you.\n") - sb.WriteString("System auto-sends reminders. Call `clear_ask_user` when user replies.\n") - } else { - sb.WriteString("Create a task with `team_tasks` (with `assignee`), then the system dispatches automatically.\n") - sb.WriteString("Tasks auto-complete when the member finishes.\n\n") - sb.WriteString("Rules:\n") - sb.WriteString("- Always specify `assignee` when creating tasks\n") - sb.WriteString("- Create all tasks first, then briefly tell the user what you delegated\n") - sb.WriteString("- Do NOT say \"done\", \"xong\", \"finished\", or any completion language after delegating — delegation is NOT completion\n") - sb.WriteString("- Do NOT phrase delegation as a question or request (\"nha?\", \"nhé?\", \"okay?\"). State it declaratively\n") - sb.WriteString("- Results arrive automatically — do NOT present partial results\n") - } + sb.WriteString("\n## Follow-up Reminders\n\n") + sb.WriteString("When you need user input/decision: create+claim task, then `ask_user` with text=. ONLY use when you have a question for the user — NOT for waiting on teammates or status updates.\n") + sb.WriteString("IMPORTANT: Present the question directly to the user in your response. `ask_user` only sets up periodic REMINDERS in case they don't reply — it does NOT present the question for you.\n") + sb.WriteString("System auto-sends reminders. Call `clear_ask_user` when user replies.\n") sb.WriteString("\nFor simple questions about team composition, answer directly from the member list above.\n") } else { diff --git a/internal/tools/team_tasks_read.go b/internal/tools/team_tasks_read.go index 1b711a66..95c24625 100644 --- a/internal/tools/team_tasks_read.go +++ b/internal/tools/team_tasks_read.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "sync" "time" @@ -143,8 +144,45 @@ func (t *TeamTasksTool) toDetailItem(ctx context.Context, task *store.TeamTaskDa } } +// buildCreateHint generates task creation guidance with member+model info. +// Injected into search/list results so weaker models (MiniMax, Qwen) get +// actionable hints before calling create. +func (t *TeamTasksTool) buildCreateHint(ctx context.Context, teamID, leadAgentID, callerAgentID uuid.UUID) string { + members, err := t.manager.cachedListMembers(ctx, teamID, callerAgentID) + if err != nil || len(members) == 0 { + return "" + } + + var sb strings.Builder + sb.WriteString("[Task creation guide]\nAvailable members and their models:\n") + for _, m := range members { + if m.AgentID == leadAgentID { + continue // skip lead from member list + } + model := "" + if ag, err := t.manager.cachedGetAgentByID(ctx, m.AgentID); err == nil { + model = ag.Model + } + entry := fmt.Sprintf("- %s (%s)", m.AgentKey, model) + if m.Frontmatter != "" { + fm := m.Frontmatter + if len([]rune(fm)) > 80 { + fm = string([]rune(fm)[:80]) + "…" + } + entry += " — " + fm + } + sb.WriteString(entry + "\n") + } + sb.WriteString("\nBefore creating a task:\n") + sb.WriteString("1. DESCRIPTION: Include clear objective, relevant context, constraints, and expected output format. The member will only see this description — make it self-contained.\n") + sb.WriteString("2. COMPLEXITY: If a task requires multiple steps or different skills, break it into separate tasks with blocked_by.\n") + sb.WriteString("3. MODEL MATCH: Assign complex reasoning/analysis tasks to members with stronger models. Simple tasks can go to any.\n") + sb.WriteString("4. INDEPENDENCE: Each task must be completable without asking the lead for clarification.") + return sb.String() +} + func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]any) *Result { - team, _, err := t.manager.resolveTeam(ctx) + team, agentID, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) } @@ -208,6 +246,9 @@ func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]any) *R if hasMore { resp["has_more"] = true } + if hint := t.buildCreateHint(ctx, team.ID, team.LeadAgentID, agentID); hint != "" { + resp["hint"] = hint + } out, _ := json.Marshal(resp) return SilentResult(string(out)) @@ -322,7 +363,7 @@ func (t *TeamTasksTool) executeGet(ctx context.Context, args map[string]any) *Re } func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]any) *Result { - team, _, err := t.manager.resolveTeam(ctx) + team, agentID, err := t.manager.resolveTeam(ctx) if err != nil { return ErrorResult(err.Error()) } @@ -358,9 +399,14 @@ func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]any) items = append(items, t.toListItem(ctx, task)) } - out, _ := json.Marshal(map[string]any{ + resp := map[string]any{ "tasks": items, "count": len(items), - }) + } + if hint := t.buildCreateHint(ctx, team.ID, team.LeadAgentID, agentID); hint != "" { + resp["hint"] = hint + } + + out, _ := json.Marshal(resp) return SilentResult(string(out)) } diff --git a/internal/tools/team_tasks_tool.go b/internal/tools/team_tasks_tool.go index 0bf7def8..6b13786e 100644 --- a/internal/tools/team_tasks_tool.go +++ b/internal/tools/team_tasks_tool.go @@ -94,27 +94,9 @@ func (t *TeamTasksTool) Parameters() map[string]any { } } -// v2Actions lists team_tasks actions that require team version >= 2. -var v2Actions = map[string]bool{ - "approve": true, "reject": true, "review": true, "comment": true, - "progress": true, "attach": true, "update": true, - "ask_user": true, "clear_ask_user": true, "retry": true, -} - func (t *TeamTasksTool) Execute(ctx context.Context, args map[string]any) *Result { action, _ := args["action"].(string) - // Gate v2-only actions: resolve team once and check version. - if v2Actions[action] { - team, _, err := t.manager.resolveTeam(ctx) - if err != nil { - return ErrorResult(err.Error()) - } - if !IsTeamV2(team) { - return ErrorResult(fmt.Sprintf("action '%s' requires team version 2 — upgrade in team settings", action)) - } - } - switch action { case "list": return t.executeList(ctx, args) diff --git a/internal/tools/team_tool_dispatch.go b/internal/tools/team_tool_dispatch.go index aed8c5fd..b8980984 100644 --- a/internal/tools/team_tool_dispatch.go +++ b/internal/tools/team_tool_dispatch.go @@ -80,6 +80,13 @@ func (m *TeamToolManager) dispatchTaskToAgent(ctx context.Context, task *store.T } } + // Hint: guide member on available team_tasks actions. + content += "\n\n[Instructions]\n" + + "- Use team_tasks(action=\"progress\", percent=N, text=\"...\") to report progress\n" + + "- Use team_tasks(action=\"comment\", text=\"...\") to share findings or ask questions — comments are included in the task report sent to the leader\n" + + "- When done: team_tasks(action=\"complete\", result=\"summary of your work\")\n" + + "- Write output files to team workspace so lead can review" + // Use task's stored channel/chat as primary source for routing. // Falls back to ctx values for initial dispatch (task just created, fields match ctx). originChannel := task.Channel diff --git a/internal/tools/team_tool_helpers.go b/internal/tools/team_tool_helpers.go index fbb42a0a..3f800381 100644 --- a/internal/tools/team_tool_helpers.go +++ b/internal/tools/team_tool_helpers.go @@ -53,20 +53,6 @@ func (m *TeamToolManager) agentDisplayName(ctx context.Context, key string) stri // Version helpers // ============================================================ -// IsTeamV2 checks if team has version >= 2 in settings. -// Returns false for nil team, nil/empty settings, or version < 2. -func IsTeamV2(team *store.TeamData) bool { - if team == nil || team.Settings == nil { - return false - } - var s struct { - Version int `json:"version"` - } - if json.Unmarshal(team.Settings, &s) != nil { - return false - } - return s.Version >= 2 -} // ============================================================ // Follow-up settings helpers @@ -78,12 +64,8 @@ const ( ) // followupDelayMinutes returns the team's followup_interval_minutes setting, or the default. -// Returns 0 for v1 teams (followup disabled). func (m *TeamToolManager) followupDelayMinutes(team *store.TeamData) int { - if !IsTeamV2(team) { - return 0 - } - if team.Settings == nil { + if team == nil || team.Settings == nil { return defaultFollowupDelayMinutes } var settings map[string]any @@ -97,12 +79,8 @@ func (m *TeamToolManager) followupDelayMinutes(team *store.TeamData) int { } // followupMaxReminders returns the team's followup_max_reminders setting, or the default. -// Returns 0 for v1 teams (followup disabled). func (m *TeamToolManager) followupMaxReminders(team *store.TeamData) int { - if !IsTeamV2(team) { - return 0 - } - if team.Settings == nil { + if team == nil || team.Settings == nil { return defaultFollowupMaxReminders } var settings map[string]any @@ -130,12 +108,8 @@ const ( ) // checkEscalation parses the team's escalation_mode and escalation_actions settings. -// Returns EscalationNone for v1 teams. func (m *TeamToolManager) checkEscalation(team *store.TeamData, action string) EscalationResult { - if !IsTeamV2(team) { - return EscalationNone - } - if team.Settings == nil { + if team == nil || team.Settings == nil { return EscalationNone } var settings map[string]any diff --git a/ui/web/src/i18n/locales/en/teams.json b/ui/web/src/i18n/locales/en/teams.json index 41236b0a..8f12d22c 100644 --- a/ui/web/src/i18n/locales/en/teams.json +++ b/ui/web/src/i18n/locales/en/teams.json @@ -241,7 +241,7 @@ "versionAdvancedDesc": "All V1 features plus execution locking, follow-up reminders, review workflow, progress tracking, comments, audit trail, and auto-recovery.", "whatsNew": "What's different?", "versionModal": { - "title": "Team Versions", + "title": "Super Team Features", "feature": "Feature", "taskManagement": "Task Management", "taskManagementDesc": "Create, assign, complete, cancel tasks", @@ -263,7 +263,7 @@ "escalationPolicyDesc": "Control what agents can do autonomously", "comingSoon": "Coming soon", "downgradeNote": "Downgrade is safe — V2 data is preserved but hidden.", - "betaNote": "V2 is currently in Beta.", + "betaNote": "Some features are currently in Beta.", "gotIt": "Got it" } }, diff --git a/ui/web/src/i18n/locales/vi/teams.json b/ui/web/src/i18n/locales/vi/teams.json index 97a42cc2..90e85eba 100644 --- a/ui/web/src/i18n/locales/vi/teams.json +++ b/ui/web/src/i18n/locales/vi/teams.json @@ -241,7 +241,7 @@ "versionAdvancedDesc": "Tất cả tính năng V1 cùng khóa thực thi, nhắc nhở theo dõi, quy trình duyệt, theo dõi tiến độ, bình luận, nhật ký hoạt động và tự phục hồi.", "whatsNew": "Có gì mới?", "versionModal": { - "title": "Phiên bản Team", + "title": "Tính năng Super Team", "feature": "Tính năng", "taskManagement": "Quản lý nhiệm vụ", "taskManagementDesc": "Tạo, phân công, hoàn thành, hủy nhiệm vụ", @@ -263,7 +263,7 @@ "escalationPolicyDesc": "Kiểm soát những gì agent có thể tự thực hiện", "comingSoon": "Sắp ra mắt", "downgradeNote": "Hạ cấp an toàn — dữ liệu V2 được giữ lại nhưng ẩn đi.", - "betaNote": "V2 hiện đang ở giai đoạn Beta.", + "betaNote": "Một số tính năng đang ở giai đoạn Beta.", "gotIt": "Đã hiểu" } }, diff --git a/ui/web/src/i18n/locales/zh/teams.json b/ui/web/src/i18n/locales/zh/teams.json index 9cd940bb..48a74128 100644 --- a/ui/web/src/i18n/locales/zh/teams.json +++ b/ui/web/src/i18n/locales/zh/teams.json @@ -241,7 +241,7 @@ "versionAdvancedDesc": "所有V1功能,外加执行锁定、跟进提醒、审核工作流、进度追踪、评论、审计日志和自动恢复。", "whatsNew": "有什么不同?", "versionModal": { - "title": "团队版本", + "title": "超级团队功能", "feature": "功能", "taskManagement": "任务管理", "taskManagementDesc": "创建、分配、完成、取消任务", @@ -263,7 +263,7 @@ "escalationPolicyDesc": "控制代理可以自主执行的操作", "comingSoon": "即将推出", "downgradeNote": "降级是安全的 — V2数据会保留但隐藏。", - "betaNote": "V2目前处于Beta阶段。", + "betaNote": "部分功能目前处于Beta阶段。", "gotIt": "知道了" } }, diff --git a/ui/web/src/pages/teams/board/board-header.tsx b/ui/web/src/pages/teams/board/board-header.tsx index 1917d025..f220df72 100644 --- a/ui/web/src/pages/teams/board/board-header.tsx +++ b/ui/web/src/pages/teams/board/board-header.tsx @@ -1,8 +1,10 @@ +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ArrowLeft, Settings, Trash2, Users } from "lucide-react"; import { useTranslation } from "react-i18next"; -import type { TeamData, TeamMemberData, TeamAccessSettings } from "@/types/team"; +import type { TeamData, TeamMemberData } from "@/types/team"; +import { TeamFeaturesModal } from "../team-features-modal"; interface BoardHeaderProps { team: TeamData; @@ -11,13 +13,11 @@ interface BoardHeaderProps { onDelete: () => void; onSettings: () => void; onMembers: () => void; - onV2Click?: () => void; } -export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMembers, onV2Click }: BoardHeaderProps) { +export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMembers }: BoardHeaderProps) { const { t } = useTranslation("teams"); - const settings = (team.settings ?? {}) as TeamAccessSettings; - const isV2 = (settings.version ?? 1) >= 2; + const [featuresOpen, setFeaturesOpen] = useState(false); const leadMember = members.find((m) => m.role === "lead"); const leadName = leadMember?.display_name || leadMember?.agent_key || team.lead_display_name || team.lead_agent_key; @@ -36,14 +36,11 @@ export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMem {team.status} - {isV2 && ( - - v2 Super Team (Beta) +
{leadName && ( @@ -75,6 +72,8 @@ export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMem {t("delete.title")} + +
); } diff --git a/ui/web/src/pages/teams/board/team-info-dialog.tsx b/ui/web/src/pages/teams/board/team-info-dialog.tsx index 394269bc..31e9f0e7 100644 --- a/ui/web/src/pages/teams/board/team-info-dialog.tsx +++ b/ui/web/src/pages/teams/board/team-info-dialog.tsx @@ -5,8 +5,8 @@ import { import { Badge } from "@/components/ui/badge"; import { useTranslation } from "react-i18next"; import { TeamSettingsTab } from "../team-settings-tab"; -import { TeamVersionModal } from "../team-version-modal"; -import type { TeamData, TeamMemberData, TeamAccessSettings } from "@/types/team"; +import { TeamFeaturesModal } from "../team-features-modal"; +import type { TeamData, TeamMemberData } from "@/types/team"; interface TeamInfoDialogProps { open: boolean; @@ -21,10 +21,7 @@ export function TeamInfoDialog({ open, onOpenChange, team, teamId, members, onSaved, }: TeamInfoDialogProps) { const { t } = useTranslation("teams"); - const [versionModalOpen, setVersionModalOpen] = useState(false); - - const settings = (team.settings ?? {}) as TeamAccessSettings; - const isV2 = (settings.version ?? 1) >= 2; + const [featuresOpen, setFeaturesOpen] = useState(false); // Resolve lead name from members (more reliable than team.lead_display_name which can be empty) const leadMember = members.find((m) => m.role === "lead"); @@ -41,14 +38,11 @@ export function TeamInfoDialog({ {team.status} - {isV2 && ( - setVersionModalOpen(true)} - > - v2 Super Team (Beta) + @@ -76,8 +70,7 @@ export function TeamInfoDialog({ - - + ); } diff --git a/ui/web/src/pages/teams/team-detail-page.tsx b/ui/web/src/pages/teams/team-detail-page.tsx index 394b5460..e081aa19 100644 --- a/ui/web/src/pages/teams/team-detail-page.tsx +++ b/ui/web/src/pages/teams/team-detail-page.tsx @@ -8,7 +8,6 @@ import { BoardContainer } from "./board/board-container"; import { TeamInfoDialog } from "./board/team-info-dialog"; import { TeamMembersDialog } from "./board/team-members-dialog"; import { TeamWorkspaceDialog } from "./board/team-workspace-dialog"; -import { TeamVersionModal } from "./team-version-modal"; import type { TeamData, TeamMemberData, TeamAccessSettings, ScopeEntry } from "@/types/team"; interface TeamDetailPageProps { @@ -41,7 +40,6 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) { const [infoOpen, setInfoOpen] = useState(false); const [membersOpen, setMembersOpen] = useState(false); const [workspaceOpen, setWorkspaceOpen] = useState(false); - const [versionModalOpen, setVersionModalOpen] = useState(false); const reload = useCallback(async () => { try { @@ -97,7 +95,6 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) { onDelete={() => setDeleteOpen(true)} onSettings={() => setInfoOpen(true)} onMembers={() => setMembersOpen(true)} - onV2Click={() => setVersionModalOpen(true)} /> - {/* V2 version comparison modal */} - - {/* Delete confirmation */} void; +} + +const FEATURES = [ + { key: "taskManagement", icon: ListTodo }, + { key: "sharedWorkspace", icon: FolderOpen }, + { key: "executionLocking", icon: Lock }, + { key: "followupReminders", icon: Bell }, + { key: "progressTracking", icon: BarChart3 }, + { key: "autoRecovery", icon: RotateCcw }, + { key: "commentsAudit", icon: MessageSquare, comingSoon: true }, + { key: "escalationPolicy", icon: ShieldCheck, comingSoon: true }, + { key: "reviewWorkflow", icon: ClipboardCheck, comingSoon: true }, +] as const; + +export function TeamFeaturesModal({ open, onOpenChange }: TeamFeaturesModalProps) { + const { t } = useTranslation("teams"); + + return ( + + + + + {t("settings.versionModal.title")} + + Beta + + + {t("settings.versionModal.title")} + + +
+ {/* Feature list */} +
+ {FEATURES.map((f) => { + const Icon = f.icon; + const dimmed = "comingSoon" in f && f.comingSoon; + return ( +
+
+ +
+

+ {t(`settings.versionModal.${f.key}`)} + {dimmed && ( + + {t("settings.versionModal.comingSoon")} + + )} +

+

+ {t(`settings.versionModal.${f.key}Desc`)} +

+
+
+
+ {dimmed ? ( + + ) : ( + + )} +
+
+ ); + })} +
+ + {/* Notes */} +
+

{t("settings.versionModal.betaNote")}

+
+
+ +
+ +
+
+
+ ); +} diff --git a/ui/web/src/pages/teams/team-settings-tab.tsx b/ui/web/src/pages/teams/team-settings-tab.tsx index 52442ddf..10eb3b4a 100644 --- a/ui/web/src/pages/teams/team-settings-tab.tsx +++ b/ui/web/src/pages/teams/team-settings-tab.tsx @@ -3,12 +3,11 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Combobox } from "@/components/ui/combobox"; -import { X, Save, Bell, ShieldAlert, Clock, Info, FolderLock, FolderSync, Zap, Bot, Loader2 } from "lucide-react"; +import { X, Save, Bell, ShieldAlert, Clock, FolderLock, FolderSync, Zap, Bot, Loader2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { CHANNEL_TYPES } from "@/constants/channels"; import type { TeamData, TeamAccessSettings, TeamNotifyConfig, EscalationMode, EscalationAction } from "@/types/team"; import { useTeams } from "./hooks/use-teams"; -import { TeamVersionModal } from "./team-version-modal"; interface TeamSettingsTabProps { teamId: string; @@ -66,7 +65,6 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) // Parse initial settings const initial = (team.settings ?? {}) as TeamAccessSettings; - const [version, setVersion] = useState(initial.version ?? 1); const [allowUserIds, setAllowUserIds] = useState(initial.allow_user_ids ?? []); const [denyUserIds, setDenyUserIds] = useState(initial.deny_user_ids ?? []); const [allowChannels, setAllowChannels] = useState(initial.allow_channels ?? []); @@ -88,10 +86,8 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) const [followupInterval, setFollowupInterval] = useState(initial.followup_interval_minutes ?? 30); const [followupMaxReminders, setFollowupMaxReminders] = useState(initial.followup_max_reminders ?? 0); const [workspaceScope, setWorkspaceScope] = useState(initial.workspace_scope ?? "isolated"); - const isTeamV2 = version >= 2; const [saving, setSaving] = useState(false); - const [versionModalOpen, setVersionModalOpen] = useState(false); // Load known users for combobox useEffect(() => { @@ -101,7 +97,6 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) // Reset when team changes useEffect(() => { const s = (team.settings ?? {}) as TeamAccessSettings; - setVersion(s.version ?? 1); setAllowUserIds(s.allow_user_ids ?? []); setDenyUserIds(s.deny_user_ids ?? []); setAllowChannels(s.allow_channels ?? []); @@ -157,70 +152,19 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps) if (followupInterval !== 30) settings.followup_interval_minutes = followupInterval; if (followupMaxReminders !== 0) settings.followup_max_reminders = followupMaxReminders; settings.workspace_scope = workspaceScope || "isolated"; - if (version >= 2) settings.version = version; await updateTeamSettings(teamId, settings); onSaved(); } catch { // toast shown by hook } finally { setSaving(false); } - }, [teamId, version, allowUserIds, denyUserIds, allowChannels, denyChannels, notifyDispatched, notifyProgress, notifyFailed, notifyCompleted, notifyCommented, notifyNewTask, notifySlowTool, notifyMode, memberRequestsEnabled, memberRequestsAutoDispatch, escalationMode, escalationActions, followupInterval, followupMaxReminders, workspaceScope, updateTeamSettings, onSaved, t]); + }, [teamId, allowUserIds, denyUserIds, allowChannels, denyChannels, notifyDispatched, notifyProgress, notifyFailed, notifyCompleted, notifyCommented, notifyNewTask, notifySlowTool, notifyMode, memberRequestsEnabled, memberRequestsAutoDispatch, escalationMode, escalationActions, followupInterval, followupMaxReminders, workspaceScope, updateTeamSettings, onSaved]); const userOptions = knownUsers.map((u) => ({ value: u, label: u })); const channelOptions = CHANNEL_TYPES.map((c) => ({ value: c.value, label: c.label })); return (
- {/* Team Version */} -
-
-

{t("settings.teamVersion")}

- -
-
- - -
-
- {/* Notifications */}

{t("settings.notifications")}

@@ -384,8 +328,8 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
- {/* Escalation Policy (V2 only — coming soon) */} - {isTeamV2 &&
+ {/* Escalation Policy (coming soon) */} +

{t("settings.escalationPolicy")} {t("settings.versionModal.comingSoon")} @@ -403,10 +347,10 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)

- } + - {/* Follow-up Reminders (V2 only) */} - {isTeamV2 &&
+ {/* Follow-up Reminders */} +

{t("settings.followupReminders")}

@@ -445,7 +389,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
-
} + {/* User Access Control */}
@@ -517,7 +461,6 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
- ); } diff --git a/ui/web/src/pages/teams/team-version-modal.tsx b/ui/web/src/pages/teams/team-version-modal.tsx deleted file mode 100644 index bcb04bad..00000000 --- a/ui/web/src/pages/teams/team-version-modal.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Check, Minus, - ListTodo, FolderOpen, Lock, Bell, - ClipboardCheck, BarChart3, MessageSquare, RotateCcw, ShieldCheck, -} from "lucide-react"; -import { useTranslation } from "react-i18next"; - -interface TeamVersionModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -const FEATURES = [ - { key: "taskManagement", icon: ListTodo, v1: true }, - { key: "sharedWorkspace", icon: FolderOpen, v1: true }, - { key: "executionLocking", icon: Lock, v1: false }, - { key: "followupReminders", icon: Bell, v1: false }, - { key: "progressTracking", icon: BarChart3, v1: false }, - { key: "autoRecovery", icon: RotateCcw, v1: false }, - { key: "commentsAudit", icon: MessageSquare, v1: false, comingSoon: true }, - { key: "escalationPolicy", icon: ShieldCheck, v1: false, comingSoon: true }, - { key: "reviewWorkflow", icon: ClipboardCheck, v1: false, comingSoon: true }, -] as const; - -export function TeamVersionModal({ open, onOpenChange }: TeamVersionModalProps) { - const { t } = useTranslation("teams"); - - return ( - - - - - {t("settings.versionModal.title")} - - Beta - - - {t("settings.versionModal.title")} - - -
- {/* Version columns header */} -
-
-

V1

-

{t("settings.versionBasicDesc")}

-
-
-

V2 Super Team

-

{t("settings.versionAdvancedDesc")}

-
-
- - {/* Feature list */} -
- {FEATURES.map((f) => { - const Icon = f.icon; - const dimmed = "comingSoon" in f && f.comingSoon; - return ( -
-
- -
-

- {t(`settings.versionModal.${f.key}`)} - {dimmed && ( - - {t("settings.versionModal.comingSoon")} - - )} -

-

- {t(`settings.versionModal.${f.key}Desc`)} -

-
-
-
- {f.v1 ? ( - - ) : ( - - )} -
-
- {dimmed ? ( - - ) : ( - - )} -
-
- ); - })} -
- - {/* Column labels for V1/V2 */} -
- - V1 - V2 -
- - {/* Notes */} -
-

{t("settings.versionModal.downgradeNote")}

-

{t("settings.versionModal.betaNote")}

-
- -
- -
- -
-
-
- ); -}