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.
This commit is contained in:
parent
a6f9515966
commit
c55787d6e1
6 changed files with 160 additions and 118 deletions
|
|
@ -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: "<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: "<taskId>", note?: "<optional 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.`,
|
||||
|
|
|
|||
|
|
@ -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: "<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: "<taskId>", note?: "<optional note>", notifyOwner: true }
|
||||
5. If review fails and changes are needed, use MCP tool review_request_changes:
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
default: 'Default',
|
||||
opus: 'Opus 4.6',
|
||||
sonnet: 'Sonnet 4.5',
|
||||
haiku: 'Haiku 4.5',
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<>
|
||||
<div className="relative h-0">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute -top-[19px] right-0 z-10 flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||
onClick={() => setStatusBlockCollapsed((prev) => !prev)}
|
||||
aria-label={statusBlockCollapsed ? 'Expand status' : 'Collapse status'}
|
||||
>
|
||||
<ChevronRight
|
||||
size={12}
|
||||
className={`shrink-0 transition-transform duration-150 ${statusBlockCollapsed ? '' : 'rotate-90'}`}
|
||||
/>
|
||||
Status
|
||||
</button>
|
||||
</div>
|
||||
{!statusBlockCollapsed && (
|
||||
<div className="mt-5">
|
||||
<PendingRepliesBlock
|
||||
members={members}
|
||||
pendingRepliesByMember={pendingRepliesByMember}
|
||||
pendingCrossTeamReplies={pendingCrossTeamReplies}
|
||||
onMemberClick={onMemberClick}
|
||||
/>
|
||||
<ActiveTasksBlock
|
||||
members={members}
|
||||
tasks={tasks}
|
||||
onMemberClick={onMemberClick}
|
||||
onTaskClick={onTaskClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<StatusBlock
|
||||
members={members}
|
||||
tasks={tasks}
|
||||
messages={messages}
|
||||
pendingRepliesByMember={pendingRepliesByMember}
|
||||
onMemberClick={onMemberClick}
|
||||
onTaskClick={onTaskClick}
|
||||
/>
|
||||
<ActivityTimeline
|
||||
messages={filteredMessages}
|
||||
teamName={teamName}
|
||||
|
|
|
|||
102
src/renderer/components/team/messages/StatusBlock.tsx
Normal file
102
src/renderer/components/team/messages/StatusBlock.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { computePendingCrossTeamReplies } from '@renderer/utils/crossTeamPendingReplies';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
import { ActiveTasksBlock } from '../activity/ActiveTasksBlock';
|
||||
import { PendingRepliesBlock } from '../activity/PendingRepliesBlock';
|
||||
|
||||
import type { InboxMessage, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
|
||||
|
||||
interface StatusBlockProps {
|
||||
members: ResolvedTeamMember[];
|
||||
tasks: TeamTaskWithKanban[];
|
||||
messages: InboxMessage[];
|
||||
pendingRepliesByMember: Record<string, number>;
|
||||
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 (
|
||||
<>
|
||||
<div className="relative h-0">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute -top-[19px] right-0 z-10 flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||
onClick={() => setCollapsed((prev) => !prev)}
|
||||
aria-label={collapsed ? 'Expand status' : 'Collapse status'}
|
||||
>
|
||||
<ChevronRight
|
||||
size={12}
|
||||
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
|
||||
/>
|
||||
Status
|
||||
</button>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="mt-5">
|
||||
<PendingRepliesBlock
|
||||
members={members}
|
||||
pendingRepliesByMember={pendingRepliesByMember}
|
||||
pendingCrossTeamReplies={pendingCrossTeamReplies}
|
||||
onMemberClick={onMemberClick}
|
||||
/>
|
||||
<ActiveTasksBlock
|
||||
members={members}
|
||||
tasks={tasks}
|
||||
onMemberClick={onMemberClick}
|
||||
onTaskClick={onTaskClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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 };
|
||||
|
|
|
|||
Loading…
Reference in a new issue