fix: use merge-base for diffStat in merger and show files changed on done task cards

The merger was using `git diff HEAD..branch --stat` which includes
artifacts from other tasks when branches fork from older main commits.
Switch to `git diff $(merge-base)..branch --stat` so commit messages
only describe the branch's own changes.

Also surface the "files changed" button on done task cards using
mergeDetails, opening the same ChangedFilesModal with commit-backed
diffs (matching the Changes tab in the task modal).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-04 08:33:53 -07:00
parent f882c5aed9
commit d3a3cc71d6
9 changed files with 74 additions and 17 deletions

View File

@@ -83,7 +83,7 @@ function AppInner() {
const [terminalOpen, setTerminalOpen] = useState(false);
const [filesOpen, setFilesOpen] = useState(false);
const [fileBrowserWorkspace, setFileBrowserWorkspace] = useState("project");
const [changedFilesState, setChangedFilesState] = useState<{ taskId: string; worktree: string | undefined; column: string } | null>(null);
const [changedFilesState, setChangedFilesState] = useState<{ taskId: string; worktree: string | undefined; column: string; commitSha?: string } | null>(null);
const [activityLogOpen, setActivityLogOpen] = useState(false);
const [gitManagerOpen, setGitManagerOpen] = useState(false);
const [workflowStepsOpen, setWorkflowStepsOpen] = useState(false);
@@ -469,8 +469,8 @@ function AppInner() {
setFilesOpen(true);
}, []);
const handleOpenChangedFiles = useCallback((taskId: string, worktree: string | undefined, column: string) => {
setChangedFilesState({ taskId, worktree, column });
const handleOpenChangedFiles = useCallback((taskId: string, worktree: string | undefined, column: string, commitSha?: string) => {
setChangedFilesState({ taskId, worktree, column, commitSha });
}, []);
const handleCloseChangedFiles = useCallback(() => {
@@ -705,6 +705,7 @@ function AppInner() {
worktree={changedFilesState.worktree}
column={changedFilesState.column}
projectId={currentProject?.id}
commitSha={changedFilesState.commitSha}
isOpen={true}
onClose={handleCloseChangedFiles}
/>

View File

@@ -35,7 +35,7 @@ interface BoardProps {
* Called when the user clicks the "Subtask" button in the inline create card.
*/
onSubtaskBreakdown?: (description: string) => void;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string, commitSha?: string) => void;
favoriteProviders?: string[];
favoriteModels?: string[];
onToggleFavorite?: (provider: string) => void;

View File

@@ -19,6 +19,7 @@ interface ChangedFilesModalProps {
worktree: string | undefined;
column: string;
projectId?: string;
commitSha?: string;
isOpen: boolean;
onClose: () => void;
}
@@ -66,6 +67,7 @@ export function ChangedFilesModal({
worktree,
column,
projectId,
commitSha,
isOpen,
onClose,
}: ChangedFilesModalProps) {
@@ -74,6 +76,7 @@ export function ChangedFilesModal({
worktree,
column,
projectId,
commitSha,
);
const [isMobile, setIsMobile] = useState(false);

View File

@@ -46,7 +46,7 @@ interface ColumnProps {
* Called when the user clicks the "Subtask" button in the inline create card.
*/
onSubtaskBreakdown?: (description: string) => void;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string, commitSha?: string) => void;
favoriteProviders?: string[];
favoriteModels?: string[];
onToggleFavorite?: (provider: string) => void;

View File

@@ -44,7 +44,7 @@ interface TaskCardProps {
) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string, commitSha?: string) => void;
}
function areTaskBadgeInfosEqual(
@@ -688,6 +688,20 @@ function TaskCardComponent({
</span>
</button>
)}
{task.column === "done" && task.mergeDetails?.filesChanged != null && task.mergeDetails.filesChanged > 0 && (
<button
type="button"
className="card-session-files"
onClick={(e) => {
e.stopPropagation();
onOpenFilesForTask?.(task.id, task.worktree, task.column, task.mergeDetails?.commitSha);
}}
disabled={!onOpenFilesForTask}
>
<Folder size={12} />
<span>{task.mergeDetails.filesChanged} files changed</span>
</button>
)}
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy) && (
<div className="card-meta">
{task.dependencies && task.dependencies.length > 0 && (

View File

@@ -16,7 +16,7 @@ interface WorktreeGroupProps {
id: string,
updates: { title?: string; description?: string; dependencies?: string[] }
) => Promise<Task>;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string, commitSha?: string) => void;
}
function WorktreeGroupComponent({

View File

@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback } from "react";
import { fetchTaskFileDiffs, type TaskFileDiff } from "../api";
import { fetchTaskFileDiffs, fetchCommitDiff, type TaskFileDiff } from "../api";
import { parsePatch } from "../components/CommitDiffTab";
const ACTIVE_COLUMNS = new Set(["in-progress", "in-review"]);
@@ -17,14 +18,19 @@ export function useChangedFiles(
worktree: string | undefined,
column: string,
projectId?: string,
commitSha?: string,
): UseChangedFilesResult {
const [files, setFiles] = useState<TaskFileDiff[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedFile, setSelectedFile] = useState<TaskFileDiff | null>(null);
const isDone = column === "done";
useEffect(() => {
if (!taskId || !worktree || !ACTIVE_COLUMNS.has(column)) {
// For active tasks: need worktree
// For done tasks: need commitSha
if (!taskId || (!isDone && (!worktree || !ACTIVE_COLUMNS.has(column))) || (isDone && !commitSha)) {
setFiles([]);
setLoading(false);
setError(null);
@@ -38,7 +44,21 @@ export function useChangedFiles(
setLoading(true);
setError(null);
try {
const result = await fetchTaskFileDiffs(taskId, projectId);
let result: TaskFileDiff[];
if (isDone && commitSha) {
// Done task: fetch from commit history
const data = await fetchCommitDiff(commitSha);
const parsed = parsePatch(data.patch || "");
result = parsed.map((f) => ({
path: f.path,
status: f.status === "unknown" ? "modified" as const : f.status,
diff: f.patch,
oldPath: undefined,
}));
} else {
// Active task: fetch from worktree
result = await fetchTaskFileDiffs(taskId, projectId);
}
if (cancelled) return;
setFiles(result);
setSelectedFile((current) => {
@@ -68,7 +88,7 @@ export function useChangedFiles(
return () => {
cancelled = true;
};
}, [taskId, worktree, column, projectId]);
}, [taskId, worktree, column, projectId, commitSha, isDone]);
const resetSelection = useCallback(() => {
setSelectedFile(null);