From 52677b55d0a0d0c12f6f43705bdee3e6d17dde28 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 19 Apr 2026 20:57:13 +0300 Subject: [PATCH] feat(team): enhance team messaging functionality and UI - Integrated pending replies state management for team members. - Updated TeamDetailView to initialize pending replies from state. - Added logic to refresh team messages and member activity on tab focus. - Improved UI components by increasing dialog content width for better layout. - Enhanced member draft rows with avatar support for better visual representation. - Implemented reconciliation logic for pending replies based on message history. - Updated tests to cover new functionality and ensure reliability. --- .../components/team/TeamDetailView.tsx | 47 ++++- .../team/dialogs/CreateTeamDialog.tsx | 2 +- .../team/dialogs/EditTeamDialog.tsx | 9 +- .../team/dialogs/LaunchTeamDialog.tsx | 2 +- .../team/dialogs/SendMessageDialog.tsx | 13 +- .../components/team/members/LeadModelRow.tsx | 17 +- .../team/members/MemberDraftRow.tsx | 28 ++- .../team/members/MembersEditorSection.tsx | 3 + .../team/messages/MessageComposer.tsx | 3 + .../team/messages/MessagesPanel.tsx | 104 +++++++++-- .../team/sidebar/teamSidebarUiState.ts | 12 ++ .../team/messages/MessagesPanel.test.ts | 163 +++++++++++++++++- 12 files changed, 365 insertions(+), 38 deletions(-) diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index a34d71d7..338b8255 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -107,6 +107,10 @@ import { ScheduleSection } from './schedule/ScheduleSection'; import { TeamSidebarHost } from './sidebar/TeamSidebarHost'; import { TeamSidebarPortalSource } from './sidebar/TeamSidebarPortalSource'; import { TeamSidebarRail } from './sidebar/TeamSidebarRail'; +import { + getTeamPendingRepliesState, + setTeamPendingRepliesState, +} from './sidebar/teamSidebarUiState'; import { ClaudeLogsSection } from './ClaudeLogsSection'; import { CollapsibleTeamSection } from './CollapsibleTeamSection'; import { ProcessesSection } from './ProcessesSection'; @@ -914,7 +918,9 @@ export const TeamDetailView = ({ initialTab?: MemberDetailTab; initialActivityFilter?: MemberActivityFilter; } | null>(null); - const [pendingRepliesByMember, setPendingRepliesByMember] = useState>({}); + const [pendingRepliesByMember, setPendingRepliesByMember] = useState>(() => + getTeamPendingRepliesState(teamName) + ); const [createTaskDialog, setCreateTaskDialog] = useState({ open: false, defaultSubject: '', @@ -1211,6 +1217,8 @@ export const TeamDetailView = ({ clearProvisioningError, isTeamProvisioning, refreshTeamData, + refreshTeamMessagesHead, + refreshMemberActivityMeta, syncTeamPendingReplyRefresh, kanbanFilterQuery, clearKanbanFilter, @@ -1262,6 +1270,8 @@ export const TeamDetailView = ({ loading: s.selectedTeamName === teamName ? s.selectedTeamLoading : false, error: s.selectedTeamName === teamName ? s.selectedTeamError : null, refreshTeamData: s.refreshTeamData, + refreshTeamMessagesHead: s.refreshTeamMessagesHead, + refreshMemberActivityMeta: s.refreshMemberActivityMeta, syncTeamPendingReplyRefresh: s.syncTeamPendingReplyRefresh, kanbanFilterQuery: s.kanbanFilterQuery, clearKanbanFilter: s.clearKanbanFilter, @@ -1285,6 +1295,7 @@ export const TeamDetailView = ({ const tabId = useTabIdOptional(); const activeTabId = useStore((s) => s.activeTabId); const isThisTabActive = tabId ? activeTabId === tabId : false; + const wasInteractiveRef = useRef(false); useEffect(() => { const now = Date.now(); @@ -1348,6 +1359,14 @@ export const TeamDetailView = ({ } }, [tabId, initTabUIState]); + useEffect(() => { + setPendingRepliesByMember(getTeamPendingRepliesState(teamName)); + }, [teamName]); + + useEffect(() => { + setTeamPendingRepliesState(teamName, pendingRepliesByMember); + }, [pendingRepliesByMember, teamName]); + useEffect(() => { const wasProvisioning = wasProvisioningRef.current; wasProvisioningRef.current = isTeamProvisioning; @@ -1386,6 +1405,32 @@ export const TeamDetailView = ({ } }, [isThisTabActive, teamName, storedTeamName, loading, selectTeam]); + useEffect(() => { + const isInteractive = isThisTabActive && isPaneFocused; + const justBecameInteractive = isInteractive && !wasInteractiveRef.current; + wasInteractiveRef.current = isInteractive; + if (!justBecameInteractive || !teamName) { + return; + } + + void (async () => { + try { + const headResult = await refreshTeamMessagesHead(teamName); + if (headResult.feedChanged) { + await refreshMemberActivityMeta(teamName); + } + } catch { + // Best-effort refresh on tab focus. + } + })(); + }, [ + isPaneFocused, + isThisTabActive, + refreshMemberActivityMeta, + refreshTeamMessagesHead, + teamName, + ]); + // Fetch active teams when launch dialog opens (for conflict warning) useEffect(() => { if (!launchDialogOpen) return; diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index 9b230ef8..19e2e7b3 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -1220,7 +1220,7 @@ export const CreateTeamDialog = ({ } }} > - + {initialData ? 'Copy Team' : 'Create Team'} diff --git a/src/renderer/components/team/dialogs/EditTeamDialog.tsx b/src/renderer/components/team/dialogs/EditTeamDialog.tsx index 94bb0e47..9eaa1c4c 100644 --- a/src/renderer/components/team/dialogs/EditTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/EditTeamDialog.tsx @@ -23,7 +23,11 @@ import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors' import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; -import { buildMemberColorMap, displayMemberName } from '@renderer/utils/memberHelpers'; +import { + agentAvatarUrl, + buildMemberColorMap, + displayMemberName, +} from '@renderer/utils/memberHelpers'; import { parseNumericSuffixName } from '@shared/utils/teamMemberName'; import { Loader2 } from 'lucide-react'; @@ -451,7 +455,7 @@ export const EditTeamDialog = ({ return ( !nextOpen && onClose()}> - + Edit Team Change team name, description and color @@ -518,6 +522,7 @@ export const EditTeamDialog = ({ {dialogTitle} diff --git a/src/renderer/components/team/dialogs/SendMessageDialog.tsx b/src/renderer/components/team/dialogs/SendMessageDialog.tsx index f8806e2c..51e4f0d0 100644 --- a/src/renderer/components/team/dialogs/SendMessageDialog.tsx +++ b/src/renderer/components/team/dialogs/SendMessageDialog.tsx @@ -76,8 +76,8 @@ interface SendMessageDialogProps { onClose: () => void; } -// Sticky action mode — survives dialog close/reopen (component remount) -// Default: 'delegate' for teams (overridden to 'do' if solo/no teammates) +// Sticky action mode within the current session. +// Each dialog open still re-derives the default from the current team shape. let stickyActionMode: ActionMode = 'delegate'; export const SendMessageDialog = ({ @@ -168,7 +168,12 @@ export const SendMessageDialog = ({ useEffect(() => { if (open && !prevOpenRef.current) { const leadName = members.find((m) => isLeadMember(m))?.name; - setMember(defaultRecipient ?? leadName ?? ''); + const nextRecipient = defaultRecipient ?? leadName ?? ''; + const nextRecipientMember = members.find((candidate) => candidate.name === nextRecipient); + const nextCanDelegate = + members.length > 1 && Boolean(nextRecipientMember && isLeadMember(nextRecipientMember)); + setMember(nextRecipient); + setActionMode(nextCanDelegate ? 'delegate' : 'do'); setQuote(quotedMessage); setQuoteExpanded(false); prevResultRef.current = lastResult; @@ -188,6 +193,8 @@ export const SendMessageDialog = ({ defaultChip, quotedMessage, lastResult, + members, + setActionMode, textDraft, chipDraft, ]); diff --git a/src/renderer/components/team/members/LeadModelRow.tsx b/src/renderer/components/team/members/LeadModelRow.tsx index f3417a2f..cd81ed2b 100644 --- a/src/renderer/components/team/members/LeadModelRow.tsx +++ b/src/renderer/components/team/members/LeadModelRow.tsx @@ -13,6 +13,7 @@ import { Label } from '@renderer/components/ui/label'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; +import { agentAvatarUrl } from '@renderer/utils/memberHelpers'; import { isAnthropicHaikuTeamModel } from '@renderer/utils/teamModelCatalog'; import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors'; import { AlertTriangle, ChevronDown, ChevronRight, Info } from 'lucide-react'; @@ -63,7 +64,7 @@ export const LeadModelRow = ({ return (