From 2de3a3d6358fc62494050c0c6b5ac1bb8175cb75 Mon Sep 17 00:00:00 2001 From: weishu Date: Fri, 2 Jan 2026 21:45:27 +0800 Subject: [PATCH] fix: expand codex custom prompt --- bun.lock | 3 + cli/package.json | 1 + cli/src/modules/common/slashCommands.ts | 77 +++++++++++++++---- .../AssistantChat/HappyComposer.tsx | 14 +++- web/src/hooks/queries/useSlashCommands.ts | 4 +- web/src/hooks/useActiveSuggestions.ts | 2 + web/src/types/api.ts | 1 + 7 files changed, 81 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index 074c4634..985eadbb 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/cli/package.json b/cli/package.json index 50dc4cc4..c2f89dd5 100644 --- a/cli/package.json +++ b/cli/package.json @@ -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": { diff --git a/cli/src/modules/common/slashCommands.ts b/cli/src/modules/common/slashCommands.ts index 70f4339f..fe64da1c 100644 --- a/cli/src/modules/common/slashCommands.ts +++ b/cli/src/modules/common/slashCommands.ts @@ -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 = { ], }; +/** + * 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 | 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 { @@ -72,29 +98,46 @@ async function scanUserCommands(agent: string): Promise { 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 => { + 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 []; diff --git a/web/src/components/AssistantChat/HappyComposer.tsx b/web/src/components/AssistantChat/HappyComposer.tsx index ab04bde8..96a941ce 100644 --- a/web/src/components/AssistantChat/HappyComposer.tsx +++ b/web/src/components/AssistantChat/HappyComposer.tsx @@ -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 diff --git a/web/src/hooks/queries/useSlashCommands.ts b/web/src/hooks/queries/useSlashCommands.ts index 2cb6be34..a507ed1a 100644 --- a/web/src/hooks/queries/useSlashCommands.ts +++ b/web/src/hooks/queries/useSlashCommands.ts @@ -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]) diff --git a/web/src/hooks/useActiveSuggestions.ts b/web/src/hooks/useActiveSuggestions.ts index c5dcb521..4d14de9d 100644 --- a/web/src/hooks/useActiveSuggestions.ts +++ b/web/src/hooks/useActiveSuggestions.ts @@ -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 { diff --git a/web/src/types/api.ts b/web/src/types/api.ts index 85e03087..c4ec680f 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -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 = {