feat: enrich Codex skill metadata display

This commit is contained in:
shiyue
2026-06-18 08:42:57 +08:00
parent 216f87e3b4
commit c50ee527ea
5 changed files with 731 additions and 31 deletions

View File

@@ -134,6 +134,7 @@ function connectWs(port, password) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
const messages = [];
let settled = false;
ws.on('open', () => {
ws.send(JSON.stringify({ type: 'auth', password }));
@@ -141,10 +142,20 @@ function connectWs(port, password) {
ws.on('message', (buf) => {
const msg = JSON.parse(String(buf));
messages.push(msg);
if (msg.type === 'auth_result' && msg.success) resolve({ ws, messages, token: msg.token });
if (msg.type === 'auth_result' && !msg.success) reject(new Error('Auth failed'));
if (msg.type === 'auth_result' && msg.success) {
settled = true;
resolve({ ws, messages, token: msg.token });
}
if (msg.type === 'auth_result' && !msg.success) {
settled = true;
reject(new Error('Auth failed'));
}
});
ws.on('error', (err) => {
if (settled) return;
settled = true;
reject(err);
});
ws.on('error', reject);
});
}
@@ -500,6 +511,8 @@ function assertFrontendComposerMcpContract() {
assert(menuStart >= 0 && menuEnd > menuStart, 'Frontend should define showCmdMenu before requestComposerSuggestions');
const menuBlock = source.slice(menuStart, menuEnd);
assert(/item\.kind\s*===\s*'mcp'/.test(menuBlock) && menuBlock.includes("'MCP'"), 'Composer menu should render MCP item labels');
assert(source.includes('function renderComposerMentionsStrip(meta)'), 'Frontend should define composer mention strip renderer');
assert(source.includes("className = 'msg-mentions'"), 'Frontend should render a dedicated mention strip container');
}
async function main() {
@@ -537,6 +550,39 @@ async function main() {
'',
'Use this only in regression tests.',
].join('\n'));
mkdirp(path.join(skillDir, 'agents'));
fs.writeFileSync(path.join(skillDir, 'agents', 'openai.yaml'), [
'interface:',
' display_name: "Regression Docs"',
' short_description: "Regression skill metadata for composer suggestions."',
' brand_color: "#2f6f64"',
' default_prompt: "Use Regression Docs for regression metadata coverage."',
'',
'dependencies:',
' tools:',
' - type: "mcp"',
' value: "openaiDeveloperDocs"',
' description: "Regression docs MCP server"',
' transport: "streamable_http"',
' url: "https://developers.openai.com/mcp"',
].join('\n'));
const codexPromptsDir = path.join(homeDir, '.codex', 'prompts');
mkdirp(path.join(codexPromptsDir, 'nested-tool'));
fs.writeFileSync(path.join(codexPromptsDir, 'quick-note.md'), [
'---',
'title: Quick Note',
'description: Prompt file shortcut from ~/.codex/prompts.',
'---',
'',
'Prompt body from @quick-note.',
].join('\n'));
fs.writeFileSync(path.join(codexPromptsDir, 'nested-tool', 'prompt.md'), [
'---',
'description: Directory-style prompt shortcut.',
'---',
'',
'Prompt body from @nested-tool.',
].join('\n'));
fs.writeFileSync(path.join(configDir, 'prompts.json'), JSON.stringify({
prompts: [
{
@@ -618,6 +664,18 @@ async function main() {
const codexInitCwd = path.join(tempRoot, 'codex-space');
mkdirp(codexInitCwd);
const projectSkillDir = path.join(codexInitCwd, '.agents', 'skills', 'project-skill');
mkdirp(projectSkillDir);
fs.writeFileSync(path.join(projectSkillDir, 'SKILL.md'), [
'---',
'name: project-skill',
'description: Project-scoped skill for composer suggestions.',
'---',
'',
'# Project Skill',
'',
'Use this only in regression tests.',
].join('\n'));
fs.writeFileSync(path.join(codexInitCwd, 'context.txt'), 'Composer file context body.');
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: codexInitCwd, mode: 'plan' }));
const codexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === codexInitCwd);
@@ -667,15 +725,33 @@ async function main() {
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-skill', trigger: '$', query: 'reg', sessionId: codexSession.sessionId, agent: 'codex' }));
const skillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-skill');
assert(skillComposer.items.some((item) => item.kind === 'skill' && item.name === 'regression-skill'), 'Composer skill suggestions should include local Codex skill');
const metadataSkill = skillComposer.items.find((item) => item.kind === 'skill' && item.name === 'regression-skill');
assert(metadataSkill?.title === 'Regression Docs', 'Composer skill suggestions should expose openai.yaml display_name');
assert(/metadata coverage/.test(metadataSkill?.defaultPromptPreview || ''), 'Composer skill suggestions should expose default prompt preview');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-project-skill', trigger: '$', query: 'project', sessionId: codexSession.sessionId, agent: 'codex' }));
const projectSkillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-project-skill');
assert(projectSkillComposer.items.some((item) => item.kind === 'skill' && item.name === 'project-skill'), 'Composer skill suggestions should include project-scoped skill from session cwd');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-skill-mcp', trigger: '$', query: 'ccweb', sessionId: codexSession.sessionId, agent: 'codex' }));
const skillMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-skill-mcp');
assert(skillMcpComposer.items.some((item) => item.kind === 'mcp' && item.name === 'ccweb_list_conversations'), 'Composer skill trigger suggestions should include ccweb MCP tools');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-skill-declared-mcp', trigger: '$', query: 'openai', sessionId: codexSession.sessionId, agent: 'codex' }));
const skillDeclaredMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-skill-declared-mcp');
assert(skillDeclaredMcpComposer.items.some((item) => item.kind === 'mcp' && item.name === 'openaiDeveloperDocs' && item.state === 'declared'), 'Composer skill trigger suggestions should include declared MCP dependencies from openai.yaml');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-prompt', trigger: '@', query: 'ship', sessionId: codexSession.sessionId, agent: 'codex' }));
const promptComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-prompt');
assert(promptComposer.items.some((item) => item.kind === 'prompt' && item.name === 'shipit'), 'Composer prompt suggestions should include configured prompt');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-prompt-file', trigger: '@', query: 'quick', sessionId: codexSession.sessionId, agent: 'codex' }));
const promptFileComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-prompt-file');
assert(promptFileComposer.items.some((item) => item.kind === 'prompt' && item.name === 'quick-note'), 'Composer prompt suggestions should include ~/.codex/prompts/*.md shortcuts');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-prompt-dir', trigger: '@', query: 'nested', sessionId: codexSession.sessionId, agent: 'codex' }));
const promptDirComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-prompt-dir');
assert(promptDirComposer.items.some((item) => item.kind === 'prompt' && item.name === 'nested-tool'), 'Composer prompt suggestions should include ~/.codex/prompts/<name>/prompt.md shortcuts');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-prompt-mcp', trigger: '@', query: 'ccweb', sessionId: codexSession.sessionId, agent: 'codex' }));
const promptMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-prompt-mcp');
assert(promptMcpComposer.items.some((item) => item.kind === 'mcp' && item.name === 'ccweb_list_conversations'), 'Composer prompt trigger suggestions should include ccweb MCP tools');
@@ -686,7 +762,7 @@ async function main() {
ws.send(JSON.stringify({
type: 'message',
text: '@shipit @context.txt $regression-skill run composer regression',
text: '@shipit @quick-note @context.txt $regression-skill $project-skill run composer regression',
sessionId: codexSession.sessionId,
mode: 'plan',
agent: 'codex',
@@ -694,16 +770,23 @@ async function main() {
const composerExpanded = await nextMessage(messages, ws, (msg) => (
msg.type === 'text_delta' &&
/BEGIN CC-WEB PROMPT: shipit/.test(msg.text || '') &&
/BEGIN CC-WEB PROMPT: quick-note/.test(msg.text || '') &&
/Composer file context body/.test(msg.text || '')
));
assert(/Regression prompt body from @shipit/.test(composerExpanded.text || ''), 'Composer runtime prompt should expand @prompt content');
assert(/Prompt body from @quick-note/.test(composerExpanded.text || ''), 'Composer runtime prompt should expand ~/.codex/prompts prompt shortcuts');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexSession.sessionId);
const storedComposerSession = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexSession.sessionId}.json`), 'utf8'));
const storedComposerMessage = storedComposerSession.messages.find((message) => message.content === '@shipit @context.txt $regression-skill run composer regression');
const storedComposerMessage = storedComposerSession.messages.find((message) => message.content === '@shipit @quick-note @context.txt $regression-skill $project-skill run composer regression');
assert(storedComposerMessage, 'Composer message should persist original user text');
assert(storedComposerMessage.composerMentions?.some((mention) => mention.kind === 'prompt' && mention.name === 'shipit'), 'Composer message should persist prompt mention metadata');
assert(storedComposerMessage.composerMentions?.some((mention) => mention.kind === 'prompt' && mention.name === 'quick-note'), 'Composer message should persist ~/.codex/prompts prompt mention metadata');
assert(storedComposerMessage.composerMentions?.some((mention) => mention.kind === 'file' && mention.name === 'context.txt'), 'Composer message should persist file mention metadata');
assert(storedComposerMessage.composerMentions?.some((mention) => mention.kind === 'skill' && mention.name === 'regression-skill'), 'Composer message should persist skill mention metadata');
assert(storedComposerMessage.composerMentions?.some((mention) => mention.kind === 'skill' && mention.name === 'project-skill'), 'Composer message should persist project-scoped skill mention metadata');
const storedRegressionSkillMention = storedComposerMessage.composerMentions?.find((mention) => mention.kind === 'skill' && mention.name === 'regression-skill');
assert(storedRegressionSkillMention?.title === 'Regression Docs', 'Stored skill mention should persist display title from openai.yaml');
assert(storedRegressionSkillMention?.dependencies?.some((dep) => dep.value === 'openaiDeveloperDocs' && dep.state === 'declared'), 'Stored skill mention should persist MCP dependency metadata');
const mcpList = await callInternalMcp(port, internalMcpToken, {
tool: 'ccweb_list_conversations',