mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): allow multiline tooltips
This commit is contained in:
parent
6cbd3a6ac3
commit
f2aeea95e0
4 changed files with 82 additions and 28 deletions
|
|
@ -8,6 +8,7 @@ import { ContextUsagePanel } from '@/app/shell/context-usage-panel'
|
|||
import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
|
||||
import { TooltipDetails } from '@/components/ui/tooltip'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { Activity, AlertCircle, Clock, Command, FolderOpen, Globe, Hash, Loader2, Terminal } from '@/lib/icons'
|
||||
import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
|
||||
|
|
@ -233,15 +234,27 @@ export function useStatusbarItems({
|
|||
? `${base} · ${updateApply.stage === 'restart' ? copy.restart : copy.update}`
|
||||
: `${base}${behindHint}`
|
||||
|
||||
const tooltip = [
|
||||
const tooltipPrimary = appVersion ? copy.desktopVersion(appVersion) : (sha ? copy.commit(sha) : null)
|
||||
const tooltipDetails = [
|
||||
applying ? updateApply.message || copy.updateInProgress : null,
|
||||
!applying && behind > 0 && copy.commitsBehind(behind, branch ?? '...'),
|
||||
appVersion && copy.desktopVersion(appVersion),
|
||||
sha && copy.commit(sha),
|
||||
appVersion && sha && copy.commit(sha),
|
||||
branch && copy.branch(branch)
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.filter((detail): detail is string => Boolean(detail))
|
||||
|
||||
const tooltip = tooltipPrimary ? (
|
||||
<>
|
||||
<span>{tooltipPrimary}</span>
|
||||
{tooltipDetails.length > 0 && (
|
||||
<TooltipDetails className="flex flex-col gap-0.5">
|
||||
{tooltipDetails.map(detail => (
|
||||
<span key={detail}>{detail}</span>
|
||||
))}
|
||||
</TooltipDetails>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
|
||||
return {
|
||||
className: !applying && behind > 0 ? 'text-primary hover:text-primary' : undefined,
|
||||
|
|
@ -253,7 +266,7 @@ export function useStatusbarItems({
|
|||
// their client is behind. Listed in the menu, but locked on.
|
||||
lockedVisible: true,
|
||||
onSelect: () => openUpdateOverlayFor('client'),
|
||||
title: tooltip || undefined,
|
||||
title: tooltip,
|
||||
toggleLabel: copy.toggleVersion,
|
||||
variant: 'action'
|
||||
}
|
||||
|
|
@ -290,14 +303,25 @@ export function useStatusbarItems({
|
|||
? `${base} · ${backendUpdateApply.stage === 'restart' ? copy.restart : copy.update}`
|
||||
: `${base}${behindHint}`
|
||||
|
||||
const tooltip = [
|
||||
const tooltipDetails = [
|
||||
applying ? backendUpdateApply.message || copy.updateInProgress : null,
|
||||
!applying && behind > 0 && copy.commitsBehind(behind, 'main'),
|
||||
!applying && behind <= 0 && updateAvailable && copy.update,
|
||||
backendVersion && copy.backendVersion(backendVersion)
|
||||
!applying && behind <= 0 && updateAvailable && copy.update
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
.filter((detail): detail is string => Boolean(detail))
|
||||
|
||||
const tooltip = backendVersion ? (
|
||||
<>
|
||||
<span>{copy.backendVersion(backendVersion)}</span>
|
||||
{tooltipDetails.length > 0 && (
|
||||
<TooltipDetails className="flex flex-col gap-0.5">
|
||||
{tooltipDetails.map(detail => (
|
||||
<span key={detail}>{detail}</span>
|
||||
))}
|
||||
</TooltipDetails>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
|
||||
return {
|
||||
className: !applying && updateAvailable ? 'text-primary hover:text-primary' : undefined,
|
||||
|
|
@ -307,7 +331,7 @@ export function useStatusbarItems({
|
|||
label,
|
||||
lockedVisible: true,
|
||||
onSelect: () => openUpdateOverlayFor('backend'),
|
||||
title: tooltip || undefined,
|
||||
title: tooltip,
|
||||
toggleLabel: copy.toggleBackendVersion,
|
||||
variant: 'action'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export interface StatusbarItem {
|
|||
onSelect?: (modifiers: StatusbarSelectModifiers) => void
|
||||
/** Keybind action id — when set, the tooltip shows the label + keybind hint. */
|
||||
actionId?: string
|
||||
title?: string
|
||||
title?: ReactNode
|
||||
to?: string
|
||||
variant?: 'action' | 'link' | 'menu' | 'text'
|
||||
/** Plain-text name for the bar's right-click show/hide menu. An item without
|
||||
|
|
@ -212,7 +212,11 @@ const StatusbarItemView = memo(function StatusbarItemView({
|
|||
return <>{item.render()}</>
|
||||
}
|
||||
|
||||
const tooltipLabel = item.actionId ? <TipKeybindLabel actionId={item.actionId} text={item.title} /> : item.title
|
||||
const tooltipLabel = item.actionId ? (
|
||||
<TipKeybindLabel actionId={item.actionId} text={typeof item.title === 'string' ? item.title : undefined} />
|
||||
) : (
|
||||
item.title
|
||||
)
|
||||
|
||||
const content = (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { MemoryRouter } from 'react-router-dom'
|
|||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { StatusbarControls, type StatusbarItem } from '@/app/shell/statusbar-controls'
|
||||
import { TooltipDetails } from '@/components/ui/tooltip'
|
||||
import {
|
||||
$statusbarHiddenIds,
|
||||
$statusbarVisible,
|
||||
|
|
@ -55,6 +56,32 @@ function openContextMenu(target: HTMLElement) {
|
|||
}
|
||||
|
||||
describe('statusbar item visibility', () => {
|
||||
it('groups version-tooltip details below its primary label in one panel', async () => {
|
||||
bar([
|
||||
item('version-client', 'v1.2.3', {
|
||||
title: (
|
||||
<>
|
||||
<span>Hermes Desktop v1.2.3</span>
|
||||
<TooltipDetails className="flex flex-col gap-0.5">
|
||||
<span>Commit abc1234</span>
|
||||
<span>Branch ethie/hermes-version</span>
|
||||
</TooltipDetails>
|
||||
</>
|
||||
)
|
||||
})
|
||||
])
|
||||
|
||||
fireEvent.pointerMove(screen.getByRole('button', { name: 'v1.2.3' }), { pointerType: 'mouse' })
|
||||
|
||||
const tooltip = await screen.findByRole('tooltip')
|
||||
const label = tooltip.firstElementChild
|
||||
|
||||
expect(tooltip.textContent).toBe('Hermes Desktop v1.2.3Commit abc1234Branch ethie/hermes-version')
|
||||
expect(label?.classList.contains('bg-foreground')).toBe(true)
|
||||
expect(label?.classList.contains('flex-col')).toBe(true)
|
||||
expect(screen.getByText('Commit abc1234').parentElement?.classList.contains('text-background/65')).toBe(true)
|
||||
})
|
||||
|
||||
it('hides the route/toggle items out of the box and keeps status items', () => {
|
||||
bar([
|
||||
item('cron', 'Cron'),
|
||||
|
|
|
|||
|
|
@ -96,11 +96,9 @@ function TooltipContent({
|
|||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
// Transparent, width-capped wrapper. The visible chip is the inner inline
|
||||
// span so `box-decoration-break: clone` gives a marker-style background
|
||||
// that hugs EACH wrapped line (bg only on the text, ragged right — no
|
||||
// rectangular dead space). No fade transition — once the hover delay
|
||||
// elapses the chip appears at once.
|
||||
// Transparent, width-capped wrapper. The visible panel is the inner span
|
||||
// so all lines share one stable background. No fade transition — once
|
||||
// the hover delay elapses the panel appears at once.
|
||||
// pointer-events-none: the tip must never steal hover/clicks from the
|
||||
// chrome underneath (titlebar tools, adjacent tabs, etc.).
|
||||
className={cn('pointer-events-none z-(--z-over-modal) w-fit max-w-64 select-none', className)}
|
||||
|
|
@ -108,14 +106,9 @@ function TooltipContent({
|
|||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
>
|
||||
{/* bg-foreground/text-background auto-inverts per theme. leading-normal
|
||||
keeps lines readable; py-1 makes the cloned line-boxes overlap just
|
||||
enough to read as one continuous fill (no gaps between lines). */}
|
||||
{/* [&>*]:!inline-flex: a block-level label child (e.g. `flex`) collapses
|
||||
this inline decoration's geometry, so Radix measures a zero-size chip
|
||||
and parks an empty rectangle in the corner (#62022). Force any direct
|
||||
child inline-flex so every call site stays safe. */}
|
||||
<span className="box-decoration-clone inline bg-foreground px-1.5 py-1 text-[11px] font-bold leading-normal text-background [font-family:Arial,sans-serif] [&>*]:!inline-flex">
|
||||
{/* bg-foreground/text-background auto-inverts per theme. A column keeps
|
||||
primary text and optional detail rows aligned inside one panel. */}
|
||||
<span className="inline-flex max-w-full flex-col items-start gap-1 bg-foreground px-1.5 py-1 text-[11px] font-bold leading-normal text-background [font-family:Arial,sans-serif]">
|
||||
{children}
|
||||
</span>
|
||||
</TooltipPrimitive.Content>
|
||||
|
|
@ -198,11 +191,16 @@ function TipHintLabel({ text, hint }: TipHintLabelProps) {
|
|||
return (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span>{text}</span>
|
||||
<span className="opacity-55">{hint}</span>
|
||||
<TooltipDetails>{hint}</TooltipDetails>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** Muted secondary content inside a tooltip. The caller owns its layout. */
|
||||
function TooltipDetails({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return <span className={cn('text-background/65 font-normal', className)} {...props} />
|
||||
}
|
||||
|
||||
interface TipKeybindLabelProps {
|
||||
/** Keybind action id — pulls the label from i18n AND the combo from the store. */
|
||||
actionId: string
|
||||
|
|
@ -229,6 +227,7 @@ export {
|
|||
TipKeybindLabel,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipDetails,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue