diff --git a/src/renderer/components/sidebar/SessionItem.tsx b/src/renderer/components/sidebar/SessionItem.tsx index 35098c35..b05565b2 100644 --- a/src/renderer/components/sidebar/SessionItem.tsx +++ b/src/renderer/components/sidebar/SessionItem.tsx @@ -8,9 +8,10 @@ import { useCallback, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useStore } from '@renderer/store'; +import { formatSessionLabel, parseSessionTitle } from '@renderer/utils/sessionTitleParser'; import { formatTokensCompact } from '@shared/utils/tokenFormatting'; import { formatDistanceToNowStrict } from 'date-fns'; -import { EyeOff, MessageSquare, Pin } from 'lucide-react'; +import { EyeOff, MessageSquare, Pin, Play, RotateCw, Users } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { OngoingIndicator } from '../common/OngoingIndicator'; @@ -178,7 +179,7 @@ export const SessionItem = ({ type: 'session', sessionId: session.id, projectId: activeProjectId, - label: session.firstMessage?.slice(0, 50) ?? 'Session', + label: formatSessionLabel(session.firstMessage), }, forceNewTab ? { forceNewTab } : { replaceActiveTab: true } ); @@ -191,7 +192,7 @@ export const SessionItem = ({ setContextMenu({ x: e.clientX, y: e.clientY }); }, []); - const sessionLabel = session.firstMessage?.slice(0, 50) ?? 'Session'; + const sessionLabel = formatSessionLabel(session.firstMessage); const handleOpenInCurrentPane = useCallback(() => { if (!activeProjectId) return; @@ -253,49 +254,86 @@ export const SessionItem = ({ ...(isHidden ? { opacity: 0.5 } : {}), }} > - {/* First line: title + ongoing indicator + pin/hidden icons */} -
- {multiSelectActive && ( - onToggleSelect?.()} - onClick={(e) => e.stopPropagation()} - className="size-3.5 shrink-0 accent-blue-500" - /> - )} - {session.isOngoing && } - {isPinned && } - {isHidden && } - - {session.firstMessage ?? 'Untitled'} - -
- - {/* Second line: message count + time + context consumption */} -
- - - {session.messageCount} - - · - {formatShortTime(new Date(session.createdAt))} - {session.contextConsumption != null && session.contextConsumption > 0 && ( + {(() => { + const parsed = parseSessionTitle(session.firstMessage); + const isTeam = parsed.kind !== 'regular'; + return ( <> - · - + {/* First line: title + ongoing indicator + pin/hidden icons */} +
+ {multiSelectActive && ( + onToggleSelect?.()} + onClick={(e) => e.stopPropagation()} + className="size-3.5 shrink-0 accent-blue-500" + /> + )} + {session.isOngoing && } + {isPinned && } + {isHidden && } + {isTeam ? ( + + + {parsed.displayText} + + ) : ( + + {parsed.displayText} + + )} +
+ + {/* Second line: metadata */} +
+ {isTeam && parsed.projectName && ( + <> + {parsed.projectName} + · + + )} + {isTeam && ( + <> + + {parsed.kind === 'team-resume' ? ( + + ) : ( + + )} + {parsed.kind === 'team-resume' ? 'resume' : 'new'} + + · + + )} + + + {session.messageCount} + + · + {formatShortTime(new Date(session.createdAt))} + {session.contextConsumption != null && session.contextConsumption > 0 && ( + <> + · + + + )} +
- )} -
+ ); + })()} {contextMenu && diff --git a/src/renderer/components/team/TeamSessionsSection.tsx b/src/renderer/components/team/TeamSessionsSection.tsx index 13929447..7e19cb64 100644 --- a/src/renderer/components/team/TeamSessionsSection.tsx +++ b/src/renderer/components/team/TeamSessionsSection.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { resolveProjectIdByPath } from '@renderer/utils/projectLookup'; +import { formatSessionLabel } from '@renderer/utils/sessionTitleParser'; import { formatDistanceToNowStrict } from 'date-fns'; import { AlertCircle, @@ -69,7 +70,7 @@ export const TeamSessionsSection = ({ type: 'session', sessionId: session.id, projectId, - label: session.firstMessage?.slice(0, 50) ?? 'Session', + label: formatSessionLabel(session.firstMessage), }, { forceNewTab: true } ); @@ -173,7 +174,7 @@ const SessionRow = ({ onToggleFilter, }: SessionRowProps): React.JSX.Element => { const timeAgo = formatShortTime(new Date(session.createdAt)); - const label = session.firstMessage ?? 'Untitled session'; + const label = formatSessionLabel(session.firstMessage); return (
{ selectedTeamName, selectedTeamData, selectedTeamLoading, + selectedTeamError, selectTeam, openTeamTab, setPendingReviewRequest, @@ -32,6 +37,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => { selectedTeamName: s.selectedTeamName, selectedTeamData: s.selectedTeamData, selectedTeamLoading: s.selectedTeamLoading, + selectedTeamError: s.selectedTeamError, selectTeam: s.selectTeam, openTeamTab: s.openTeamTab, setPendingReviewRequest: s.setPendingReviewRequest, @@ -41,6 +47,11 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => { const teamName = globalTaskDetail?.teamName ?? ''; const taskId = globalTaskDetail?.taskId ?? ''; + const hasTargetTeamData = hasSelectedTargetTeamData( + teamName, + selectedTeamName, + selectedTeamData?.teamName + ); // Load full team data in the background to enable "as before" details (logs/changes/members). useEffect(() => { @@ -65,13 +76,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => { teamName, ]); - const isFullTeamLoaded = selectedTeamName === teamName && !!selectedTeamData; - // Team data is still loading when: - // - selectTeam() hasn't updated selectedTeamName yet (team switch pending) - // - selectedTeamName matches but IPC fetch is still in flight - const isThisTeamLoading = - selectedTeamName !== teamName || - (selectedTeamName === teamName && selectedTeamLoading && !selectedTeamData); + const isFullTeamLoaded = hasTargetTeamData; const taskMap = useMemo(() => { const map = new Map(); @@ -119,12 +124,21 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => { const kanbanTaskState = isFullTeamLoaded ? selectedTeamData?.kanbanState.tasks[taskId] : undefined; + const loading = shouldKeepGlobalTaskDialogLoading({ + teamName, + taskId, + selectedTeamName, + selectedTeamDataPresent: hasTargetTeamData, + selectedTeamLoading, + selectedTeamError, + hasTaskInMap: taskMap.has(taskId), + }); return ( { if (!v && lightboxOpenRef.current) return; - if (!v) onClose(); + if (!v) handleClose(); }} > { const isLead = session.id === leadSessionId; const isSelected = filter.sessionId === session.id; - const label = session.firstMessage?.slice(0, 50) ?? session.id.slice(0, 8); + const label = formatSessionLabel(session.firstMessage) || session.id.slice(0, 8); return (