Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
type GeminiClient,
type UserFeedbackPayload,
SlashCommandStatus,
MCPDiscoveryState,
makeFakeConfig,
coreEvents,
CoreEvent,
Expand Down Expand Up @@ -389,21 +390,49 @@ describe('useSlashCommandProcessor', () => {
});

describe('Command Execution Logic', () => {
it('should display an error for an unknown command', async () => {
it('should treat unknown commands as regular input', async () => {
const result = await setupProcessorHook();
await waitFor(() => expect(result.current.slashCommands).toBeDefined());

let handled: Awaited<
ReturnType<typeof result.current.handleSlashCommand>
>;
await act(async () => {
await result.current.handleSlashCommand('/nonexistent');
handled = await result.current.handleSlashCommand('/nonexistent');
});

// Expect 2 calls: one for the user's input, one for the error message.
expect(mockAddItem).toHaveBeenCalledTimes(2);
expect(mockAddItem).toHaveBeenLastCalledWith(
{
// Unknown commands should return false so the input is sent to the model
expect(handled!).toBe(false);
// Should not add anything to history (the regular flow will handle it)
expect(mockAddItem).not.toHaveBeenCalled();
});

it('should show MCP loading warning for unknown commands when MCP is loading', async () => {
vi.spyOn(mockConfig, 'getMcpClientManager').mockReturnValue({
getDiscoveryState: () => MCPDiscoveryState.IN_PROGRESS,
} as ReturnType<typeof mockConfig.getMcpClientManager>);

const result = await setupProcessorHook();
await waitFor(() => expect(result.current.slashCommands).toBeDefined());

let handled: Awaited<
ReturnType<typeof result.current.handleSlashCommand>
>;
await act(async () => {
handled = await result.current.handleSlashCommand('/mcp-command');
});

// When MCP is loading, should handle the command (show warning)
expect(handled!).not.toBe(false);
// Should add user input and error message to history
expect(mockAddItem).toHaveBeenCalledWith(
{ type: MessageType.USER, text: '/mcp-command' },
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Unknown command: /nonexistent',
},
}),
expect.any(Number),
);
});
Expand Down Expand Up @@ -769,19 +798,17 @@ describe('useSlashCommandProcessor', () => {
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(1));

let handled: Awaited<
ReturnType<typeof result.current.handleSlashCommand>
>;
await act(async () => {
// Use uppercase when command is lowercase
await result.current.handleSlashCommand('/Test');
handled = await result.current.handleSlashCommand('/Test');
});

// It should fail and call addItem with an error
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Unknown command: /Test',
},
expect.any(Number),
);
// Case mismatch means it's not a known command, so treat as regular input
expect(handled!).toBe(false);
expect(mockAddItem).not.toHaveBeenCalled();
});

it('should correctly match an altName', async () => {
Expand Down
48 changes: 30 additions & 18 deletions packages/cli/src/ui/hooks/slashCommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,36 @@ export const useSlashCommandProcessor = (
return false;
}

const {
commandToExecute,
args,
canonicalPath: resolvedCommandPath,
} = parseSlashCommand(trimmed, commands);

// If the input doesn't match any known command, check if MCP servers
// are still loading (the command might come from an MCP server).
// Otherwise, treat it as regular text input (e.g. file paths like
// /home/user/file.txt) and let it be sent to the model.
if (!commandToExecute) {
const isMcpLoading =
config?.getMcpClientManager()?.getDiscoveryState() ===
MCPDiscoveryState.IN_PROGRESS;
if (isMcpLoading) {
setIsProcessing(true);
if (addToHistory) {
addItem({ type: MessageType.USER, text: trimmed }, Date.now());
}
addMessage({
type: MessageType.ERROR,
content: `Unknown command: ${trimmed}. Command might have been from an MCP server but MCP servers are not done loading.`,
timestamp: new Date(),
});
setIsProcessing(false);
return { type: 'handled' };
}
return false;
}

setIsProcessing(true);

if (addToHistory) {
Expand All @@ -373,11 +403,6 @@ export const useSlashCommandProcessor = (
}

let hasError = false;
const {
commandToExecute,
args,
canonicalPath: resolvedCommandPath,
} = parseSlashCommand(trimmed, commands);

const subcommand =
resolvedCommandPath.length > 1
Expand Down Expand Up @@ -654,19 +679,6 @@ export const useSlashCommandProcessor = (
}
}

const isMcpLoading =
config?.getMcpClientManager()?.getDiscoveryState() ===
MCPDiscoveryState.IN_PROGRESS;
const errorMessage = isMcpLoading
? `Unknown command: ${trimmed}. Command might have been from an MCP server but MCP servers are not done loading.`
: `Unknown command: ${trimmed}`;

addMessage({
type: MessageType.ERROR,
content: errorMessage,
timestamp: new Date(),
});

return { type: 'handled' };
} catch (e: unknown) {
hasError = true;
Expand Down
Loading