From 5c8dbd0bea3ea98ea628d8432e75c13f7499d82b Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Thu, 10 Sep 2026 14:13:35 -0700 Subject: [PATCH] feat(native-chat): show the reported argument hint in the slash picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `argumentHint` was carried to the renderer but nothing read it. Show it beside the command token — `/goal ` 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. --- .../NativeChatAutocompleteMenus.test.tsx | 41 +++++++++++++++++++ .../NativeChatAutocompleteMenus.tsx | 9 +++- .../native-chat/native-chat-picker-items.ts | 5 +++ src/shared/native-chat-slash-commands.test.ts | 12 +++++- src/shared/native-chat-slash-commands.ts | 3 ++ 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx index 88506c51838..60c70baee66 100644 --- a/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx @@ -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( + ' + }, + { 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('') + 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( ) : null} - {item.token} + + {item.token} + {item.kind === 'command' && item.argumentHint ? ( + + {item.argumentHint} + + ) : null} + {item.description ? ( {item.description} ) : null} diff --git a/src/renderer/src/components/native-chat/native-chat-picker-items.ts b/src/renderer/src/components/native-chat/native-chat-picker-items.ts index 1a21a2461c6..4786fcbecb2 100644 --- a/src/renderer/src/components/native-chat/native-chat-picker-items.ts +++ b/src/renderer/src/components/native-chat/native-chat-picker-items.ts @@ -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. ``. */ + 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 diff --git a/src/shared/native-chat-slash-commands.test.ts b/src/shared/native-chat-slash-commands.test.ts index 5294b27a467..33219d8f7db 100644 --- a/src/shared/native-chat-slash-commands.test.ts +++ b/src/shared/native-chat-slash-commands.test.ts @@ -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: '', kindUnspecified: true } ]) - ).toEqual([{ name: 'opsx:apply', description: 'Apply the plan', kindUnspecified: true }]) + ).toEqual([ + { + name: 'opsx:apply', + description: 'Apply the plan', + argumentHint: '', + kindUnspecified: true + } + ]) }) }) diff --git a/src/shared/native-chat-slash-commands.ts b/src/shared/native-chat-slash-commands.ts index 95dbaba5ed5..6360ca05f83 100644 --- a/src/shared/native-chat-slash-commands.ts +++ b/src/shared/native-chat-slash-commands.ts @@ -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. ``. */ + 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 } : {}) } })