Files
hapi/cli/src/codex/utils/slashCommands.ts
T
bbe99f5c4c feat(codex): /personality slash + in-session app-server params (#1265)
* feat(codex): support /personality via in-session override

Intercept /personality in the Codex slash layer, keep the value in CLI
memory only, and forward it on thread/turn start when set. Unset means
omit the field so Codex config/thread defaults apply—no Hub DB or web UI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(codex): refuse fake /personality clear (sticky thread setting)

turn/start.personality sticks for later turns; omitting the field does not
restore config.toml. Drop default|auto|clear success paths and require an
explicit friendly|pragmatic|none instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 23:26:32 +08:00

337 lines
11 KiB
TypeScript

import { CODEX_PERMISSION_MODES } from '@hapi/protocol/modes';
import type { CodexPermissionMode } from '@hapi/protocol/types';
import type { ReasoningEffort } from '../appServerTypes';
import type { CodexPersonality, EnhancedMode } from '../loop';
import type { SlashCommand } from '@/modules/common/slashCommands';
import { parseReasoningEffortValue } from './reasoningEffort';
export const MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4_000;
const CODEX_PERSONALITIES = ['friendly', 'pragmatic', 'none'] as const satisfies readonly CodexPersonality[];
const UNSUPPORTED_CODEX_BUILTIN_COMMANDS = new Set([
'compat',
'diff',
'init',
'login',
'logout',
'mcp',
'new',
'prompts',
'quit',
'redo',
'review',
'undo'
]);
export type CodexSlashResolution =
| { kind: 'passthrough' }
| {
kind: 'handled';
message: string;
updates?: {
collaborationMode?: EnhancedMode['collaborationMode'];
permissionMode?: CodexPermissionMode;
model?: string | null;
modelReasoningEffort?: ReasoningEffort | null;
serviceTier?: string | null;
proactiveMultiAgent?: boolean;
personality?: CodexPersonality;
};
}
| {
kind: 'replace';
text: string;
message?: string;
updates?: {
collaborationMode?: EnhancedMode['collaborationMode'];
permissionMode?: CodexPermissionMode;
model?: string | null;
modelReasoningEffort?: ReasoningEffort | null;
serviceTier?: string | null;
proactiveMultiAgent?: boolean;
personality?: CodexPersonality;
};
}
| {
kind: 'goal';
action: 'show' | 'set' | 'pause' | 'resume' | 'clear';
objective?: string;
message?: string;
};
export function resolveCodexSlashCommand(
text: string,
state: {
commands?: readonly SlashCommand[];
permissionMode: CodexPermissionMode;
collaborationMode: EnhancedMode['collaborationMode'];
model?: string;
modelReasoningEffort?: ReasoningEffort;
serviceTier?: string | null;
proactiveMultiAgent?: boolean;
personality?: CodexPersonality;
}
): CodexSlashResolution {
const match = /^\s*\/([a-z0-9:_-]+)(?:\s+([\s\S]*))?$/i.exec(text);
if (!match) return { kind: 'passthrough' };
const command = match[1]?.toLowerCase();
const rest = match[2]?.trim() ?? '';
if (!command) return { kind: 'passthrough' };
const custom = state.commands?.find((candidate) =>
candidate.source !== 'builtin' && candidate.name.toLowerCase() === command
);
if (custom?.content) {
return {
kind: 'replace',
text: rest ? `${custom.content}\n\nUser arguments: ${rest}` : custom.content,
message: `Expanded /${custom.name}`
};
}
if (command === 'plan') {
const lowerRest = rest.toLowerCase();
if (lowerRest === 'off' || lowerRest === 'default' || lowerRest === 'exit' || lowerRest === 'disable') {
return {
kind: 'handled',
message: 'Codex plan mode disabled',
updates: { collaborationMode: 'default' }
};
}
if (rest) {
return {
kind: 'replace',
text: rest,
message: 'Codex plan mode enabled',
updates: { collaborationMode: 'plan' }
};
}
return {
kind: 'handled',
message: 'Codex plan mode enabled',
updates: { collaborationMode: 'plan' }
};
}
if (command === 'agent') {
const value = rest.toLowerCase();
if (value === 'status') {
return {
kind: 'handled',
message: `Codex proactive multi-agent mode: ${state.proactiveMultiAgent ? 'on' : 'off'}`
};
}
if (value && !['on', 'enable', 'enabled', 'off', 'disable', 'disabled'].includes(value)) {
return {
kind: 'handled',
message: 'Usage: /agent [on|off|status]'
};
}
const enabled = value
? ['on', 'enable', 'enabled'].includes(value)
: !state.proactiveMultiAgent;
return {
kind: 'handled',
message: `Codex proactive multi-agent mode ${enabled ? 'enabled' : 'disabled'}`,
updates: { proactiveMultiAgent: enabled }
};
}
if (command === 'goal') {
const lowerRest = rest.toLowerCase();
if (!rest) {
return { kind: 'goal', action: 'show' };
}
if (lowerRest === 'clear') {
return { kind: 'goal', action: 'clear' };
}
if (lowerRest === 'pause') {
return { kind: 'goal', action: 'pause' };
}
if (lowerRest === 'resume') {
return { kind: 'goal', action: 'resume' };
}
const objective = rest.trim();
if (!objective) {
return {
kind: 'handled',
message: 'Goal objective must not be empty.'
};
}
if ([...objective].length > MAX_CODEX_GOAL_OBJECTIVE_CHARS) {
return {
kind: 'handled',
message: `Goal objective must be at most ${MAX_CODEX_GOAL_OBJECTIVE_CHARS} characters.`
};
}
return {
kind: 'goal',
action: 'set',
objective
};
}
if (command === 'default' || command === 'execute') {
return {
kind: 'handled',
message: 'Codex collaboration mode: default',
updates: { collaborationMode: 'default' }
};
}
if (command === 'status') {
return {
kind: 'handled',
message: [
'**Codex status**',
'',
`- permission: \`${state.permissionMode}\``,
`- collaboration: \`${state.collaborationMode}\``,
`- model: \`${state.model ?? 'auto'}\``,
`- reasoning: \`${state.modelReasoningEffort ?? 'default'}\``,
`- personality: \`${state.personality ?? 'unset'}\``
].join('\n')
};
}
if (command === 'personality') {
if (!rest) {
return {
kind: 'handled',
message: `Codex personality: ${state.personality ?? 'unset (Codex config / thread sticky)'}`
};
}
// turn/start.personality sticks for subsequent turns; omitting later does not
// restore config.toml. Only explicit friendly|pragmatic|none are valid.
if (rest === 'default' || rest === 'auto' || rest === 'clear') {
return {
kind: 'handled',
message: 'Codex personality is sticky on the thread; set friendly, pragmatic, or none (cannot restore config.toml by clearing)'
};
}
if (!(CODEX_PERSONALITIES as readonly string[]).includes(rest)) {
return {
kind: 'handled',
message: `Unknown Codex personality: ${rest}`
};
}
return {
kind: 'handled',
message: `Codex personality set to ${rest}`,
updates: { personality: rest as CodexPersonality }
};
}
if (command === 'model') {
if (!rest) {
return { kind: 'handled', message: `Codex model: ${state.model ?? 'auto'}` };
}
const model = rest === 'auto' ? null : rest;
return {
kind: 'handled',
message: `Codex model set to ${model ?? 'auto'}`,
updates: { model }
};
}
if (command === 'reasoning' || command === 'effort') {
if (!rest) {
return { kind: 'handled', message: `Codex reasoning effort: ${state.modelReasoningEffort ?? 'default'}` };
}
if (rest === 'default' || rest === 'auto') {
return {
kind: 'handled',
message: 'Codex reasoning effort set to default',
updates: { modelReasoningEffort: null }
};
}
const effort = parseReasoningEffortValue(rest);
return {
kind: 'handled',
message: `Codex reasoning effort set to ${effort}`,
updates: { modelReasoningEffort: effort }
};
}
if (command === 'fast') {
const arg = rest.toLowerCase();
if (arg === '' || arg === 'on') {
return {
kind: 'handled',
message: 'Codex Fast mode enabled',
updates: { serviceTier: 'fast' }
};
}
if (arg === 'off') {
return {
kind: 'handled',
message: 'Codex Fast mode disabled',
updates: { serviceTier: 'standard' }
};
}
if (arg === 'status') {
const on = state.serviceTier === 'fast';
return {
kind: 'handled',
message: `Codex Fast mode: ${on ? 'on' : 'off'}`
};
}
return {
kind: 'handled',
message: 'Usage: /fast [on|off|status]'
};
}
if (command === 'permissions' || command === 'permission') {
if (!rest) {
return { kind: 'handled', message: `Codex permission mode: ${state.permissionMode}` };
}
if (!(CODEX_PERMISSION_MODES as readonly string[]).includes(rest)) {
return {
kind: 'handled',
message: `Unknown Codex permission mode: ${rest}`
};
}
return {
kind: 'handled',
message: `Codex permission mode set to ${rest}`,
updates: { permissionMode: rest as CodexPermissionMode }
};
}
if (command === 'help') {
return {
kind: 'handled',
message: [
'**Supported Codex slash commands**',
'',
'- `/plan [prompt]` — enable plan mode, optionally send prompt',
'- `/plan off` — return to default mode',
'- `/goal [objective]` — set or view the persistent goal',
'- `/goal pause|resume|clear` — update the current goal',
'- `/clear` — reset current Codex thread context',
'- `/compact` — compact current Codex thread context',
'- `/status` — show current Codex session config',
'- `/model [name|auto]` — show or set model',
'- `/reasoning [level|default]` — show or set reasoning effort',
'- `/personality [friendly|pragmatic|none]` — show or set response style (sticky on thread)',
'- `/fast [on|off|status]` — toggle Fast mode (GPT-5.5 / GPT-5.4, ChatGPT login)',
'- `/permissions [default|read-only|safe-yolo|yolo]` — show or set permission mode',
'',
'Custom `/commands` from `.codex/prompts` are expanded before sending.'
].join('\n')
};
}
if (UNSUPPORTED_CODEX_BUILTIN_COMMANDS.has(command)) {
return {
kind: 'handled',
message: `/${command} is a Codex CLI command that is not supported in HAPI sessions yet.`
};
}
return { kind: 'passthrough' };
}