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.
|
- 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:
|
3. Use MCP tool task_complete BEFORE sending your final reply:
|
||||||
{ teamName: "${teamName}", taskId: "<taskId>" }
|
{ 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:
|
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 }
|
{ teamName: "${teamName}", taskId: "<taskId>", note?: "<optional note>", notifyOwner: true }
|
||||||
5. If review fails and changes are needed, use MCP tool review_request_changes:
|
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 = [
|
const lines = [
|
||||||
`Member briefing for ${requestedMemberName} on team "${context.teamName}" (${context.teamName}).`,
|
`Member briefing for ${requestedMemberName} on team "${context.teamName}" (${context.teamName}).`,
|
||||||
`Role: ${role}.`,
|
`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}.`,
|
`Team lead: ${leadName}.`,
|
||||||
buildMemberLanguageInstruction(config),
|
buildMemberLanguageInstruction(config),
|
||||||
`You must NOT start work, claim tasks, or improvise task/process protocol before reading and following this briefing.`,
|
`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.
|
- Introduce yourself briefly (name and role) and confirm you are ready.
|
||||||
- Then wait for task assignments.
|
- 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.
|
- 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()}
|
${buildTeammateAgentBlockReminder()}
|
||||||
${actionModeProtocol}`;
|
${actionModeProtocol}`;
|
||||||
}
|
}
|
||||||
|
|
@ -474,6 +475,7 @@ ${actionModeProtocol}
|
||||||
- Before you start any needsFix or pending task, call task_get for that specific task.
|
- 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.
|
- 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.
|
- 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.`;
|
- 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.
|
- 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.
|
- 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.
|
- 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.`;
|
- 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.
|
- 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:
|
3. Use MCP tool task_complete BEFORE sending your final reply:
|
||||||
{ teamName: "${teamName}", taskId: "<taskId>" }
|
{ 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:
|
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 }
|
{ teamName: "${teamName}", taskId: "<taskId>", note?: "<optional note>", notifyOwner: true }
|
||||||
5. If review fails and changes are needed, use MCP tool review_request_changes:
|
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,
|
selectedContextPhase,
|
||||||
setSelectedContextPhase,
|
setSelectedContextPhase,
|
||||||
} = useTabUI();
|
} = useTabUI();
|
||||||
|
const activeTabId = useStore((s) => s.activeTabId);
|
||||||
|
const isThisTabActive = tabId ? activeTabId === tabId : false;
|
||||||
const [isContextButtonHovered, setIsContextButtonHovered] = useState(false);
|
const [isContextButtonHovered, setIsContextButtonHovered] = useState(false);
|
||||||
|
|
||||||
// Messages panel resize
|
// Messages panel resize
|
||||||
|
|
@ -368,6 +370,18 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
void fetchDeletedTasks(teamName);
|
void fetchDeletedTasks(teamName);
|
||||||
}, [teamName, selectTeam, fetchDeletedTasks]);
|
}, [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)
|
// Fetch active teams when launch dialog opens (for conflict warning)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!launchDialogOpen) return;
|
if (!launchDialogOpen) return;
|
||||||
|
|
@ -496,9 +510,6 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
[visibleContextTokens, lastAiGroupTotalTokens]
|
[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.
|
// 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.
|
// This keeps the button value current even when the panel is closed.
|
||||||
// For offline teams: fetch once on mount so the percentage shows immediately.
|
// 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 &&
|
launchParams?.model &&
|
||||||
(() => {
|
(() => {
|
||||||
const MODEL_LABELS: Record<string, string> = {
|
const MODEL_LABELS: Record<string, string> = {
|
||||||
|
default: 'Default',
|
||||||
opus: 'Opus 4.6',
|
opus: 'Opus 4.6',
|
||||||
sonnet: 'Sonnet 4.5',
|
sonnet: 'Sonnet 4.5',
|
||||||
haiku: 'Haiku 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 { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded';
|
||||||
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { computePendingCrossTeamReplies } from '@renderer/utils/crossTeamPendingReplies';
|
|
||||||
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
||||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||||
|
|
@ -15,7 +14,6 @@ import {
|
||||||
CheckCheck,
|
CheckCheck,
|
||||||
ChevronsDownUp,
|
ChevronsDownUp,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
ChevronRight,
|
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
|
|
@ -23,12 +21,11 @@ import {
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { ActiveTasksBlock } from '../activity/ActiveTasksBlock';
|
|
||||||
import { ActivityTimeline } from '../activity/ActivityTimeline';
|
import { ActivityTimeline } from '../activity/ActivityTimeline';
|
||||||
import { PendingRepliesBlock } from '../activity/PendingRepliesBlock';
|
|
||||||
import { CollapsibleTeamSection } from '../CollapsibleTeamSection';
|
import { CollapsibleTeamSection } from '../CollapsibleTeamSection';
|
||||||
import { MessageComposer } from './MessageComposer';
|
import { MessageComposer } from './MessageComposer';
|
||||||
import { MessagesFilterPopover } from './MessagesFilterPopover';
|
import { MessagesFilterPopover } from './MessagesFilterPopover';
|
||||||
|
import { StatusBlock } from './StatusBlock';
|
||||||
|
|
||||||
import type { MessagesFilterState } from './MessagesFilterPopover';
|
import type { MessagesFilterState } from './MessagesFilterPopover';
|
||||||
import type { ActionMode } from './ActionModeSelector';
|
import type { ActionMode } from './ActionModeSelector';
|
||||||
|
|
@ -110,15 +107,6 @@ export const MessagesPanel = ({
|
||||||
const [messagesFilterOpen, setMessagesFilterOpen] = useState(false);
|
const [messagesFilterOpen, setMessagesFilterOpen] = useState(false);
|
||||||
const [messagesCollapsed, setMessagesCollapsed] = useState(true);
|
const [messagesCollapsed, setMessagesCollapsed] = useState(true);
|
||||||
const [sidebarSearchVisible, setSidebarSearchVisible] = useState(false);
|
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(() => {
|
const filteredMessages = useMemo(() => {
|
||||||
return filterTeamMessages(messages, {
|
return filterTeamMessages(messages, {
|
||||||
|
|
@ -148,28 +136,6 @@ export const MessagesPanel = ({
|
||||||
markAllRead(keys);
|
markAllRead(keys);
|
||||||
}, [filteredMessages, readSet, markAllRead]);
|
}, [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
|
// Auto-clear pending replies when a member actually responds
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(pendingRepliesByMember).length === 0) return;
|
if (Object.keys(pendingRepliesByMember).length === 0) return;
|
||||||
|
|
@ -312,42 +278,14 @@ export const MessagesPanel = ({
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
onCrossTeamSend={handleCrossTeamSend}
|
onCrossTeamSend={handleCrossTeamSend}
|
||||||
/>
|
/>
|
||||||
{/* Status block: button floats right (absolute, no layout impact);
|
<StatusBlock
|
||||||
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}
|
members={members}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
|
messages={messages}
|
||||||
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
onMemberClick={onMemberClick}
|
onMemberClick={onMemberClick}
|
||||||
onTaskClick={onTaskClick}
|
onTaskClick={onTaskClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ActivityTimeline
|
<ActivityTimeline
|
||||||
messages={filteredMessages}
|
messages={filteredMessages}
|
||||||
teamName={teamName}
|
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.
|
* Parse raw model string from TeamLaunchRequest back into base model + extended context flag.
|
||||||
* E.g. 'opus[1m]' → { model: 'opus', extendedContext: true }
|
* E.g. 'opus[1m]' → { model: 'opus', extendedContext: true }
|
||||||
|
|
@ -1412,9 +1404,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
|
|
||||||
// Persist per-team launch params (model, effort, extended context)
|
// Persist per-team launch params (model, effort, extended context)
|
||||||
const { model: baseModel, extendedContext } = parseModelString(request.model);
|
const { model: baseModel, extendedContext } = parseModelString(request.model);
|
||||||
if (baseModel) {
|
|
||||||
const params: TeamLaunchParams = {
|
const params: TeamLaunchParams = {
|
||||||
model: baseModel,
|
model: baseModel || 'default',
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
extendedContext,
|
extendedContext,
|
||||||
};
|
};
|
||||||
|
|
@ -1425,7 +1416,6 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
[request.teamName]: params,
|
[request.teamName]: params,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextRuns = { ...state.provisioningRuns };
|
const nextRuns = { ...state.provisioningRuns };
|
||||||
|
|
@ -1545,9 +1535,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
|
|
||||||
// Persist per-team launch params (model, effort, extended context)
|
// Persist per-team launch params (model, effort, extended context)
|
||||||
const { model: baseModel, extendedContext } = parseModelString(request.model);
|
const { model: baseModel, extendedContext } = parseModelString(request.model);
|
||||||
if (baseModel) {
|
|
||||||
const params: TeamLaunchParams = {
|
const params: TeamLaunchParams = {
|
||||||
model: baseModel,
|
model: baseModel || 'default',
|
||||||
effort: request.effort,
|
effort: request.effort,
|
||||||
extendedContext,
|
extendedContext,
|
||||||
};
|
};
|
||||||
|
|
@ -1558,15 +1547,6 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
[request.teamName]: params,
|
[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 };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextRuns = { ...state.provisioningRuns };
|
const nextRuns = { ...state.provisioningRuns };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue