mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +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:
@@ -6,6 +6,7 @@ import { Session } from "./session"
|
||||
import { claudeLocalLauncher } from "./claudeLocalLauncher"
|
||||
import { claudeRemoteLauncher } from "./claudeRemoteLauncher"
|
||||
import { ApiClient } from "@/lib"
|
||||
import type { SessionModelMode } from "@/api/types"
|
||||
|
||||
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
|
||||
|
||||
@@ -43,6 +44,9 @@ export async function loop(opts: LoopOptions) {
|
||||
const logPath = logger.logFilePath;
|
||||
const startedBy = opts.startedBy ?? 'terminal';
|
||||
const startingMode = opts.startingMode ?? 'local';
|
||||
const modelMode: SessionModelMode = opts.model === 'sonnet' || opts.model === 'opus'
|
||||
? opts.model
|
||||
: 'default';
|
||||
let session = new Session({
|
||||
api: opts.api,
|
||||
client: opts.session,
|
||||
@@ -58,7 +62,9 @@ export async function loop(opts: LoopOptions) {
|
||||
mode: startingMode,
|
||||
startedBy,
|
||||
startingMode,
|
||||
hookSettingsPath: opts.hookSettingsPath
|
||||
hookSettingsPath: opts.hookSettingsPath,
|
||||
permissionMode: opts.permissionMode ?? 'default',
|
||||
modelMode
|
||||
});
|
||||
|
||||
// Notify that session is ready
|
||||
|
||||
+55
-30
@@ -5,7 +5,7 @@ import { ApiClient } from '@/api/api';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { restoreTerminalState } from '@/ui/terminalState';
|
||||
import { loop } from '@/claude/loop';
|
||||
import { AgentState, Metadata } from '@/api/types';
|
||||
import { AgentState, Metadata, SessionModelMode } from '@/api/types';
|
||||
import packageJson from '../../package.json';
|
||||
import { readSettings } from '@/persistence';
|
||||
import { EnhancedMode, PermissionMode } from './loop';
|
||||
@@ -190,40 +190,37 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
}));
|
||||
|
||||
// Forward messages to the queue
|
||||
let currentPermissionMode = options.permissionMode;
|
||||
let currentPermissionMode: PermissionMode = options.permissionMode ?? 'default';
|
||||
let currentModel = options.model; // Track current model state
|
||||
let currentModelMode: SessionModelMode = currentModel === 'sonnet' || currentModel === 'opus' ? currentModel : 'default';
|
||||
let currentFallbackModel: string | undefined = undefined; // Track current fallback model
|
||||
let currentCustomSystemPrompt: string | undefined = undefined; // Track current custom system prompt
|
||||
let currentAppendSystemPrompt: string | undefined = undefined; // Track current append system prompt
|
||||
let currentAllowedTools: string[] | undefined = undefined; // Track current allowed tools
|
||||
let currentDisallowedTools: string[] | undefined = undefined; // Track current disallowed tools
|
||||
|
||||
const syncSessionModes = () => {
|
||||
const sessionInstance = currentSessionRef.current;
|
||||
if (!sessionInstance) {
|
||||
return;
|
||||
}
|
||||
sessionInstance.setPermissionMode(currentPermissionMode);
|
||||
sessionInstance.setModelMode(currentModelMode);
|
||||
logger.debug(`[loop] Synced session modes for keepalive: permissionMode=${currentPermissionMode}, modelMode=${currentModelMode}`);
|
||||
};
|
||||
session.onUserMessage((message) => {
|
||||
|
||||
// Resolve permission mode from meta
|
||||
let messagePermissionMode = currentPermissionMode;
|
||||
if (message.meta?.permissionMode) {
|
||||
const validModes: PermissionMode[] = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
||||
if (validModes.includes(message.meta.permissionMode as PermissionMode)) {
|
||||
messagePermissionMode = message.meta.permissionMode as PermissionMode;
|
||||
currentPermissionMode = messagePermissionMode;
|
||||
logger.debug(`[loop] Permission mode updated from user message to: ${currentPermissionMode}`);
|
||||
|
||||
} else {
|
||||
logger.debug(`[loop] Invalid permission mode received: ${message.meta.permissionMode}`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[loop] User message received with no permission mode override, using current: ${currentPermissionMode}`);
|
||||
}
|
||||
|
||||
// Resolve model - use message.meta.model if provided, otherwise use current model
|
||||
let messageModel = currentModel;
|
||||
if (message.meta?.hasOwnProperty('model')) {
|
||||
messageModel = message.meta.model || undefined; // null becomes undefined
|
||||
currentModel = messageModel;
|
||||
logger.debug(`[loop] Model updated from user message: ${messageModel || 'reset to default'}`);
|
||||
} else {
|
||||
logger.debug(`[loop] User message received with no model override, using current: ${currentModel || 'default'}`);
|
||||
const sessionPermissionMode = currentSessionRef.current?.getPermissionMode();
|
||||
if (
|
||||
sessionPermissionMode === 'default'
|
||||
|| sessionPermissionMode === 'acceptEdits'
|
||||
|| sessionPermissionMode === 'bypassPermissions'
|
||||
|| sessionPermissionMode === 'plan'
|
||||
) {
|
||||
currentPermissionMode = sessionPermissionMode;
|
||||
}
|
||||
const messagePermissionMode = currentPermissionMode;
|
||||
const messageModel = currentModel;
|
||||
logger.debug(`[loop] User message received with permission mode: ${currentPermissionMode}, model: ${currentModelMode}`);
|
||||
|
||||
// Resolve custom system prompt - use message.meta.customSystemPrompt if provided, otherwise use current
|
||||
let messageCustomSystemPrompt = currentCustomSystemPrompt;
|
||||
@@ -281,7 +278,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
if (specialCommand.type === 'compact') {
|
||||
logger.debug('[start] Detected /compact command');
|
||||
const enhancedMode: EnhancedMode = {
|
||||
permissionMode: messagePermissionMode || 'default',
|
||||
permissionMode: messagePermissionMode ?? 'default',
|
||||
model: messageModel,
|
||||
fallbackModel: messageFallbackModel,
|
||||
customSystemPrompt: messageCustomSystemPrompt,
|
||||
@@ -297,7 +294,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
if (specialCommand.type === 'clear') {
|
||||
logger.debug('[start] Detected /clear command');
|
||||
const enhancedMode: EnhancedMode = {
|
||||
permissionMode: messagePermissionMode || 'default',
|
||||
permissionMode: messagePermissionMode ?? 'default',
|
||||
model: messageModel,
|
||||
fallbackModel: messageFallbackModel,
|
||||
customSystemPrompt: messageCustomSystemPrompt,
|
||||
@@ -312,7 +309,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
|
||||
// Push with resolved permission mode, model, system prompts, and tools
|
||||
const enhancedMode: EnhancedMode = {
|
||||
permissionMode: messagePermissionMode || 'default',
|
||||
permissionMode: messagePermissionMode ?? 'default',
|
||||
model: messageModel,
|
||||
fallbackModel: messageFallbackModel,
|
||||
customSystemPrompt: messageCustomSystemPrompt,
|
||||
@@ -383,6 +380,33 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
|
||||
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; modelMode?: SessionModelMode };
|
||||
|
||||
if (config.permissionMode !== undefined) {
|
||||
const validModes: PermissionMode[] = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
||||
if (!validModes.includes(config.permissionMode)) {
|
||||
throw new Error('Invalid permission mode');
|
||||
}
|
||||
currentPermissionMode = config.permissionMode;
|
||||
}
|
||||
|
||||
if (config.modelMode !== undefined) {
|
||||
const validModels: SessionModelMode[] = ['default', 'sonnet', 'opus'];
|
||||
if (!validModels.includes(config.modelMode)) {
|
||||
throw new Error('Invalid model mode');
|
||||
}
|
||||
currentModelMode = config.modelMode;
|
||||
currentModel = config.modelMode === 'default' ? undefined : config.modelMode;
|
||||
}
|
||||
|
||||
syncSessionModes();
|
||||
return { applied: { permissionMode: currentPermissionMode, modelMode: currentModelMode } };
|
||||
});
|
||||
|
||||
// Create claude loop
|
||||
await loop({
|
||||
path: workingDirectory,
|
||||
@@ -401,6 +425,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
|
||||
},
|
||||
onSessionReady: (sessionInstance) => {
|
||||
currentSessionRef.current = sessionInstance;
|
||||
syncSessionModes();
|
||||
},
|
||||
mcpServers: {
|
||||
'hapi': {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ApiClient, ApiSessionClient } from '@/lib';
|
||||
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { AgentSessionBase } from '@/agent/sessionBase';
|
||||
import type { SessionModelMode } from '@/api/types';
|
||||
import type { EnhancedMode } from './loop';
|
||||
import type { PermissionMode } from './loop';
|
||||
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
|
||||
|
||||
type LocalLaunchFailure = {
|
||||
@@ -36,6 +38,8 @@ export class Session extends AgentSessionBase<EnhancedMode> {
|
||||
startedBy: 'daemon' | 'terminal';
|
||||
startingMode: 'local' | 'remote';
|
||||
hookSettingsPath: string;
|
||||
permissionMode?: PermissionMode;
|
||||
modelMode?: SessionModelMode;
|
||||
}) {
|
||||
super({
|
||||
api: opts.api,
|
||||
@@ -51,7 +55,9 @@ export class Session extends AgentSessionBase<EnhancedMode> {
|
||||
applySessionIdToMetadata: (metadata, sessionId) => ({
|
||||
...metadata,
|
||||
claudeSessionId: sessionId
|
||||
})
|
||||
}),
|
||||
permissionMode: opts.permissionMode,
|
||||
modelMode: opts.modelMode
|
||||
});
|
||||
|
||||
this.claudeEnvVars = opts.claudeEnvVars;
|
||||
@@ -61,8 +67,18 @@ export class Session extends AgentSessionBase<EnhancedMode> {
|
||||
this.hookSettingsPath = opts.hookSettingsPath;
|
||||
this.startedBy = opts.startedBy;
|
||||
this.startingMode = opts.startingMode;
|
||||
this.permissionMode = opts.permissionMode;
|
||||
this.modelMode = opts.modelMode;
|
||||
}
|
||||
|
||||
setPermissionMode = (mode: PermissionMode): void => {
|
||||
this.permissionMode = mode;
|
||||
};
|
||||
|
||||
setModelMode = (mode: SessionModelMode): void => {
|
||||
this.modelMode = mode;
|
||||
};
|
||||
|
||||
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
|
||||
this.localLaunchFailure = { message, exitReason };
|
||||
};
|
||||
|
||||
@@ -121,6 +121,7 @@ export class PermissionHandler {
|
||||
|
||||
handleModeChange(mode: PermissionMode) {
|
||||
this.permissionMode = mode;
|
||||
this.session.setPermissionMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,6 +149,7 @@ export class PermissionHandler {
|
||||
// Update permission mode
|
||||
if (response.mode) {
|
||||
this.permissionMode = response.mode;
|
||||
this.session.setPermissionMode(response.mode);
|
||||
}
|
||||
|
||||
// Handle
|
||||
|
||||
Reference in New Issue
Block a user