| ... | ... | @@ -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 | 2103 | function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-fold" }> }) { |
| 2067 | 2104 | const ctx = use(TimelineRowCtx); |
| 2068 | 2105 | const Icon = row.expanded ? ChevronDownIcon : ChevronRightIcon; |
| 2069 | 2106 | |
| 2070 | 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 | 2109 | <button |
| 2073 | 2110 | type="button" |
| 2074 | 2111 | aria-expanded={row.expanded} |
| ... | ... | @@ -2079,6 +2116,11 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn- |
| 2079 | 2116 | <span>{row.label}</span> |
| 2080 | 2117 | <Icon className="size-3.5" /> |
| 2081 | 2118 | </button> |
| 2119 | <TimelineRowTimestamp |
| 2120 | createdAt={row.createdAt} |
| 2121 | timestampFormat={ctx.timestampFormat} |
| 2122 | className="ms-auto" |
| 2123 | /> |
| 2082 | 2124 | </div> |
| 2083 | 2125 | ); |
| 2084 | 2126 | } |
| ... | ... | @@ -2770,7 +2812,7 @@ function WorkGroupToggleTimelineRow({ |
| 2770 | 2812 | return ( |
| 2771 | 2813 | <button |
| 2772 | 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 | 2816 | aria-label={row.hasFailure ? `${row.summary}, tool call failed` : undefined} |
| 2775 | 2817 | aria-expanded={row.expanded} |
| 2776 | 2818 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} |
| ... | ... | @@ -2786,6 +2828,7 @@ function WorkGroupToggleTimelineRow({ |
| 2786 | 2828 | /> |
| 2787 | 2829 | </span> |
| 2788 | 2830 | <span className="min-w-0 flex-1 truncate text-secondary-label">{row.summary}</span> |
| 2831 | <TimelineRowTimestamp createdAt={row.createdAt} timestampFormat={ctx.timestampFormat} /> |
| 2789 | 2832 | </button> |
| 2790 | 2833 | ); |
| 2791 | 2834 | } |
| ... | ... | @@ -4234,7 +4277,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 4234 | 4277 | onToggleEntry?: ((collapsed: boolean) => void) | undefined; |
| 4235 | 4278 | }) { |
| 4236 | 4279 | const { workEntry, workspaceRoot, isExpandedToolGroupEntry, displayLabel } = props; |
| 4237 | | const { threadRef, onImageExpand } = use(TimelineRowCtx); |
| 4280 | const { threadRef, onImageExpand, timestampFormat } = use(TimelineRowCtx); |
| 4238 | 4281 | const groupView = use(WorkGroupViewCtx); |
| 4239 | 4282 | const [expanded, setExpanded] = useState( |
| 4240 | 4283 | () => groupView?.state.expandedEntries.has(workEntry.id) ?? false, |
| ... | ... | @@ -4336,7 +4379,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 4336 | 4379 | return ( |
| 4337 | 4380 | <div |
| 4338 | 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 | 4383 | isExpandedToolGroupEntry ? "py-0" : "py-0.5", |
| 4341 | 4384 | expanded && "mb-1", |
| 4342 | 4385 | canExpand && |
| ... | ... | @@ -4406,6 +4449,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 4406 | 4449 | )} |
| 4407 | 4450 | /> |
| 4408 | 4451 | </span> |
| 4452 | <TimelineRowTimestamp createdAt={workEntry.createdAt} timestampFormat={timestampFormat} /> |
| 4409 | 4453 | </div> |
| 4410 | 4454 | </div> |
| 4411 | 4455 | {expanded && viewedImage && threadRef ? ( |