diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index e16fd04f..4d3358f0 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; import LinkPreview from './LinkPreview'; import GooseResponseForm from './GooseResponseForm'; import { extractUrls } from '../utils/urlUtils'; +import { formatMessageTimestamp } from '../utils/timeUtils'; import MarkdownContent from './MarkdownContent'; import ToolCallWithResponse from './ToolCallWithResponse'; import { @@ -39,6 +40,9 @@ export default function GooseMessage({ // Extract text content from the message let textContent = getTextContent(message); + // Memoize the timestamp + const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]); + // Get tool requests from the message const toolRequests = getToolRequests(message); @@ -116,35 +120,47 @@ export default function GooseMessage({ {textContent && (
0 ? 'rounded-b-none' : ''}`} + className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : 'rounded-bl-none'}`} >
{}
{/* Only show MessageCopyLink if there's text content and no tool requests/responses */} - {textContent && message.content.every((content) => content.type === 'text') && ( -
- -
- )} +
+ {toolRequests.length === 0 && ( +
+ {timestamp} +
+ )} + {textContent && message.content.every((content) => content.type === 'text') && ( +
+ +
+ )} +
)} {toolRequests.length > 0 && ( -
- {toolRequests.map((toolRequest) => ( - - ))} +
+
+ {toolRequests.map((toolRequest) => ( + + ))} +
+
+ {timestamp} +
)} diff --git a/ui/desktop/src/components/MessageCopyLink.tsx b/ui/desktop/src/components/MessageCopyLink.tsx index f19994f1..524a4c6c 100644 --- a/ui/desktop/src/components/MessageCopyLink.tsx +++ b/ui/desktop/src/components/MessageCopyLink.tsx @@ -51,7 +51,7 @@ export default function MessageCopyLink({ text, contentRef }: MessageCopyLinkPro return (
-
- +
+
+ {timestamp} +
+
+ +
diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index 8601229c..40c84ae4 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -11,7 +11,8 @@ import { LoaderCircle, } from 'lucide-react'; import { type SessionDetails } from '../../sessions'; -import { SessionHeaderCard, SessionMessages, formatDate } from './SessionViewComponents'; +import { SessionHeaderCard, SessionMessages } from './SessionViewComponents'; +import { formatMessageTimestamp } from '../../utils/timeUtils'; import { createSharedSession } from '../../sharedSessions'; import { Modal, ModalContent } from '../ui/modal'; import { Button } from '../ui/button'; @@ -120,7 +121,7 @@ const SessionHistoryView: React.FC = ({
- {formatDate(session.messages[0]?.created)} + {formatMessageTimestamp(session.messages[0]?.created)} diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx index 5ad2cec7..f0521d2a 100644 --- a/ui/desktop/src/components/sessions/SessionListView.tsx +++ b/ui/desktop/src/components/sessions/SessionListView.tsx @@ -14,6 +14,7 @@ import { Button } from '../ui/button'; import BackButton from '../ui/BackButton'; import { ScrollArea } from '../ui/scroll-area'; import { View, ViewOptions } from '../../App'; +import { formatMessageTimestamp } from '../../utils/timeUtils'; interface SessionListViewProps { setView: (view: View, viewOptions?: ViewOptions) => void; @@ -45,29 +46,6 @@ const SessionListView: React.FC = ({ setView, onSelectSess } }; - // Format date to be more readable - // eg. "10:39 PM, Feb 28, 2025" - const formatDateString = (dateString: string) => { - try { - const date = new Date(dateString); - const time = new Intl.DateTimeFormat('en-US', { - hour: 'numeric', - minute: 'numeric', - hour12: true, - }).format(date); - - const dateStr = new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }).format(date); - - return `${time}, ${dateStr}`; - } catch (e) { - return dateString; - } - }; - return (
@@ -115,7 +93,9 @@ const SessionListView: React.FC = ({ setView, onSelectSess
- {formatDateString(session.modified)} + + {formatMessageTimestamp(Date.parse(session.modified) / 1000)} +
diff --git a/ui/desktop/src/components/sessions/SessionViewComponents.tsx b/ui/desktop/src/components/sessions/SessionViewComponents.tsx index cf19e2a7..978d67d4 100644 --- a/ui/desktop/src/components/sessions/SessionViewComponents.tsx +++ b/ui/desktop/src/components/sessions/SessionViewComponents.tsx @@ -8,31 +8,7 @@ import MarkdownContent from '../MarkdownContent'; import ToolCallWithResponse from '../ToolCallWithResponse'; import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message'; import { type Message } from '../../types/message'; - -/** - * Format a timestamp into a human-readable date string - */ -export const formatDate = (timestamp: number) => { - const date = new Date(timestamp * 1000); - - const getOrdinal = (n: number) => { - const s = ['th', 'st', 'nd', 'rd']; - const v = n % 100; - return n + (s[(v - 20) % 10] || s[v] || s[0]); - }; - - const hours = date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true, - }); - - const month = date.toLocaleString('en-US', { month: 'short' }); - const day = getOrdinal(date.getDate()); - const year = date.getFullYear(); - - return `${hours}, ${month} ${day}, ${year}`; -}; +import { formatMessageTimestamp } from '../../utils/timeUtils'; /** * Get tool responses map from messages @@ -166,7 +142,7 @@ export const SessionMessages: React.FC = ({ {message.role === 'user' ? 'You' : 'Goose'} - {new Date(message.created * 1000).toLocaleTimeString()} + {formatMessageTimestamp(message.created)}
diff --git a/ui/desktop/src/components/sessions/SharedSessionView.tsx b/ui/desktop/src/components/sessions/SharedSessionView.tsx index 16761eca..a9155c20 100644 --- a/ui/desktop/src/components/sessions/SharedSessionView.tsx +++ b/ui/desktop/src/components/sessions/SharedSessionView.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { Calendar, MessageSquareText, Folder, Target } from 'lucide-react'; import { type SharedSessionDetails } from '../../sharedSessions'; -import { SessionHeaderCard, SessionMessages, formatDate } from './SessionViewComponents'; +import { SessionHeaderCard, SessionMessages } from './SessionViewComponents'; +import { formatMessageTimestamp } from '../../utils/timeUtils'; interface SharedSessionViewProps { session: SharedSessionDetails | null; @@ -32,7 +33,7 @@ const SharedSessionView: React.FC = ({
- {formatDate(session.messages[0]?.created)} + {formatMessageTimestamp(session.messages[0]?.created)} diff --git a/ui/desktop/src/utils/timeUtils.ts b/ui/desktop/src/utils/timeUtils.ts new file mode 100644 index 00000000..2625e796 --- /dev/null +++ b/ui/desktop/src/utils/timeUtils.ts @@ -0,0 +1,30 @@ +export function formatMessageTimestamp(timestamp: number): string { + // Convert from Unix timestamp (seconds) to milliseconds + const date = new Date(timestamp * 1000); + const now = new Date(); + + // Format time as HH:MM AM/PM + const timeStr = date.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + + // Check if the message is from today + if ( + date.getDate() === now.getDate() && + date.getMonth() === now.getMonth() && + date.getFullYear() === now.getFullYear() + ) { + return timeStr; + } + + // If not today, format as MM/DD/YYYY HH:MM AM/PM + const dateStr = date.toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }); + + return `${dateStr} ${timeStr}`; +}