+
!p.stoppedAt).length}
defaultOpen
>
-
+
)}
diff --git a/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx b/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx
index 1ef04f11..51c4f4ea 100644
--- a/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx
+++ b/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx
@@ -63,7 +63,7 @@ export const GlobalTaskDetailDialog = (): React.JSX.Element | null => {
members={activeMembers}
onClose={closeGlobalTaskDetail}
onOwnerChange={undefined}
- footerExtra={
+ headerExtra={
{currentTask.subject}
{currentTask.activeForm ? (
@@ -460,18 +461,9 @@ export const TaskDetailDialog = ({
- {footerExtra ? (
-
- {footerExtra}
-
-
- ) : (
-
- )}
+
diff --git a/src/renderer/components/team/review/ReviewFileTree.tsx b/src/renderer/components/team/review/ReviewFileTree.tsx
index c8dac96a..44fb38d7 100644
--- a/src/renderer/components/team/review/ReviewFileTree.tsx
+++ b/src/renderer/components/team/review/ReviewFileTree.tsx
@@ -1,8 +1,17 @@
-import { useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
-import { Check, Circle, CircleDot, File, FolderOpen, X as XIcon } from 'lucide-react';
+import {
+ Check,
+ ChevronRight,
+ Circle,
+ CircleDot,
+ File,
+ Folder,
+ FolderOpen,
+ X as XIcon,
+} from 'lucide-react';
import type { HunkDecision } from '@shared/types';
import type { FileChangeSummary } from '@shared/types/review';
@@ -92,7 +101,7 @@ function getFileStatus(
return 'mixed';
}
-const FileStatusIcon = ({ status }: { status: FileStatus }) => {
+const FileStatusIcon = ({ status }: { status: FileStatus }): JSX.Element => {
switch (status) {
case 'accepted':
return
;
@@ -116,6 +125,8 @@ const TreeItem = ({
viewedSet,
onMarkViewed,
onUnmarkViewed,
+ collapsedFolders,
+ onToggleFolder,
}: {
node: TreeNode;
selectedFilePath: string | null;
@@ -126,7 +137,9 @@ const TreeItem = ({
viewedSet?: Set
;
onMarkViewed?: (filePath: string) => void;
onUnmarkViewed?: (filePath: string) => void;
-}) => {
+ collapsedFolders: Set;
+ onToggleFolder: (fullPath: string) => void;
+}): JSX.Element => {
if (node.isFile && node.file) {
const isSelected = node.file.filePath === selectedFilePath;
const isActive = node.file.filePath === activeFilePath && !isSelected;
@@ -184,38 +197,81 @@ const TreeItem = ({
);
}
+ const isOpen = !collapsedFolders.has(node.fullPath);
+ const FolderIcon = isOpen ? FolderOpen : Folder;
+
return (
-
onToggleFolder(node.fullPath)}
+ className="flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-xs text-text-muted transition-colors hover:bg-surface-raised hover:text-text"
style={{ paddingLeft: `${depth * 12 + 8}px` }}
+ aria-label={isOpen ? `Collapse ${node.name}` : `Expand ${node.name}`}
>
-
+
+
{node.name}
-
- {[...node.children]
- .sort((a, b) => {
- if (a.isFile !== b.isFile) return a.isFile ? 1 : -1;
- return a.name.localeCompare(b.name);
- })
- .map((child) => (
-
- ))}
+
+ {isOpen &&
+ [...node.children]
+ .sort((a, b) => {
+ if (a.isFile !== b.isFile) return a.isFile ? 1 : -1;
+ return a.name.localeCompare(b.name);
+ })
+ .map((child) => (
+
+ ))}
);
};
+function applyExpandAncestors(prev: Set, ancestors: string[]): Set {
+ const collapsedAncestors = ancestors.filter((a) => prev.has(a));
+ if (collapsedAncestors.length === 0) return prev;
+ const next = new Set(prev);
+ for (const a of collapsedAncestors) {
+ next.delete(a);
+ }
+ return next;
+}
+
+function getAncestorFolderPaths(tree: TreeNode[], filePath: string): string[] {
+ const paths: string[] = [];
+
+ function walk(nodes: TreeNode[], ancestors: string[]): boolean {
+ for (const node of nodes) {
+ if (node.isFile && node.file?.filePath === filePath) {
+ paths.push(...ancestors);
+ return true;
+ }
+ if (!node.isFile) {
+ if (walk(node.children, [...ancestors, node.fullPath])) return true;
+ }
+ }
+ return false;
+ }
+
+ walk(tree, []);
+ return paths;
+}
+
export const ReviewFileTree = ({
files,
selectedFilePath,
@@ -224,9 +280,35 @@ export const ReviewFileTree = ({
onMarkViewed,
onUnmarkViewed,
activeFilePath,
-}: ReviewFileTreeProps) => {
+}: ReviewFileTreeProps): JSX.Element => {
const hunkDecisions = useStore((state) => state.hunkDecisions);
const tree = useMemo(() => buildTree(files), [files]);
+ const [collapsedFolders, setCollapsedFolders] = useState>(() => new Set());
+
+ const toggleFolder = useCallback((fullPath: string) => {
+ setCollapsedFolders((prev) => {
+ const next = new Set(prev);
+ if (next.has(fullPath)) {
+ next.delete(fullPath);
+ } else {
+ next.add(fullPath);
+ }
+ return next;
+ });
+ }, []);
+
+ // Auto-expand parent folders when a file is selected or becomes active
+ useEffect(() => {
+ const targetPath = selectedFilePath ?? activeFilePath;
+ if (!targetPath) return;
+
+ const ancestors = getAncestorFolderPaths(tree, targetPath);
+ if (ancestors.length === 0) return;
+
+ queueMicrotask(() => {
+ setCollapsedFolders((prev) => applyExpandAncestors(prev, ancestors));
+ });
+ }, [selectedFilePath, activeFilePath, tree]);
// Auto-scroll tree to active file when scroll-spy updates
useEffect(() => {
@@ -263,6 +345,8 @@ export const ReviewFileTree = ({
viewedSet={viewedSet}
onMarkViewed={onMarkViewed}
onUnmarkViewed={onUnmarkViewed}
+ collapsedFolders={collapsedFolders}
+ onToggleFolder={toggleFolder}
/>
))}
diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts
index a274f2fa..5c0bff6b 100644
--- a/src/renderer/store/slices/teamSlice.ts
+++ b/src/renderer/store/slices/teamSlice.ts
@@ -199,7 +199,7 @@ export const createTeamSlice: StateCreator