feat(codex): support file mentions (#774)

* feat(codex): support file mentions

* fix(codex): quote file mention paths with spaces

* fix(codex): keep punctuation outside file mentions

* fix(codex): avoid parsing literal at-mentions

* fix(codex): scope file mention autocomplete
This commit is contained in:
NightWatcher314
2026-07-24 11:00:48 +08:00
committed by GitHub
parent 40c1789357
commit 5300e1ee39
4 changed files with 95 additions and 3 deletions
+5
View File
@@ -166,6 +166,11 @@ export type UserInput =
type: 'skill';
name: string;
path: string;
}
| {
type: 'mention';
name: string;
path: string;
};
export type SandboxPolicy =
@@ -3,6 +3,7 @@ import type { EnhancedMode } from '../loop';
import {
buildThreadStartParams,
buildTurnStartParams,
buildUserInputFromMessage,
codexCollaborationSpawnAgentInstructions,
supportsReasoningSummary
} from './appServerConfig';
@@ -505,4 +506,34 @@ describe('appServerConfig', () => {
expect(params.collaborationMode).toBeUndefined();
expect(params.model).toBe('o3');
});
it('builds mention inputs from quoted @file tokens', () => {
expect(buildUserInputFromMessage('please inspect @"src/index.ts" now')).toEqual([
{ type: 'text', text: 'please inspect ' },
{ type: 'mention', name: 'index.ts', path: 'src/index.ts' },
{ type: 'text', text: ' now' }
]);
});
it('builds mention inputs from quoted @file tokens with spaces', () => {
expect(buildUserInputFromMessage('please inspect @"docs/My File.md" now')).toEqual([
{ type: 'text', text: 'please inspect ' },
{ type: 'mention', name: 'My File.md', path: 'docs/My File.md' },
{ type: 'text', text: ' now' }
]);
});
it('builds mention inputs from quoted root-level @file tokens', () => {
expect(buildUserInputFromMessage('please inspect @"package.json" now')).toEqual([
{ type: 'text', text: 'please inspect ' },
{ type: 'mention', name: 'package.json', path: 'package.json' },
{ type: 'text', text: ' now' }
]);
});
it('keeps literal at-mentions as text', () => {
expect(buildUserInputFromMessage('please ask @alice to upgrade @types/node.')).toEqual([
{ type: 'text', text: 'please ask @alice to upgrade @types/node.' }
]);
});
});
+43 -2
View File
@@ -7,7 +7,8 @@ import type {
SandboxMode,
SandboxPolicy,
ThreadStartParams,
TurnStartParams
TurnStartParams,
UserInput
} from '../appServerTypes';
import { resolveCodexPermissionModeConfig } from './permissionModeConfig';
@@ -127,6 +128,46 @@ function appendCollaborationInstructions(developerInstructions: string): string
return `${developerInstructions}\n\n${codexCollaborationSpawnAgentInstructions}`;
}
function mentionNameFromPath(path: string): string {
const parts = path.split(/[\\/]/).filter(Boolean);
return parts[parts.length - 1] ?? path;
}
export function buildUserInputFromMessage(message: string): UserInput[] {
const inputs: UserInput[] = [];
const mentionPattern = /(^|\s)@"((?:\\.|[^"\\])*)"/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = mentionPattern.exec(message)) !== null) {
const prefix = match[1] ?? '';
const rawPath = match[2] ?? '';
const pathText = rawPath;
const path = pathText.replace(/\\(["\\])/g, '$1');
if (!path) continue;
const atIndex = match.index + prefix.length;
const textBeforeMention = message.slice(lastIndex, atIndex);
if (textBeforeMention) {
inputs.push({ type: 'text', text: textBeforeMention });
}
inputs.push({
type: 'mention',
name: mentionNameFromPath(path),
path
});
lastIndex = mentionPattern.lastIndex - (rawPath.length - pathText.length);
}
const remainder = message.slice(lastIndex);
if (remainder || inputs.length === 0) {
inputs.push({ type: 'text', text: remainder });
}
return inputs;
}
export function buildThreadStartParams(args: {
cwd: string;
mode: EnhancedMode;
@@ -192,7 +233,7 @@ export function buildTurnStartParams(args: {
const params: TurnStartParams = {
threadId: args.threadId,
cwd: args.cwd,
input: [{ type: 'text', text: args.message }]
input: buildUserInputFromMessage(args.message)
};
const allowCliOverrides = args.mode?.permissionMode === 'default';