diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index dc77bcad..26c0439a 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -134,14 +134,33 @@ export class TeamDataService { kanbanTaskState?: KanbanState['tasks'][string] ): TeamTaskWithKanban { const reviewState = this.resolveTaskReviewState(task); + const reviewer = kanbanTaskState?.reviewer ?? this.resolveReviewerFromHistory(task) ?? null; return { ...task, reviewState, kanbanColumn: getKanbanColumnFromReviewState(reviewState), - reviewer: kanbanTaskState?.reviewer ?? null, + reviewer, }; } + /** + * Extract reviewer name from task history events as a fallback + * when kanban state doesn't have it (e.g. review done via MCP agent-teams). + */ + private resolveReviewerFromHistory(task: TeamTask): string | null { + if (!task.historyEvents?.length) return null; + for (let i = task.historyEvents.length - 1; i >= 0; i--) { + const event = task.historyEvents[i]; + if (event.type === 'review_approved' && event.actor) { + return event.actor; + } + if (event.type === 'review_requested' && event.reviewer) { + return event.reviewer; + } + } + return null; + } + async listTeams(): Promise { return this.configReader.listTeams(); } diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 3bbddb81..42f36a8d 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -55,6 +55,7 @@ import { import { useShallow } from 'zustand/react/shallow'; import { AddMemberDialog } from './dialogs/AddMemberDialog'; +import type { AddMemberEntry } from './dialogs/AddMemberDialog'; import { CreateTaskDialog } from './dialogs/CreateTaskDialog'; import { EditTeamDialog } from './dialogs/EditTeamDialog'; import { LaunchTeamDialog } from './dialogs/LaunchTeamDialog'; @@ -1403,6 +1404,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele badge={filteredTasks.length} defaultOpen forceOpen={kanbanSearch.trim().length > 0} + contentClassName="overflow-x-visible" action={ - - - + + + + ); diff --git a/src/renderer/components/team/dialogs/TaskCommentInput.tsx b/src/renderer/components/team/dialogs/TaskCommentInput.tsx index 024731c7..3d8e93c3 100644 --- a/src/renderer/components/team/dialogs/TaskCommentInput.tsx +++ b/src/renderer/components/team/dialogs/TaskCommentInput.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; -import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions'; @@ -19,7 +18,9 @@ import { import { MAX_TEXT_LENGTH } from '@shared/constants'; import { ImagePlus, Mic, Send, Trash2, X } from 'lucide-react'; +import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer'; import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox'; +import { MemberBadge } from '@renderer/components/team/MemberBadge'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { CommentAttachmentPayload, ResolvedTeamMember } from '@shared/types'; @@ -27,6 +28,7 @@ import type { CommentAttachmentPayload, ResolvedTeamMember } from '@shared/types const MAX_ATTACHMENTS = 5; const MAX_FILE_SIZE = 20 * 1024 * 1024; const ACCEPTED_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']); +const LONG_QUOTE_THRESHOLD = 200; interface TaskCommentInputProps { teamName: string; @@ -64,6 +66,7 @@ export const TaskCommentInput = ({ const [pendingAttachments, setPendingAttachments] = useState([]); const [attachError, setAttachError] = useState(null); const [lightboxIndex, setLightboxIndex] = useState(null); + const [quoteExpanded, setQuoteExpanded] = useState(false); const fileInputRef = useRef(null); const mentionSuggestions = useMemo( @@ -195,31 +198,17 @@ export const TaskCommentInput = ({ return (
{replyTo ? ( -
-
-
- Replying to{' '} - { - const rc = colorMap.get(replyTo.author); - return rc ? getTeamColorSet(rc).text : 'var(--color-text-secondary)'; - })(), - }} - > - @{replyTo.author} - -
-
- {replyTo.text} -
-
+
+ {/* Decorative quotation mark */} + + “ + + + ) : null}
) : null} @@ -286,6 +298,7 @@ export const TaskCommentInput = ({ /> {statusLabel} + {currentTask.reviewState === 'approved' && currentTask.reviewer ? ( + + ) : null} {currentTask.reviewState === 'needsFix' ? ( Links {relatedIds.map((id) => { const depTask = taskMap.get(id); + const label = depTask + ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` + : `#${deriveTaskDisplayId(id)}`; return ( - + + + + + {label} + ); })}
@@ -679,20 +690,24 @@ export const TaskDetailDialog = ({ Linked from {relatedByIds.map((id) => { const depTask = taskMap.get(id); + const label = depTask + ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` + : `#${deriveTaskDisplayId(id)}`; return ( - + + + + + {label} + ); })}
@@ -991,26 +1006,28 @@ export const TaskDetailDialog = ({ {blockedByIds.map((id) => { const depTask = taskMap.get(id); const isCompleted = depTask?.status === 'completed'; + const label = depTask + ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` + : `#${deriveTaskDisplayId(id)}`; return ( - + + + + + {label} + ); })} @@ -1025,26 +1042,28 @@ export const TaskDetailDialog = ({ {blocksIds.map((id) => { const depTask = taskMap.get(id); const isCompleted = depTask?.status === 'completed'; + const label = depTask + ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` + : `#${deriveTaskDisplayId(id)}`; return ( - + + + + + {label} + ); })} diff --git a/src/renderer/components/team/review/ChangeReviewDialog.tsx b/src/renderer/components/team/review/ChangeReviewDialog.tsx index 72f689fb..68fe0286 100644 --- a/src/renderer/components/team/review/ChangeReviewDialog.tsx +++ b/src/renderer/components/team/review/ChangeReviewDialog.tsx @@ -201,6 +201,28 @@ export const ChangeReviewDialog = ({ () => sortItemsAsTree(activeChangeSet?.files ?? [], (f) => f.relativePath), [activeChangeSet] ); + const loadingFiles = useMemo( + () => sortedFiles.filter((file) => fileContentsLoading[file.filePath]), + [sortedFiles, fileContentsLoading] + ); + const globalDiffLoadingState = useMemo(() => { + if (loadingFiles.length === 0) return null; + + const preferredFile = + (activeFilePath + ? loadingFiles.find((file) => file.filePath === activeFilePath) + : undefined) ?? loadingFiles[0]; + const snippetCount = loadingFiles.reduce( + (sum, file) => sum + file.snippets.filter((snippet) => !snippet.isError).length, + 0 + ); + + return { + loadingFilesCount: loadingFiles.length, + snippetCount, + activeFileName: preferredFile?.relativePath ?? preferredFile?.filePath, + }; + }, [activeFilePath, loadingFiles]); // File paths for viewed tracking const allFilePaths = useMemo(() => sortedFiles.map((f) => f.filePath), [sortedFiles]); @@ -1217,6 +1239,7 @@ export const ChangeReviewDialog = ({ files={sortedFiles} fileContents={fileContents} fileContentsLoading={fileContentsLoading} + globalDiffLoadingState={globalDiffLoadingState} viewedSet={viewedSet} editedContents={editedContents} hunkDecisions={hunkDecisions} diff --git a/src/renderer/components/team/review/ContinuousScrollView.tsx b/src/renderer/components/team/review/ContinuousScrollView.tsx index e21d74c9..e81ea249 100644 --- a/src/renderer/components/team/review/ContinuousScrollView.tsx +++ b/src/renderer/components/team/review/ContinuousScrollView.tsx @@ -12,6 +12,7 @@ import { } from './CodeMirrorDiffUtils'; import { FileSectionDiff } from './FileSectionDiff'; import { FileSectionHeader } from './FileSectionHeader'; +import { FullDiffLoadingBanner } from './FullDiffLoadingBanner'; import type { EditorView } from '@codemirror/view'; import type { FileChangeWithContent, HunkDecision } from '@shared/types'; @@ -22,6 +23,11 @@ interface ContinuousScrollViewProps { files: FileChangeSummary[]; fileContents: Record; fileContentsLoading: Record; + globalDiffLoadingState?: { + loadingFilesCount: number; + snippetCount: number; + activeFileName?: string; + } | null; viewedSet: Set; editedContents: Record; hunkDecisions: Record; @@ -67,6 +73,7 @@ export const ContinuousScrollView = ({ files, fileContents, fileContentsLoading, + globalDiffLoadingState, viewedSet, editedContents, hunkDecisions, @@ -227,6 +234,13 @@ export const ContinuousScrollView = ({ return (
+ {globalDiffLoadingState ? ( + + ) : null} {files.map((file) => { const filePath = file.filePath; const content = fileContents[filePath] ?? null; diff --git a/src/renderer/components/team/review/FileSectionDiff.tsx b/src/renderer/components/team/review/FileSectionDiff.tsx index 57711df0..846694ac 100644 --- a/src/renderer/components/team/review/FileSectionDiff.tsx +++ b/src/renderer/components/team/review/FileSectionDiff.tsx @@ -87,9 +87,6 @@ export const FileSectionDiff = ({ return (
-
- Loading full diff... -
diff --git a/src/renderer/components/team/review/FileSectionPlaceholder.tsx b/src/renderer/components/team/review/FileSectionPlaceholder.tsx index 4fe5df16..3c7dac55 100644 --- a/src/renderer/components/team/review/FileSectionPlaceholder.tsx +++ b/src/renderer/components/team/review/FileSectionPlaceholder.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { FileDiff, LoaderCircle } from 'lucide-react'; + interface FileSectionPlaceholderProps { fileName: string; } @@ -7,17 +9,31 @@ interface FileSectionPlaceholderProps { export const FileSectionPlaceholder = ({ fileName, }: FileSectionPlaceholderProps): React.ReactElement => ( -
-
- {fileName} -
+
+
+
+ +
+
+
+ {fileName} + + + Loading + +
+

Preparing a full editor diff for this file.

+
-
-
-
-
-
+
+
+
+
+
+
+
+
); diff --git a/src/renderer/components/team/review/FullDiffLoadingBanner.tsx b/src/renderer/components/team/review/FullDiffLoadingBanner.tsx new file mode 100644 index 00000000..b3e1fc55 --- /dev/null +++ b/src/renderer/components/team/review/FullDiffLoadingBanner.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +import { Clock3, FileDiff, LoaderCircle, Sparkles } from 'lucide-react'; + +interface FullDiffLoadingBannerProps { + loadingFilesCount: number; + snippetCount: number; + activeFileName?: string; +} + +export const FullDiffLoadingBanner = ({ + loadingFilesCount, + snippetCount, + activeFileName, +}: FullDiffLoadingBannerProps): React.ReactElement => { + const title = + loadingFilesCount === 1 ? 'Preparing Full Diff' : `Preparing ${loadingFilesCount} Full Diffs`; + const subtitle = + loadingFilesCount === 1 + ? activeFileName + ? `Finalizing the exact editor diff for ${activeFileName}.` + : 'Finalizing the exact editor diff for the current file.' + : 'Resolving exact before/after baselines for the files currently loading.'; + + return ( +
+
+
+
+
+ +
+ +
+
+ + + {title} + + {activeFileName ? ( + {activeFileName} + ) : null} +
+ +

{subtitle}

+ +
+ + + {snippetCount} snippet{snippetCount === 1 ? '' : 's'} ready + + + + Editor view loading + + + + {loadingFilesCount} file{loadingFilesCount === 1 ? '' : 's'} in progress + +
+
+
+ +
+
+
+
+

+ Snippet previews stay visible below while the exact baselines are reconstructed. +

+
+
+ + +
+ ); +};