Skip to content
Closed
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
11 changes: 9 additions & 2 deletions .gemini/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
"gemma": true,
"memoryManager": true,
"topicUpdateNarration": true,
"voiceMode": true
"voiceMode": true,
"enhanceCommand": true
},
"general": {
"devtools": true
"devtools": true,
"plan": {
"enabled": true
}
},
"agents": {
"overrides": {}
}
}
1 change: 1 addition & 0 deletions docs/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ they appear in the UI.
| Auto Memory | `experimental.autoMemory` | Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox. | `false` |
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
| Enhance Command | `experimental.enhanceCommand` | Enable the experimental /enhance slash command. | `false` |

### Skills

Expand Down
5 changes: 5 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Deprecated: Use general.topicUpdateNarration instead.
- **Default:** `false`

- **`experimental.enhanceCommand`** (boolean):
- **Description:** Enable the experimental /enhance slash command.
- **Default:** `false`
- **Requires restart:** Yes

#### `skills`

- **`skills.enabled`** (boolean):
Expand Down
1 change: 1 addition & 0 deletions docs/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ available combinations.
| `input.openExternalEditor` | Open the current prompt or the plan in an external editor. | `Ctrl+G`<br />`Ctrl+Shift+G` |
| `input.deprecatedOpenExternalEditor` | Deprecated command to open external editor. | `Ctrl+X` |
| `input.paste` | Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
| `input.enhancePrompt` | Enhance the current prompt using an LLM. | `Alt+E` |

#### App Controls

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,7 @@ export async function loadCliConfig(
extensionRegistryURI,
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
enableEnhanceCommand: settings.experimental?.enhanceCommand ?? false,
plan: settings.general?.plan?.enabled ?? true,
voiceMode: settings.experimental?.voiceMode,
tracker: settings.experimental?.taskTracker,
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,15 @@ const SETTINGS_SCHEMA = {
description: 'Deprecated: Use general.topicUpdateNarration instead.',
showInDialog: false,
},
enhanceCommand: {
type: 'boolean',
label: 'Enhance Command',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable the experimental /enhance slash command.',
showInDialog: true,
},
},
},
extensions: {
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ describe('BuiltinCommandLoader', () => {
isAdminEnabled: vi.fn().mockReturnValue(true),
}),
isVoiceModeEnabled: vi.fn().mockReturnValue(true),
isEnhanceCommandEnabled: vi.fn().mockReturnValue(true),
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: 'other',
}),
Expand Down Expand Up @@ -310,6 +311,22 @@ describe('BuiltinCommandLoader', () => {
expect(agentsCmd).toBeUndefined();
});

it('should include enhance command when experimental enhance command is enabled', async () => {
(mockConfig.isEnhanceCommandEnabled as Mock).mockReturnValue(true);
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
const enhanceCmd = commands.find((c) => c.name === 'enhance');
expect(enhanceCmd).toBeDefined();
});

it('should exclude enhance command when experimental enhance command is disabled', async () => {
(mockConfig.isEnhanceCommandEnabled as Mock).mockReturnValue(false);
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
const enhanceCmd = commands.find((c) => c.name === 'enhance');
expect(enhanceCmd).toBeUndefined();
});

