-
Notifications
You must be signed in to change notification settings - Fork 5k
feat(web): reveal timestamps on tool rows and turn folds #8641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1167,6 +1167,40 @@ function RevertUserMessageButton({ messageId }: { messageId: MessageId }) { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Hover-revealed wall-clock time with a full-date tooltip — the same metadata | ||
| * presentation as message rows, for work entries and turn folds. The parent | ||
| * carries the `group/timeline-row` class that drives the reveal; keyboard | ||
| * focus anywhere in that parent reveals it too, since the span itself is not | ||
| * focusable. Rows without any other focusable element pass | ||
| * `keyboardReachable` so the span becomes the tab stop and tooltip target. | ||
| */ | ||
| function TimelineRowTimestamp({ | ||
| createdAt, | ||
| timestampFormat, | ||
| keyboardReachable = false, | ||
| }: { | ||
| createdAt: string; | ||
| timestampFormat: TimestampFormat; | ||
| keyboardReachable?: boolean; | ||
| }) { | ||
| return ( | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={ | ||
| <span | ||
| className="me-1 shrink-0 rounded-md text-muted-foreground text-xs tabular-nums opacity-0 transition-opacity duration-200 group-hover/timeline-row:opacity-100 group-focus-within/timeline-row:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" | ||
| {...(keyboardReachable ? { tabIndex: 0 } : {})} | ||
| /> | ||
| } | ||
| > | ||
| {formatDayAwareTimestamp(createdAt, timestampFormat)} | ||
| </TooltipTrigger> | ||
| <TooltipPopup>{formatChatTimestampTooltip(createdAt, timestampFormat)}</TooltipPopup> | ||
| </Tooltip> | ||
| ); | ||
| } | ||
|
|
||
| function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-fold" }> }) { | ||
| const ctx = use(TimelineRowCtx); | ||
| const Icon = row.expanded ? ChevronDownIcon : ChevronRightIcon; | ||
|
|
@@ -1178,9 +1212,10 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn- | |
| aria-expanded={row.expanded} | ||
| data-scroll-anchor-ignore | ||
| onClick={() => ctx.onToggleTurnFold(row.turnId)} | ||
| className="flex cursor-pointer select-none items-center gap-1 rounded-md px-1 text-sm leading-relaxed text-muted-foreground tabular-nums transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" | ||
| className="group/timeline-row flex cursor-pointer select-none items-center gap-1 rounded-md px-1 text-sm leading-relaxed text-muted-foreground tabular-nums transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" | ||
| > | ||
| <span>{row.label}</span> | ||
| <TimelineRowTimestamp createdAt={row.createdAt} timestampFormat={ctx.timestampFormat} /> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idle timestamps reserve in-flow widthMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit f8fb116. Configure here. |
||
| <Icon className="size-3.5" /> | ||
| </button> | ||
| </div> | ||
|
|
@@ -1381,7 +1416,7 @@ const WorkGroupSection = memo(function WorkGroupSection({ | |
| groupedEntries: Extract<MessagesTimelineRow, { kind: "work" }>["groupedEntries"]; | ||
| isExpandedToolGroupEntry: boolean; | ||
| }) { | ||
| const { workspaceRoot } = use(TimelineRowCtx); | ||
| const { timestampFormat, workspaceRoot } = use(TimelineRowCtx); | ||
| const nonEmptyEntries = useMemo( | ||
| () => | ||
| groupedEntries.filter((entry) => workEntryIsVisibleInGroup(entry, isExpandedToolGroupEntry)), | ||
|
|
@@ -1401,6 +1436,7 @@ const WorkGroupSection = memo(function WorkGroupSection({ | |
| <SimpleWorkEntryRow | ||
| key={workEntry.id} | ||
| workEntry={workEntry} | ||
| timestampFormat={timestampFormat} | ||
| workspaceRoot={workspaceRoot} | ||
| isExpandedToolGroupEntry={isExpandedToolGroupEntry} | ||
| /> | ||
|
|
@@ -1539,7 +1575,7 @@ function WorkGroupToggleTimelineRow({ | |
| return ( | ||
| <button | ||
| type="button" | ||
| 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" | ||
| className="group/tool-group group/timeline-row 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" | ||
| aria-label={row.hasFailure ? `${row.summary}, tool call failed` : undefined} | ||
| aria-expanded={row.expanded} | ||
| onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} | ||
|
|
@@ -1551,6 +1587,7 @@ function WorkGroupToggleTimelineRow({ | |
| /> | ||
| </span> | ||
| <span className="min-w-0 flex-1 truncate text-secondary-label">{row.summary}</span> | ||
| <TimelineRowTimestamp createdAt={row.createdAt} timestampFormat={ctx.timestampFormat} /> | ||
| </button> | ||
| ); | ||
| } | ||
|
|
@@ -2504,17 +2541,19 @@ const AgentSpawnCtaRow = memo(function AgentSpawnCtaRow(props: { workEntry: Time | |
|
|
||
| const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | ||
| workEntry: TimelineWorkEntry; | ||
| timestampFormat: TimestampFormat; | ||
| workspaceRoot: string | undefined; | ||
| isExpandedToolGroupEntry: boolean; | ||
| }) { | ||
| const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props; | ||
| const { workEntry, timestampFormat, workspaceRoot, isExpandedToolGroupEntry } = props; | ||
| // Before any hooks: spawn CTA rows render their own component. | ||
| if (workEntry.agentSpawn) { | ||
| return <AgentSpawnCtaRow workEntry={workEntry} />; | ||
| } | ||
| return ( | ||
| <PlainWorkEntryRow | ||
| workEntry={workEntry} | ||
| timestampFormat={timestampFormat} | ||
| workspaceRoot={workspaceRoot} | ||
| isExpandedToolGroupEntry={isExpandedToolGroupEntry} | ||
| /> | ||
|
|
@@ -2523,10 +2562,11 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { | |
|
|
||
| const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | ||
| workEntry: TimelineWorkEntry; | ||
| timestampFormat: TimestampFormat; | ||
| workspaceRoot: string | undefined; | ||
| isExpandedToolGroupEntry: boolean; | ||
| }) { | ||
| const { workEntry, workspaceRoot, isExpandedToolGroupEntry } = props; | ||
| const { workEntry, timestampFormat, workspaceRoot, isExpandedToolGroupEntry } = props; | ||
| const [expanded, setExpanded] = useState(false); | ||
| const iconConfig = workToneIcon(workEntry.tone); | ||
| const showWarningIndicator = workEntry.sourceActivityKind === "runtime.warning"; | ||
|
|
@@ -2581,7 +2621,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | |
| return ( | ||
| <div | ||
| className={cn( | ||
| "flex flex-col rounded-md px-0.5 transition-colors", | ||
| "group/timeline-row flex flex-col rounded-md px-0.5 transition-colors", | ||
| isExpandedToolGroupEntry ? "py-0" : "py-0.5", | ||
| canExpand && | ||
| "cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70", | ||
|
|
@@ -2606,6 +2646,13 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | |
| <span className={cn("min-w-0 flex-1 truncate", headingClass)}>{displayText}</span> | ||
| </p> | ||
| </div> | ||
| <TimelineRowTimestamp | ||
| createdAt={workEntry.createdAt} | ||
| timestampFormat={timestampFormat} | ||
| // Rows that cannot expand have no other focusable element, so | ||
| // the timestamp itself becomes the keyboard path to its reveal. | ||
| keyboardReachable={!canExpand} | ||
| /> | ||
| <span | ||
| className={cn( | ||
| "flex size-4 shrink-0 items-center justify-center", | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keyboardReachableturns the timestamp into a role-less focusable element, andPlainWorkEntryRowpasses it for every row wherebuildToolCallExpandedBodyreturnsnull(info rows, compaction notices, user-input rows, tool rows without a command/detail/changed files). In a long transcript that adds one tab stop per such row, and each stop lands on a<span>with no role or name beyond the time string — assistive tech announces focusable static text, and keyboard users have to traverse the whole activity log to reach the next real control.The message-row timestamps this component mirrors (user meta row and assistant meta row) deliberately stay non-focusable: the reveal rides on the row's existing control via
focus-within, and the time text is still in the DOM for screen readers even whileopacity-0. Suggest droppingkeyboardReachable/tabIndexhere (and thekeyboardReachable={!canExpand}prop at thePlainWorkEntryRowcall site) so the reveal stays hover/group-focus-within-driven; if a keyboard path is genuinely required for those rows, put the focusable semantics on the row itself rather than on the timestamp span.Posted via Macroscope — UI Consistency