feat: enrich Codex skill metadata display
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user