- Updated task management instructions in tasks.js to clarify the process for handling newly assigned tasks that must wait due to ongoing work, emphasizing the importance of leaving comments with reasons and estimated completion times. - Improved member briefing messages to include critical reminders about task status and comment handling. - Enhanced TeamDataService to implement task comment notification features, ensuring leads are notified of teammate comments on tasks. - Refactored related UI components to support better interaction and visibility of task statuses and notifications.
579 lines
20 KiB
TypeScript
579 lines
20 KiB
TypeScript
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import { Badge } from '@renderer/components/ui/badge';
|
|
import { Button } from '@renderer/components/ui/button';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
|
import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded';
|
|
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
|
import { useStableTeamMentionMeta } from '@renderer/hooks/useStableTeamMentionMeta';
|
|
import { useStore } from '@renderer/store';
|
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
|
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
|
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
|
import {
|
|
Bell,
|
|
CheckCheck,
|
|
ChevronsDownUp,
|
|
ChevronsUpDown,
|
|
MessageSquare,
|
|
PanelLeftClose,
|
|
PanelLeft,
|
|
Search,
|
|
X,
|
|
} from 'lucide-react';
|
|
|
|
import { ActivityTimeline } from '../activity/ActivityTimeline';
|
|
import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup';
|
|
import { MessageExpandDialog } from '../activity/MessageExpandDialog';
|
|
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';
|
|
import type { TimelineItem } from '../activity/LeadThoughtsGroup';
|
|
import type { InboxMessage, ResolvedTeamMember, TaskRef, TeamTaskWithKanban } from '@shared/types';
|
|
|
|
interface TimeWindow {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
interface MessagesPanelProps {
|
|
teamName: string;
|
|
position: 'sidebar' | 'inline';
|
|
onTogglePosition: () => void;
|
|
/** Active (non-removed) members. */
|
|
members: ResolvedTeamMember[];
|
|
/** All team tasks. */
|
|
tasks: TeamTaskWithKanban[];
|
|
/** All raw messages from team data. */
|
|
messages: InboxMessage[];
|
|
/** Whether the team is alive. */
|
|
isTeamAlive?: boolean;
|
|
/** Live lead activity status for the current team. */
|
|
leadActivity?: string;
|
|
/** Latest lead context timestamp for the current team. */
|
|
leadContextUpdatedAt?: string;
|
|
/** Time window for filtering. */
|
|
timeWindow: TimeWindow | null;
|
|
/** Team session IDs for timeline. */
|
|
teamSessionIds: Set<string>;
|
|
/** Current lead session ID. */
|
|
currentLeadSessionId?: string;
|
|
/** Pending replies tracker (shared with parent for MemberList). */
|
|
pendingRepliesByMember: Record<string, number>;
|
|
/** Update pending replies tracker. */
|
|
onPendingReplyChange: (updater: (prev: Record<string, number>) => Record<string, number>) => void;
|
|
/** Callback when a member is clicked in the timeline. */
|
|
onMemberClick?: (member: ResolvedTeamMember) => void;
|
|
/** Callback when a task is clicked from timeline or status block. */
|
|
onTaskClick?: (task: TeamTaskWithKanban) => void;
|
|
/** Callback to open create task dialog from a message. */
|
|
onCreateTaskFromMessage?: (subject: string, description: string) => void;
|
|
/** Callback to open reply dialog for a message. */
|
|
onReplyToMessage?: (message: InboxMessage) => void;
|
|
/** Callback when "Restart team" is clicked. */
|
|
onRestartTeam?: () => void;
|
|
/** Callback when a task ID link is clicked. */
|
|
onTaskIdClick?: (taskId: string) => void;
|
|
}
|
|
|
|
export const MessagesPanel = memo(function MessagesPanel({
|
|
teamName,
|
|
position,
|
|
onTogglePosition,
|
|
members,
|
|
tasks,
|
|
messages,
|
|
isTeamAlive,
|
|
leadActivity,
|
|
leadContextUpdatedAt,
|
|
timeWindow,
|
|
teamSessionIds,
|
|
currentLeadSessionId,
|
|
pendingRepliesByMember,
|
|
onPendingReplyChange,
|
|
onMemberClick,
|
|
onTaskClick,
|
|
onCreateTaskFromMessage,
|
|
onReplyToMessage,
|
|
onRestartTeam,
|
|
onTaskIdClick,
|
|
}: MessagesPanelProps): React.JSX.Element {
|
|
const sendTeamMessage = useStore((s) => s.sendTeamMessage);
|
|
const sendCrossTeamMessage = useStore((s) => s.sendCrossTeamMessage);
|
|
const sendingMessage = useStore((s) => s.sendingMessage);
|
|
const sendMessageError = useStore((s) => s.sendMessageError);
|
|
const lastSendMessageResult = useStore((s) => s.lastSendMessageResult);
|
|
const teams = useStore((s) => s.teams);
|
|
const openTeamTab = useStore((s) => s.openTeamTab);
|
|
|
|
const composerTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
const handleExpandContent = useCallback(() => {
|
|
composerTextareaRef.current?.focus();
|
|
}, []);
|
|
|
|
const [messagesSearchQuery, setMessagesSearchQuery] = useState('');
|
|
const [messagesFilter, setMessagesFilter] = useState<MessagesFilterState>({
|
|
from: new Set(),
|
|
to: new Set(),
|
|
showNoise: false,
|
|
});
|
|
const [messagesFilterOpen, setMessagesFilterOpen] = useState(false);
|
|
const [messagesCollapsed, setMessagesCollapsed] = useState(true);
|
|
const [sidebarSearchVisible, setSidebarSearchVisible] = useState(false);
|
|
const [expandedItemKey, setExpandedItemKey] = useState<string | null>(null);
|
|
|
|
const filteredMessages = useMemo(() => {
|
|
return filterTeamMessages(messages, {
|
|
timeWindow,
|
|
filter: messagesFilter,
|
|
searchQuery: messagesSearchQuery,
|
|
});
|
|
}, [messages, timeWindow, messagesFilter, messagesSearchQuery]);
|
|
|
|
// Resolve the expanded item from filtered messages
|
|
const expandedItem = useMemo<TimelineItem | null>(() => {
|
|
if (!expandedItemKey) return null;
|
|
if (!expandedItemKey.startsWith('thoughts-')) {
|
|
const msg = filteredMessages.find((m) => toMessageKey(m) === expandedItemKey);
|
|
return msg ? { type: 'message', message: msg } : null;
|
|
}
|
|
const allItems = groupTimelineItems(filteredMessages);
|
|
return (
|
|
allItems.find(
|
|
(item) =>
|
|
item.type === 'lead-thoughts' && getThoughtGroupKey(item.group) === expandedItemKey
|
|
) ?? null
|
|
);
|
|
}, [expandedItemKey, filteredMessages]);
|
|
|
|
// Auto-clear stale expanded key
|
|
useEffect(() => {
|
|
if (expandedItemKey && expandedItem === null) {
|
|
setExpandedItemKey(null);
|
|
}
|
|
}, [expandedItemKey, expandedItem]);
|
|
|
|
const handleExpandItem = useCallback((key: string) => {
|
|
setExpandedItemKey(key);
|
|
}, []);
|
|
|
|
const handleExpandDialogChange = useCallback((open: boolean) => {
|
|
if (!open) setExpandedItemKey(null);
|
|
}, []);
|
|
|
|
const { readSet, markRead, markAllRead } = useTeamMessagesRead(teamName);
|
|
const { expandedSet, toggle: toggleExpandOverride } = useTeamMessagesExpanded(teamName);
|
|
|
|
const messagesUnreadCount = useMemo(
|
|
() => filteredMessages.filter((m) => !m.read && !readSet.has(toMessageKey(m))).length,
|
|
[filteredMessages, readSet]
|
|
);
|
|
|
|
const handleMessageVisible = useCallback(
|
|
(message: InboxMessage) => markRead(toMessageKey(message)),
|
|
[markRead]
|
|
);
|
|
|
|
const readState = useMemo(() => ({ readSet, getMessageKey: toMessageKey }), [readSet]);
|
|
|
|
const { teamNames, teamColorByName } = useStableTeamMentionMeta(teams);
|
|
|
|
const handleMarkAllRead = useCallback(() => {
|
|
const keys = filteredMessages
|
|
.filter((m) => !m.read && !readSet.has(toMessageKey(m)))
|
|
.map((m) => toMessageKey(m));
|
|
markAllRead(keys);
|
|
}, [filteredMessages, readSet, markAllRead]);
|
|
|
|
// Auto-clear pending replies when a member actually responds
|
|
useEffect(() => {
|
|
if (Object.keys(pendingRepliesByMember).length === 0) return;
|
|
const next = { ...pendingRepliesByMember };
|
|
let changed = false;
|
|
for (const [memberName, sentAtMs] of Object.entries(pendingRepliesByMember)) {
|
|
const hasReply = messages.some((m) => {
|
|
if (m.from !== memberName) return false;
|
|
const ts = Date.parse(m.timestamp);
|
|
return Number.isFinite(ts) && ts > sentAtMs;
|
|
});
|
|
if (hasReply) {
|
|
delete next[memberName];
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) onPendingReplyChange(() => next);
|
|
}, [messages, pendingRepliesByMember, onPendingReplyChange]);
|
|
|
|
const handleSend = useCallback(
|
|
(
|
|
member: string,
|
|
text: string,
|
|
summary?: string,
|
|
attachments?: Parameters<typeof sendTeamMessage>[1] extends { attachments?: infer A }
|
|
? A
|
|
: never,
|
|
actionMode?: ActionMode,
|
|
taskRefs?: TaskRef[]
|
|
) => {
|
|
const sentAtMs = Date.now();
|
|
onPendingReplyChange((prev) => ({ ...prev, [member]: sentAtMs }));
|
|
void sendTeamMessage(teamName, {
|
|
member,
|
|
text,
|
|
summary,
|
|
attachments,
|
|
actionMode,
|
|
taskRefs,
|
|
}).catch(() => {
|
|
onPendingReplyChange((prev) => {
|
|
if (prev[member] !== sentAtMs) return prev;
|
|
const next = { ...prev };
|
|
delete next[member];
|
|
return next;
|
|
});
|
|
});
|
|
},
|
|
[teamName, sendTeamMessage, onPendingReplyChange]
|
|
);
|
|
|
|
const handleCrossTeamSend = useCallback(
|
|
(
|
|
toTeam: string,
|
|
text: string,
|
|
summary?: string,
|
|
actionMode?: ActionMode,
|
|
taskRefs?: TaskRef[]
|
|
) => {
|
|
void sendCrossTeamMessage({
|
|
fromTeam: teamName,
|
|
fromMember: 'user',
|
|
toTeam,
|
|
text,
|
|
taskRefs,
|
|
actionMode,
|
|
summary,
|
|
});
|
|
},
|
|
[teamName, sendCrossTeamMessage]
|
|
);
|
|
|
|
// ---- Shared content (used in both modes) ----
|
|
const searchAndFilterControls = (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex min-w-0 flex-1 items-center gap-1.5 rounded-md border border-[var(--color-border)] bg-transparent px-2 py-1">
|
|
<Search size={12} className="shrink-0 text-[var(--color-text-muted)]" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
value={messagesSearchQuery}
|
|
onChange={(e) => setMessagesSearchQuery(e.target.value)}
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="min-w-0 flex-1 bg-transparent text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none"
|
|
/>
|
|
{messagesSearchQuery && (
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded p-0.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
|
|
onClick={() => setMessagesSearchQuery('')}
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<MessagesFilterPopover
|
|
filter={messagesFilter}
|
|
messages={messages}
|
|
open={messagesFilterOpen}
|
|
onOpenChange={setMessagesFilterOpen}
|
|
onApply={setMessagesFilter}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
const searchAndFilterBar = (
|
|
<div className="flex items-center gap-2">
|
|
{searchAndFilterControls}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="pointer-events-auto size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setMessagesCollapsed((v) => !v);
|
|
}}
|
|
>
|
|
{messagesCollapsed ? <ChevronsUpDown size={14} /> : <ChevronsDownUp size={14} />}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{messagesCollapsed ? 'Expand all messages' : 'Collapse all messages'}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
|
|
const messagesContent = (
|
|
<>
|
|
<MessageComposer
|
|
teamName={teamName}
|
|
members={members}
|
|
isTeamAlive={isTeamAlive}
|
|
sending={sendingMessage}
|
|
sendError={sendMessageError}
|
|
lastResult={lastSendMessageResult}
|
|
textareaRef={composerTextareaRef}
|
|
onSend={handleSend}
|
|
onCrossTeamSend={handleCrossTeamSend}
|
|
/>
|
|
<StatusBlock
|
|
members={members}
|
|
tasks={tasks}
|
|
messages={messages}
|
|
pendingRepliesByMember={pendingRepliesByMember}
|
|
onMemberClick={onMemberClick}
|
|
onTaskClick={onTaskClick}
|
|
/>
|
|
<ActivityTimeline
|
|
messages={filteredMessages}
|
|
teamName={teamName}
|
|
members={members}
|
|
readState={readState}
|
|
allCollapsed={messagesCollapsed}
|
|
expandOverrides={expandedSet}
|
|
onToggleExpandOverride={toggleExpandOverride}
|
|
teamSessionIds={teamSessionIds}
|
|
currentLeadSessionId={currentLeadSessionId}
|
|
isTeamAlive={isTeamAlive}
|
|
leadActivity={leadActivity}
|
|
leadContextUpdatedAt={leadContextUpdatedAt}
|
|
teamNames={teamNames}
|
|
teamColorByName={teamColorByName}
|
|
onTeamClick={openTeamTab}
|
|
onMemberClick={onMemberClick}
|
|
onCreateTaskFromMessage={onCreateTaskFromMessage}
|
|
onReplyToMessage={onReplyToMessage}
|
|
onMessageVisible={handleMessageVisible}
|
|
onRestartTeam={onRestartTeam}
|
|
onTaskIdClick={onTaskIdClick}
|
|
onExpandItem={handleExpandItem}
|
|
onExpandContent={handleExpandContent}
|
|
/>
|
|
<MessageExpandDialog
|
|
expandedItem={expandedItem}
|
|
open={expandedItemKey !== null}
|
|
onOpenChange={handleExpandDialogChange}
|
|
teamName={teamName}
|
|
members={members}
|
|
onCreateTaskFromMessage={onCreateTaskFromMessage}
|
|
onReplyToMessage={onReplyToMessage}
|
|
onMemberClick={onMemberClick}
|
|
onTaskIdClick={onTaskIdClick}
|
|
onRestartTeam={onRestartTeam}
|
|
teamNames={teamNames}
|
|
teamColorByName={teamColorByName}
|
|
onTeamClick={openTeamTab}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
// ---- Sidebar mode ----
|
|
if (position === 'sidebar') {
|
|
return (
|
|
<div className="flex size-full flex-col overflow-hidden bg-[var(--color-surface)]">
|
|
{/* Header */}
|
|
<div className="flex shrink-0 items-center gap-2 border-b border-[var(--color-border)] bg-[var(--color-section-bg)] px-3 py-2">
|
|
<MessageSquare size={14} className="shrink-0 text-[var(--color-text-muted)]" />
|
|
<span className="text-sm font-medium text-[var(--color-text)]">Messages</span>
|
|
{filteredMessages.length > 0 && (
|
|
<Badge
|
|
variant="secondary"
|
|
className="px-1.5 py-0.5 text-[10px] font-normal leading-none"
|
|
>
|
|
{filteredMessages.length}
|
|
</Badge>
|
|
)}
|
|
{messagesUnreadCount > 0 && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Badge
|
|
variant="secondary"
|
|
className="bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-normal leading-none text-blue-600 dark:text-blue-400"
|
|
>
|
|
{messagesUnreadCount} new
|
|
</Badge>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">{messagesUnreadCount} unread</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
{messagesUnreadCount > 0 && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-1 rounded-md px-1.5 py-1 text-[11px] text-blue-400 transition-colors hover:bg-blue-500/10"
|
|
onClick={handleMarkAllRead}
|
|
>
|
|
<CheckCheck size={12} />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">Mark all as read</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={() => {
|
|
void window.electronAPI.openExternal(
|
|
'https://github.com/777genius/claude-notifications-go'
|
|
);
|
|
}}
|
|
>
|
|
<Bell size={12} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Desktop notifications plugin</TooltipContent>
|
|
</Tooltip>
|
|
<div className="ml-auto flex items-center gap-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={() => setMessagesCollapsed((v) => !v)}
|
|
>
|
|
{messagesCollapsed ? <ChevronsUpDown size={14} /> : <ChevronsDownUp size={14} />}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{messagesCollapsed ? 'Expand all messages' : 'Collapse all messages'}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={() => setSidebarSearchVisible((v) => !v)}
|
|
>
|
|
{sidebarSearchVisible ? <X size={14} /> : <Search size={14} />}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{sidebarSearchVisible ? 'Hide search' : 'Search messages'}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={onTogglePosition}
|
|
>
|
|
<PanelLeftClose size={14} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">Move to inline</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
{/* Search & filter bar (toggleable) */}
|
|
{sidebarSearchVisible && (
|
|
<div className="shrink-0 border-b border-[var(--color-border)] px-3 py-1.5">
|
|
{searchAndFilterControls}
|
|
</div>
|
|
)}
|
|
{/* Scrollable content */}
|
|
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden px-3 py-2">
|
|
{messagesContent}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ---- Inline mode (wrapped in CollapsibleTeamSection) ----
|
|
return (
|
|
<CollapsibleTeamSection
|
|
sectionId="messages"
|
|
title="Messages"
|
|
icon={<MessageSquare size={14} />}
|
|
badge={filteredMessages.length}
|
|
secondaryBadge={
|
|
filteredMessages.length > 0 && messagesUnreadCount > 0 ? messagesUnreadCount : undefined
|
|
}
|
|
afterBadge={
|
|
messagesUnreadCount > 0 ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="pointer-events-auto flex items-center gap-1 rounded-md px-1.5 py-1 text-[11px] text-blue-400 transition-colors hover:bg-blue-500/10"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleMarkAllRead();
|
|
}}
|
|
>
|
|
<CheckCheck size={12} />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">Mark all as read</TooltipContent>
|
|
</Tooltip>
|
|
) : undefined
|
|
}
|
|
headerExtra={
|
|
<>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
void window.electronAPI.openExternal(
|
|
'https://github.com/777genius/claude-notifications-go'
|
|
);
|
|
}}
|
|
>
|
|
<Bell size={12} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Desktop notifications plugin</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onTogglePosition();
|
|
}}
|
|
>
|
|
<PanelLeft size={14} />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Move to sidebar</TooltipContent>
|
|
</Tooltip>
|
|
</>
|
|
}
|
|
defaultOpen
|
|
action={<div className="flex items-center gap-2 pl-2 pr-2">{searchAndFilterBar}</div>}
|
|
>
|
|
{messagesContent}
|
|
</CollapsibleTeamSection>
|
|
);
|
|
});
|