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)