From f83ff89780efd081410e061365daf32986ba8aa4 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:52:13 -0700 Subject: [PATCH] fix(desktop): reveal complete slash help across the window Preserve producer descriptions without identity wrappers and size the existing themed tooltip to the viewport. Replace skipped and structural tests with two behavioral invariants. Native Electron before/after hover, click and keyboard verification passed; campaign suite validation remains queued. --- apps/desktop/DESIGN.md | 2 + .../trigger-popover-description.test.tsx | 24 +++++ .../chat/composer/trigger-popover.test.tsx | 90 +------------------ .../src/app/chat/composer/trigger-popover.tsx | 8 +- hermes_cli/commands_completion.py | 13 +-- .../test_command_description_fidelity.py | 25 ++++++ tui_gateway/methods_tools.py | 10 +-- 7 files changed, 66 insertions(+), 106 deletions(-) create mode 100644 apps/desktop/src/app/chat/composer/trigger-popover-description.test.tsx create mode 100644 tests/tui_gateway/test_command_description_fidelity.py diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index cfa8ed889c..de6846536c 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -146,6 +146,8 @@ trigger within 300ms opens instantly. The cooldown starts on close, so a hover a second later waits again. Close is immediate. `OverflowTip` stays on its own longer delay (list titles must not trail while scanning). +**Slash descriptions.** Keep autocomplete rows single-line and ellipsized, but reveal the complete catalog description in the shared themed tooltip when hovering anywhere on a slash row. Size that tooltip to the window with collision padding and word wrapping; it must not intercept row selection. Catalog and completion producers preserve the full author-supplied description. + **Keybind hints in tooltips.** On a tipped button bound to a rebindable hotkey, use `` — it reads the i18n label and the current combo from `$bindings`. Pass `text={...}` only when the label is diff --git a/apps/desktop/src/app/chat/composer/trigger-popover-description.test.tsx b/apps/desktop/src/app/chat/composer/trigger-popover-description.test.tsx new file mode 100644 index 0000000000..eccb7e75fa --- /dev/null +++ b/apps/desktop/src/app/chat/composer/trigger-popover-description.test.tsx @@ -0,0 +1,24 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, expect, it, vi } from 'vitest' + +import { I18nProvider } from '@/i18n' + +import { ComposerTriggerPopover } from './trigger-popover' + +afterEach(cleanup) + +it('reveals the complete slash description on hover without intercepting selection', async () => { + const description = 'Complete command help '.repeat(20) + const item = { id: '/proof', type: 'slash', label: 'proof', metadata: { display: '/proof', meta: description } } + const onPick = vi.fn() + render( + + + + ) + const row = screen.getByRole('button') + fireEvent.pointerMove(row, { pointerType: 'mouse' }) + expect((await screen.findByRole('tooltip')).textContent).toBe(description) + fireEvent.click(row) + expect(onPick).toHaveBeenCalledWith(item) +}) 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 515db86ebf..5b4a294313 100644 --- a/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +++ b/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx @@ -1,5 +1,4 @@ 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' @@ -108,93 +107,6 @@ 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 @@ -287,4 +199,4 @@ describe('ComposerTriggerPopover keyboard scrolling', () => { expect(drawer.scrollTop).toBe(30) }) -}) +}) \ No newline at end of file diff --git a/apps/desktop/src/app/chat/composer/trigger-popover.tsx b/apps/desktop/src/app/chat/composer/trigger-popover.tsx index 151597017c..ddc29d86f9 100644 --- a/apps/desktop/src/app/chat/composer/trigger-popover.tsx +++ b/apps/desktop/src/app/chat/composer/trigger-popover.tsx @@ -200,7 +200,13 @@ export function ComposerTriggerPopover({ return ( {showHeader &&
{group}
} - +