mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 } : {})
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user