Skip to content

feat(web,hub): rich hover tooltips on session-list attention indicators#941

Merged
tiann merged 4 commits into
tiann:mainfrom
heavygee:feat/session-list-rich-tooltips
Jun 18, 2026
Merged

feat(web,hub): rich hover tooltips on session-list attention indicators#941
tiann merged 4 commits into
tiann:mainfrom
heavygee:feat/session-list-rich-tooltips

Conversation

@heavygee

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #698 / #699: replace generic native title tooltips on session-list attention dots and the schedule clock with rich, accessible hover copy.

  • HoverTooltip — lightweight CSS-driven tooltip primitive (no portal/positioning JS).
  • pendingRequests[] on SessionSummary — capped (5), oldest-first structured slice of pending tool requests so permission/input dots can list actual tool names without a per-row detail fetch. Additive; older hubs degrade gracefully.
  • Attention dot bodies — permission/input list blocking tools (+ honest +N more overflow when single-kind); background shows task count; unread shows title only (relative time already on the row).
  • Schedule clock — hub adds nextScheduledAt (MIN future scheduled_at per session); tooltip shows relative + absolute fire time (e.g. "Fires in 26m · Jun 17, 12:06 PM") and "+N more" when multiple are queued.
  • i18n — en + zh-CN for all new strings.

Closes #940

Test plan

  • bun run typecheck (cli + web + hub)
  • bun test shared/src/sessionSummary.test.ts
  • npx vitest run web/src/components/SessionAttentionIndicator.test.tsx
  • npx vitest run web/src/lib/scheduledTime.test.ts
  • bun test hub/src/store/messages.test.ts (batch minFutureScheduledAtBySessionIds)
  • Manual: hover permission/input/background/unread dots — tooltip names the reason
  • Manual: hover schedule clock — shows countdown + absolute fire time
  • Manual: session with 2+ scheduled messages — "Next … · +N more"

Made with Cursor

heavygee and others added 3 commits June 17, 2026 12:13
The session-row attention dots and the future-scheduled clock icon used
plain `title=""` attributes which gave only a one-word label ("Permission
required"). Replace those with hover/focus-revealed tooltips that name
*which* tools are blocking, count background tasks, surface the
"updated Nm ago" timestamp, and explain the pending schedule.

To make per-tool copy possible without an extra round trip,
`SessionSummary` now carries a structured slice of the pending tool
requests, capped at `PENDING_REQUEST_SUMMARY_CAP = 5` oldest-first:

  pendingRequests: Array<{ id; kind; tool; since }>

`pendingRequestsCount` remains the authoritative total;
`pendingRequestKinds` is still derived from the FULL request set so a
single `'input'` request beyond the cap still surfaces its kind on the
session row.

The tooltip primitive (`HoverTooltip`) is a CSS-driven reveal — no
portal, no positioning JS — so it composes cheaply inside the existing
session-row `<button>` and stays out of the way on touch devices, which
keep getting the same `aria-label` the old `title=""` attribute provided
to screen readers.

Test coverage: shared derivation + cap + tie-break + full-set kind
behaviour; web tooltip render across all four attention kinds plus
mixed-kind overflow suppression and aria-label exposure.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two operator-feedback fixes on the new session-list HoverTooltip:

1. Tooltip background was bg-[var(--app-bg)] - the same variable as the
   session row underneath - so the tooltip looked translucent and the row
   text bled through. Switch to bg-[var(--app-secondary-bg)] (#2C2C2E
   dark / #f3f4f6 light, both opaque) and bump shadow-md -> shadow-lg.
   Telegram-themed clients still pick up tg-theme-secondary-bg-color so
   the tooltip stays on-theme.

2. The 'unread' attention dot tooltip rendered 'New activity / Updated 5m
   ago', but the relative-time pill ('5m ago') is already on the right
   edge of the same session row. The tooltip body just duplicated info.
   Render only the title for the unread case; drop the
   session.tooltip.unread.body i18n key from en + zh-CN.

The other tooltip kinds (permission/input list tools, background lists
task count) keep their bodies - those facts are not visible elsewhere on
the row.

Co-authored-by: Cursor <cursoragent@cursor.com>
The schedule clock tooltip previously said only "Will fire when due."
while the row already showed a relative updated-at pill. Extend the
session-list API with nextScheduledAt (MIN future scheduled_at per
session, same filter as futureScheduledMessageCount) and render:

- single scheduled: "Fires in 5m · Jun 16, 1:45 PM"
- multiple: "Next in 5m · Jun 16, 1:45 PM · +2 more"

Extract formatScheduledTime from QueuedMessagesBar into web/lib/
scheduledTime.ts alongside formatFutureRelativeTime and the tooltip
composer. SSE upsert preserves nextScheduledAt until the list refetch
that already runs on schedule-related events.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Tooltip content is not reachable from keyboard focus — the new tooltip puts aria-describedby/aria-label on an inner non-focusable <span>, while the actual focus target is the outer session-row <button> in SessionList.tsx. Because focus stays on the button, group-focus-within on this descendant never matches, and screen readers focus the button without the new tooltip description. This regresses the stated accessible hover-copy path for both attention dots and the schedule clock. Evidence: web/src/components/HoverTooltip.tsx:40 and web/src/components/SessionList.tsx:672.
    Suggested fix:
    // Have the focused row own the description and expose a parent-focus trigger.
    <button
        type="button"
        aria-describedby={tooltipId}
        className="group/session-row ..."
    >
        ...
        <HoverTooltip
            id={tooltipId}
            label={scheduledLabel}
            target={<ScheduleIcon className="h-3.5 w-3.5 text-[var(--app-hint)]" />}
            revealOnParentFocusClass="group-focus-visible/session-row:opacity-100 group-focus-visible/session-row:visible"
        >
            ...
        </HoverTooltip>
    </button>

Questions

  • None.

Summary

  • Review mode: initial
  • One accessibility regression found in the new tooltip primitive/usage. Residual risk: I did not run the full test suite in this automation pass.

Testing

  • Not run (automation)

HAPI Bot

Comment thread web/src/components/HoverTooltip.tsx Outdated
Address PR tiann#941 Major review: aria-describedby and tooltip visibility
were on a non-focusable inner span, so keyboard users tabbing the session
row button never received the rich tooltip description and
group-focus-within never matched.

- Session row button owns aria-describedby (attention + schedule ids)
- Add group/session-row + SESSION_ROW_TOOLTIP_FOCUS_CLASS reveal on
  :focus-visible
- HoverTooltip takes required id; drop inner aria-label/describedby
- useSessionRowTooltipIds helper composes stable row tooltip ids
- Tests for id wiring and parent-focus reveal classes

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • No issues found.

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • No issues found in the latest full PR diff. The previous bot finding about tooltip keyboard/focus ownership appears addressed by moving aria-describedby to the session row and exposing parent focus-visible reveal classes. Residual risk: I did not run the full test suite in this automation pass.

Testing

  • Not run (automation)

HAPI Bot

@tiann
tiann merged commit ce67823 into tiann:main Jun 18, 2026
2 checks passed
yoyoworms added a commit to yoyoworms/hapi that referenced this pull request Jun 20, 2026
…ate, drag-drop

Merge tiann/hapi upstream (33 commits, v0.20.2) into the liuxin fork.
Notable adoptions:

- file viewer download button (tiann#926) — the download UI the user asked
  about; decodes existing base64 content to a Blob, no new endpoint
- Claude Code 'auto' permission mode (tiann#879)
- in-app PWA update prompt when a new service worker is ready (tiann#946) —
  PwaUpdateBannerWithStatusOffset, adopted WITHOUT enabling voice
- drag-and-drop files onto chat to attach (tiann#936) + Web Share Target (tiann#933)
- Codex fast mode / service-tier toggle (tiann#904), session service_tier
  column + merge-preserve
- four hub-restart-cascade cleanup bugs (tiann#923), stale-PID detection
  after abnormal runner shutdown (tiann#931), OutgoingMessageQueue flush
  before next turn (tiann#909), inactive-session send error surface (tiann#922)
- OLED black theme + per-appearance custom colors (tiann#937), session-list
  attention tooltips (tiann#941), active-only filter + paginated Show-N-more
  (tiann#903), file-explorer state persistence (tiann#911)

Conflict resolutions (22 files) — fork features preserved:
- store/index.ts: both forks bumped schema to V10 with different
  migrateFromV9ToV10; combined into one migration doing BOTH our
  content_uuid dedup index (messages) AND upstream service_tier (sessions)
- socket/server.ts: collapsed duplicate maxHttpBufferSize key into
  Math.max(SOCKET_MAX_HTTP_BUFFER_SIZE, 100MB) — keeps our 100MB upload
  floor while honoring upstream's image-buffer constant
- spawnSession plumbing (apiMachine/rpcGateway/syncEngine): unioned our
  sandbox+continueLatest with upstream's serviceTier across signature,
  RPC payload, and the resume call site
- App.tsx: kept signOut + voice-disabled; adopted PWA update banner
  (PwaUpdateProvider auto-merged into App() wrapper)
- SessionChat: nested both wrappers — HappyChatProvider > ShareSeedConsumer
  + DragDropZone > thread
- sessionModel.test: split the two same-named merge tests (our codex
  dedup + upstream service-tier) back into separate it() blocks
- useSendMessage: kept fork queue-stop AND upstream onError surface;
  keyed onError by non-null sid
- startHappyServer: kept fork's manual-rename skip in change_title
- schemas/locales/StatusBar/HappyComposer: unioned both sides

Post-merge type fixes: pinned-session test mock gained pendingRequests +
nextScheduledAt (new required SessionSummary fields); legacy-CLI model
backfill in applySessionConfig now extracts modelId from upstream's Pi
{provider,modelId} union.

Validation: typecheck green (cli/web/hub); full suite 2260 tests pass
incl. the runner stress test.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web,hub): rich hover tooltips on session-list attention indicators

2 participants