mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -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.' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
+16
-1
@@ -971,11 +971,26 @@ function SessionPage() {
|
||||
} = useSkills(api, sessionId)
|
||||
|
||||
const getAutocompleteSuggestions = useCallback(async (query: string) => {
|
||||
if (query.startsWith('@')) {
|
||||
if (agentType !== 'codex' || !api || !sessionId) return []
|
||||
const search = query.slice(1)
|
||||
const response = await api.searchSessionFiles(sessionId, search, 50)
|
||||
if (!response.success || !response.files) return []
|
||||
return response.files.map((file) => {
|
||||
const mentionText = `@"${file.fullPath.replace(/(["\\])/g, '\\$1')}"`
|
||||
return {
|
||||
key: mentionText,
|
||||
text: mentionText,
|
||||
label: `@${file.fileName}`,
|
||||
description: file.filePath || file.fullPath
|
||||
}
|
||||
})
|
||||
}
|
||||
if (query.startsWith('$')) {
|
||||
return await getSkillSuggestions(query)
|
||||
}
|
||||
return await getSlashSuggestions(query)
|
||||
}, [getSkillSuggestions, getSlashSuggestions])
|
||||
}, [agentType, api, sessionId, getSkillSuggestions, getSlashSuggestions])
|
||||
|
||||
const refreshSelectedSession = useCallback(() => {
|
||||
void refetchSession()
|
||||
|
||||
Reference in New Issue
Block a user