feat(agents): hover details on tick beats + arrow-key navigation between snapshots - #267
cardosofede wants to merge 6 commits into
Conversation
…hots
- TickSpine: hovering or focusing a beat opens a portalled card with the
tick's time, state, journal line and every deed (errors in red), capped
at 6 with '+N more'. Replaces the native title tooltip.
- TickOverlay: the tick view gets prev/next buttons, an N/M counter and
ArrowLeft/ArrowRight navigation (ignored while typing or with modifiers).
Steps replace history so Back still closes the overlay in one press, and
neighbouring snapshots are prefetched.
- WorkspaceUrlAdapter.set takes an optional { replace } override.
|
| const ticks = useMemo( | ||
| () => | ||
| journalContent ? parseJournal(journalContent).ticks.map((t) => t.tick) : [], | ||
| [journalContent], | ||
| ); | ||
| const { prev, next, index } = adjacentTicks(ticks, tick); |
There was a problem hiding this comment.
Journal ticks can lack snapshots
When a run is blocked, the engine records a journal tick but does not write a snapshot. Arrow navigation includes that tick anyway. Selecting it makes the snapshot request return 404, so the overlay shows “Select a snapshot to view details” instead of a snapshot. Navigation should use ticks with available snapshots or explain journal-only ticks.
Knowledge Base Used: Frontend application
| onClick={() => onSelectTick(entry.tick)} | ||
| onMouseEnter={(e) => showCard(entry.tick, e.currentTarget)} | ||
| onMouseLeave={hideCard} | ||
| onFocus={(e) => showCard(entry.tick, e.currentTarget)} | ||
| onBlur={hideCard} |
There was a problem hiding this comment.
Selecting a keyboard-focused beat does not blur it, so its hover card stays open when the tick overlay appears. The card has a higher z-index than the overlay and can cover part of the snapshot until focus moves elsewhere. Dismiss the card when the beat is selected or the overlay opens.
| const above = vh - anchor.bottom < CARD_MIN_ROOM && anchor.top > vh - anchor.bottom; | ||
| const position = above | ||
| ? { left, bottom: vh - anchor.top + CARD_GAP } | ||
| : { left, top: anchor.bottom + CARD_GAP }; |
There was a problem hiding this comment.
The card flips above a beat only when less than 240 pixels remain below it, but six deeds with summaries and error text can make the card much taller. On a short viewport, it can open below the beat and clip the details. Use the card’s measured height for placement or constrain its height and allow scrolling.
The beat under the pointer (or focus) scales to 2x width and 1.2x height, covering the gaps beside it. A transform rather than a width change, so neighbours do not reflow under the cursor.
Each beat is now a fixed 16px button with the bar drawn inside it, so the targets sit edge to edge while the bars get an 8px gap. Hovering grows only the inner bar and gives it a 2px contour (the selected beat keeps its primary ring), without reflowing the neighbours.
The hovered beat's real width and height animate (8x20 -> 14x26, 200ms ease-out), so its neighbours slide over and the 8px gap stays fixed, instead of a fixed slot with the bar growing inside it. Hover is tracked on the strip: a pointer in a gap keeps the nearest beat, so sliding along the row never flickers. Respects prefers-reduced-motion.
| } | ||
| className={`flex items-center gap-1 ${ | ||
| // The vertical padding is room for the selected beat's ring, which an | ||
| className={`flex items-center gap-2 ${ |
There was a problem hiding this comment.
The pointer tracker shows the nearest beat’s card while the pointer is in the new 8px gap, but only the beat buttons handle clicks. Clicking where the card says “Click to open this tick” therefore does nothing. Keep the clickable targets contiguous or make a gap click select the beat shown on the card.
| ref={stripRef} | ||
| data-testid="tick-spine" | ||
| onPointerMove={trackPointer} | ||
| onPointerLeave={hideCard} |
There was a problem hiding this comment.
|
Hey @cardosofede, I added a fix for hovering effect to make it better here #269 |
fix/Improve tick hovering
|
LGTM! |
| onBlur={hideCard} | ||
| data-beat-hovered={isHovered || undefined} | ||
| data-beat-selected={isSelected || undefined} | ||
| className="relative h-5 w-2 shrink-0 rounded-sm outline-none" |
There was a problem hiding this comment.
Keyboard focus becomes invisible
When a beat has keyboard focus, moving the pointer to another beat or out of the strip moves or clears its visible ring. The focused button also has outline-none, so users can no longer see which beat Enter will open.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const offset = hoveredIndex < 0 || index === hoveredIndex | ||
| ? 0 | ||
| : index < hoveredIndex ? -3 : 3; |
There was a problem hiding this comment.
Summary
Two review-ergonomics improvements to the agent run screen:
Tick strip hover card — hovering (or keyboard-focusing) a beat in the tick strip now opens a card with:
The card is portalled and fixed-positioned so the strip's horizontal scroller can't clip it, and it clamps/flips to stay in the viewport. Replaces the native
titletooltip (kept asaria-label).Arrow keys between snapshots — the tick overlay now has ‹ › buttons, an
N/Mposition counter, and ←/→ keyboard navigation through the session's ticks.WorkspaceUrlAdapter.setgained an optional{ replace }override)Test plan
TickSpine.test.tsx(hover card content, deed cap, pre-log ticks, focus),TickOverlay.test.tsx(adjacentTicks, arrows, modifiers/inputs, buttons, close)tsc -b,lint:ci,buildgreen🤖 Generated with Claude Code