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>
137 lines
4.8 KiB
TypeScript
137 lines
4.8 KiB
TypeScript
import type { PendingRequest, SessionSummary } from '@/types/api'
|
|
import type { SessionAttention } from '@/lib/sessionAttention'
|
|
import { getSessionAttentionLabelKey } from '@/lib/sessionAttention'
|
|
import { useTranslation } from '@/lib/use-translation'
|
|
import { HoverTooltip, SESSION_ROW_TOOLTIP_FOCUS_CLASS } from '@/components/HoverTooltip'
|
|
|
|
const ATTENTION_DOT_CLASS: Record<SessionAttention['kind'], string> = {
|
|
permission: 'bg-amber-500 animate-pulse',
|
|
input: 'bg-blue-500',
|
|
background: 'bg-blue-400',
|
|
unread: 'bg-[var(--app-link)]'
|
|
}
|
|
|
|
/**
|
|
* Visible attention dot + hover tooltip explaining the indicator.
|
|
*
|
|
* The tooltip body composes from `summary.pendingRequests` (capped oldest-first;
|
|
* see `PENDING_REQUEST_SUMMARY_CAP` in `@hapi/protocol`) for permission / input
|
|
* attention; from counts and timestamps for background / unread.
|
|
*/
|
|
export function SessionAttentionIndicator(props: {
|
|
attention: SessionAttention
|
|
summary: SessionSummary
|
|
label: string
|
|
tooltipId: string
|
|
}) {
|
|
const { t } = useTranslation()
|
|
const dot = (
|
|
<span
|
|
className={`inline-flex h-2 w-2 shrink-0 rounded-full ${ATTENTION_DOT_CLASS[props.attention.kind]}`}
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<HoverTooltip
|
|
id={props.tooltipId}
|
|
target={dot}
|
|
side="bottom"
|
|
align="start"
|
|
className="shrink-0"
|
|
revealOnParentFocusClass={SESSION_ROW_TOOLTIP_FOCUS_CLASS}
|
|
>
|
|
<AttentionTooltipBody
|
|
attention={props.attention}
|
|
summary={props.summary}
|
|
label={props.label}
|
|
t={t}
|
|
/>
|
|
</HoverTooltip>
|
|
)
|
|
}
|
|
|
|
function AttentionTooltipBody(props: {
|
|
attention: SessionAttention
|
|
summary: SessionSummary
|
|
label: string
|
|
t: (key: string, params?: Record<string, string | number>) => string
|
|
}) {
|
|
const { attention, summary, label, t } = props
|
|
return (
|
|
<span className="block">
|
|
<span className="block font-medium">{label}</span>
|
|
<AttentionTooltipDetail attention={attention} summary={summary} t={t} />
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function AttentionTooltipDetail(props: {
|
|
attention: SessionAttention
|
|
summary: SessionSummary
|
|
t: (key: string, params?: Record<string, string | number>) => string
|
|
}) {
|
|
const { attention, summary, t } = props
|
|
|
|
if (attention.kind === 'permission' || attention.kind === 'input') {
|
|
const wantedKind = attention.kind
|
|
const items = (summary.pendingRequests ?? [])
|
|
.filter((req): req is PendingRequest => req.kind === wantedKind)
|
|
if (items.length === 0) {
|
|
return null
|
|
}
|
|
// Overflow is only knowable per-kind when all pending requests in the
|
|
// session share that kind — otherwise `pendingRequestsCount` mixes the
|
|
// counts of both kinds. Suppress the "+N more" hint in the mixed case
|
|
// rather than report a wrong number.
|
|
const kinds = summary.pendingRequestKinds ?? []
|
|
const onlyThisKind = kinds.length === 1 && kinds[0] === wantedKind
|
|
const overflow = onlyThisKind
|
|
? Math.max(0, (summary.pendingRequestsCount ?? items.length) - items.length)
|
|
: 0
|
|
const bodyKey = wantedKind === 'permission'
|
|
? 'session.tooltip.permission.body'
|
|
: 'session.tooltip.input.body'
|
|
return (
|
|
<span className="block mt-1">
|
|
<span className="block text-[var(--app-hint)]">{t(bodyKey)}</span>
|
|
<ul className="mt-0.5 list-disc pl-4">
|
|
{items.map(req => (
|
|
<li key={req.id} className="font-mono text-[11px] break-all">
|
|
{req.tool}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{overflow > 0 ? (
|
|
<span className="mt-0.5 block text-[var(--app-hint)]">
|
|
{t('session.tooltip.moreCount', { count: overflow })}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
if (attention.kind === 'background') {
|
|
const count = summary.backgroundTaskCount ?? 0
|
|
if (count <= 0) return null
|
|
const key = count === 1
|
|
? 'session.tooltip.background.count.one'
|
|
: 'session.tooltip.background.count.other'
|
|
return (
|
|
<span className="mt-1 block text-[var(--app-hint)]">
|
|
{t(key, { count })}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
// 'unread' deliberately has no body: the relative-time pill is already
|
|
// rendered in the session row, so a tooltip body would just duplicate it.
|
|
return null
|
|
}
|
|
|
|
export function getAttentionLabel(
|
|
attention: SessionAttention,
|
|
t: (key: string) => string
|
|
): string {
|
|
return t(getSessionAttentionLabelKey(attention))
|
|
}
|