mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-21 21:29:47 +00:00
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:
@@ -24,6 +24,7 @@ interface LoopOptions {
|
||||
api: ApiClient;
|
||||
codexArgs?: string[];
|
||||
codexCliOverrides?: CodexCliOverrides;
|
||||
permissionMode?: PermissionMode;
|
||||
onSessionReady?: (session: CodexSession) => void;
|
||||
}
|
||||
|
||||
@@ -43,7 +44,8 @@ export async function loop(opts: LoopOptions): Promise<void> {
|
||||
startedBy,
|
||||
startingMode,
|
||||
codexArgs: opts.codexArgs,
|
||||
codexCliOverrides: opts.codexCliOverrides
|
||||
codexCliOverrides: opts.codexCliOverrides,
|
||||
permissionMode: opts.permissionMode ?? 'default'
|
||||
});
|
||||
|
||||
if (opts.onSessionReady) {
|
||||
|
||||
+35
-28
@@ -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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiClient, ApiSessionClient } from '@/lib';
|
||||
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
||||
import { AgentSessionBase } from '@/agent/sessionBase';
|
||||
import type { EnhancedMode } from './loop';
|
||||
import type { EnhancedMode, PermissionMode } from './loop';
|
||||
import type { CodexCliOverrides } from './utils/codexCliOverrides';
|
||||
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
|
||||
|
||||
@@ -30,6 +30,7 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
|
||||
startingMode: 'local' | 'remote';
|
||||
codexArgs?: string[];
|
||||
codexCliOverrides?: CodexCliOverrides;
|
||||
permissionMode?: PermissionMode;
|
||||
}) {
|
||||
super({
|
||||
api: opts.api,
|
||||
@@ -45,15 +46,21 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
|
||||
applySessionIdToMetadata: (metadata, sessionId) => ({
|
||||
...metadata,
|
||||
codexSessionId: sessionId
|
||||
})
|
||||
}),
|
||||
permissionMode: opts.permissionMode
|
||||
});
|
||||
|
||||
this.codexArgs = opts.codexArgs;
|
||||
this.codexCliOverrides = opts.codexCliOverrides;
|
||||
this.startedBy = opts.startedBy;
|
||||
this.startingMode = opts.startingMode;
|
||||
this.permissionMode = opts.permissionMode;
|
||||
}
|
||||
|
||||
setPermissionMode = (mode: PermissionMode): void => {
|
||||
this.permissionMode = mode;
|
||||
};
|
||||
|
||||
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
|
||||
this.localLaunchFailure = { message, exitReason };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user