fix: expand codex custom prompt

This commit is contained in:
weishu
2026-01-02 21:45:29 +08:00
parent c112f3f93e
commit 2de3a3d635
7 changed files with 81 additions and 21 deletions
+3
View File
@@ -31,6 +31,7 @@
"react": "^19.2.3",
"socket.io-client": "^4.8.3",
"tar": "^7.5.2",
"yaml": "^2.8.2",
"zod": "^4.2.1",
},
"devDependencies": {
@@ -829,6 +830,8 @@
"@twsxtd/hapi-linux-x64": ["@twsxtd/hapi-linux-x64@0.3.3", "", { "os": "linux", "cpu": "x64", "bin": { "hapi": "bin/hapi" } }, "sha512-c+1iMVNlpyc5eXPXBnybahuH+U1aQAsavyR3DyRpk5EfNKcpp1Df9N4k14IjYvcKo3awIcpElV55n0xIaDDF3A=="],
"@twsxtd/hapi-win32-x64": ["@twsxtd/hapi-win32-x64@0.3.3", "", { "os": "win32", "cpu": "x64", "bin": { "hapi": "bin/hapi.exe" } }, "sha512-hHhzpDtoqD8VaqmfTdBKNGNnzOq3561rlPYJqWc1e1Q3D0lt1v85UiwOSikkxJdn/8DPr9+dfLhYrrrF48iCVA=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+1
View File
@@ -64,6 +64,7 @@
"react": "^19.2.3",
"socket.io-client": "^4.8.3",
"tar": "^7.5.2",
"yaml": "^2.8.2",
"zod": "^4.2.1"
},
"devDependencies": {
+60 -17
View File
@@ -1,11 +1,13 @@
import { readdir } from 'fs/promises';
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
import { homedir } from 'os';
import { parse as parseYaml } from 'yaml';
export interface SlashCommand {
name: string;
description?: string;
source: 'builtin' | 'user';
content?: string; // Expanded content for Codex user prompts
}
export interface ListSlashCommandsRequest {
@@ -42,6 +44,29 @@ const BUILTIN_COMMANDS: Record<string, SlashCommand[]> = {
],
};
/**
* Parse frontmatter from a markdown file content.
* Returns the description (from frontmatter) and the body content.
*/
function parseFrontmatter(fileContent: string): { description?: string; content: string } {
// Match frontmatter: starts with ---, ends with ---
const match = fileContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (match) {
const yamlContent = match[1];
const body = match[2].trim();
try {
const parsed = parseYaml(yamlContent) as Record<string, unknown> | null;
const description = typeof parsed?.description === 'string' ? parsed.description : undefined;
return { description, content: body };
} catch {
// Invalid YAML - the --- block is not valid frontmatter, return entire file
return { content: fileContent.trim() };
}
}
// No frontmatter, entire file is content
return { content: fileContent.trim() };
}
/**
* Get the user commands directory for an agent type.
* Returns null if the agent doesn't support user commands.
@@ -64,6 +89,7 @@ function getUserCommandsDir(agent: string): string | null {
/**
* Scan a directory for user-defined commands (*.md files).
* For Codex, reads file content and parses frontmatter.
* Returns the command names (filename without extension).
*/
async function scanUserCommands(agent: string): Promise<SlashCommand[]> {
@@ -72,29 +98,46 @@ async function scanUserCommands(agent: string): Promise<SlashCommand[]> {
return [];
}
const shouldReadContent = agent === 'codex';
try {
const entries = await readdir(dir, { withFileTypes: true });
const commands: SlashCommand[] = [];
const mdFiles = entries.filter(e => e.isFile() && e.name.endsWith('.md'));
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.md')) continue;
// Read all files in parallel
const commands = await Promise.all(
mdFiles.map(async (entry): Promise<SlashCommand | null> => {
const name = entry.name.slice(0, -3);
if (!name) return null;
// Remove .md extension to get command name
const name = entry.name.slice(0, -3);
if (!name) continue;
const command: SlashCommand = {
name,
description: 'Custom command',
source: 'user',
};
commands.push({
name,
description: 'Custom command',
source: 'user',
});
}
if (shouldReadContent) {
try {
const filePath = join(dir, entry.name);
const fileContent = await readFile(filePath, 'utf-8');
const parsed = parseFrontmatter(fileContent);
if (parsed.description) {
command.description = parsed.description;
}
command.content = parsed.content;
} catch {
// Failed to read file, keep default description
}
}
// Sort alphabetically
commands.sort((a, b) => a.name.localeCompare(b.name));
return command;
})
);
return commands;
// Filter nulls and sort alphabetically
return commands
.filter((cmd): cmd is SlashCommand => cmd !== null)
.sort((a, b) => a.name.localeCompare(b.name));
} catch {
// Directory doesn't exist or not accessible - return empty array
return [];
@@ -154,12 +154,20 @@ export function HappyComposer(props: {
const suggestion = suggestions[index]
if (!suggestion || !textareaRef.current) return
// For Codex user prompts with content, expand the content instead of command name
let textToInsert = suggestion.text
let addSpace = true
if (agentFlavor === 'codex' && suggestion.source === 'user' && suggestion.content) {
textToInsert = suggestion.content
addSpace = false
}
const result = applySuggestion(
inputState.text,
inputState.selection,
suggestion.text,
textToInsert,
autocompletePrefixes,
true
addSpace
)
api.composer().setText(result.text)
@@ -180,7 +188,7 @@ export function HappyComposer(props: {
}, 0)
haptic('light')
}, [api, suggestions, inputState, autocompletePrefixes, haptic])
}, [api, suggestions, inputState, autocompletePrefixes, haptic, agentFlavor])
const abortDisabled = controlsDisabled || isAborting || !threadIsRunning
const switchDisabled = controlsDisabled || isSwitching || !controlledByUser
+3 -1
View File
@@ -89,7 +89,9 @@ export function useSlashCommands(
key: `/${cmd.name}`,
text: `/${cmd.name}`,
label: `/${cmd.name}`,
description: cmd.description ?? (cmd.source === 'user' ? 'Custom command' : undefined)
description: cmd.description ?? (cmd.source === 'user' ? 'Custom command' : undefined),
content: cmd.content,
source: cmd.source
}))
}, [commands])
+2
View File
@@ -5,6 +5,8 @@ export interface Suggestion {
text: string
label: string
description?: string
content?: string // Expanded content for Codex user prompts
source?: 'builtin' | 'user'
}
interface SuggestionOptions {
+1
View File
@@ -189,6 +189,7 @@ export type SlashCommand = {
name: string
description?: string
source: 'builtin' | 'user'
content?: string // Expanded content for Codex user prompts
}
export type SlashCommandsResponse = {