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