fix(ui): prevent over-eager slash subcommand completion (#20136)

This commit is contained in:
Keith Guerin
2026-03-24 20:11:09 -07:00
committed by GitHub
parent 73526416cf
commit 46aa3fd193
4 changed files with 169 additions and 157 deletions

View File

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -464,7 +464,7 @@ describe('useSlashCompletion', () => {
() =>
useTestHarnessForSlashCompletion(
true,
'/chat',
'/chat ',
slashCommands,
mockCommandContext,
),
@@ -484,7 +484,7 @@ describe('useSlashCompletion', () => {
() =>
useTestHarnessForSlashCompletion(
true,
'/resume',
'/resume ',
slashCommands,
mockCommandContext,
),
@@ -513,53 +513,6 @@ describe('useSlashCompletion', () => {
unmountResume();
});
it('should show the grouped /resume menu for unique /resum prefix input', async () => {
const slashCommands = [
createTestCommand({
name: 'resume',
description: 'Resume command',
action: vi.fn(),
subCommands: [
createTestCommand({
name: 'list',
description: 'List checkpoints',
suggestionGroup: 'checkpoints',
}),
createTestCommand({
name: 'save',
description: 'Save checkpoint',
suggestionGroup: 'checkpoints',
}),
],
}),
];
const { result, unmount } = await renderHook(() =>
useTestHarnessForSlashCompletion(
true,
'/resum',
slashCommands,
mockCommandContext,
),
);
await resolveMatch();
await waitFor(() => {
expect(result.current.suggestions[0]).toMatchObject({
label: 'list',
sectionTitle: 'auto',
submitValue: '/resume',
});
expect(result.current.isPerfectMatch).toBe(false);
expect(result.current.suggestions.slice(1).map((s) => s.label)).toEqual(
expect.arrayContaining(['list', 'save']),
);
});
unmount();
});
it('should sort exact altName matches to the top', async () => {
const slashCommands = [
createTestCommand({
@@ -594,7 +547,7 @@ describe('useSlashCompletion', () => {
unmount();
});
it('should suggest subcommands when a parent command is fully typed without a trailing space', async () => {
it('should suggest the command itself instead of subcommands when a parent command is fully typed without a trailing space', async () => {
const slashCommands = [
createTestCommand({
name: 'chat',
@@ -618,18 +571,47 @@ describe('useSlashCompletion', () => {
await resolveMatch();
await waitFor(() => {
// Should show the auto-session entry plus subcommands of 'chat'
expect(result.current.suggestions).toHaveLength(3);
expect(result.current.suggestions[0]).toMatchObject({
label: 'list',
sectionTitle: 'auto',
submitValue: '/chat',
});
expect(result.current.suggestions.map((s) => s.label)).toEqual(
expect.arrayContaining(['list', 'save']),
);
// completionStart should be at the end of '/chat' to append subcommands
expect(result.current.completionStart).toBe(5);
// Should show 'chat' as the suggestion, NOT its subcommands
expect(result.current.suggestions).toHaveLength(1);
expect(result.current.suggestions[0].label).toBe('chat');
// completionStart should be at 1 (to replace 'chat')
expect(result.current.completionStart).toBe(1);
});
unmount();
});
it('should NOT suggest subcommands when a parent command is fully typed without a trailing space (fix for over-eager completion)', async () => {
const slashCommands = [
createTestCommand({
name: 'stats',
description: 'Check session stats',
action: vi.fn(), // Has action
subCommands: [
createTestCommand({
name: 'session',
description: 'Show session-specific usage statistics',
}),
],
}),
];
const { result, unmount } = await renderHook(() =>
useTestHarnessForSlashCompletion(
true,
'/stats',
slashCommands,
mockCommandContext,
),
);
await resolveMatch();
await waitFor(() => {
// Should show 'stats' as the suggestion, NOT 'session'
expect(result.current.suggestions).toHaveLength(1);
expect(result.current.suggestions[0].label).toBe('stats');
// isPerfectMatch should be true because it has an action
expect(result.current.isPerfectMatch).toBe(true);
});
unmount();
});