Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
83 changes: 83 additions & 0 deletions packages/cli/src/services/FileCommandLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1402,4 +1402,87 @@ describe('FileCommandLoader', () => {
expect(commands[0].description).toBe('d'.repeat(97) + '...');
});
});

describe('CommandSource Assignment', () => {
it('assigns CommandSource.USER to user commands', async () => {
const userCommandsDir = Storage.getUserCommandsDir();
mock({
[userCommandsDir]: {
'user-cmd.toml': 'prompt = "User prompt"',
},
});

const loader = new FileCommandLoader(null);
const commands = await loader.loadCommands(signal);

expect(commands).toHaveLength(1);
expect(commands[0].source).toBe('user');
});

it('assigns CommandSource.PROJECT to project commands', async () => {
const projectCommandsDir = new Storage(
process.cwd(),
).getProjectCommandsDir();
mock({
[projectCommandsDir]: {
'project-cmd.toml': 'prompt = "Project prompt"',
},
});

const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => false),
isTrustedFolder: vi.fn(() => false),
} as unknown as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);

// 0 is user (empty), 1 is project
const projectCmd = commands.find((c) => c.name === 'project-cmd');
expect(projectCmd).toBeDefined();
expect(projectCmd?.source).toBe('project');
});

it('assigns CommandSource.EXTENSION to extension commands', async () => {
const extensionDir = path.join(
process.cwd(),
GEMINI_DIR,
'extensions',
'test-ext',
);

mock({
[extensionDir]: {
'gemini-extension.json': JSON.stringify({
name: 'test-ext',
version: '1.0.0',
}),
commands: {
'ext-cmd.toml': 'prompt = "Extension prompt"',
},
},
});

const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'test-ext',
version: '1.0.0',
isActive: true,
path: extensionDir,
},
]),
getFolderTrust: vi.fn(() => false),
isTrustedFolder: vi.fn(() => false),
} as unknown as Config;
const loader = new FileCommandLoader(mockConfig);
const commands = await loader.loadCommands(signal);

