Diff reload is quite jumpy (vibe-kanban) (#104)

* Perfect! I have successfully fixed the jumpy diff reload issue. Here's a summary of what I implemented:

## Fix Summary

The problem was that when the coding agent was running, the diff refreshed every 2 seconds and showed a full loading spinner that replaced the entire diff content, creating a jarring user experience.

**Key Changes Made:**

1. **Added separate loading state** - Created `isBackgroundRefreshing` state to track background updates separately from initial loading
2. **Modified `fetchDiff` function** - Added an `isBackgroundRefresh` parameter to differentiate between initial loads and background refreshes
3. **Updated diff rendering** - The full loading spinner now only appears on initial load, while background refreshes show a subtle spinning indicator with "Updating..." text
4. **Improved user experience** - The diff content remains visible during background refreshes, with just a small indicator showing the update is in progress

**The fix ensures:**
- Initial diff loads still show the full loading spinner for proper UX
- Background refreshes (every 2 seconds during coding agent execution) show a subtle updating indicator
- The diff content remains visible and stable during background updates
- No more jarring disappearing/reappearing of the entire diff section

The build and linting all pass successfully, confirming the implementation is solid and ready for use.

* Fixed! The formatting issues in `TaskDetailsPanel.tsx` have been resolved. All files now use Prettier code style.
This commit is contained in:
Louis Knight-Webb
2025-07-08 17:48:28 +01:00
committed by GitHub
parent 76d03564ad
commit 5368d827ae

View File

@@ -88,6 +88,7 @@ export function TaskDetailsPanel({
const [diff, setDiff] = useState<WorktreeDiff | null>(null);
const [diffLoading, setDiffLoading] = useState(true);
const [diffError, setDiffError] = useState<string | null>(null);
const [isBackgroundRefreshing, setIsBackgroundRefreshing] = useState(false);
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set()
);
@@ -132,43 +133,54 @@ export function TaskDetailsPanel({
const diffLoadingRef = useRef(false);
// Fetch diff when attempt changes
const fetchDiff = useCallback(async () => {
if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) {
setDiff(null);
setDiffLoading(false);
return;
}
const fetchDiff = useCallback(
async (isBackgroundRefresh = false) => {
if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) {
setDiff(null);
setDiffLoading(false);
return;
}
// Prevent multiple concurrent requests
if (diffLoadingRef.current) {
return;
}
// Prevent multiple concurrent requests
if (diffLoadingRef.current) {
return;
}
try {
diffLoadingRef.current = true;
setDiffLoading(true);
setDiffError(null);
const response = await makeRequest(
`/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/diff`
);
try {
diffLoadingRef.current = true;
if (isBackgroundRefresh) {
setIsBackgroundRefreshing(true);
} else {
setDiffLoading(true);
}
setDiffError(null);
const response = await makeRequest(
`/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/diff`
);
if (response.ok) {
const result: ApiResponse<WorktreeDiff> = await response.json();
if (result.success && result.data) {
setDiff(result.data);
if (response.ok) {
const result: ApiResponse<WorktreeDiff> = await response.json();
if (result.success && result.data) {
setDiff(result.data);
} else {
setDiffError('Failed to load diff');
}
} else {
setDiffError('Failed to load diff');
}
} else {
} catch (err) {
setDiffError('Failed to load diff');
} finally {
diffLoadingRef.current = false;
if (isBackgroundRefresh) {
setIsBackgroundRefreshing(false);
} else {
setDiffLoading(false);
}
}
} catch (err) {
setDiffError('Failed to load diff');
} finally {
diffLoadingRef.current = false;
setDiffLoading(false);
}
}, [projectId, selectedAttempt?.id, selectedAttempt?.task_id]);
},
[projectId, selectedAttempt?.id, selectedAttempt?.task_id]
);
useEffect(() => {
if (isOpen) {
@@ -185,11 +197,11 @@ export function TaskDetailsPanel({
if (isCodingAgentRunning) {
// Immediately refresh diff when coding agent starts running
fetchDiff();
fetchDiff(true);
// Then refresh diff every 2 seconds while coding agent is active
const interval = setInterval(() => {
fetchDiff();
fetchDiff(true);
}, 2000);
return () => {
@@ -867,9 +879,19 @@ export function TaskDetailsPanel({
<div
className={`flex items-center justify-between ${areAllFilesCollapsed() ? 'mb-1' : 'mb-3'}`}
>
<div className="text-sm text-muted-foreground">
{diff.files.length} file
{diff.files.length !== 1 ? 's' : ''} changed
<div className="flex items-center gap-2">
<div className="text-sm text-muted-foreground">
{diff.files.length} file
{diff.files.length !== 1 ? 's' : ''} changed
</div>
{isBackgroundRefreshing && (
<div className="flex items-center gap-1">
<div className="animate-spin h-3 w-3 border border-blue-500 border-t-transparent rounded-full"></div>
<span className="text-xs text-blue-600 dark:text-blue-400">
Updating...
</span>
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button