From c55787d6e1458c67f63af706e62814430b214ab6 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 14 Mar 2026 00:09:05 +0200 Subject: [PATCH] feat: enhance task management protocols and UI components - Updated task management instructions in tasks.js and TeamProvisioningService.ts to emphasize the importance of moving tasks to in_progress before performing follow-up work. - Added critical reminders in member briefing and task status protocols to ensure proper task handling and prevent issues with comment-driven work. - Refactored TeamDetailView to improve tab management and data consistency after hot module replacement. - Simplified MessagesPanel by integrating the StatusBlock component, enhancing the display of team status and pending replies. - Cleaned up teamSlice by removing unnecessary functions and ensuring default model handling for team launch parameters. --- agent-teams-controller/src/internal/tasks.js | 4 + .../services/team/TeamProvisioningService.ts | 6 ++ .../components/team/TeamDetailView.tsx | 18 +++- .../team/messages/MessagesPanel.tsx | 80 ++------------ .../components/team/messages/StatusBlock.tsx | 102 ++++++++++++++++++ src/renderer/store/slices/teamSlice.ts | 68 +++++------- 6 files changed, 160 insertions(+), 118 deletions(-) create mode 100644 src/renderer/components/team/messages/StatusBlock.tsx diff --git a/agent-teams-controller/src/internal/tasks.js b/agent-teams-controller/src/internal/tasks.js index 29b7021b..4302e487 100644 --- a/agent-teams-controller/src/internal/tasks.js +++ b/agent-teams-controller/src/internal/tasks.js @@ -369,6 +369,9 @@ function buildMemberTaskProtocol(teamName) { - Do NOT start multiple tasks at once unless the team lead explicitly directs parallel work. 3. Use MCP tool task_complete BEFORE sending your final reply: { teamName: "${teamName}", taskId: "" } + - If a new task comment means you must do more real work on that same task, FIRST run task_start again before doing the follow-up work. + - After that follow-up work is finished, run task_complete again before your reply. + - Never do comment-driven implementation/fix work while the task is still shown as pending, review, completed, or approved. 4. If you are asked to review and the task is accepted, move it to APPROVED (not DONE) with MCP tool review_approve: { teamName: "${teamName}", taskId: "", note?: "", notifyOwner: true } 5. If review fails and changes are needed, use MCP tool review_request_changes: @@ -495,6 +498,7 @@ async function memberBriefing(context, memberName) { const lines = [ `Member briefing for ${requestedMemberName} on team "${context.teamName}" (${context.teamName}).`, `Role: ${role}.`, + `CRITICAL: If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on that same task, FIRST move it to in_progress with task_start, THEN do the work, and when finished move it to done with task_complete. Never skip this reopen -> work -> done cycle.`, `Team lead: ${leadName}.`, buildMemberLanguageInstruction(config), `You must NOT start work, claim tasks, or improvise task/process protocol before reading and following this briefing.`, diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 889bae9d..41705b47 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -442,6 +442,7 @@ After member_briefing succeeds: - Introduce yourself briefly (name and role) and confirm you are ready. - Then wait for task assignments. - When you later receive work or reconnect after a restart, use task_briefing as your compact queue view. Use task_get when you need the full task context before starting a pending/needsFix task or when the in_progress briefing details are not enough. +- CRITICAL: If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on that same task, FIRST move it to in_progress with task_start, THEN do the work, and when finished move it to done with task_complete. Never skip this reopen -> work -> done cycle. ${buildTeammateAgentBlockReminder()} ${actionModeProtocol}`; } @@ -474,6 +475,7 @@ ${actionModeProtocol} - Before you start any needsFix or pending task, call task_get for that specific task. - If you are the one about to do the implementation/fixes and the owner is missing or someone else, run task_set_owner to yourself immediately before task_start. - Only then run task_start when you truly begin. + - If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on it, FIRST run task_start, then do the work, and when finished run task_complete again. Never skip this reopen -> work -> done cycle. - If you have no tasks, wait for new assignments.`; } @@ -513,6 +515,7 @@ ${actionModeProtocol} - Before you start any needsFix or pending task, call task_get for that specific task. - If you are the one about to do the implementation/fixes and the owner is missing or someone else, run task_set_owner to yourself immediately before task_start. - Only then run task_start when you truly begin. + - If a task gets a new comment and you are going to do additional implementation/fix/follow-up work on it, FIRST run task_start, then do the work, and when finished run task_complete again. Never skip this reopen -> work -> done cycle. - If you have no tasks, wait for new assignments.`; } @@ -603,6 +606,9 @@ function buildTaskStatusProtocol(teamName: string): string { - Do NOT start multiple tasks at once unless the team lead explicitly directs parallel work. 3. Use MCP tool task_complete BEFORE sending your final reply: { teamName: "${teamName}", taskId: "" } + - If a new task comment means you must do more real work on that same task, FIRST run task_start again before doing the follow-up work. + - After that follow-up work is finished, run task_complete again before your reply. + - Never do comment-driven implementation/fix work while the task is still shown as pending, review, completed, or approved. 4. If you are asked to review and the task is accepted, move it to APPROVED (not DONE) with MCP tool review_approve: { teamName: "${teamName}", taskId: "", note?: "", notifyOwner: true } 5. If review fails and changes are needed, use MCP tool review_request_changes: diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 0b6d068a..6f4b6638 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -303,6 +303,8 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele selectedContextPhase, setSelectedContextPhase, } = useTabUI(); + const activeTabId = useStore((s) => s.activeTabId); + const isThisTabActive = tabId ? activeTabId === tabId : false; const [isContextButtonHovered, setIsContextButtonHovered] = useState(false); // Messages panel resize @@ -368,6 +370,18 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele void fetchDeletedTasks(teamName); }, [teamName, selectTeam, fetchDeletedTasks]); + // Recovery: after HMR, all mounted TeamDetailView effects re-run simultaneously. + // With CSS display-toggle (all tabs stay mounted), the last selectTeam() call wins + // and other tabs get stuck with mismatched data (permanent skeleton). + // Re-trigger selectTeam when this tab becomes active and store data is stale. + const storedTeamName = data?.teamName; + useEffect(() => { + if (!isThisTabActive || !teamName || loading) return; + if (storedTeamName != null && storedTeamName !== teamName) { + void selectTeam(teamName); + } + }, [isThisTabActive, teamName, storedTeamName, loading, selectTeam]); + // Fetch active teams when launch dialog opens (for conflict warning) useEffect(() => { if (!launchDialogOpen) return; @@ -496,9 +510,6 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele [visibleContextTokens, lastAiGroupTotalTokens] ); - const activeTabId = useStore((s) => s.activeTabId); - const isThisTabActive = tabId ? activeTabId === tabId : false; - // Keep lead-session context fresh in the background while the team tab is active. // This keeps the button value current even when the panel is closed. // For offline teams: fetch once on mount so the percentage shows immediately. @@ -1097,6 +1108,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele launchParams?.model && (() => { const MODEL_LABELS: Record = { + default: 'Default', opus: 'Opus 4.6', sonnet: 'Sonnet 4.5', haiku: 'Haiku 4.5', diff --git a/src/renderer/components/team/messages/MessagesPanel.tsx b/src/renderer/components/team/messages/MessagesPanel.tsx index f57126d7..7acdcb91 100644 --- a/src/renderer/components/team/messages/MessagesPanel.tsx +++ b/src/renderer/components/team/messages/MessagesPanel.tsx @@ -6,7 +6,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded'; import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead'; import { useStore } from '@renderer/store'; -import { computePendingCrossTeamReplies } from '@renderer/utils/crossTeamPendingReplies'; import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering'; import { toMessageKey } from '@renderer/utils/teamMessageKey'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; @@ -15,7 +14,6 @@ import { CheckCheck, ChevronsDownUp, ChevronsUpDown, - ChevronRight, MessageSquare, PanelLeftClose, PanelLeft, @@ -23,12 +21,11 @@ import { X, } from 'lucide-react'; -import { ActiveTasksBlock } from '../activity/ActiveTasksBlock'; import { ActivityTimeline } from '../activity/ActivityTimeline'; -import { PendingRepliesBlock } from '../activity/PendingRepliesBlock'; import { CollapsibleTeamSection } from '../CollapsibleTeamSection'; import { MessageComposer } from './MessageComposer'; import { MessagesFilterPopover } from './MessagesFilterPopover'; +import { StatusBlock } from './StatusBlock'; import type { MessagesFilterState } from './MessagesFilterPopover'; import type { ActionMode } from './ActionModeSelector'; @@ -110,15 +107,6 @@ export const MessagesPanel = ({ const [messagesFilterOpen, setMessagesFilterOpen] = useState(false); const [messagesCollapsed, setMessagesCollapsed] = useState(true); const [sidebarSearchVisible, setSidebarSearchVisible] = useState(false); - const [statusBlockCollapsed, setStatusBlockCollapsed] = useState(false); - const [pendingRepliesNowMs, setPendingRepliesNowMs] = useState(() => Date.now()); - - useEffect(() => { - const intervalId = window.setInterval(() => { - setPendingRepliesNowMs(Date.now()); - }, 1000); - return () => window.clearInterval(intervalId); - }, []); const filteredMessages = useMemo(() => { return filterTeamMessages(messages, { @@ -148,28 +136,6 @@ export const MessagesPanel = ({ markAllRead(keys); }, [filteredMessages, readSet, markAllRead]); - const pendingCrossTeamReplies = useMemo( - () => computePendingCrossTeamReplies(messages, pendingRepliesNowMs), - [messages, pendingRepliesNowMs] - ); - - /** Whether the Status block has any visible items (pending replies or active tasks). */ - const hasStatusItems = useMemo(() => { - const hasPendingReplies = Object.keys(pendingRepliesByMember).some((name) => - members.some((m) => m.name === name) - ); - if (hasPendingReplies) return true; - if (pendingCrossTeamReplies.length > 0) return true; - - const tMap = new Map(tasks.map((t) => [t.id, t])); - return members.some((m) => { - if (!m.currentTaskId) return false; - const task = tMap.get(m.currentTaskId); - if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false; - return true; - }); - }, [members, tasks, pendingRepliesByMember, pendingCrossTeamReplies.length]); - // Auto-clear pending replies when a member actually responds useEffect(() => { if (Object.keys(pendingRepliesByMember).length === 0) return; @@ -312,42 +278,14 @@ export const MessagesPanel = ({ onSend={handleSend} onCrossTeamSend={handleCrossTeamSend} /> - {/* Status block: button floats right (absolute, no layout impact); - expanded content renders full-width in normal flow. */} - {hasStatusItems && ( - <> -
- -
- {!statusBlockCollapsed && ( -
- - -
- )} - - )} + ; + onMemberClick?: (member: ResolvedTeamMember) => void; + onTaskClick?: (task: TeamTaskWithKanban) => void; +} + +/** + * Self-contained status section that owns its own 1-second timer for + * cross-team pending reply TTL tracking. Isolates the timer-driven + * re-renders from the rest of MessagesPanel / ActivityTimeline so that + * text selection in messages is not disrupted. + */ +export const StatusBlock = ({ + members, + tasks, + messages, + pendingRepliesByMember, + onMemberClick, + onTaskClick, +}: StatusBlockProps): React.JSX.Element | null => { + const [collapsed, setCollapsed] = useState(false); + const [nowMs, setNowMs] = useState(() => Date.now()); + + const pendingCrossTeamReplies = useMemo( + () => computePendingCrossTeamReplies(messages, nowMs), + [messages, nowMs] + ); + + /** Whether the Status block has any visible items. */ + const hasItems = useMemo(() => { + const hasPendingReplies = Object.keys(pendingRepliesByMember).some((name) => + members.some((m) => m.name === name) + ); + if (hasPendingReplies) return true; + if (pendingCrossTeamReplies.length > 0) return true; + + const tMap = new Map(tasks.map((t) => [t.id, t])); + return members.some((m) => { + if (!m.currentTaskId) return false; + const task = tMap.get(m.currentTaskId); + if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false; + return true; + }); + }, [members, tasks, pendingRepliesByMember, pendingCrossTeamReplies.length]); + + // Only run the 1-second timer when the block actually has content to show. + useEffect(() => { + if (!hasItems) return; + const id = window.setInterval(() => setNowMs(Date.now()), 1000); + return () => window.clearInterval(id); + }, [hasItems]); + + if (!hasItems) return null; + + return ( + <> +
+ +
+ {!collapsed && ( +
+ + +
+ )} + + ); +}; diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index f45ffb98..3d47803a 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -537,14 +537,6 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void { } } -function removeLaunchParams(teamName: string): void { - try { - localStorage.removeItem(LAUNCH_PARAMS_PREFIX + teamName); - } catch { - // ignore - } -} - /** * Parse raw model string from TeamLaunchRequest back into base model + extended context flag. * E.g. 'opus[1m]' → { model: 'opus', extendedContext: true } @@ -1412,20 +1404,18 @@ export const createTeamSlice: StateCreator = (set, // Persist per-team launch params (model, effort, extended context) const { model: baseModel, extendedContext } = parseModelString(request.model); - if (baseModel) { - const params: TeamLaunchParams = { - model: baseModel, - effort: request.effort, - extendedContext, - }; - saveLaunchParams(request.teamName, params); - set((state) => ({ - launchParamsByTeam: { - ...state.launchParamsByTeam, - [request.teamName]: params, - }, - })); - } + const params: TeamLaunchParams = { + model: baseModel || 'default', + effort: request.effort, + extendedContext, + }; + saveLaunchParams(request.teamName, params); + set((state) => ({ + launchParamsByTeam: { + ...state.launchParamsByTeam, + [request.teamName]: params, + }, + })); set((state) => { const nextRuns = { ...state.provisioningRuns }; @@ -1545,28 +1535,18 @@ export const createTeamSlice: StateCreator = (set, // Persist per-team launch params (model, effort, extended context) const { model: baseModel, extendedContext } = parseModelString(request.model); - if (baseModel) { - const params: TeamLaunchParams = { - model: baseModel, - effort: request.effort, - extendedContext, - }; - saveLaunchParams(request.teamName, params); - set((state) => ({ - launchParamsByTeam: { - ...state.launchParamsByTeam, - [request.teamName]: params, - }, - })); - } else { - // No model selected — clear stored params - removeLaunchParams(request.teamName); - set((state) => { - const updated = { ...state.launchParamsByTeam }; - delete updated[request.teamName]; - return { launchParamsByTeam: updated }; - }); - } + const params: TeamLaunchParams = { + model: baseModel || 'default', + effort: request.effort, + extendedContext, + }; + saveLaunchParams(request.teamName, params); + set((state) => ({ + launchParamsByTeam: { + ...state.launchParamsByTeam, + [request.teamName]: params, + }, + })); set((state) => { const nextRuns = { ...state.provisioningRuns };