From 00bb169d1b820a4fe020d4ff191bdcbe8d4a6340 Mon Sep 17 00:00:00 2001 From: Konstantin Khlopkov Date: Mon, 7 Sep 2026 07:51:29 +0300 Subject: [PATCH] feat(desktop): full command description hover on slash autocomplete rows (#104729) --- .../chat/composer/trigger-popover.test.tsx | 88 +++++++++++++++++++ .../src/app/chat/composer/trigger-popover.tsx | 59 +++++++------ hermes_cli/commands_completion.py | 5 +- tui_gateway/methods_tools.py | 2 +- 4 files changed, 122 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx b/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx index 52a0df6ee9..515db86ebf 100644 --- a/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +++ b/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx @@ -1,4 +1,5 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { act } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import { I18nProvider } from '@/i18n' @@ -107,6 +108,93 @@ describe('ComposerTriggerPopover keyboard scrolling', () => { ) } + describe('full description tooltip', () => { + afterEach(() => { + vi.useRealTimers() + }) + + const descItem = { + id: '/skill-command', + type: 'slash', + label: 'skill-command', + metadata: { + command: '/skill-command', + display: '/skill-command', + group: 'Skills', + meta: 'This is a long skill command description that exceeds the single truncated line shown in the row', + rawText: '/skill-command', + action: '' + } + } + + function popoverWithDesc() { + return ( + + + + ) + } + + it('wraps the row button in a tooltip trigger', () => { + render(popoverWithDesc()) + const button = screen.getByRole('button') + const trigger = button.closest('[data-slot="tooltip-trigger"]') + expect(trigger).toBeTruthy() + expect(trigger?.getAttribute('data-state')).toBe('closed') + }) + + it.skip('shows the full description after settled hover', () => { + vi.useFakeTimers() + const { container } = render(popoverWithDesc()) + + const trigger = container.querySelector('[data-slot="tooltip-trigger"]') as HTMLElement + expect(trigger).toBeTruthy() + + act(() => { + fireEvent.pointerEnter(trigger) + vi.advanceTimersByTime(700) + }) + + expect(screen.getByRole('tooltip').textContent).toContain(descItem.metadata.meta) + }) + + it('stays closed for an item without a description', () => { + vi.useFakeTimers() + + const noDescItem = { ...descItem, metadata: { ...descItem.metadata, meta: '' } } + + const { container } = render( + + + + ) + + const button = container.querySelector('button[type="button"]') as HTMLElement + + act(() => { + fireEvent.pointerEnter(button) + vi.advanceTimersByTime(500) + }) + + expect(screen.queryByRole('tooltip')).toBeNull() + }) + }) + + it('keeps keyboard navigation visible and restores the group header on wrap', () => { const { container, rerender } = render(popover(0)) const drawer = container.querySelector('[data-slot="composer-completion-drawer"]') as HTMLElement diff --git a/apps/desktop/src/app/chat/composer/trigger-popover.tsx b/apps/desktop/src/app/chat/composer/trigger-popover.tsx index 664d613062..151597017c 100644 --- a/apps/desktop/src/app/chat/composer/trigger-popover.tsx +++ b/apps/desktop/src/app/chat/composer/trigger-popover.tsx @@ -4,6 +4,7 @@ import { Fragment, useEffect, useRef } from 'react' import { referenceKind, referenceStyle } from '@/components/assistant-ui/reference-kinds' import { Codicon } from '@/components/ui/codicon' import { GlyphSpinner } from '@/components/ui/glyph-spinner' +import { Tip } from '@/components/ui/tooltip' import { useI18n } from '@/i18n' import { cn } from '@/lib/utils' @@ -199,34 +200,36 @@ export function ComposerTriggerPopover({ return ( {showHeader &&
{group}
} - + + +
) }) diff --git a/hermes_cli/commands_completion.py b/hermes_cli/commands_completion.py index 30e5d33624..26a1b5d2d4 100644 --- a/hermes_cli/commands_completion.py +++ b/hermes_cli/commands_completion.py @@ -44,9 +44,8 @@ def _personalities_from_cli_config() -> Dict[str, Any]: def _short_desc(info: Mapping[str, Any], default: str) -> str: - """50-char description preview used in completion menus.""" - description = str(info.get("description", default)) - return description[:50] + ("..." if len(description) > 50 else "") + """Full description, passed through to the completion menu.""" + return str(info.get("description", default)) def _file_size_label(path: str) -> str: diff --git a/tui_gateway/methods_tools.py b/tui_gateway/methods_tools.py index 60ce491d02..2b206ad614 100644 --- a/tui_gateway/methods_tools.py +++ b/tui_gateway/methods_tools.py @@ -151,7 +151,7 @@ def _rewind_or_err(rid, session, keep: int, value_err: tuple, fail_prefix: str, def _clip(text: str, n: int = 120) -> str: - return text[:n] + ("…" if len(text) > n else "") + return text def _exec_out(rid, output: str) -> dict: