feat: add real-time permission and model mode synchronization close #21

Implements bidirectional sync of permission/model modes between CLI sessions and web app. Adds Codex-specific permission modes (read-only, safe-yolo, yolo) alongside Claude's modes. Web can now control CLI session state via RPC set-session-config handler, while CLI broadcasts state changes through keep-alive payloads. UI controls are flavor-aware, showing appropriate modes for Claude vs Codex vs Gemini. Type centralization in api/types eliminates circular dependencies.
This commit is contained in:
weishu
2025-12-28 20:51:18 +08:00
parent 2dc09a80db
commit 334cebc2da
21 changed files with 361 additions and 120 deletions
+35 -28
View File
@@ -25,6 +25,7 @@ export { emitReadyIfIdle } from './utils/emitReadyIfIdle';
export async function runCodex(opts: {
startedBy?: 'daemon' | 'terminal';
codexArgs?: string[];
permissionMode?: PermissionMode;
}): Promise<void> {
const workingDirectory = process.cwd();
const sessionTag = randomUUID();
@@ -99,43 +100,29 @@ export async function runCodex(opts: {
}));
const codexCliOverrides = parseCodexCliOverrides(opts.codexArgs);
const sessionWrapperRef: { current: CodexSession | null } = { current: null };
let currentPermissionMode: PermissionMode | undefined = undefined;
let currentModel: string | undefined = undefined;
let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default';
const syncSessionMode = () => {
const sessionInstance = sessionWrapperRef.current;
if (!sessionInstance) {
return;
}
sessionInstance.setPermissionMode(currentPermissionMode);
logger.debug(`[Codex] Synced session permission mode for keepalive: ${currentPermissionMode}`);
};
session.onUserMessage((message) => {
let messagePermissionMode = currentPermissionMode;
if (message.meta?.permissionMode) {
const validModes: PermissionMode[] = ['default', 'read-only', 'safe-yolo', 'yolo'];
if (validModes.includes(message.meta.permissionMode as PermissionMode)) {
messagePermissionMode = message.meta.permissionMode as PermissionMode;
currentPermissionMode = messagePermissionMode;
logger.debug(`[Codex] Permission mode updated from user message to: ${currentPermissionMode}`);
} else {
logger.debug(`[Codex] Invalid permission mode received: ${message.meta.permissionMode}`);
}
} else {
logger.debug(`[Codex] User message received with no permission mode override, using current: ${currentPermissionMode ?? 'default (effective)'}`);
}
let messageModel = currentModel;
if (message.meta?.hasOwnProperty('model')) {
messageModel = message.meta.model || undefined;
currentModel = messageModel;
logger.debug(`[Codex] Model updated from user message: ${messageModel || 'reset to default'}`);
} else {
logger.debug(`[Codex] User message received with no model override, using current: ${currentModel || 'default'}`);
}
const messagePermissionMode = currentPermissionMode;
logger.debug(`[Codex] User message received with permission mode: ${currentPermissionMode}`);
const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode || 'default',
model: messageModel
permissionMode: messagePermissionMode ?? 'default'
};
messageQueue.push(message.content.text, enhancedMode);
});
const sessionWrapperRef: { current: CodexSession | null } = { current: null };
let cleanupStarted = false;
let exitCode = 0;
let archiveReason = 'User terminated';
@@ -200,6 +187,24 @@ export async function runCodex(opts: {
registerKillSessionHandler(session.rpcHandlerManager, cleanup);
session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid session config payload');
}
const config = payload as { permissionMode?: PermissionMode };
if (config.permissionMode !== undefined) {
const validModes: PermissionMode[] = ['default', 'read-only', 'safe-yolo', 'yolo'];
if (!validModes.includes(config.permissionMode)) {
throw new Error('Invalid permission mode');
}
currentPermissionMode = config.permissionMode;
}
syncSessionMode();
return { applied: { permissionMode: currentPermissionMode } };
});
let loopError: unknown = null;
try {
await loop({
@@ -211,6 +216,7 @@ export async function runCodex(opts: {
codexArgs: opts.codexArgs,
codexCliOverrides,
startedBy,
permissionMode: currentPermissionMode,
onModeChange: (newMode) => {
session.sendSessionEvent({ type: 'switch', mode: newMode });
session.updateAgentState((currentState) => ({
@@ -220,6 +226,7 @@ export async function runCodex(opts: {
},
onSessionReady: (instance) => {
sessionWrapperRef.current = instance;
syncSessionMode();
}
});
} catch (error) {