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
32 changes: 25 additions & 7 deletions packages/cli/src/ui/utils/commandUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ describe('commandUtils', () => {
it('should return true when query starts with @', () => {
expect(isAtCommand('@file')).toBe(true);
expect(isAtCommand('@path/to/file')).toBe(true);
expect(isAtCommand('@')).toBe(true);
});

it('should return true when query contains @ preceded by whitespace', () => {
Expand All @@ -172,17 +171,36 @@ describe('commandUtils', () => {
expect(isAtCommand(' @file')).toBe(true);
});

it('should return false when query does not start with @ and has no spaced @', () => {
it('should return true when @ is preceded by non-whitespace (external editor scenario)', () => {
// When a user composes a prompt in an external editor, @-references may
// appear after punctuation characters such as ':' or '(' without a space.
// The processor must still recognise these as @-commands so that the
// referenced files are pre-loaded before the query is sent to the model.
expect(isAtCommand('check:@file.py')).toBe(true);
expect(isAtCommand('analyze(@file.py)')).toBe(true);
expect(isAtCommand('hello@file')).toBe(true);
expect(isAtCommand('text@path/to/file')).toBe(true);
expect(isAtCommand('user@host')).toBe(true);
});

it('should return false when query does not contain any @<path> pattern', () => {
expect(isAtCommand('file')).toBe(false);
expect(isAtCommand('hello')).toBe(false);
expect(isAtCommand('')).toBe(false);
expect(isAtCommand('[email protected]')).toBe(false);
expect(isAtCommand('user@host')).toBe(false);
// A bare '@' with no following path characters is not an @-command.
expect(isAtCommand('@')).toBe(false);
});

it('should return false when @ is not preceded by whitespace', () => {
expect(isAtCommand('hello@file')).toBe(false);
expect(isAtCommand('text@path')).toBe(false);
it('should return false when @ is escaped with a backslash', () => {
expect(isAtCommand('\\@file')).toBe(false);
});

it('should return true for multi-line external editor prompts with @-references', () => {
expect(isAtCommand('Please review:\n@src/main.py\nand fix bugs.')).toBe(
true,
);
// @file after a colon on the same line.
expect(isAtCommand('Files:@src/a.py,@src/b.py')).toBe(true);
});
});

Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/ui/utils/commandUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,29 @@ import type { SlashCommand } from '../commands/types.js';
import fs from 'node:fs';
import type { Writable } from 'node:stream';
import type { Settings } from '../../config/settingsSchema.js';
import { AT_COMMAND_PATH_REGEX_SOURCE } from '../hooks/atCommandProcessor.js';

// Pre-compiled regex for detecting @<path> patterns consistent with parseAllAtCommands.
// Uses the same AT_COMMAND_PATH_REGEX_SOURCE so that isAtCommand is true whenever
// parseAllAtCommands would find at least one atPath part.
const AT_COMMAND_DETECT_REGEX = new RegExp(
`(?<!\\\\)@${AT_COMMAND_PATH_REGEX_SOURCE}`,
);

/**
* Checks if a query string potentially represents an '@' command.
* It triggers if the query starts with '@' or contains '@' preceded by whitespace
* and followed by a non-whitespace character.
* Returns true if the query contains any '@<path>' pattern that would be
* recognised by the @ command processor, regardless of what character
* precedes the '@' sign. This ensures that prompts written in an external
* editor (where '@' may follow punctuation like ':' or '(') are correctly
* identified and their referenced files pre-loaded before the query is sent
* to the model.
*
* @param query The input query string.
* @returns True if the query looks like an '@' command, false otherwise.
*/
export const isAtCommand = (query: string): boolean =>
// Check if starts with @ OR has a space, then @
query.startsWith('@') || /\s@/.test(query);
AT_COMMAND_DETECT_REGEX.test(query);

/**
* Checks if a query string potentially represents an '/' command.
Expand Down