Allow for slash commands to opt-out of autocompletion and /help discovery. (#7847)

This commit is contained in:
DeWitt Clinton
2025-09-06 14:16:58 -07:00
committed by GitHub
parent c031f538df
commit 6b4c12eb04
8 changed files with 103 additions and 59 deletions

View File

@@ -349,36 +349,4 @@ describe('CommandService', () => {
expect(deployExtension).toBeDefined();
expect(deployExtension?.description).toBe('[gcp] Deploy to Google Cloud');
});
it('should filter out hidden commands', async () => {
const visibleCommand = createMockCommand('visible', CommandKind.BUILT_IN);
const hiddenCommand = {
...createMockCommand('hidden', CommandKind.BUILT_IN),
hidden: true,
};
const initiallyVisibleCommand = createMockCommand(
'initially-visible',
CommandKind.BUILT_IN,
);
const hiddenOverrideCommand = {
...createMockCommand('initially-visible', CommandKind.FILE),
hidden: true,
};
const mockLoader = new MockCommandLoader([
visibleCommand,
hiddenCommand,
initiallyVisibleCommand,
hiddenOverrideCommand,
]);
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
);
const commands = service.getCommands();
expect(commands).toHaveLength(1);
expect(commands[0].name).toBe('visible');
});
});

View File

@@ -85,9 +85,7 @@ export class CommandService {
});
}
const finalCommands = Object.freeze(
Array.from(commandMap.values()).filter((cmd) => !cmd.hidden),
);
const finalCommands = Object.freeze(Array.from(commandMap.values()));
return new CommandService(finalCommands);
}

View File

@@ -9,8 +9,8 @@ import { CommandKind, type SlashCommand } from './types.js';
export const corgiCommand: SlashCommand = {
name: 'corgi',
description: 'Toggles corgi mode.',
kind: CommandKind.BUILT_IN,
hidden: true,
kind: CommandKind.BUILT_IN,
action: (context, _args) => {
context.ui.toggleCorgiMode();
},

View File

@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/** @vitest-environment jsdom */
import { render } from 'ink-testing-library';
import { describe, it, expect } from 'vitest';
import { Help } from './Help.js';
import type { SlashCommand } from '../commands/types.js';
import { CommandKind } from '../commands/types.js';
const mockCommands: readonly SlashCommand[] = [
{
name: 'test',
description: 'A test command',
kind: CommandKind.BUILT_IN,
},
{
name: 'hidden',
description: 'A hidden command',
hidden: true,
kind: CommandKind.BUILT_IN,
},
{
name: 'parent',
description: 'A parent command',
kind: CommandKind.BUILT_IN,
subCommands: [
{
name: 'visible-child',
description: 'A visible child command',
kind: CommandKind.BUILT_IN,
},
{
name: 'hidden-child',
description: 'A hidden child command',
hidden: true,
kind: CommandKind.BUILT_IN,
},
],
},
];
describe('Help Component', () => {
it('should not render hidden commands', () => {
const { lastFrame } = render(<Help commands={mockCommands} />);
const output = lastFrame();
expect(output).toContain('/test');
expect(output).not.toContain('/hidden');
});
it('should not render hidden subcommands', () => {
const { lastFrame } = render(<Help commands={mockCommands} />);
const output = lastFrame();
expect(output).toContain('visible-child');
expect(output).not.toContain('hidden-child');
});
});

View File

@@ -65,7 +65,7 @@ export const Help: React.FC<Help> = ({ commands }) => (
Commands:
</Text>
{commands
.filter((command) => command.description)
.filter((command) => command.description && !command.hidden)
.map((command: SlashCommand) => (
<Box key={command.name} flexDirection="column">
<Text color={Colors.Foreground}>
@@ -79,15 +79,17 @@ export const Help: React.FC<Help> = ({ commands }) => (
{command.description && ' - ' + command.description}
</Text>
{command.subCommands &&
command.subCommands.map((subCommand) => (
<Text key={subCommand.name} color={Colors.Foreground}>
<Text bold color={Colors.AccentPurple}>
{' '}
{subCommand.name}
command.subCommands
.filter((subCommand) => !subCommand.hidden)
.map((subCommand) => (
<Text key={subCommand.name} color={Colors.Foreground}>
<Text bold color={Colors.AccentPurple}>
{' '}
{subCommand.name}
</Text>
{subCommand.description && ' - ' + subCommand.description}
</Text>
{subCommand.description && ' - ' + subCommand.description}
</Text>
))}
))}
</Box>
))}
<Text color={Colors.Foreground}>

View File

@@ -223,18 +223,6 @@ describe('useSlashCommandProcessor', () => {
expect(fileAction).toHaveBeenCalledTimes(1);
expect(builtinAction).not.toHaveBeenCalled();
});
it('should not include hidden commands in the command list', async () => {
const visibleCommand = createTestCommand({ name: 'visible' });
const hiddenCommand = createTestCommand({ name: 'hidden', hidden: true });
const result = setupProcessorHook([visibleCommand, hiddenCommand]);
await waitFor(() => {
expect(result.current.slashCommands).toHaveLength(1);
});
expect(result.current.slashCommands[0].name).toBe('visible');
});
});
describe('Command Execution Logic', () => {

View File

@@ -347,6 +347,31 @@ describe('useSlashCompletion', () => {
expect(result.current.suggestions).toHaveLength(0);
});
it('should not suggest hidden commands', async () => {
const slashCommands = [
createTestCommand({
name: 'visible',
description: 'A visible command',
}),
createTestCommand({
name: 'hidden',
description: 'A hidden command',
hidden: true,
}),
];
const { result } = renderHook(() =>
useTestHarnessForSlashCompletion(
true,
'/',
slashCommands,
mockCommandContext,
),
);
expect(result.current.suggestions.length).toBe(1);
expect(result.current.suggestions[0].label).toBe('visible');
});
});
describe('Sub-Commands', () => {

View File

@@ -225,7 +225,7 @@ function useCommandSuggestions(
if (partial === '') {
// If no partial query, show all available commands
potentialSuggestions = commandsToSearch.filter(
(cmd) => cmd.description,
(cmd) => cmd.description && !cmd.hidden,
);
} else {
// Use fuzzy search for non-empty partial queries with fallback
@@ -400,7 +400,7 @@ export function useSlashCompletion(props: UseSlashCompletionProps): {
const commandMap = new Map<string, SlashCommand>();
commands.forEach((cmd) => {
if (cmd.description) {
if (cmd.description && !cmd.hidden) {
commandItems.push(cmd.name);
commandMap.set(cmd.name, cmd);