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}
} - +