feat: enrich Codex skill metadata display
This commit is contained in:
426
server.js
426
server.js
@@ -48,6 +48,9 @@ const COMPOSER_SUGGESTION_LIMIT = 20;
|
||||
const COMPOSER_FILE_CONTEXT_MAX_BYTES = 60 * 1024;
|
||||
const COMPOSER_MAX_FILE_MENTIONS = 4;
|
||||
const COMPOSER_MAX_PROMPT_MENTIONS = 4;
|
||||
const COMPOSER_SKILL_METADATA_MAX_BYTES = 64 * 1024;
|
||||
const COMPOSER_SKILL_DEFAULT_PROMPT_PREVIEW_CHARS = 180;
|
||||
const COMPOSER_SKILL_TEXT_MAX_CHARS = 240;
|
||||
const SESSION_MAX_JSON_BYTES = readPositiveIntEnv('CC_WEB_SESSION_MAX_JSON_BYTES', 10 * 1024 * 1024, { min: 512 * 1024 });
|
||||
const SESSION_LOAD_MAX_BYTES = readPositiveIntEnv('CC_WEB_SESSION_LOAD_MAX_BYTES', 32 * 1024 * 1024, { min: SESSION_MAX_JSON_BYTES });
|
||||
const SESSION_META_FULL_PARSE_MAX_BYTES = readPositiveIntEnv('CC_WEB_SESSION_META_FULL_PARSE_MAX_BYTES', 512 * 1024, { min: 64 * 1024 });
|
||||
@@ -1514,6 +1517,156 @@ function parseSimpleFrontmatter(text) {
|
||||
return { meta, body: raw.slice(match[0].length) };
|
||||
}
|
||||
|
||||
function stripSimpleYamlComment(line) {
|
||||
const value = String(line || '');
|
||||
let quote = '';
|
||||
let escaped = false;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const ch = value[i];
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '\\' && quote === '"') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === quote) quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === '\'') {
|
||||
quote = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === '#' && (i === 0 || /\s/.test(value[i - 1]))) {
|
||||
return value.slice(0, i).trimEnd();
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseSimpleYamlScalar(rawValue) {
|
||||
const value = stripSimpleYamlComment(rawValue).trim();
|
||||
if (!value) return '';
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
if (value === 'null' || value === '~') return null;
|
||||
if (value.startsWith('"') && value.endsWith('"')) {
|
||||
try { return JSON.parse(value); } catch { return value.slice(1, -1); }
|
||||
}
|
||||
if (value.startsWith('\'') && value.endsWith('\'')) {
|
||||
return value.slice(1, -1).replace(/''/g, '\'');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseSimpleYamlKeyValue(text) {
|
||||
const value = String(text || '').trim();
|
||||
if (!value) return null;
|
||||
let quote = '';
|
||||
let escaped = false;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const ch = value[i];
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '\\' && quote === '"') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === quote) quote = '';
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === '\'') {
|
||||
quote = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch !== ':') continue;
|
||||
const key = value.slice(0, i).trim();
|
||||
if (!/^[A-Za-z0-9_-]+$/.test(key)) return null;
|
||||
return { key, value: parseSimpleYamlScalar(value.slice(i + 1)) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseOpenAiSkillYaml(text) {
|
||||
const parsed = { interface: {}, policy: {}, dependencies: { tools: [] } };
|
||||
let section = '';
|
||||
let dependencyList = '';
|
||||
let currentTool = null;
|
||||
|
||||
for (const rawLine of String(text || '').split(/\r?\n/)) {
|
||||
const line = stripSimpleYamlComment(rawLine);
|
||||
if (!line.trim()) continue;
|
||||
const indent = line.match(/^\s*/)?.[0]?.length || 0;
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (indent === 0) {
|
||||
const sectionName = trimmed.endsWith(':') ? trimmed.slice(0, -1).trim() : '';
|
||||
if (['interface', 'policy', 'dependencies'].includes(sectionName)) {
|
||||
section = sectionName;
|
||||
dependencyList = '';
|
||||
currentTool = null;
|
||||
} else {
|
||||
section = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!section) continue;
|
||||
|
||||
if (section === 'dependencies') {
|
||||
if (indent === 2 && trimmed === 'tools:') {
|
||||
dependencyList = 'tools';
|
||||
currentTool = null;
|
||||
continue;
|
||||
}
|
||||
if (dependencyList === 'tools' && indent >= 4 && trimmed.startsWith('-')) {
|
||||
currentTool = {};
|
||||
parsed.dependencies.tools.push(currentTool);
|
||||
const rest = trimmed.slice(1).trim();
|
||||
const item = parseSimpleYamlKeyValue(rest);
|
||||
if (item) currentTool[item.key] = item.value;
|
||||
continue;
|
||||
}
|
||||
if (dependencyList === 'tools' && currentTool && indent >= 6) {
|
||||
const item = parseSimpleYamlKeyValue(trimmed);
|
||||
if (item) currentTool[item.key] = item.value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indent >= 2) {
|
||||
const item = parseSimpleYamlKeyValue(trimmed);
|
||||
if (item) parsed[section][item.key] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeComposerTextValue(value, maxChars = COMPOSER_SKILL_TEXT_MAX_CHARS) {
|
||||
const text = String(value || '').trim().replace(/\s+/g, ' ');
|
||||
return text ? truncateTextValue(text, maxChars, '...') : '';
|
||||
}
|
||||
|
||||
function normalizeSkillBrandColor(value) {
|
||||
const color = String(value || '').trim();
|
||||
return /^#[0-9A-Fa-f]{3}(?:[0-9A-Fa-f]{3})?$/.test(color) ? color : '';
|
||||
}
|
||||
|
||||
function normalizeSkillIconUrl(value) {
|
||||
const url = String(value || '').trim();
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
if (/^data:image\/(?:png|jpe?g|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(url) && url.length <= 12000) {
|
||||
return url;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeComposerKey(value) {
|
||||
return String(value || '').trim().replace(/^[@$]/, '').toLowerCase();
|
||||
}
|
||||
@@ -1551,19 +1704,162 @@ function collectFilesByName(rootDir, fileNames, options = {}) {
|
||||
return found;
|
||||
}
|
||||
|
||||
function composerSkillRoots() {
|
||||
function collectFilesByExtension(rootDir, extensions, options = {}) {
|
||||
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 4;
|
||||
const maxFiles = Number.isFinite(options.maxFiles) ? options.maxFiles : 200;
|
||||
const ignoreDirs = new Set(['node_modules', '.git', 'sessions', 'logs']);
|
||||
const normalizedExtensions = new Set(
|
||||
[...extensions].map((ext) => String(ext || '').trim().toLowerCase()).filter(Boolean)
|
||||
);
|
||||
const found = [];
|
||||
|
||||
function walk(dir, depth) {
|
||||
if (found.length >= maxFiles || depth > maxDepth) return;
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { numeric: true, sensitivity: 'base' }));
|
||||
for (const entry of entries) {
|
||||
if (found.length >= maxFiles) return;
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isFile() && normalizedExtensions.has(path.extname(entry.name).toLowerCase())) {
|
||||
found.push(entryPath);
|
||||
} else if (entry.isDirectory() && !ignoreDirs.has(entry.name)) {
|
||||
walk(entryPath, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const realRoot = fs.realpathSync(rootDir);
|
||||
if (fs.statSync(realRoot).isDirectory()) walk(realRoot, 0);
|
||||
} catch {}
|
||||
return found;
|
||||
}
|
||||
|
||||
function hasProjectRootMarker(dir) {
|
||||
return ['.git', '.hg', '.sl'].some((marker) => {
|
||||
try { return fs.existsSync(path.join(dir, marker)); } catch { return false; }
|
||||
});
|
||||
}
|
||||
|
||||
function findNearestProjectRoot(startDir) {
|
||||
const start = normalizeExistingDirPath(startDir);
|
||||
if (!start) return null;
|
||||
let dir = start;
|
||||
const seen = new Set();
|
||||
while (dir && !seen.has(dir)) {
|
||||
seen.add(dir);
|
||||
if (hasProjectRootMarker(dir)) return dir;
|
||||
const parent = path.dirname(dir);
|
||||
if (!parent || parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
function composerProjectSkillRoots(cwd) {
|
||||
const start = normalizeExistingDirPath(cwd);
|
||||
const projectRoot = findNearestProjectRoot(start);
|
||||
if (!start || !projectRoot) return [];
|
||||
const roots = [];
|
||||
const codexHome = getCodexHomeDir();
|
||||
if (codexHome) roots.push(path.join(codexHome, 'skills'));
|
||||
roots.push(path.join(__dirname, '.codex', 'skills'));
|
||||
roots.push(path.join(__dirname, '.agents', 'skills'));
|
||||
let dir = start;
|
||||
const seen = new Set();
|
||||
while (dir && !seen.has(dir)) {
|
||||
seen.add(dir);
|
||||
roots.push(path.join(dir, '.agents', 'skills'));
|
||||
roots.push(path.join(dir, '.codex', 'skills'));
|
||||
if (dir === projectRoot) break;
|
||||
const parent = path.dirname(dir);
|
||||
if (!parent || parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
function loadCodexSkills() {
|
||||
function composerSkillRoots(options = {}) {
|
||||
const roots = [];
|
||||
const session = options.session || (options.sessionId ? loadSession(options.sessionId) : null);
|
||||
roots.push(...composerProjectSkillRoots(options.cwd || session?.cwd));
|
||||
const codexHome = getCodexHomeDir();
|
||||
if (codexHome) roots.push(path.join(codexHome, 'skills'));
|
||||
const homeDir = normalizeExistingDirPath(process.env.HOME || process.env.USERPROFILE || '');
|
||||
if (homeDir) roots.push(path.join(homeDir, '.agents', 'skills'));
|
||||
roots.push(path.join(__dirname, '.codex', 'skills'));
|
||||
roots.push(path.join(__dirname, '.agents', 'skills'));
|
||||
roots.push('/etc/codex/skills');
|
||||
const seen = new Set();
|
||||
return roots.filter((root) => {
|
||||
const key = path.resolve(String(root || ''));
|
||||
if (!key || seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSkillMcpUrl(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return ['http:', 'https:'].includes(parsed.protocol) ? parsed.toString() : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSkillMcpTool(rawTool) {
|
||||
if (!rawTool || typeof rawTool !== 'object') return null;
|
||||
const type = String(rawTool.type || '').trim().toLowerCase();
|
||||
const value = normalizeMcpServerName(rawTool.value || rawTool.name || rawTool.server);
|
||||
if (type !== 'mcp' || !isLikelyMcpServerName(value)) return null;
|
||||
return {
|
||||
type: 'mcp',
|
||||
value,
|
||||
name: value,
|
||||
server: value,
|
||||
description: normalizeComposerTextValue(rawTool.description || `MCP server: ${value}`),
|
||||
transport: normalizeComposerTextValue(rawTool.transport, 80),
|
||||
url: normalizeSkillMcpUrl(rawTool.url),
|
||||
};
|
||||
}
|
||||
|
||||
function loadOpenAiSkillMetadata(skillPath) {
|
||||
const skillDir = path.dirname(skillPath);
|
||||
const metadataPath = path.join(skillDir, 'agents', 'openai.yaml');
|
||||
try {
|
||||
const stat = fs.statSync(metadataPath);
|
||||
if (!stat.isFile() || stat.size > COMPOSER_SKILL_METADATA_MAX_BYTES) return null;
|
||||
const parsed = parseOpenAiSkillYaml(fs.readFileSync(metadataPath, 'utf8'));
|
||||
const iface = parsed.interface || {};
|
||||
const policy = parsed.policy || {};
|
||||
const tools = Array.isArray(parsed.dependencies?.tools) ? parsed.dependencies.tools : [];
|
||||
const mcpTools = tools.map((tool) => normalizeSkillMcpTool(tool)).filter(Boolean);
|
||||
return {
|
||||
metadataSource: path.relative(process.cwd(), metadataPath) || metadataPath,
|
||||
displayName: normalizeComposerTextValue(iface.display_name, 100),
|
||||
shortDescription: normalizeComposerTextValue(iface.short_description, COMPOSER_SKILL_TEXT_MAX_CHARS),
|
||||
iconSmall: normalizeSkillIconUrl(iface.icon_small),
|
||||
iconLarge: normalizeSkillIconUrl(iface.icon_large),
|
||||
brandColor: normalizeSkillBrandColor(iface.brand_color),
|
||||
defaultPromptPreview: normalizeComposerTextValue(iface.default_prompt, COMPOSER_SKILL_DEFAULT_PROMPT_PREVIEW_CHARS),
|
||||
policy: {
|
||||
allowImplicitInvocation: policy.allow_implicit_invocation !== false,
|
||||
},
|
||||
dependencies: { tools: mcpTools },
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadCodexSkills(options = {}) {
|
||||
const seen = new Set();
|
||||
const skills = [];
|
||||
for (const root of composerSkillRoots()) {
|
||||
for (const root of composerSkillRoots(options)) {
|
||||
for (const skillPath of collectFilesByName(root, new Set(['SKILL.md']), { maxDepth: 5, maxFiles: 300 })) {
|
||||
let text = '';
|
||||
try { text = fs.readFileSync(skillPath, 'utf8'); } catch { continue; }
|
||||
@@ -1574,13 +1870,24 @@ function loadCodexSkills() {
|
||||
const key = normalizeComposerKey(name);
|
||||
if (!key || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const metadata = loadOpenAiSkillMetadata(skillPath);
|
||||
const title = metadata?.displayName || name;
|
||||
const description = metadata?.shortDescription || String(meta.description || 'Codex skill').trim();
|
||||
skills.push({
|
||||
kind: 'skill',
|
||||
name,
|
||||
label: `$${name}`,
|
||||
description: String(meta.description || 'Codex skill').trim(),
|
||||
title,
|
||||
description,
|
||||
insertion: `$${name}`,
|
||||
source: path.relative(process.cwd(), skillPath) || skillPath,
|
||||
metadataSource: metadata?.metadataSource || '',
|
||||
iconSmall: metadata?.iconSmall || '',
|
||||
iconLarge: metadata?.iconLarge || '',
|
||||
brandColor: metadata?.brandColor || '',
|
||||
defaultPromptPreview: metadata?.defaultPromptPreview || '',
|
||||
policy: metadata?.policy || { allowImplicitInvocation: true },
|
||||
dependencies: metadata?.dependencies || { tools: [] },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1627,19 +1934,19 @@ function loadConfiguredPrompts() {
|
||||
function loadPromptFiles() {
|
||||
const prompts = [];
|
||||
for (const root of composerPromptRoots()) {
|
||||
const files = collectFilesByName(root, new Set(['prompt.md', 'PROMPT.md']), { maxDepth: 5, maxFiles: 200 });
|
||||
try {
|
||||
if (fs.existsSync(root)) {
|
||||
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||||
if (entry.isFile() && /\.(md|txt)$/i.test(entry.name)) files.push(path.join(root, entry.name));
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
const files = Array.from(new Set([
|
||||
...collectFilesByName(root, new Set(['prompt.md', 'PROMPT.md']), { maxDepth: 5, maxFiles: 300 }),
|
||||
...collectFilesByExtension(root, new Set(['.md', '.txt']), { maxDepth: 5, maxFiles: 300 }),
|
||||
]));
|
||||
for (const filePath of files) {
|
||||
let text = '';
|
||||
try { text = fs.readFileSync(filePath, 'utf8'); } catch { continue; }
|
||||
const { meta, body } = parseSimpleFrontmatter(text);
|
||||
const fallbackName = path.basename(filePath).replace(/\.(md|txt)$/i, '');
|
||||
const baseName = path.basename(filePath).replace(/\.(md|txt)$/i, '');
|
||||
const relName = normalizeRelativeBrowserPath(path.relative(root, filePath)).replace(/\.(md|txt)$/i, '');
|
||||
const fallbackName = /^prompt$/i.test(baseName)
|
||||
? path.basename(path.dirname(filePath))
|
||||
: relName;
|
||||
const prompt = normalizePromptRecord({
|
||||
name: meta.name || fallbackName,
|
||||
title: meta.title || meta.name || fallbackName,
|
||||
@@ -1754,17 +2061,69 @@ function mcpServerSuggestion(name, options = {}) {
|
||||
server,
|
||||
source: options.source || 'mcp',
|
||||
itemType: 'server',
|
||||
state: options.state || (options.source === 'skill-dependency' ? 'declared' : 'configured'),
|
||||
dependencyOf: options.dependencyOf || '',
|
||||
transport: options.transport || '',
|
||||
url: options.url || '',
|
||||
};
|
||||
}
|
||||
|
||||
function listComposerMcpItems(sessionId) {
|
||||
function summarizeSkillDependencies(skill) {
|
||||
const tools = Array.isArray(skill?.dependencies?.tools) ? skill.dependencies.tools : [];
|
||||
return tools
|
||||
.filter((tool) => tool?.type === 'mcp' && isLikelyMcpServerName(tool.value || tool.name || tool.server))
|
||||
.map((tool) => ({
|
||||
type: 'mcp',
|
||||
value: normalizeMcpServerName(tool.value || tool.name || tool.server),
|
||||
name: normalizeMcpServerName(tool.value || tool.name || tool.server),
|
||||
description: normalizeComposerTextValue(tool.description || ''),
|
||||
transport: normalizeComposerTextValue(tool.transport || '', 80),
|
||||
url: normalizeSkillMcpUrl(tool.url || ''),
|
||||
state: 'declared',
|
||||
}));
|
||||
}
|
||||
|
||||
function summarizeSkillForMention(skill, configuredMcpNames = new Set()) {
|
||||
const dependencies = summarizeSkillDependencies(skill).map((dependency) => ({
|
||||
...dependency,
|
||||
state: configuredMcpNames.has(normalizeMcpServerName(dependency.value)) ? 'configured' : 'declared',
|
||||
}));
|
||||
return {
|
||||
kind: 'skill',
|
||||
name: skill.name,
|
||||
label: `$${skill.name}`,
|
||||
title: skill.title || skill.name,
|
||||
description: skill.description || '',
|
||||
source: skill.source || '',
|
||||
metadataSource: skill.metadataSource || '',
|
||||
iconSmall: skill.iconSmall || '',
|
||||
iconLarge: skill.iconLarge || '',
|
||||
brandColor: skill.brandColor || '',
|
||||
defaultPromptPreview: skill.defaultPromptPreview || '',
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
function listComposerMcpItems(options = {}) {
|
||||
const sessionId = typeof options === 'string' ? options : options.sessionId;
|
||||
const agent = typeof options === 'object' ? options.agent : '';
|
||||
const skills = typeof options === 'object' && Array.isArray(options.skills)
|
||||
? options.skills
|
||||
: isCodexLikeAgent(agent)
|
||||
? loadCodexSkills(options)
|
||||
: [];
|
||||
const items = [];
|
||||
const seen = new Set();
|
||||
const configured = new Set();
|
||||
const push = (item) => {
|
||||
if (!item?.server && !item?.name) return;
|
||||
const key = `${item.kind || ''}:${item.server || ''}:${item.name || item.label || item.insertion || ''}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
if (item.kind === 'mcp' && item.itemType === 'server' && item.state !== 'declared') {
|
||||
const server = normalizeMcpServerName(item.server || item.name);
|
||||
if (server) configured.add(server);
|
||||
}
|
||||
items.push(item);
|
||||
};
|
||||
|
||||
@@ -1794,6 +2153,20 @@ function listComposerMcpItems(sessionId) {
|
||||
itemType: 'tool',
|
||||
});
|
||||
}
|
||||
for (const skill of skills) {
|
||||
for (const tool of summarizeSkillDependencies(skill)) {
|
||||
const server = normalizeMcpServerName(tool.value || tool.name || tool.server);
|
||||
if (!server || configured.has(server)) continue;
|
||||
push(mcpServerSuggestion(server, {
|
||||
source: 'skill-dependency',
|
||||
state: 'declared',
|
||||
dependencyOf: skill.name,
|
||||
description: tool.description || `MCP server: ${server}`,
|
||||
transport: tool.transport || '',
|
||||
url: tool.url || '',
|
||||
}));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -1864,8 +2237,9 @@ function listComposerFileSuggestions(sessionId, query) {
|
||||
return items.slice(0, COMPOSER_SUGGESTION_LIMIT);
|
||||
}
|
||||
|
||||
function listComposerSuggestions(trigger, query, sessionId, agent) {
|
||||
const mcpItems = filterComposerItems(listComposerMcpItems(sessionId), query);
|
||||
function listComposerSuggestions(trigger, query, sessionId, agent, session = null) {
|
||||
const skillItems = isCodexLikeAgent(agent) ? loadCodexSkills({ session }) : [];
|
||||
const mcpItems = filterComposerItems(listComposerMcpItems({ sessionId, agent, skills: skillItems }), query);
|
||||
if (trigger === '/') {
|
||||
const commands = filterComposerItems(COMPOSER_COMMANDS.map((cmd) => ({
|
||||
kind: 'command',
|
||||
@@ -1877,7 +2251,7 @@ function listComposerSuggestions(trigger, query, sessionId, agent) {
|
||||
return mergeComposerSuggestionGroups(commands, mcpItems);
|
||||
}
|
||||
if (trigger === '$') {
|
||||
const skills = isCodexLikeAgent(agent) ? filterComposerItems(loadCodexSkills(), query) : [];
|
||||
const skills = filterComposerItems(skillItems, query);
|
||||
return mergeComposerSuggestionGroups(mcpItems, skills);
|
||||
}
|
||||
if (trigger === '@') {
|
||||
@@ -1905,7 +2279,8 @@ function handleComposerSuggestions(ws, msg) {
|
||||
}
|
||||
const sessionId = sanitizeId(msg.sessionId || '');
|
||||
const agent = normalizeAgent(msg.agent);
|
||||
const items = listComposerSuggestions(trigger, query, sessionId, agent);
|
||||
const session = sessionId ? loadSession(sessionId) : null;
|
||||
const items = listComposerSuggestions(trigger, query, sessionId, agent, session);
|
||||
return wsSend(ws, { type: 'composer_suggestions', requestId, trigger, query, items });
|
||||
}
|
||||
|
||||
@@ -1969,13 +2344,16 @@ function resolveComposerDecorators(text, session, agent) {
|
||||
}
|
||||
|
||||
const promptsByName = getComposerPromptMap();
|
||||
const skillsByName = new Map(loadCodexSkills().map((skill) => [normalizeComposerKey(skill.name), skill]));
|
||||
const skillsByName = new Map(loadCodexSkills({ session }).map((skill) => [normalizeComposerKey(skill.name), skill]));
|
||||
const mentions = [];
|
||||
const promptBlocks = [];
|
||||
const fileBlocks = [];
|
||||
const seenPromptNames = new Set();
|
||||
const seenFilePaths = new Set();
|
||||
const seenSkillNames = new Set();
|
||||
const configuredMcpNames = new Set(listComposerMcpItems({ sessionId: session?.id, agent, skills: [...skillsByName.values()] })
|
||||
.filter((item) => item.kind === 'mcp' && item.itemType === 'server' && item.state !== 'declared')
|
||||
.map((item) => normalizeMcpServerName(item.server || item.name)));
|
||||
|
||||
const re = /(^|\s)([@$])([^\s]+)/g;
|
||||
let match;
|
||||
@@ -1989,7 +2367,7 @@ function resolveComposerDecorators(text, session, agent) {
|
||||
const skill = skillsByName.get(key);
|
||||
if (skill && !seenSkillNames.has(key)) {
|
||||
seenSkillNames.add(key);
|
||||
mentions.push({ kind: 'skill', name: skill.name, label: `$${skill.name}`, source: skill.source || '' });
|
||||
mentions.push(summarizeSkillForMention(skill, configuredMcpNames));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user