fix: correct log order sorting and add configurable log order setting (#1720)

* feat: add configurable log order setting for task detail view

Add a new "Log Order" setting in Display Settings that allows users to
choose how logs are displayed in the task detail view:
- Chronological (oldest first): Oldest logs appear at top, auto-scroll to bottom
- Reverse-chronological (newest first): Newest logs appear at top, auto-scroll to top

Changes:
- Add logOrder property to AppSettings type ('chronological' | 'reverse-chronological')
- Set default value to 'chronological' to maintain current behavior
- Add English and French i18n translations
- Add Select component in DisplaySettings UI
- Update TaskLogs component to apply log order
- Update scroll behavior in useTaskDetail hook based on log order

* fix: increase log order dropdown width to prevent text truncation

* fix: address PR review comments - reactive settings and memoized entries

- Add reactive settings access at top of useTaskDetail hook
- Update auto-scroll useEffect to include settings.logOrder in dependency array
- Update handleLogsScroll to use reactive settings for consistency
- Add useMemo to PhaseLogSection to avoid re-calculating sorted entries on every render

Fixes review comments from PR #1720

* refactor: use focused selectors for logOrder to avoid unnecessary re-renders

- Replace wide subscription to settings object with focused selector for logOrder
- In useTaskDetail: use logOrder selector instead of full settings object
- In TaskLogs PhaseLogSection: subscribe only to logOrder instead of entire settings
- This ensures components only re-render when logOrder specifically changes

* fix: correct log order sorting and improve timestamp display

This commit fixes inverted log order logic and improves UX for task logs.

Bug Fixes:
- Fix inverted log order sorting: chronological now correctly shows oldest
  entries first (entries are naturally chronological from append() in backend)
- Fix auto-scroll not triggering when new logs arrive by adding phaseLogs
  to useEffect dependency array

UX Improvements:
- Add max-height and internal scrolling to log order dropdown to prevent
  viewport expansion
- Change timestamp format to use system locale (toLocaleString) which
  displays date and time according to user's OS settings, making it more
  readable for European users who prefer 24-hour format

Files changed:
- TaskLogs.tsx: fix sorting logic, update timestamp formatting
- useTaskDetail.ts: add phaseLogs to auto-scroll dependency array
- DisplaySettings.tsx: add max-height to SelectContent

* fix: preserve log entry state when toggling log order

Use stable timestamp as React key instead of timestamp+index to prevent
component remounting when log order changes. This preserves the isExpanded
state for log detail views when users toggle between chronological and
reverse-chronological order.

Previously, the key included the array index which changed on reorder,
causing React to unmount and remount all LogEntry components, losing
any expanded detail view state.

---------

Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
Burak
2026-02-05 14:54:41 +01:00
committed by GitHub
parent acb131b721
commit 2e2b82365f
7 changed files with 85 additions and 11 deletions
@@ -7,6 +7,7 @@ import { SettingsSection } from './SettingsSection';
import { useSettingsStore } from '../../stores/settings-store';
import { UI_SCALE_MIN, UI_SCALE_MAX, UI_SCALE_DEFAULT, UI_SCALE_STEP } from '../../../shared/constants';
import type { AppSettings } from '../../../shared/types';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
interface DisplaySettingsProps {
settings: AppSettings;
@@ -240,6 +241,39 @@ export function DisplaySettings({ settings, onSettingsChange }: DisplaySettingsP
<span>{UI_SCALE_MAX}%</span>
</div>
</div>
{/* Log Order Setting */}
<div className="space-y-3">
<div className="flex items-center justify-between max-w-md">
<div className="space-y-1">
<Label htmlFor="logOrder" className="text-sm font-medium text-foreground">
{t('logOrder.label')}
</Label>
<p className="text-sm text-muted-foreground">
{t('logOrder.description')}
</p>
</div>
<Select
value={settings.logOrder || 'chronological'}
onValueChange={(value) => onSettingsChange({
...settings,
logOrder: value as 'chronological' | 'reverse-chronological'
})}
>
<SelectTrigger id="logOrder" className="w-72">
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-60 overflow-y-auto">
<SelectItem value="chronological">
{t('logOrder.chronological')}
</SelectItem>
<SelectItem value="reverse-chronological">
{t('logOrder.reverseChronological')}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</SettingsSection>
);
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import {
Terminal,
Loader2,
@@ -21,6 +21,7 @@ import {
import { Badge } from '../ui/badge';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../ui/collapsible';
import { cn } from '../../lib/utils';
import { useSettingsStore } from '../../stores/settings-store';
import type { Task, TaskLogs, TaskLogPhase, TaskPhaseLog, TaskLogEntry, TaskMetadata } from '../../../shared/types';
import type { PhaseModelConfig, ThinkingLevel, ModelTypeShort } from '../../../shared/types/settings';
@@ -176,9 +177,17 @@ interface PhaseLogSectionProps {
function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, phaseConfig }: PhaseLogSectionProps) {
const Icon = PHASE_ICONS[phase];
const logOrder = useSettingsStore(s => s.settings.logOrder);
const status = phaseLog?.status || 'pending';
const hasEntries = (phaseLog?.entries.length || 0) > 0;
// Memoize sorted entries to avoid re-calculating on every render
// Entries are naturally in chronological order (oldest first from append())
const displayedEntries = useMemo(() => {
const entries = phaseLog?.entries || [];
return logOrder === 'reverse-chronological' ? [...entries].reverse() : entries;
}, [phaseLog?.entries, logOrder]);
const getStatusBadge = () => {
switch (status) {
case 'active':
@@ -273,8 +282,8 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p
{!hasEntries ? (
<p className="text-xs text-muted-foreground italic">No logs yet</p>
) : (
phaseLog?.entries.map((entry, idx) => (
<LogEntry key={`${entry.timestamp}-${idx}`} entry={entry} />
displayedEntries.map((entry) => (
<LogEntry key={`${entry.timestamp}-${entry.type}-${entry.content}`} entry={entry} />
))
)}
</div>
@@ -314,7 +323,8 @@ function LogEntry({ entry }: LogEntryProps) {
const formatTime = (timestamp: string) => {
try {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
// Use system locale for date and time formatting
return date.toLocaleString();
} catch {
return '';
}
@@ -1,5 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { useProjectStore } from '../../../stores/project-store';
import { useSettingsStore } from '../../../stores/settings-store';
import { checkTaskRunning, isIncompleteHumanReview, getTaskProgress, useTaskStore, loadTasks, hasRecentActivity } from '../../../stores/task-store';
import type { Task, TaskLogs, TaskLogPhase, WorktreeStatus, WorktreeDiff, MergeConflict, MergeStats, GitConflictInfo, ImageAttachment } from '../../../../shared/types';
@@ -93,6 +94,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) {
const [isCreatingPR, setIsCreatingPR] = useState(false);
const selectedProject = useProjectStore((state) => state.getSelectedProject());
const logOrder = useSettingsStore(s => s.settings.logOrder);
const isRunning = task.status === 'in_progress';
// isActiveTask includes ai_review for stuck detection (CHANGELOG documents this feature)
const isActiveTask = task.status === 'in_progress' || task.status === 'ai_review';
@@ -131,19 +133,31 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) {
return () => clearInterval(intervalId);
}, [task.id, isActiveTask]);
// Handle scroll events in logs to detect if user scrolled up
// Handle scroll events in logs to detect if user scrolled away from anchor
const handleLogsScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
const isNearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 100;
setIsUserScrolledUp(!isNearBottom);
const isReverseOrder = logOrder === 'reverse-chronological';
// Check distance from top for reverse order, bottom for chronological
const isAtAnchor = isReverseOrder
? target.scrollTop < 100
: target.scrollHeight - target.scrollTop - target.clientHeight < 100;
setIsUserScrolledUp(!isAtAnchor);
};
// Auto-scroll logs to bottom only if user hasn't scrolled up
// Auto-scroll logs to anchor (top for reverse, bottom for chronological) only if user hasn't scrolled away
useEffect(() => {
if (activeTab === 'logs' && logsEndRef.current && !isUserScrolledUp) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
const isReverseOrder = logOrder === 'reverse-chronological';
if (activeTab === 'logs' && !isUserScrolledUp) {
if (isReverseOrder && logsContainerRef.current) {
logsContainerRef.current.scrollTo({ top: 0, behavior: 'smooth' });
} else if (!isReverseOrder && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}
}, [activeTab, isUserScrolledUp]);
}, [activeTab, isUserScrolledUp, logOrder, phaseLogs]);
// Reset scroll state when switching to logs tab
useEffect(() => {
@@ -53,6 +53,8 @@ export const DEFAULT_APP_SETTINGS = {
changelogEmojiLevel: 'none' as const,
// UI Scale (default 100% - standard size)
uiScale: UI_SCALE_DEFAULT,
// Log order setting for task detail view (default chronological - oldest first)
logOrder: 'chronological' as const,
// Beta updates opt-in (receive pre-release versions)
betaUpdates: false,
// Language preference (default to English)
@@ -185,6 +185,12 @@
"comfortable": "Comfortable",
"large": "Large"
},
"logOrder": {
"label": "Log Order",
"description": "Choose how logs are displayed in the task detail view",
"chronological": "Chronological (oldest first)",
"reverseChronological": "Reverse-chronological (newest first)"
},
"general": {
"otherAgentSettings": "Other Agent Settings",
"otherAgentSettingsDescription": "Additional agent configuration options",
@@ -185,6 +185,12 @@
"comfortable": "Confortable",
"large": "Grand"
},
"logOrder": {
"label": "Ordre des journaux",
"description": "Choisir comment les journaux sont affichés dans la vue détaillée des tâches",
"chronological": "Chronologique (plus ancien en premier)",
"reverseChronological": "Chronologique inverse (plus récent en premier)"
},
"general": {
"otherAgentSettings": "Autres paramètres de l'agent",
"otherAgentSettingsDescription": "Options de configuration supplémentaires de l'agent",
@@ -269,6 +269,8 @@ export interface AppSettings {
changelogEmojiLevel?: ChangelogEmojiLevel;
// UI Scale setting (75-200%, default 100)
uiScale?: number;
// Log order setting for task detail view
logOrder?: 'chronological' | 'reverse-chronological';
// Beta updates opt-in (receive pre-release updates)
betaUpdates?: boolean;
// Migration flags (internal use)