mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix: expand codex custom prompt
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user