Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

fix(grunt/tui): #N ticket-hover — single shared floating card, no flicker - #15

Merged
terrxo merged 1 commit into
devfrom
grunt-fix/ticket-hover-no-flicker
May 27, 2026
Merged

terrxo merged 1 commit into
devfrom
grunt-fix/ticket-hover-no-flicker

Conversation

@terrxo

@terrxo terrxo commented May 27, 2026

Copy link
Copy Markdown

v2 fix for hivemind anomalyco#233. Closes the 'flickering and nothing loaded' regression Nik flagged after v1.

v1 root cause

v1 TicketRef rendered a per-ref tooltip box as a child of the trigger text. Without explicit position coords, opentui placed it overlapping/adjacent to the text → mouse moved off text onto tooltip → onMouseOut → tooltip hid → mouse back on text → onMouseOver → flicker loop. Fetch couldn't visually complete because the tooltip was vanishing too fast.

v2 architecture

Single floating tooltip at a screen anchor, fed by a global signal in the Hivemind context:

  • Hivemind context owns hoveredTicketId signal + ticketCache + fetchTicket + ticketDetail accessor
  • TicketRef is now ONLY a colored clickable text. On hover, sets hive.setHoveredTicket(id). Sets null on leave. No tooltip child.
  • TicketHoverCard (new) renders ONCE at app root. position='absolute' top=1 right=2. Reads hoveredTicketId; shows the detail. Far from any mouse-interactive surface → no flicker.

Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.8b07799be.dirty.

…-ref overlap flicker

Refs hivemind anomalyco#233. v2 of the hover fix after Nik flagged: 'hovers broken
like flickering and nothing loaded'.

## v1 bug (root cause)

The v1 TicketRef rendered its tooltip as a child <box position='absolute'>
inside the same column as the trigger <text>. Without explicit top/left,
opentui placed the tooltip in the natural flow position — directly below
or overlapping the trigger text. Mouse over text fired onMouseOver →
tooltip showed → mouse was no longer on the text (now on tooltip area or
in the gap) → onMouseOut fired → tooltip hid → mouse back on text →
onMouseOver again → flicker loop. Also blocked the fetch from completing
visibly: even if the data loaded, the tooltip vanished before render.

## v2 architecture

Single global tooltip, screen-anchored, fed by a global signal:

1. **Hivemind context** owns:
   - hoveredTicketId signal (number | null)
   - setHoveredTicket(id) — also kicks off lazy fetch + populates cache
   - ticketCache (Map<number, TicketDetail | null>)
   - ticketDetail(id) — reactive read of cache
   - fetchTicket(id) — dedup'd in-flight, AbortController timeout

2. **TicketRef** is now ONLY a colored clickable text. No tooltip child.
   onMouseOver/Out call hive.setHoveredTicket(props.id) / setHoveredTicket(null).
   onMouseUp opens hivemind-ui via the existing 'open' npm pkg.

3. **TicketHoverCard** is a NEW single-instance component rendered once
   at app root (in app.tsx). Reads hive.hoveredTicketId() — when set,
   renders the detail at position='absolute' top=1 right=2. Far from
   any mouse-interactive surface, so hover state doesn't ping-pong.

Result:
- TicketRef stays inline with text flow (just a <text>, not a <box>)
- Tooltip renders ONCE at a fixed location, fed by whichever ref is hovered
- Cache shared across all refs, fetch fires on first hover of each id
- onMouseOver/Out events stay stable — no overlap, no flicker

## Files

| File | Change |
|---|---|
| packages/opencode/src/cli/cmd/tui/context/hivemind.tsx | + hoveredTicketId / setHoveredTicket / ticketDetail / fetchTicket + cache |
| packages/opencode/src/cli/cmd/tui/component/ticket-ref.tsx | Stripped tooltip; just colored clickable <text> |
| packages/opencode/src/cli/cmd/tui/component/ticket-hover-card.tsx | NEW single-instance floating tooltip |
| packages/opencode/src/cli/cmd/tui/app.tsx | Renders <TicketHoverCard /> once at app root |

## Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.8b07799be.dirty.
@terrxo
terrxo merged commit f86e664 into dev May 27, 2026
@terrxo
terrxo deleted the grunt-fix/ticket-hover-no-flicker branch May 27, 2026 00:37
@github-actions

Copy link
Copy Markdown

Hey! Your PR title fix(grunt/tui): #N ticket-hover — single shared floating card, no flicker doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

terrxo added a commit that referenced this pull request May 27, 2026
…hored near trigger, open/close delays, sticky) (#16)

Refs hivemind anomalyco#233. Closes Nik feedback after PR #15: 'works but I'd much
rather want the thing that works like shadcn-svelte's hovercard'.

## What changed vs PR #15

PR #15 moved the tooltip to a fixed top-right screen anchor to fix the
flicker. That worked but produced wrong UX — Nik wanted shadcn HoverCard
ergonomics: card pops up NEAR the trigger after a small open delay,
STAYS open if cursor moves into the card body (close delay gives time to
traverse the gap), closes after a brief delay so a small detour doesn't
dismiss it.

## How shadcn-style is achieved

Hivemind context now owns:
- hoveredTicket signal: { id, anchorX, anchorY } | null
- triggerHoverEnter(id, x, y) — clears any pending close-timer, schedules
  open after OPEN_DELAY_MS (150ms) at the anchor coords
- triggerHoverLeave() — cancels pending open if still scheduled; otherwise
  schedules close after CLOSE_DELAY_MS (200ms) so cursor can traverse to
  the card
- setCardHovered(bool) — called by the card's own onMouseOver/Out. true
  cancels any pending close timer; false starts one.

Sticky behavior emerges naturally: trigger leave fires close-timer →
within 200ms cursor reaches card body → setCardHovered(true) cancels the
timer → card stays. When cursor leaves card → setCardHovered(false)
restarts the timer → card closes after 200ms.

## TicketRef changes

onMouseOver now passes evt.x, evt.y to hive.triggerHoverEnter() so the
card knows where to anchor. onMouseOut calls hive.triggerHoverLeave().
Click handler unchanged.

## TicketHoverCard changes

- Reads anchor coords from hive.hoveredTicket()
- Positions just below + slightly right of the cursor (left=anchorX+1, top=anchorY+1)
- Right-edge overflow guard: if card would push off the right, flips so its right
  edge aligns at the cursor.
- Bottom-edge guard: flips above the cursor if near terminal bottom.
- Adds onMouseOver={hive.setCardHovered(true)} + onMouseOut={hive.setCardHovered(false)}
  for the sticky behavior.

## Why no flicker even though card is near trigger

PR #15 was right that a per-ref tooltip overlapping its own text caused flicker.
This PR keeps the single-instance shared-card pattern but moves the position from
fixed top-right back to anchored-near-cursor. The flicker risk only re-emerges if
the card overlapped the trigger text — but we offset by +1 in both axes, so the
trigger column is always clear. And the sticky-hover state machine handles the
gap traversal cleanly.

## Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.f86e6640f.dirty.
terrxo added a commit that referenced this pull request May 28, 2026
…-ref overlap flicker (#15)

Refs hivemind anomalyco#233. v2 of the hover fix after Nik flagged: 'hovers broken
like flickering and nothing loaded'.

## v1 bug (root cause)

The v1 TicketRef rendered its tooltip as a child <box position='absolute'>
inside the same column as the trigger <text>. Without explicit top/left,
opentui placed the tooltip in the natural flow position — directly below
or overlapping the trigger text. Mouse over text fired onMouseOver →
tooltip showed → mouse was no longer on the text (now on tooltip area or
in the gap) → onMouseOut fired → tooltip hid → mouse back on text →
onMouseOver again → flicker loop. Also blocked the fetch from completing
visibly: even if the data loaded, the tooltip vanished before render.

## v2 architecture

Single global tooltip, screen-anchored, fed by a global signal:

1. **Hivemind context** owns:
   - hoveredTicketId signal (number | null)
   - setHoveredTicket(id) — also kicks off lazy fetch + populates cache
   - ticketCache (Map<number, TicketDetail | null>)
   - ticketDetail(id) — reactive read of cache
   - fetchTicket(id) — dedup'd in-flight, AbortController timeout

2. **TicketRef** is now ONLY a colored clickable text. No tooltip child.
   onMouseOver/Out call hive.setHoveredTicket(props.id) / setHoveredTicket(null).
   onMouseUp opens hivemind-ui via the existing 'open' npm pkg.

3. **TicketHoverCard** is a NEW single-instance component rendered once
   at app root (in app.tsx). Reads hive.hoveredTicketId() — when set,
   renders the detail at position='absolute' top=1 right=2. Far from
   any mouse-interactive surface, so hover state doesn't ping-pong.

Result:
- TicketRef stays inline with text flow (just a <text>, not a <box>)
- Tooltip renders ONCE at a fixed location, fed by whichever ref is hovered
- Cache shared across all refs, fetch fires on first hover of each id
- onMouseOver/Out events stay stable — no overlap, no flicker

## Files

| File | Change |
|---|---|
| packages/opencode/src/cli/cmd/tui/context/hivemind.tsx | + hoveredTicketId / setHoveredTicket / ticketDetail / fetchTicket + cache |
| packages/opencode/src/cli/cmd/tui/component/ticket-ref.tsx | Stripped tooltip; just colored clickable <text> |
| packages/opencode/src/cli/cmd/tui/component/ticket-hover-card.tsx | NEW single-instance floating tooltip |
| packages/opencode/src/cli/cmd/tui/app.tsx | Renders <TicketHoverCard /> once at app root |

## Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.8b07799be.dirty.
terrxo added a commit that referenced this pull request May 28, 2026
…hored near trigger, open/close delays, sticky) (#16)

Refs hivemind anomalyco#233. Closes Nik feedback after PR #15: 'works but I'd much
rather want the thing that works like shadcn-svelte's hovercard'.

## What changed vs PR #15

PR #15 moved the tooltip to a fixed top-right screen anchor to fix the
flicker. That worked but produced wrong UX — Nik wanted shadcn HoverCard
ergonomics: card pops up NEAR the trigger after a small open delay,
STAYS open if cursor moves into the card body (close delay gives time to
traverse the gap), closes after a brief delay so a small detour doesn't
dismiss it.

## How shadcn-style is achieved

Hivemind context now owns:
- hoveredTicket signal: { id, anchorX, anchorY } | null
- triggerHoverEnter(id, x, y) — clears any pending close-timer, schedules
  open after OPEN_DELAY_MS (150ms) at the anchor coords
- triggerHoverLeave() — cancels pending open if still scheduled; otherwise
  schedules close after CLOSE_DELAY_MS (200ms) so cursor can traverse to
  the card
- setCardHovered(bool) — called by the card's own onMouseOver/Out. true
  cancels any pending close timer; false starts one.

Sticky behavior emerges naturally: trigger leave fires close-timer →
within 200ms cursor reaches card body → setCardHovered(true) cancels the
timer → card stays. When cursor leaves card → setCardHovered(false)
restarts the timer → card closes after 200ms.

## TicketRef changes

onMouseOver now passes evt.x, evt.y to hive.triggerHoverEnter() so the
card knows where to anchor. onMouseOut calls hive.triggerHoverLeave().
Click handler unchanged.

## TicketHoverCard changes

- Reads anchor coords from hive.hoveredTicket()
- Positions just below + slightly right of the cursor (left=anchorX+1, top=anchorY+1)
- Right-edge overflow guard: if card would push off the right, flips so its right
  edge aligns at the cursor.
- Bottom-edge guard: flips above the cursor if near terminal bottom.
- Adds onMouseOver={hive.setCardHovered(true)} + onMouseOut={hive.setCardHovered(false)}
  for the sticky behavior.

## Why no flicker even though card is near trigger

PR #15 was right that a per-ref tooltip overlapping its own text caused flicker.
This PR keeps the single-instance shared-card pattern but moves the position from
fixed top-right back to anchored-near-cursor. The flicker risk only re-emerges if
the card overlapped the trigger text — but we offset by +1 in both axes, so the
trigger column is always clear. And the sticky-hover state machine handles the
gap traversal cleanly.

## Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.f86e6640f.dirty.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant