mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* feat(web): rich hover tooltips on session-list attention indicators
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>
* feat(web): opaque tooltip surface; drop redundant 'updated Nm ago' body
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>
* feat(web,hub): show scheduled fire time in session-list clock tooltip
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>
* fix(web): wire session-row keyboard focus to HoverTooltip a11y
Address PR #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>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
/**
|
|
* Formatting helpers for scheduled-send UX (queued bar + session-list clock tooltip).
|
|
*/
|
|
|
|
/** Locale-aware absolute fire time, e.g. "Jun 16, 1:45 PM". */
|
|
export function formatScheduledTime(scheduledAt: number): string {
|
|
const date = new Date(scheduledAt)
|
|
const now = new Date()
|
|
const opts: Intl.DateTimeFormatOptions = {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}
|
|
if (date.getFullYear() !== now.getFullYear()) {
|
|
opts.year = 'numeric'
|
|
}
|
|
return date.toLocaleString(undefined, opts)
|
|
}
|
|
|
|
/** Relative countdown until a future epoch-ms, e.g. "in 5m". Returns null when invalid. */
|
|
export function formatFutureRelativeTime(
|
|
value: number,
|
|
t: (key: string, params?: Record<string, string | number>) => string
|
|
): string | null {
|
|
const ms = value < 1_000_000_000_000 ? value * 1000 : value
|
|
if (!Number.isFinite(ms)) return null
|
|
const delta = ms - Date.now()
|
|
if (delta <= 0) return t('session.time.soon')
|
|
if (delta < 60_000) return t('session.time.inLessThanMinute')
|
|
const minutes = Math.ceil(delta / 60_000)
|
|
if (minutes < 60) return t('session.time.inMinutes', { n: minutes })
|
|
const hours = Math.ceil(minutes / 60)
|
|
if (hours < 24) return t('session.time.inHours', { n: hours })
|
|
const days = Math.ceil(hours / 24)
|
|
if (days < 7) return t('session.time.inDays', { n: days })
|
|
return formatScheduledTime(ms)
|
|
}
|
|
|
|
/** "in 5m · Jun 16, 1:45 PM" for tooltip / queued copy. */
|
|
export function formatScheduledFireLabel(
|
|
scheduledAt: number,
|
|
t: (key: string, params?: Record<string, string | number>) => string
|
|
): string | null {
|
|
const relative = formatFutureRelativeTime(scheduledAt, t)
|
|
if (!relative) return null
|
|
const absolute = formatScheduledTime(scheduledAt)
|
|
// When the countdown is already an absolute date (>7d), don't duplicate.
|
|
if (relative === absolute) return relative
|
|
return `${relative} · ${absolute}`
|
|
}
|
|
|
|
/** Session-list clock tooltip body from summary fields. */
|
|
export function formatScheduledTooltipDetail(
|
|
summary: { futureScheduledMessageCount: number; nextScheduledAt: number | null },
|
|
t: (key: string, params?: Record<string, string | number>) => string
|
|
): string {
|
|
if (summary.nextScheduledAt != null) {
|
|
const when = formatScheduledFireLabel(summary.nextScheduledAt, t)
|
|
if (when) {
|
|
if (summary.futureScheduledMessageCount > 1) {
|
|
return t('session.tooltip.scheduled.next', {
|
|
when,
|
|
more: summary.futureScheduledMessageCount - 1
|
|
})
|
|
}
|
|
return t('session.tooltip.scheduled.fires', { when })
|
|
}
|
|
}
|
|
return t('session.tooltip.scheduled.body')
|
|
}
|