| ... | @@ -2063,12 +2063,49 @@ function RevertUserMessageButton({ | ... | @@ -2063,12 +2063,49 @@ function RevertUserMessageButton({ |
| 2063 | ); | 2063 | ); |
| 2064 | } | 2064 | } |
| 2065 | | 2065 | |
| | 2066 | /** |
| | 2067 | * Hover-revealed wall-clock time with a full-date tooltip — the same metadata |
| | 2068 | * presentation as message rows, for work entries and turn folds. The parent |
| | 2069 | * carries `group/timeline-row`; hover or focus on an existing control reveals |
| | 2070 | * the time without adding a tab stop. Hidden timestamps stay outside the row |
| | 2071 | * layout. Visibility changes immediately so leaving flow cannot overlap text |
| | 2072 | * during a fade-out. Render it as the row's rightmost flex child so the |
| | 2073 | * revealed time lands at the right edge, clear of disclosure controls. |
| | 2074 | */ |
| | 2075 | function TimelineRowTimestamp({ |
| | 2076 | createdAt, |
| | 2077 | timestampFormat, |
| | 2078 | className, |
| | 2079 | }: { |
| | 2080 | createdAt: string; |
| | 2081 | timestampFormat: TimestampFormat; |
| | 2082 | className?: string; |
| | 2083 | }) { |
| | 2084 | return ( |
| | 2085 | <Tooltip> |
| | 2086 | <TooltipTrigger |
| | 2087 | render={ |
| | 2088 | <span |
| | 2089 | className={cn( |
| | 2090 | "pointer-events-none absolute me-1 shrink-0 whitespace-nowrap rounded-md text-muted-foreground text-xs tabular-nums opacity-0 group-hover/timeline-row:pointer-events-auto group-hover/timeline-row:static group-hover/timeline-row:opacity-100 group-focus-within/timeline-row:pointer-events-auto group-focus-within/timeline-row:static group-focus-within/timeline-row:opacity-100", |
| | 2091 | className, |
| | 2092 | )} |
| | 2093 | /> |
| | 2094 | } |
| | 2095 | > |
| | 2096 | {formatDayAwareTimestamp(createdAt, timestampFormat)} |
| | 2097 | </TooltipTrigger> |
| | 2098 | <TooltipPopup>{formatChatTimestampTooltip(createdAt, timestampFormat)}</TooltipPopup> |
| | 2099 | </Tooltip> |
| | 2100 | ); |
| | 2101 | } |
| | 2102 | |
| 2066 | function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-fold" }> }) { | 2103 | function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-fold" }> }) { |
| 2067 | const ctx = use(TimelineRowCtx); | 2104 | const ctx = use(TimelineRowCtx); |
| 2068 | const Icon = row.expanded ? ChevronDownIcon : ChevronRightIcon; | 2105 | const Icon = row.expanded ? ChevronDownIcon : ChevronRightIcon; |
| 2069 | | 2106 | |
| 2070 | return ( | 2107 | return ( |
| 2071 | <div className="border-b border-border/60 pb-2 pt-1"> | 2108 | <div className="group/timeline-row relative flex items-center gap-1 border-b border-border/60 pb-2 pe-0.5 pt-1"> |
| 2072 | <button | 2109 | <button |
| 2073 | type="button" | 2110 | type="button" |
| 2074 | aria-expanded={row.expanded} | 2111 | aria-expanded={row.expanded} |
| ... | @@ -2079,6 +2116,11 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn- | ... | @@ -2079,6 +2116,11 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn- |
| 2079 | <span>{row.label}</span> | 2116 | <span>{row.label}</span> |
| 2080 | <Icon className="size-3.5" /> | 2117 | <Icon className="size-3.5" /> |
| 2081 | </button> | 2118 | </button> |
| | 2119 | <TimelineRowTimestamp |
| | 2120 | createdAt={row.createdAt} |
| | 2121 | timestampFormat={ctx.timestampFormat} |
| | 2122 | className="ms-auto" |
| | 2123 | /> |
| 2082 | </div> | 2124 | </div> |
| 2083 | ); | 2125 | ); |
| 2084 | } | 2126 | } |
| ... | @@ -2770,7 +2812,7 @@ function WorkGroupToggleTimelineRow({ | ... | @@ -2770,7 +2812,7 @@ function WorkGroupToggleTimelineRow({ |
| 2770 | return ( | 2812 | return ( |
| 2771 | <button | 2813 | <button |
| 2772 | type="button" | 2814 | type="button" |
| 2773 | className="group/tool-group flex min-h-6 w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-sm leading-relaxed transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" | 2815 | className="group/tool-group group/timeline-row relative flex min-h-6 w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-sm leading-relaxed transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" |
| 2774 | aria-label={row.hasFailure ? `${row.summary}, tool call failed` : undefined} | 2816 | aria-label={row.hasFailure ? `${row.summary}, tool call failed` : undefined} |
| 2775 | aria-expanded={row.expanded} | 2817 | aria-expanded={row.expanded} |
| 2776 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} | 2818 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} |
| ... | @@ -2786,6 +2828,7 @@ function WorkGroupToggleTimelineRow({ | ... | @@ -2786,6 +2828,7 @@ function WorkGroupToggleTimelineRow({ |
| 2786 | /> | 2828 | /> |
| 2787 | </span> | 2829 | </span> |
| 2788 | <span className="min-w-0 flex-1 truncate text-secondary-label">{row.summary}</span> | 2830 | <span className="min-w-0 flex-1 truncate text-secondary-label">{row.summary}</span> |
| | 2831 | <TimelineRowTimestamp createdAt={row.createdAt} timestampFormat={ctx.timestampFormat} /> |
| 2789 | </button> | 2832 | </button> |
| 2790 | ); | 2833 | ); |
| 2791 | } | 2834 | } |
| ... | @@ -4234,7 +4277,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | ... | @@ -4234,7 +4277,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 4234 | onToggleEntry?: ((collapsed: boolean) => void) | undefined; | 4277 | onToggleEntry?: ((collapsed: boolean) => void) | undefined; |
| 4235 | }) { | 4278 | }) { |
| 4236 | const { workEntry, workspaceRoot, isExpandedToolGroupEntry, displayLabel } = props; | 4279 | const { workEntry, workspaceRoot, isExpandedToolGroupEntry, displayLabel } = props; |
| 4237 | const { threadRef, onImageExpand } = use(TimelineRowCtx); | 4280 | const { threadRef, onImageExpand, timestampFormat } = use(TimelineRowCtx); |
| 4238 | const groupView = use(WorkGroupViewCtx); | 4281 | const groupView = use(WorkGroupViewCtx); |
| 4239 | const [expanded, setExpanded] = useState( | 4282 | const [expanded, setExpanded] = useState( |
| 4240 | () => groupView?.state.expandedEntries.has(workEntry.id) ?? false, | 4283 | () => groupView?.state.expandedEntries.has(workEntry.id) ?? false, |
| ... | @@ -4336,7 +4379,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | ... | @@ -4336,7 +4379,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 4336 | return ( | 4379 | return ( |
| 4337 | <div | 4380 | <div |
| 4338 | className={cn( | 4381 | className={cn( |
| 4339 | "flex flex-col rounded-md px-0.5 transition-colors", | 4382 | "group/timeline-row relative flex flex-col rounded-md px-0.5 transition-colors", |
| 4340 | isExpandedToolGroupEntry ? "py-0" : "py-0.5", | 4383 | isExpandedToolGroupEntry ? "py-0" : "py-0.5", |
| 4341 | expanded && "mb-1", | 4384 | expanded && "mb-1", |
| 4342 | canExpand && | 4385 | canExpand && |
| ... | @@ -4406,6 +4449,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | ... | @@ -4406,6 +4449,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 4406 | )} | 4449 | )} |
| 4407 | /> | 4450 | /> |
| 4408 | </span> | 4451 | </span> |
| | 4452 | <TimelineRowTimestamp createdAt={workEntry.createdAt} timestampFormat={timestampFormat} /> |
| 4409 | </div> | 4453 | </div> |
| 4410 | </div> | 4454 | </div> |
| 4411 | {expanded && viewedImage && threadRef ? ( | 4455 | {expanded && viewedImage && threadRef ? ( |