diff --git a/apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx b/apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx index cd5f7f4530..600ec3d049 100644 --- a/apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx @@ -23,6 +23,7 @@ import { messageContentText, pickPrimaryPreviewTarget } from '@/components/assistant-ui/thread/content' +import { MessageHoverTime } from '@/components/assistant-ui/thread/message-hover-time' import { MESSAGE_PARTS_COMPONENTS } from '@/components/assistant-ui/thread/message-parts' import { ReactionPicker } from '@/components/assistant-ui/thread/message-reactions' import { ResponseMessageIds } from '@/components/assistant-ui/thread/response-group' @@ -1043,6 +1044,7 @@ const AssistantActionBar: FC = ({ } data-slot="aui_msg-actions" > + {onBranchInNewChat && ( { diff --git a/apps/desktop/src/components/assistant-ui/thread/message-hover-time.tsx b/apps/desktop/src/components/assistant-ui/thread/message-hover-time.tsx new file mode 100644 index 0000000000..2db3197768 --- /dev/null +++ b/apps/desktop/src/components/assistant-ui/thread/message-hover-time.tsx @@ -0,0 +1,69 @@ +import { useAuiState } from '@assistant-ui/react' +import { useStore } from '@nanostores/react' +import type { FC } from 'react' + +import { Tip } from '@/components/ui/tooltip' +import { useI18n } from '@/i18n' +import { DAY, fmtFullDateTime, relativeTime } from '@/lib/time' +import { cn } from '@/lib/utils' +import { $displayTimestamps } from '@/store/display-timestamps' + +import { formatMessageTimestamp } from './timestamp' + +/** When the message landed, in seconds: a reply counts from its completion. + * `createdAt` already carries the row's own timestamp (see + * `messageCreatedAt`), falling back to now only for a just-sent message. */ +const useMessageTime = (): number | undefined => + useAuiState(s => { + const completedAt = (s.message.metadata?.custom as { timelineCompletedAt?: unknown } | undefined) + ?.timelineCompletedAt + + const at = typeof completedAt === 'number' ? completedAt : (s.message.createdAt?.getTime() ?? NaN) / 1000 + + return Number.isFinite(at) && at > 0 ? at : undefined + }) + +/** Rendered only while the tip is open, so "5 min ago" is as of the hover. + * The age is only worth saying within the day; past that the label already + * names the day, and an hour-rounded "2 days ago" would contradict a + * calendar "Yesterday". */ +const HoverTimeDetail: FC<{ ms: number }> = ({ ms }) => { + const full = fmtFullDateTime.format(new Date(ms)) + + return {Date.now() - ms < DAY ? `${relativeTime(ms)} ยท ${full}` : full} +} + +const clockOnly = (time: string) => time + +/** + * The time a message landed, revealed with the message's hover actions + * ("3:27 PM"; "Yesterday, 3:27 PM"; "Sep 28, 3:27 PM"), with the age and full + * date in its tip. Visibility belongs to the host's hover group: this paints + * the label only. Stands down while `display.timestamps` is on, because the + * transcript already prints every stamp. + */ +export const MessageHoverTime: FC<{ className?: string }> = ({ className }) => { + const { t } = useI18n() + const timelineStamps = useStore($displayTimestamps) + const seconds = useMessageTime() + + if (timelineStamps || seconds === undefined) { + return null + } + + const date = new Date(seconds * 1000) + + return ( + } side="top"> + + + ) +} diff --git a/apps/desktop/src/components/assistant-ui/thread/user-message.tsx b/apps/desktop/src/components/assistant-ui/thread/user-message.tsx index 231b1739e7..94dbd803d8 100644 --- a/apps/desktop/src/components/assistant-ui/thread/user-message.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/user-message.tsx @@ -8,6 +8,7 @@ import { messageContentText, PROCESS_NOTIFICATION_RE } from '@/components/assistant-ui/thread/content' +import { MessageHoverTime } from '@/components/assistant-ui/thread/message-hover-time' import { ReactionBadge, ReactionPicker } from '@/components/assistant-ui/thread/message-reactions' import { BackgroundResult } from '@/components/assistant-ui/thread/system-message' import { threadUserOrdinal } from '@/components/assistant-ui/thread/thread-message-index' @@ -502,47 +503,48 @@ export const UserMessage: FC<{ )} - {(showStop || showRestore) && ( -
- {showStop ? ( + {/* Hover cluster, bottom-right: when it was sent, then Stop or + Restore. Its fill masks the last line's tail while shown. */} +
+ + {showStop ? ( + + ) : showRestore ? ( + - ) : ( - - - - )} -
- )} + + ) : null} +
{/* Below the bubble, same register as the assistant action row: diff --git a/apps/desktop/src/lib/time.ts b/apps/desktop/src/lib/time.ts index 5329b18368..2e2f54e490 100644 --- a/apps/desktop/src/lib/time.ts +++ b/apps/desktop/src/lib/time.ts @@ -22,6 +22,9 @@ export const fmtDayTime = new Intl.DateTimeFormat(undefined, { // Medium date + short time (command center session detail). export const fmtDateTime = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }) +// Weekday + full date + clock (message time tooltips). +export const fmtFullDateTime = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'short' }) + // Date only, "5 Jun 2026" (starmap tooltip). export const fmtDate = new Intl.DateTimeFormat(undefined, { day: 'numeric', month: 'short', year: 'numeric' })