mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(codex): support slash controls and skill discovery (#545)
* feat(codex): resolve slash controls before sending to Codex * feat(codex): discover commands and skills * fix(codex): handle slash commands before attachments * fix(codex): block remaining unsupported built-ins
This commit is contained in:
@@ -199,20 +199,12 @@ export function HappyComposer(props: {
|
||||
markSkillUsed(suggestion.text.slice(1))
|
||||
}
|
||||
|
||||
// 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 !== 'builtin' && suggestion.content) {
|
||||
textToInsert = suggestion.content
|
||||
addSpace = false
|
||||
}
|
||||
|
||||
const result = applySuggestion(
|
||||
inputState.text,
|
||||
inputState.selection,
|
||||
textToInsert,
|
||||
suggestion.text,
|
||||
autocompletePrefixes,
|
||||
addSpace
|
||||
true
|
||||
)
|
||||
|
||||
api.composer().setText(result.text)
|
||||
@@ -233,7 +225,7 @@ export function HappyComposer(props: {
|
||||
}, 0)
|
||||
|
||||
haptic('light')
|
||||
}, [api, suggestions, inputState, autocompletePrefixes, haptic, agentFlavor])
|
||||
}, [api, suggestions, inputState, autocompletePrefixes, haptic])
|
||||
|
||||
const abortDisabled = controlsDisabled || isAborting || !threadIsRunning
|
||||
const switchDisabled = controlsDisabled || isSwitching || !controlledByUser
|
||||
|
||||
@@ -20,8 +20,6 @@ import { HappyComposer } from '@/components/AssistantChat/HappyComposer'
|
||||
import { HappyThread } from '@/components/AssistantChat/HappyThread'
|
||||
import { useHappyRuntime } from '@/lib/assistant-runtime'
|
||||
import { createAttachmentAdapter } from '@/lib/attachmentAdapter'
|
||||
import { findUnsupportedCodexBuiltinSlashCommand } from '@/lib/codexSlashCommands'
|
||||
import { useToast } from '@/lib/toast-context'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import { SessionHeader } from '@/components/SessionHeader'
|
||||
import { TeamPanel } from '@/components/TeamPanel'
|
||||
@@ -67,7 +65,6 @@ export function SessionChat(props: {
|
||||
availableSlashCommands?: readonly SlashCommand[]
|
||||
}) {
|
||||
const { haptic } = usePlatform()
|
||||
const { addToast } = useToast()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const sessionInactive = !props.session.active
|
||||
@@ -349,26 +346,9 @@ export function SessionChat(props: {
|
||||
}, [navigate, props.session.id])
|
||||
|
||||
const handleSend = useCallback((text: string, attachments?: AttachmentMetadata[]) => {
|
||||
if (agentFlavor === 'codex') {
|
||||
const unsupportedCommand = findUnsupportedCodexBuiltinSlashCommand(
|
||||
text,
|
||||
props.availableSlashCommands ?? []
|
||||
)
|
||||
if (unsupportedCommand) {
|
||||
haptic.notification('error')
|
||||
addToast({
|
||||
title: t('composer.codexSlashUnsupported.title'),
|
||||
body: t('composer.codexSlashUnsupported.body', { command: `/${unsupportedCommand}` }),
|
||||
sessionId: props.session.id,
|
||||
url: `/sessions/${props.session.id}`
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
props.onSend(text, attachments)
|
||||
setForceScrollToken((token) => token + 1)
|
||||
}, [agentFlavor, props.availableSlashCommands, props.onSend, props.session.id, addToast, haptic, t])
|
||||
}, [props.onSend])
|
||||
|
||||
const attachmentAdapter = useMemo(() => {
|
||||
if (!props.session.active) {
|
||||
|
||||
@@ -49,16 +49,18 @@ export function useSlashCommands(
|
||||
retry: false, // Don't retry RPC failures
|
||||
})
|
||||
|
||||
// Merge built-in commands with user-defined and plugin commands from API
|
||||
// Merge local built-ins with commands discovered by the active CLI.
|
||||
// The CLI can expose agent-specific built-ins plus user/plugin/project commands;
|
||||
// keep local built-ins as an offline fallback, then append/override from RPC.
|
||||
const commands = useMemo(() => {
|
||||
const builtin = getBuiltinSlashCommands(agentType)
|
||||
|
||||
// If API succeeded, add user-defined and plugin commands
|
||||
if (query.data?.success && query.data.commands) {
|
||||
const extraCommands = query.data.commands.filter(
|
||||
cmd => cmd.source === 'user' || cmd.source === 'plugin' || cmd.source === 'project'
|
||||
)
|
||||
return [...builtin, ...extraCommands]
|
||||
const commandMap = new Map<string, SlashCommand>()
|
||||
for (const command of [...builtin, ...query.data.commands]) {
|
||||
commandMap.set(command.name, command)
|
||||
}
|
||||
return Array.from(commandMap.values())
|
||||
}
|
||||
|
||||
// Fallback to built-in commands only
|
||||
|
||||
@@ -2,14 +2,19 @@ import { describe, expect, it } from 'vitest'
|
||||
import { findUnsupportedCodexBuiltinSlashCommand, getBuiltinSlashCommands } from './codexSlashCommands'
|
||||
|
||||
describe('getBuiltinSlashCommands', () => {
|
||||
it('does not expose codex built-ins in remote web mode', () => {
|
||||
expect(getBuiltinSlashCommands('codex')).toEqual([])
|
||||
it('exposes HAPI-supported codex built-ins in remote web mode', () => {
|
||||
expect(getBuiltinSlashCommands('codex').map((command) => command.name)).toEqual(expect.arrayContaining([
|
||||
'plan',
|
||||
'status',
|
||||
'execute',
|
||||
'effort',
|
||||
'permission',
|
||||
]))
|
||||
})
|
||||
})
|
||||
|
||||
describe('findUnsupportedCodexBuiltinSlashCommand', () => {
|
||||
it('detects unsupported codex built-ins', () => {
|
||||
expect(findUnsupportedCodexBuiltinSlashCommand('/status', [])).toBe('status')
|
||||
expect(findUnsupportedCodexBuiltinSlashCommand(' /diff ', [])).toBe('diff')
|
||||
})
|
||||
|
||||
|
||||
@@ -11,9 +11,18 @@ const BUILTIN_COMMANDS: Record<string, SlashCommand[]> = {
|
||||
{ name: 'stats', description: 'Show your Claude Code usage statistics and activity', source: 'builtin' },
|
||||
{ name: 'status', description: 'Show Claude Code status including version, model, account, and API connectivity', source: 'builtin' },
|
||||
],
|
||||
// Codex remote turns send slash-prefixed input as plain text to app-server.
|
||||
// Hide built-ins here until remote slash command execution is implemented end-to-end.
|
||||
codex: [],
|
||||
codex: [
|
||||
{ name: 'help', description: 'Show supported HAPI Codex slash commands', source: 'builtin' },
|
||||
{ name: 'plan', description: 'Enable plan mode; use /plan off to return to default', source: 'builtin' },
|
||||
{ name: 'default', description: 'Return Codex collaboration mode to default', source: 'builtin' },
|
||||
{ name: 'execute', description: 'Return Codex collaboration mode to default', source: 'builtin' },
|
||||
{ name: 'status', description: 'Show current Codex session config', source: 'builtin' },
|
||||
{ name: 'model', description: 'Show or set Codex model, e.g. /model gpt-5.5', source: 'builtin' },
|
||||
{ name: 'reasoning', description: 'Show or set reasoning effort', source: 'builtin' },
|
||||
{ name: 'effort', description: 'Alias for /reasoning', source: 'builtin' },
|
||||
{ name: 'permissions', description: 'Show or set permission mode', source: 'builtin' },
|
||||
{ name: 'permission', description: 'Alias for /permissions', source: 'builtin' },
|
||||
],
|
||||
gemini: [
|
||||
{ name: 'about', description: 'Show version info', source: 'builtin' },
|
||||
{ name: 'clear', description: 'Clear the screen and conversation history', source: 'builtin' },
|
||||
@@ -29,7 +38,6 @@ const UNSUPPORTED_CODEX_BUILTIN_COMMANDS = new Set([
|
||||
'compat',
|
||||
'undo',
|
||||
'diff',
|
||||
'status',
|
||||
])
|
||||
|
||||
export function getBuiltinSlashCommands(agentType: string): SlashCommand[] {
|
||||
|
||||
Reference in New Issue
Block a user