From 2e2b82365f158efd4c7b613edce2a3139bbba2eb Mon Sep 17 00:00:00 2001 From: Burak <2199151+dertuerke@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:54:41 +0100 Subject: [PATCH] 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> --- .../components/settings/DisplaySettings.tsx | 34 +++++++++++++++++++ .../components/task-detail/TaskLogs.tsx | 18 +++++++--- .../task-detail/hooks/useTaskDetail.ts | 28 +++++++++++---- apps/frontend/src/shared/constants/config.ts | 2 ++ .../src/shared/i18n/locales/en/settings.json | 6 ++++ .../src/shared/i18n/locales/fr/settings.json | 6 ++++ apps/frontend/src/shared/types/settings.ts | 2 ++ 7 files changed, 85 insertions(+), 11 deletions(-) diff --git a/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx b/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx index 1b2f1393..13bd0695 100644 --- a/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx +++ b/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx @@ -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 {UI_SCALE_MAX}% + + {/* Log Order Setting */} +
+
+
+ +

+ {t('logOrder.description')} +

+
+ +
+
); diff --git a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx index 54f00093..a1f8aa62 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx @@ -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 ? (

No logs yet

) : ( - phaseLog?.entries.map((entry, idx) => ( - + displayedEntries.map((entry) => ( + )) )} @@ -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 ''; } diff --git a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts index 9ade9e6b..ca501c39 100644 --- a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts +++ b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts @@ -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) => { 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(() => { diff --git a/apps/frontend/src/shared/constants/config.ts b/apps/frontend/src/shared/constants/config.ts index ea941206..b58fc4f8 100644 --- a/apps/frontend/src/shared/constants/config.ts +++ b/apps/frontend/src/shared/constants/config.ts @@ -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) diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index e3d608f6..c1a977df 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -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", diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 60505657..056bde35 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -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", diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index a378c887..a8558478 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -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)