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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user