feat(native-chat): show the reported argument hint in the slash picker

`argumentHint` was carried to the renderer but nothing read it. Show it
beside the command token — `/goal <objective>` over the description — so a
row says how the command is invoked, not just what it does.

It sits at the row's existing 11px muted tier, subordinate to the
description, and truncates in a min-width-0 flex row; the picker also caps
the hint at 80 characters, so a provider cannot swamp the row.
This commit is contained in:
Merge Sim
2026-09-10 14:13:35 -07:00
parent ea7c2bf443
commit 5c8dbd0bea
5 changed files with 67 additions and 3 deletions
@@ -3,6 +3,8 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { NativeChatPickerMenu } from './NativeChatAutocompleteMenus'
import { buildNativeChatPickerItems } from './native-chat-picker-items'
import { sessionSlashCommandSuggestions } from '../../../../shared/native-chat-slash-commands'
import type { ComposerAutocomplete } from './native-chat-composer-state'
function autocomplete(
@@ -128,6 +130,45 @@ describe('NativeChatPickerMenu', () => {
expect(screen.getAllByText('No matching commands')).toHaveLength(2)
})
it('shows the argument hint the provider reported beside the command token', () => {
render(
<NativeChatPickerMenu
autocomplete={autocomplete({
items: buildNativeChatPickerItems(
sessionSlashCommandSuggestions('claude', [
{
name: 'goal',
kind: 'command',
description: 'Set a goal and keep working until it is met',
argumentHint: '<objective>'
},
{ name: 'clear', kind: 'command' },
{ name: 'wordy', kind: 'command', argumentHint: `<${'a'.repeat(200)}>` }
]),
[],
'',
'/'
)
})}
activeIndex={0}
listboxId="picker"
onChoose={vi.fn()}
onRetry={vi.fn()}
/>
)
const goal = screen.getByRole('option', { name: /goal/i })
expect(goal.textContent).toContain('<objective>')
expect(goal.textContent).toContain('Set a goal and keep working until it is met')
// A command the report left hintless renders its row unchanged.
expect(screen.getByRole('option', { name: /clear/i }).textContent).toBe(
'/clearClear conversation history'
)
// A hint long enough to swamp the row is capped before it reaches the DOM.
expect(screen.getByRole('option', { name: /wordy/i }).textContent).toBe(
`/wordy<${'a'.repeat(79)}`
)
})
it('announces a successful empty skill result distinctly from loading', () => {
render(
<NativeChatPickerMenu
@@ -209,7 +209,14 @@ function PickerOption({
<Package className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
) : null}
<span className="min-w-0 flex-1">
<span className="block truncate font-mono font-medium">{item.token}</span>
<span className="flex min-w-0 items-baseline gap-1.5">
<span className="min-w-0 truncate font-mono font-medium">{item.token}</span>
{item.kind === 'command' && item.argumentHint ? (
<span className="min-w-0 truncate font-mono text-[11px] text-muted-foreground">
{item.argumentHint}
</span>
) : null}
</span>
{item.description ? (
<span className="block truncate text-xs text-muted-foreground">{item.description}</span>
) : null}
@@ -21,6 +21,8 @@ export type NativeChatPickerItem =
/** Exactly what a pick inserts — the form the agent invokes. */
token: string
description?: string
/** How the provider says the command is invoked, e.g. `<objective>`. */
argumentHint?: string
skillCollision: boolean
}
| {
@@ -80,6 +82,9 @@ export function buildNativeChatPickerItems(
name: command.name,
token: `/${command.name}`,
description: command.description ? sanitizePickerText(command.description, 240) : undefined,
argumentHint: command.argumentHint
? sanitizePickerText(command.argumentHint, 80)
: undefined,
skillCollision: sharedSigil && skillNames.has(command.name)
},
stableOrder: index
+10 -2
View File
@@ -104,16 +104,24 @@ describe('a session that reports its own command surface', () => {
])
})
it('keeps a reported description on a command the curated catalog never claims', () => {
it('keeps a reported description and argument hint the curated catalog never claims', () => {
expect(
sessionSlashCommandSuggestions('codex', [
{
name: 'opsx:apply',
kind: 'command',
description: 'Apply the plan',
argumentHint: '<plan-id>',
kindUnspecified: true
}
])
).toEqual([{ name: 'opsx:apply', description: 'Apply the plan', kindUnspecified: true }])
).toEqual([
{
name: 'opsx:apply',
description: 'Apply the plan',
argumentHint: '<plan-id>',
kindUnspecified: true
}
])
})
})
+3
View File
@@ -12,6 +12,8 @@ export type SlashCommandSuggestion = {
name: string
/** Optional one-line description for the suggestion row. */
description?: string
/** Provider-authored argument sketch, e.g. `<objective>`. */
argumentHint?: string
kindUnspecified?: true
}
@@ -111,6 +113,7 @@ export function sessionSlashCommandSuggestions(
return {
name: entry.name,
...(description ? { description } : {}),
...(entry.argumentHint ? { argumentHint: entry.argumentHint } : {}),
...(entry.kindUnspecified ? { kindUnspecified: true as const } : {})
}
})