describe('chat debug command', () => {
it('should NOT add debug subcommand to chat/resume commands if not a nightly build', async () => {
vi.mocked(isNightly).mockResolvedValue(false);
Expand Down Expand Up @@ -398,6 +415,7 @@ describe('BuiltinCommandLoader profile', () => {
isAdminEnabled: vi.fn().mockReturnValue(true),
}),
isVoiceModeEnabled: vi.fn().mockReturnValue(true),
isEnhanceCommandEnabled: vi.fn().mockReturnValue(true),
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: 'other',
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { corgiCommand } from '../ui/commands/corgiCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js';
import { directoryCommand } from '../ui/commands/directoryCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js';
import { enhanceCommand } from '../ui/commands/enhanceCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { footerCommand } from '../ui/commands/footerCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js';
Expand Down Expand Up @@ -135,6 +136,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
docsCommand,
directoryCommand,
editorCommand,
...(this.config?.isEnhanceCommandEnabled() ? [enhanceCommand] : []),
...(this.config?.getExtensionsEnabled() === false
? [
{
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/test-utils/mockCommandContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const createMockCommandContext = (
pendingItem: null,
setPendingItem: vi.fn(),
loadHistory: vi.fn(),
setInput: vi.fn(),
toggleCorgiMode: vi.fn(),
toggleShortcutsHelp: vi.fn(),
toggleVimEnabled: vi.fn(),
Expand Down
201 changes: 201 additions & 0 deletions packages/cli/src/ui/commands/enhanceCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import { enhanceCommand } from './enhanceCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import { LlmRole } from '@google/gemini-cli-core';

describe('enhanceCommand', () => {
let mockContext: CommandContext;
let mockGenerateContent: Mock;

beforeEach(() => {
mockGenerateContent = vi.fn();
mockContext = createMockCommandContext({
services: {
agentContext: {
promptId: 'test-prompt-id',
geminiClient: {
getHistory: vi.fn().mockReturnValue([
{ role: 'user', parts: [{ text: 'previous user msg' }] },
{ role: 'model', parts: [{ text: 'previous model msg' }] },
]),
},
config: {
getModel: vi.fn().mockReturnValue('test-model'),
getContentGenerator: vi.fn().mockReturnValue({
generateContent: mockGenerateContent,
}),
getGemini31LaunchedSync: vi.fn().mockReturnValue(true),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
},
},
},
ui: {
addItem: vi.fn(),
setDebugMessage: vi.fn(),
},
} as unknown as CommandContext);
});

it('should have the correct name and description', () => {
expect(enhanceCommand.name).toBe('enhance');
expect(enhanceCommand.description).toBe(
'Enhance a prompt with additional context and rephrasing',
);
});

it('should show error if no prompt is provided', async () => {
if (!enhanceCommand.action) throw new Error('Action must be defined');

await enhanceCommand.action(mockContext, '');

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: expect.stringContaining('Please provide a prompt'),
}),
);
});

it('should call generateContent with correct parameters and show enhanced prompt', async () => {
if (!enhanceCommand.action) throw new Error('Action must be defined');

mockGenerateContent.mockResolvedValue({
candidates: [
{
content: {
parts: [{ text: 'Enhanced: do something' }],
},
},
],
});

await enhanceCommand.action(mockContext, 'do something');

expect(mockGenerateContent).toHaveBeenCalledWith(
expect.objectContaining({
model: 'test-model',
contents: [
{ role: 'user', parts: [{ text: 'previous user msg' }] },
{ role: 'model', parts: [{ text: 'previous model msg' }] },
{ role: 'user', parts: [{ text: 'do something' }] },
],
config: {
systemInstruction: {
role: 'system',
parts: [
{
text: expect.stringContaining(
"Generate an enhanced version of the user's prompt",
),
},
],
},
},
}),
'test-prompt-id',
LlmRole.UTILITY_TOOL,
);

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining(
'Enhanced prompt:\n\nEnhanced: do something',
),
}),
);
expect(mockContext.ui.setInput).toHaveBeenCalledWith(
'Enhanced: do something',
);
});

it('should clean the response from markdown and quotes', async () => {
if (!enhanceCommand.action) throw new Error('Action must be defined');

mockGenerateContent.mockResolvedValue({
candidates: [
{
content: {
parts: [{ text: '```markdown\n"Clean me"\n```' }],
},
},
],
});

await enhanceCommand.action(mockContext, 'dirty prompt');

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('Enhanced prompt:\n\nClean me'),
}),
);
expect(mockContext.ui.setInput).toHaveBeenCalledWith('Clean me');
});

it('should handle API errors gracefully', async () => {
if (!enhanceCommand.action) throw new Error('Action must be defined');

mockGenerateContent.mockRejectedValue(new Error('API Error'));

await enhanceCommand.action(mockContext, 'test prompt');

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: expect.stringContaining('Failed to enhance prompt: API Error'),
}),
);
});

it('should handle empty response from model', async () => {
if (!enhanceCommand.action) throw new Error('Action must be defined');

mockGenerateContent.mockResolvedValue({
candidates: [],
});

await enhanceCommand.action(mockContext, 'test prompt');

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: expect.stringContaining('Empty response from model'),
}),
);
});
it('should ignore thought parts and sanitize the output', async () => {
if (!enhanceCommand.action) throw new Error('Action must be defined');

mockGenerateContent.mockResolvedValue({
candidates: [
{
content: {
parts: [
{ thought: true, text: 'This is a thought.' },
{ text: 'Sanitized\nPrompt]' },
],
},
},
],
});

await enhanceCommand.action(mockContext, 'dirty prompt');

expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('Enhanced prompt:\n\nSanitizedPrompt'),
}),
);
expect(mockContext.ui.setInput).toHaveBeenCalledWith('SanitizedPrompt');
});
});
Loading
Loading