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)