fix: respect time format preference in UI

Add shared time formatting helpers that apply the Appearance time format preference consistently. Update visible time labels across chat, quota usage, scheduled tasks, tunnels, context details, git views, PR metadata, and passkey settings to use the selected 12-hour, 24-hour, or automatic format. Leave date-only and non-UI formatting untouched so unrelated behavior does not change.
This commit is contained in:
Bohdan Triapitsyn
2026-06-03 18:20:35 +03:00
parent 297663c2d7
commit 874dc15263
14 changed files with 143 additions and 55 deletions
@@ -1038,6 +1038,7 @@ const AssistantMessageBody = React.memo(({
const collapsibleThinkingBlocks = useUIStore((state) => state.collapsibleThinkingBlocks);
const groupReasoningBlocks = useUIStore((state) => state.groupReasoningBlocks);
const showSplitAssistantMessageActions = useUIStore((state) => state.showSplitAssistantMessageActions);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const vscodeApi = useRuntimeAPIs().vscode;
const isSortedRenderMode = chatRenderMode === 'sorted';
const collapsedPreviewCount = 7;
@@ -1757,9 +1758,9 @@ const AssistantMessageBody = React.memo(({
: (typeof messageCreatedAt === 'number' && messageCreatedAt > 0 ? messageCreatedAt : null);
if (timestamp === null) return null;
const formatted = formatTimestampForDisplay(timestamp);
const formatted = formatTimestampForDisplay(timestamp, timeFormatPreference);
return formatted.length > 0 ? formatted : null;
}, [messageCompletedAt, messageCreatedAt]);
}, [messageCompletedAt, messageCreatedAt, timeFormatPreference]);
const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1';
const canOpenMessagePreview = !isMiniChatSurface && !isMobile && !isVSCode;
@@ -1,4 +1,5 @@
const pad2 = (value: number): string => String(value).padStart(2, '0');
import { formatTimeForPreference } from '@/lib/timeFormat';
import type { TimeFormatPreference } from '@/stores/useUIStore';
const isSameDay = (left: Date, right: Date): boolean => {
return (
@@ -18,7 +19,7 @@ const isValidTimestamp = (timestamp: number): boolean => {
return Number.isFinite(timestamp) && !Number.isNaN(new Date(timestamp).getTime());
};
export const formatTimestampForDisplay = (timestamp: number): string => {
export const formatTimestampForDisplay = (timestamp: number, timeFormatPreference: TimeFormatPreference): string => {
if (!isValidTimestamp(timestamp)) {
return '';
}
@@ -26,7 +27,7 @@ export const formatTimestampForDisplay = (timestamp: number): string => {
const date = new Date(timestamp);
const now = new Date();
const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
const timePart = formatTimeForPreference(date, timeFormatPreference);
if (isSameDay(date, now)) {
return timePart;
@@ -11,6 +11,8 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessions, useSessionMessageRecords } from '@/sync/sync-context';
import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n';
import { formatDateTimeForPreference } from '@/lib/timeFormat';
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
type SessionMessage = { info: Message; parts: Part[] };
@@ -229,9 +231,9 @@ const formatMoney = (value: number): string => {
return `$${value.toFixed(2)}`;
};
const formatDateTime = (timestamp: number | null): string => {
const formatDateTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => {
if (!timestamp || !Number.isFinite(timestamp)) return '-';
return new Date(timestamp).toLocaleString(undefined, {
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
month: 'short',
day: 'numeric',
year: 'numeric',
@@ -240,9 +242,9 @@ const formatDateTime = (timestamp: number | null): string => {
});
};
const formatMessageDateMeta = (timestamp: number | null): string => {
const formatMessageDateMeta = (timestamp: number | null, timeFormatPreference: TimeFormatPreference): string => {
if (!timestamp || !Number.isFinite(timestamp)) return '-';
return new Date(timestamp).toLocaleString(undefined, {
return formatDateTimeForPreference(timestamp, timeFormatPreference, {
month: 'short',
day: 'numeric',
hour: 'numeric',
@@ -273,6 +275,7 @@ const resolveProviderAndModel = (
export const ContextPanelContent: React.FC = () => {
const { t } = useI18n();
const { currentTheme } = useThemeSystem();
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
const [expandedRawMessages, setExpandedRawMessages] = React.useState<Record<string, boolean>>({});
const [copiedRawMessageId, setCopiedRawMessageId] = React.useState<string | null>(null);
@@ -416,7 +419,7 @@ export const ContextPanelContent: React.FC = () => {
{viewModel.createdAt && (
<>
<span>&middot;</span>
<span>{formatDateTime(viewModel.createdAt)}</span>
<span>{formatDateTime(viewModel.createdAt, timeFormatPreference)}</span>
</>
)}
</div>
@@ -547,7 +550,7 @@ export const ContextPanelContent: React.FC = () => {
<span className="typography-ui-label text-foreground shrink-0">{capitalizeRole(role)}</span>
<span className="min-w-0 truncate typography-micro text-muted-foreground">{message.info.id}</span>
</span>
<span className="typography-micro text-muted-foreground shrink-0">{formatMessageDateMeta(messageCreatedAt)}</span>
<span className="typography-micro text-muted-foreground shrink-0">{formatMessageDateMeta(messageCreatedAt, timeFormatPreference)}</span>
</div>
</button>
+12 -9
View File
@@ -41,7 +41,9 @@ import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_
import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
import { updateDesktopSettings } from '@/lib/persistence';
import { formatTimeForPreference } from '@/lib/timeFormat';
import { eventMatchesShortcut, formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import type { TimeFormatPreference } from '@/stores/useUIStore';
import {
getAllModelFamilies,
getDisplayModelName,
@@ -356,6 +358,7 @@ type DesktopServicesMenuProps = {
remoteUpdateError: string | null;
onOpenRemoteUpdate: () => void;
showPredValues: boolean;
timeFormatPreference: TimeFormatPreference;
};
const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
@@ -391,6 +394,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
remoteUpdateError,
onOpenRemoteUpdate,
showPredValues,
timeFormatPreference,
}: DesktopServicesMenuProps) {
const { t } = useI18n();
return (
@@ -511,7 +515,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
<div className="flex items-center justify-between gap-3 border-b border-[var(--interactive-border)] px-4 py-2.5">
<div className="flex min-w-0 items-baseline gap-2">
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
<span className="truncate typography-micro text-muted-foreground">{formatTime(quotaLastUpdated)}</span>
<span className="truncate typography-micro text-muted-foreground">{formatTime(quotaLastUpdated, timeFormatPreference)}</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-7 w-[10.5rem]">
@@ -572,7 +576,7 @@ const DesktopServicesMenu = React.memo(function DesktopServicesMenu({
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
: null;
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference);
return (
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
<div className="flex min-w-0 items-center justify-between gap-3">
@@ -714,13 +718,10 @@ const formatCompactHeaderLabel = (value: string): string => {
return trimmed.length > 12 ? `${trimmed.slice(0, 9).trimEnd()}...` : trimmed;
};
const formatTime = (timestamp: number | null) => {
const formatTime = (timestamp: number | null, timeFormatPreference: 'auto' | '12h' | '24h') => {
if (!timestamp) return '-';
try {
return new Date(timestamp).toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
});
return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' });
} catch {
return '-';
}
@@ -791,6 +792,7 @@ export const Header: React.FC<HeaderProps> = ({
const activeMainTab = useUIStore((state) => state.activeMainTab);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
const runtimeApis = useRuntimeAPIs();
@@ -2089,6 +2091,7 @@ export const Header: React.FC<HeaderProps> = ({
remoteUpdateChecking={remoteUpdateChecking}
remoteUpdateError={remoteUpdateError}
onOpenRemoteUpdate={openRemoteInstanceUpdate}
timeFormatPreference={timeFormatPreference}
/>
<HeaderIconActionButton
title={t('header.actions.terminalPanelWithShortcut', { shortcut: shortcutLabel('toggle_terminal') })}
@@ -2445,7 +2448,7 @@ export const Header: React.FC<HeaderProps> = ({
<div className="flex flex-col min-w-0 gap-0.5">
<span className="typography-ui-header font-semibold text-foreground">{t('header.services.rateLimits')}</span>
<span className="truncate typography-micro text-muted-foreground">
{formatTime(quotaLastUpdated)}
{formatTime(quotaLastUpdated, timeFormatPreference)}
</span>
</div>
<div className="flex items-center gap-2 shrink-0">
@@ -2533,7 +2536,7 @@ export const Header: React.FC<HeaderProps> = ({
: calculateExpectedUsagePercent(paceInfo.elapsedRatio))
: null;
const metricLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference);
return (
<div key={`${group.providerId}-${label}`} className="flex flex-col gap-1.5">
<div className="flex min-w-0 items-center justify-between gap-3">
@@ -30,19 +30,18 @@ import { formatQuotaValueLabel, formatQuotaResetLabel, formatWindowLabel, QUOTA_
import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { updateDesktopSettings } from '@/lib/persistence';
import { formatTimeForPreference } from '@/lib/timeFormat';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import type { UsageWindow } from '@/types';
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView })));
const formatTime = (timestamp: number | null) => {
const formatTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference) => {
if (!timestamp) return '-';
try {
return new Date(timestamp).toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
});
return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' });
} catch {
return '-';
}
@@ -583,6 +582,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
const quotaLastUpdated = useQuotaStore((state) => state.lastUpdated);
const quotaDisplayMode = useQuotaStore((state) => state.displayMode);
const showPredValues = useQuotaStore((state) => state.showPredValues);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds);
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
const setQuotaDisplayMode = useQuotaStore((state) => state.setDisplayMode);
@@ -839,7 +839,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
</DropdownMenuLabel>
</div>
<div className="border-b border-[var(--interactive-border)] px-2 pb-2 typography-micro text-muted-foreground text-[10px]">
{t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated) })}
{t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated, timeFormatPreference) })}
</div>
{!hasRateLimits && (
<DropdownMenuItem className="cursor-default" closeOnClick={false}>
@@ -899,7 +899,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
</div>
)}
<span className="flex items-center justify-between typography-micro text-muted-foreground text-[10px]">
<span>{formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted)}</span>
<span>{formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference)}</span>
</span>
</span>
</DropdownMenuItem>
@@ -15,8 +15,9 @@ import {
type StoredPasskey,
} from '@/lib/passkeys';
import { useI18n } from '@/lib/i18n';
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
const formatTimestamp = (timestamp: number | null, neverUsedText: string) => {
const formatTimestamp = (timestamp: number | null, neverUsedText: string, timeFormatPreference: TimeFormatPreference) => {
if (!timestamp || !Number.isFinite(timestamp)) {
return neverUsedText;
}
@@ -24,11 +25,13 @@ const formatTimestamp = (timestamp: number | null, neverUsedText: string) => {
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
hour12: timeFormatPreference === 'auto' ? undefined : timeFormatPreference === '12h',
}).format(timestamp);
};
export const PasskeySettings: React.FC = () => {
const { t } = useI18n();
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const [supportsPasskeys, setSupportsPasskeys] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(true);
const [isRegistering, setIsRegistering] = React.useState(false);
@@ -230,10 +233,10 @@ export const PasskeySettings: React.FC = () => {
<span className="typography-meta text-muted-foreground truncate">
{passkey.lastUsedAt
? t('settings.openchamber.passkeys.item.lastUsed', {
time: formatTimestamp(passkey.lastUsedAt, t('settings.openchamber.passkeys.time.neverUsed')),
time: formatTimestamp(passkey.lastUsedAt, t('settings.openchamber.passkeys.time.neverUsed'), timeFormatPreference),
})
: t('settings.openchamber.passkeys.item.added', {
time: formatTimestamp(passkey.createdAt, t('settings.openchamber.passkeys.time.neverUsed')),
time: formatTimestamp(passkey.createdAt, t('settings.openchamber.passkeys.time.neverUsed'), timeFormatPreference),
})}
</span>
<Button
@@ -14,6 +14,8 @@ import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { openExternalUrl } from '@/lib/url';
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
import { formatTimeForPreference } from '@/lib/timeFormat';
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
type TunnelState =
| 'checking'
@@ -209,8 +211,8 @@ const formatRemaining = (remainingMs: number): string => {
return `${seconds}s`;
};
const formatAbsoluteTime = (timestamp: number): string => {
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const formatAbsoluteTime = (timestamp: number, timeFormatPreference: TimeFormatPreference): string => {
return formatTimeForPreference(timestamp, timeFormatPreference, { hour: '2-digit', precision: 'second' });
};
const normalizePresetHostname = (value: string): string => {
@@ -267,6 +269,7 @@ const createPresetId = (): string => {
export const TunnelSettings: React.FC = () => {
const { t } = useI18n();
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
const [state, setState] = React.useState<TunnelState>('checking');
const [tunnelInfo, setTunnelInfo] = React.useState<TunnelInfo | null>(null);
@@ -1167,7 +1170,7 @@ export const TunnelSettings: React.FC = () => {
{modeLabel}
</span>
<span className="typography-meta text-muted-foreground/80">
{t('settings.openchamber.tunnel.session.redeemedAt', { time: formatAbsoluteTime(record.createdAt) })}
{t('settings.openchamber.tunnel.session.redeemedAt', { time: formatAbsoluteTime(record.createdAt, timeFormatPreference) })}
</span>
<span className="typography-meta text-foreground">
{record.isActive
@@ -5,6 +5,7 @@ import { UsageProgressBar } from './UsageProgressBar';
import { PaceIndicator } from './PaceIndicator';
import { useQuotaStore } from '@/stores/useQuotaStore';
import { Checkbox } from '@/components/ui/checkbox';
import { useUIStore } from '@/stores/useUIStore';
interface UsageCardProps {
title: string;
@@ -25,10 +26,11 @@ export const UsageCard: React.FC<UsageCardProps> = ({
}) => {
const displayMode = useQuotaStore((state) => state.displayMode);
const showPredValues = useQuotaStore((state) => state.showPredValues);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const displayPercent = displayMode === 'remaining' ? window.remainingPercent : window.usedPercent;
const barLabel = displayMode === 'remaining' ? 'remaining' : 'used';
const percentLabel = formatQuotaValueLabel(window.valueLabel, displayPercent);
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted);
const resetLabel = formatQuotaResetLabel(window.resetAt, window.resetAfterFormatted ?? window.resetAtFormatted, timeFormatPreference);
const windowLabel = formatWindowLabel(title);
const paceInfo = React.useMemo(() => {
@@ -16,14 +16,13 @@ import { getAllModelFamilies, getDisplayModelName, sortModelFamilies, groupModel
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
import { formatTimeForPreference } from '@/lib/timeFormat';
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
const formatTime = (timestamp: number | null) => {
const formatTime = (timestamp: number | null, timeFormatPreference: TimeFormatPreference) => {
if (!timestamp) return '-';
try {
return new Date(timestamp).toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit'
});
return formatTimeForPreference(timestamp, timeFormatPreference, { fallback: '-' });
} catch {
return '-';
}
@@ -36,6 +35,7 @@ interface ModelInfo {
export const UsagePage: React.FC = () => {
const { t } = useI18n();
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const results = useQuotaStore((state) => state.results);
const selectedProviderId = useQuotaStore((state) => state.selectedProviderId);
const setSelectedProvider = useQuotaStore((state) => state.setSelectedProvider);
@@ -163,7 +163,7 @@ export const UsagePage: React.FC = () => {
{isLoading ? (
<span className="animate-pulse">{t('settings.usage.page.header.refreshing')}</span>
) : (
t('settings.usage.page.header.lastUpdated', { time: formatTime(lastUpdated) })
t('settings.usage.page.header.lastUpdated', { time: formatTime(lastUpdated, timeFormatPreference) })
)}
</p>
</div>
@@ -14,6 +14,8 @@ import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { useUIStore } from '@/stores/useUIStore';
import { formatTimeForPreference } from '@/lib/timeFormat';
import type { TimeFormatPreference } from '@/stores/useUIStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { refreshGlobalSessions } from '@/stores/useGlobalSessionsStore';
@@ -100,11 +102,11 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType<typeof useI18n>['t'])
return t('sessions.scheduledTasks.dialog.schedule.cron', { cron: task.schedule.cron || '' });
};
const formatClockTime = (value?: number): string => {
const formatClockTime = (value: number | undefined, timeFormatPreference: TimeFormatPreference): string => {
if (!value || !Number.isFinite(value)) {
return '';
}
return new Date(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
return formatTimeForPreference(value, timeFormatPreference);
};
const formatRelativeTime = (value: number | undefined, t: ReturnType<typeof useI18n>['t']): string => {
@@ -174,6 +176,7 @@ export function ScheduledTasksDialog() {
const open = useUIStore((state) => state.isScheduledTasksDialogOpen);
const setOpen = useUIStore((state) => state.setScheduledTasksDialogOpen);
const isMobile = useUIStore((state) => state.isMobile);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const projects = useProjectsStore((state) => state.projects);
const activeProject = useProjectsStore((state) => state.getActiveProject());
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
@@ -471,7 +474,7 @@ export function ScheduledTasksDialog() {
<>
<span className="text-foreground">{formatRelativeTime(nextAt, t)}</span>
<span className="text-muted-foreground/50">·</span>
<span>{formatClockTime(nextAt)}</span>
<span>{formatClockTime(nextAt, timeFormatPreference)}</span>
</>
) : (
<span></span>
@@ -18,6 +18,8 @@ import type { LanedCommit } from './gitGraph';
import { GitGraphSegment } from './GitGraphSegment';
import * as git from '@/lib/gitApi';
import { toast } from '@/components/ui/toast';
import { formatDateTimeForPreference } from '@/lib/timeFormat';
import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore';
const HISTORY_DIFF_REQUEST_TIMEOUT_MS = 15000;
const HISTORY_DIFF_LARGE_CHANGED_LINES = 500;
@@ -77,14 +79,13 @@ interface HistoryCommitRowProps {
onActionSuccess?: () => void;
}
function formatCommitDate(date: string) {
function formatCommitDate(date: string, timeFormatPreference: TimeFormatPreference) {
const value = new Date(date);
if (Number.isNaN(value.getTime())) {
return date;
}
return value.toLocaleString(undefined, {
hour12: false,
return formatDateTimeForPreference(value, timeFormatPreference, {
year: 'numeric',
month: 'short',
day: 'numeric',
@@ -146,6 +147,7 @@ export const HistoryCommitRow = React.memo(({
onActionSuccess,
}: HistoryCommitRowProps) => {
const { t } = useI18n();
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const isGraphMode = mode === 'graph';
type PendingAction =
| 'checkout' | 'cherryPick' | 'revert'
@@ -397,8 +399,8 @@ export const HistoryCommitRow = React.memo(({
{entry.author_name}
</span>
<span className="shrink-0">·</span>
<span className="truncate min-w-0" title={formatCommitDate(entry.date)}>
{formatCommitDate(entry.date)}
<span className="truncate min-w-0" title={formatCommitDate(entry.date, timeFormatPreference)}>
{formatCommitDate(entry.date, timeFormatPreference)}
</span>
</div>
<span className="shrink-0">·</span>
@@ -28,6 +28,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { Icon } from "@/components/icon/Icon";
import { useUIStore } from '@/stores/useUIStore';
import { formatDateTimeForPreference } from '@/lib/timeFormat';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSelectionStore } from '@/sync/selection-store';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -309,6 +310,7 @@ export const PullRequestSection: React.FC<{
onGeneratedDescription?: () => void;
}> = ({ directory, branch, baseBranch, trackingBranch, remotes = [], remoteBranches = [], onGeneratedDescription }) => {
const { t } = useI18n();
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const { github } = useRuntimeAPIs();
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
@@ -627,8 +629,14 @@ export const PullRequestSection: React.FC<{
if (!Number.isFinite(ts)) {
return value;
}
return new Date(ts).toLocaleString();
}, []);
return formatDateTimeForPreference(ts, timeFormatPreference, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}, [timeFormatPreference]);
const connectedGitHubLogin = React.useMemo(() => {
const login = githubAuthStatus?.user?.login;
+6 -5
View File
@@ -1,3 +1,6 @@
import { formatDateTimeForPreference, formatTimeForPreference } from '@/lib/timeFormat';
import type { TimeFormatPreference } from '@/stores/useUIStore';
export const clampPercent = (value: number | null): number | null => {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return null;
@@ -22,6 +25,7 @@ export const formatQuotaValueLabel = (
export const formatQuotaResetLabel = (
resetAt: number | null,
fallback?: string | null,
timeFormatPreference: TimeFormatPreference = 'auto',
): string => {
if (!resetAt) {
return fallback ?? '';
@@ -37,13 +41,10 @@ export const formatQuotaResetLabel = (
const isToday = resetDate.toDateString() === now.toDateString();
if (isToday) {
return resetDate.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
});
return formatTimeForPreference(resetDate, timeFormatPreference, { fallback: fallback ?? '' });
}
return resetDate.toLocaleString(undefined, {
return formatDateTimeForPreference(resetDate, timeFormatPreference, {
month: 'short',
day: 'numeric',
weekday: 'short',
+58
View File
@@ -0,0 +1,58 @@
import type { TimeFormatPreference } from '@/stores/useUIStore';
type TimePrecision = 'minute' | 'second';
const getHour12Option = (preference: TimeFormatPreference): boolean | undefined => {
if (preference === '12h') return true;
if (preference === '24h') return false;
return undefined;
};
export const getUses24HourForPreference = (preference: TimeFormatPreference, locale: string): boolean => {
if (preference === '24h') return true;
if (preference === '12h') return false;
try {
const options = new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions();
if (typeof options.hour12 === 'boolean') {
return !options.hour12;
}
return options.hourCycle === 'h23' || options.hourCycle === 'h24';
} catch {
return true;
}
};
export const formatTimeForPreference = (
timestamp: number | Date,
preference: TimeFormatPreference,
options: { precision?: TimePrecision; hour?: 'numeric' | '2-digit'; fallback?: string } = {},
): string => {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
if (!Number.isFinite(date.getTime())) {
return options.fallback ?? '';
}
return date.toLocaleTimeString(undefined, {
hour: options.hour ?? 'numeric',
minute: '2-digit',
second: options.precision === 'second' ? '2-digit' : undefined,
hour12: getHour12Option(preference),
});
};
export const formatDateTimeForPreference = (
timestamp: number | Date,
preference: TimeFormatPreference,
options: Intl.DateTimeFormatOptions,
): string => {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
if (!Number.isFinite(date.getTime())) {
return '';
}
return date.toLocaleString(undefined, {
...options,
hour12: options.hour ? getHour12Option(preference) : options.hour12,
});
};