const extCmd = commands.find((c) => c.name === 'ext-cmd');
expect(extCmd).toBeDefined();
expect(extCmd?.source).toBe('extension');
});
});
});
21 changes: 16 additions & 5 deletions packages/cli/src/services/FileCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
SlashCommand,
SlashCommandActionReturn,
} from '../ui/commands/types.js';
import { CommandKind } from '../ui/commands/types.js';
import { CommandKind, CommandSource } from '../ui/commands/types.js';
import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js';
import type {
IPromptProcessor,
Expand All @@ -39,6 +39,7 @@ interface CommandDirectory {
path: string;
extensionName?: string;
extensionId?: string;
source: CommandSource;
}

/**
Expand Down Expand Up @@ -113,6 +114,7 @@ export class FileCommandLoader implements ICommandLoader {
dirInfo.path,
dirInfo.extensionName,
dirInfo.extensionId,
dirInfo.source,
),
);

Expand Down Expand Up @@ -151,10 +153,16 @@ export class FileCommandLoader implements ICommandLoader {
const storage = this.config?.storage ?? new Storage(this.projectRoot);

// 1. User commands
dirs.push({ path: Storage.getUserCommandsDir() });
dirs.push({
path: Storage.getUserCommandsDir(),
source: CommandSource.USER,
});

// 2. Project commands (override user commands)
dirs.push({ path: storage.getProjectCommandsDir() });
dirs.push({
path: storage.getProjectCommandsDir(),
source: CommandSource.PROJECT,
});

// 3. Extension commands (processed last to detect all conflicts)
if (this.config) {
Expand All @@ -167,6 +175,7 @@ export class FileCommandLoader implements ICommandLoader {
path: path.join(ext.path, 'commands'),
extensionName: ext.name,
extensionId: ext.id,
source: CommandSource.EXTENSION,
}));

dirs.push(...extensionCommandDirs);
Expand All @@ -185,8 +194,9 @@ export class FileCommandLoader implements ICommandLoader {
private async parseAndAdaptFile(
filePath: string,
baseDir: string,
extensionName?: string,
extensionId?: string,
extensionName: string | undefined,
extensionId: string | undefined,
source: CommandSource,
): Promise<SlashCommand | null> {
let fileContent: string;
try {
Expand Down Expand Up @@ -289,6 +299,7 @@ export class FileCommandLoader implements ICommandLoader {
kind: CommandKind.FILE,
extensionName,
extensionId,
source,
action: async (
context: CommandContext,
_args: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/ui/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export enum CommandKind {
AGENT = 'agent',
}

export enum CommandSource {
USER = 'user',
PROJECT = 'project',
EXTENSION = 'extension',
}

// The standardized contract for any command in the system.
export interface SlashCommand {
name: string;
Expand All @@ -190,6 +196,7 @@ export interface SlashCommand {
hidden?: boolean;

kind: CommandKind;
source?: CommandSource;

/**
* Controls whether the command auto-executes when selected with Enter.
Expand Down
38 changes: 37 additions & 1 deletion packages/cli/src/ui/components/SuggestionsDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { render } from '../../test-utils/render.js';
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
import { describe, it, expect } from 'vitest';
import { CommandKind } from '../commands/types.js';
import { CommandKind, CommandSource } from '../commands/types.js';

describe('SuggestionsDisplay', () => {
const mockSuggestions = [
Expand Down Expand Up @@ -121,4 +121,40 @@ describe('SuggestionsDisplay', () => {
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders source tags for file commands', () => {
const sourcedSuggestions = [
{
label: 'User Cmd',
value: 'user-cmd',
commandKind: CommandKind.FILE,
source: CommandSource.USER,
},
{
label: 'Project Cmd',
value: 'project-cmd',
commandKind: CommandKind.FILE,
source: CommandSource.PROJECT,
},
{
label: 'Ext Cmd',
value: 'ext-cmd',
commandKind: CommandKind.FILE,
source: CommandSource.EXTENSION,
},
];

const { lastFrame } = render(
<SuggestionsDisplay
suggestions={sourcedSuggestions}
activeIndex={0}
isLoading={false}
width={80}
scrollOffset={0}
userInput=""
mode="reverse"
/>,
);
expect(lastFrame()).toMatchSnapshot();
});
});
19 changes: 17 additions & 2 deletions packages/cli/src/ui/components/SuggestionsDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ExpandableText, MAX_WIDTH } from './shared/ExpandableText.js';
import { CommandKind } from '../commands/types.js';
import { CommandKind, CommandSource } from '../commands/types.js';
import { Colors } from '../colors.js';
import { sanitizeForDisplay } from '../utils/textUtils.js';

Expand All @@ -17,6 +17,7 @@ export interface Suggestion {
description?: string;
matchedIndex?: number;
commandKind?: CommandKind;
source?: CommandSource;
}
interface SuggestionsDisplayProps {
suggestions: Suggestion[];
Expand Down Expand Up @@ -67,8 +68,16 @@ export function SuggestionsDisplay({
[CommandKind.AGENT]: ' [Agent]',
};

const COMMAND_SOURCE_SUFFIX: Partial<Record<CommandSource, string>> = {
[CommandSource.USER]: ' [USER]',
[CommandSource.PROJECT]: ' [PROJECT]',
[CommandSource.EXTENSION]: ' [EXT]',
};

const getFullLabel = (s: Suggestion) =>
s.label + (s.commandKind ? (COMMAND_KIND_SUFFIX[s.commandKind] ?? '') : '');
s.label +
(s.commandKind ? (COMMAND_KIND_SUFFIX[s.commandKind] ?? '') : '') +
(s.source ? (COMMAND_SOURCE_SUFFIX[s.source] ?? '') : '');
Comment thread
This conversation was marked as resolved.
Outdated

const maxLabelLength = Math.max(
...suggestions.map((s) => getFullLabel(s).length),
Expand Down Expand Up @@ -111,6 +120,12 @@ export function SuggestionsDisplay({
{COMMAND_KIND_SUFFIX[suggestion.commandKind]}
</Text>
)}
{suggestion.source &&
COMMAND_SOURCE_SUFFIX[suggestion.source] && (
<Text color={textColor}>
{COMMAND_SOURCE_SUFFIX[suggestion.source]}
</Text>
)}
</Box>
</Box>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ exports[`SuggestionsDisplay > renders MCP tag for MCP prompts 1`] = `" mcp-tool

exports[`SuggestionsDisplay > renders loading state 1`] = `" Loading suggestions..."`;

exports[`SuggestionsDisplay > renders source tags for file commands 1`] = `
" user-cmd [USER]
project-cmd [PROJECT]
ext-cmd [EXT]"
`;

exports[`SuggestionsDisplay > renders suggestions list 1`] = `
" command1 Description 1
command2 Description 2
Expand Down
Loading
Loading