mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
refactor: share session config RPC handling
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RPC_METHODS } from '@hapi/protocol/rpcMethods'
|
||||
import {
|
||||
registerSessionConfigRpc,
|
||||
resolveNullableSessionModel,
|
||||
resolveSessionConfigPermissionMode
|
||||
} from './sessionConfigRpc'
|
||||
|
||||
function createRpcHarness() {
|
||||
const registerHandler = vi.fn()
|
||||
return {
|
||||
rpcHandlerManager: { registerHandler } as never,
|
||||
getHandler: () => {
|
||||
const call = registerHandler.mock.calls.find((args) => args[0] === RPC_METHODS.SetSessionConfig)
|
||||
expect(call).toBeDefined()
|
||||
return call![1] as (payload: unknown) => Promise<unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('sessionConfigRpc', () => {
|
||||
it('rejects permission modes that are not allowed for the agent flavor', () => {
|
||||
expect(() => resolveSessionConfigPermissionMode('bypassPermissions', 'gemini')).toThrow('Invalid permission mode')
|
||||
})
|
||||
|
||||
it('accepts null model for agents that support model config', () => {
|
||||
expect(resolveNullableSessionModel(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects empty and non-string models', () => {
|
||||
expect(() => resolveNullableSessionModel('')).toThrow('Invalid model')
|
||||
expect(() => resolveNullableSessionModel(' ')).toThrow('Invalid model')
|
||||
expect(() => resolveNullableSessionModel(123)).toThrow('Invalid model')
|
||||
})
|
||||
|
||||
it('applies model null when model config is supported', async () => {
|
||||
const harness = createRpcHarness()
|
||||
const onApply = vi.fn()
|
||||
|
||||
registerSessionConfigRpc({
|
||||
rpcHandlerManager: harness.rpcHandlerManager,
|
||||
flavor: 'opencode',
|
||||
modelMode: 'nullable',
|
||||
onApply
|
||||
})
|
||||
|
||||
const result = await harness.getHandler()({ model: null }) as { applied: Record<string, unknown> }
|
||||
|
||||
expect(result.applied.model).toBeNull()
|
||||
expect(onApply).toHaveBeenCalledWith({ model: null })
|
||||
})
|
||||
|
||||
it('ignores model config for agents that do not support model changes when configured to ignore', async () => {
|
||||
const harness = createRpcHarness()
|
||||
const onApply = vi.fn()
|
||||
|
||||
registerSessionConfigRpc({
|
||||
rpcHandlerManager: harness.rpcHandlerManager,
|
||||
flavor: 'cursor',
|
||||
modelMode: 'ignore',
|
||||
appliedFallback: () => ({ permissionMode: 'default' }),
|
||||
onApply
|
||||
})
|
||||
|
||||
const result = await harness.getHandler()({ model: null }) as { applied: Record<string, unknown> }
|
||||
|
||||
expect(result.applied).toEqual({ permissionMode: 'default' })
|
||||
expect(onApply).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('rejects model config for agents configured to reject model changes', async () => {
|
||||
const harness = createRpcHarness()
|
||||
|
||||
registerSessionConfigRpc({
|
||||
rpcHandlerManager: harness.rpcHandlerManager,
|
||||
flavor: 'cursor',
|
||||
onApply: vi.fn()
|
||||
})
|
||||
|
||||
await expect(harness.getHandler()({ model: null })).rejects.toThrow('Invalid model')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { isPermissionModeAllowedForFlavor, type AgentFlavor } from '@hapi/protocol'
|
||||
import { RPC_METHODS } from '@hapi/protocol/rpcMethods'
|
||||
import { PermissionModeSchema } from '@hapi/protocol/schemas'
|
||||
import type { PermissionMode } from '@hapi/protocol/types'
|
||||
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
|
||||
|
||||
type SessionConfigState<TPermissionMode extends PermissionMode = PermissionMode> = {
|
||||
permissionMode?: TPermissionMode
|
||||
model?: string | null
|
||||
}
|
||||
|
||||
type RegisterSessionConfigRpcOptions<TPermissionMode extends PermissionMode = PermissionMode> = {
|
||||
rpcHandlerManager: RpcHandlerManager
|
||||
flavor: AgentFlavor
|
||||
modelMode?: 'nullable' | 'ignore' | 'reject'
|
||||
appliedFallback?: () => Record<string, unknown>
|
||||
onApply: (config: SessionConfigState<TPermissionMode>) => void
|
||||
onAfterApply?: () => void
|
||||
}
|
||||
|
||||
export function resolveSessionConfigPermissionMode<TPermissionMode extends PermissionMode>(
|
||||
value: unknown,
|
||||
flavor: AgentFlavor
|
||||
): TPermissionMode {
|
||||
const parsed = PermissionModeSchema.safeParse(value)
|
||||
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, flavor)) {
|
||||
throw new Error('Invalid permission mode')
|
||||
}
|
||||
return parsed.data as TPermissionMode
|
||||
}
|
||||
|
||||
export function resolveNullableSessionModel(value: unknown): string | null {
|
||||
if (value === null) {
|
||||
return null
|
||||
}
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
throw new Error('Invalid model')
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
export function registerSessionConfigRpc<TPermissionMode extends PermissionMode>({
|
||||
rpcHandlerManager,
|
||||
flavor,
|
||||
modelMode = 'reject',
|
||||
appliedFallback,
|
||||
onApply,
|
||||
onAfterApply
|
||||
}: RegisterSessionConfigRpcOptions<TPermissionMode>): void {
|
||||
rpcHandlerManager.registerHandler(RPC_METHODS.SetSessionConfig, async (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error('Invalid session config payload')
|
||||
}
|
||||
|
||||
const config = payload as { permissionMode?: unknown; model?: unknown }
|
||||
const applied: Record<string, unknown> = {}
|
||||
const next: SessionConfigState<TPermissionMode> = {}
|
||||
|
||||
if (config.permissionMode !== undefined) {
|
||||
next.permissionMode = resolveSessionConfigPermissionMode<TPermissionMode>(config.permissionMode, flavor)
|
||||
applied.permissionMode = next.permissionMode
|
||||
}
|
||||
|
||||
if (config.model !== undefined) {
|
||||
if (modelMode === 'reject') {
|
||||
throw new Error('Invalid model')
|
||||
}
|
||||
if (modelMode === 'nullable') {
|
||||
next.model = resolveNullableSessionModel(config.model)
|
||||
applied.model = next.model
|
||||
}
|
||||
}
|
||||
|
||||
onApply(next)
|
||||
onAfterApply?.()
|
||||
|
||||
return {
|
||||
applied: Object.keys(applied).length > 0
|
||||
? applied
|
||||
: (appliedFallback?.() ?? applied)
|
||||
}
|
||||
})
|
||||
}
|
||||
+12
-23
@@ -8,9 +8,7 @@ import type { CursorSession } from './session';
|
||||
import { bootstrapExistingSession, bootstrapSession } from '@/agent/sessionFactory';
|
||||
import { registerLocalHandoffHandler } from '@/agent/localHandoff';
|
||||
import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle';
|
||||
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
|
||||
import { RPC_METHODS } from '@hapi/protocol/rpcMethods';
|
||||
import { PermissionModeSchema } from '@hapi/protocol/schemas';
|
||||
import { registerSessionConfigRpc } from '@/agent/sessionConfigRpc';
|
||||
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
|
||||
import { getInvokedCwd } from '@/utils/invokedCwd';
|
||||
|
||||
@@ -105,26 +103,17 @@ export async function runCursor(opts: {
|
||||
return removed;
|
||||
});
|
||||
|
||||
const resolvePermissionMode = (value: unknown): PermissionMode => {
|
||||
const parsed = PermissionModeSchema.safeParse(value);
|
||||
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, 'cursor')) {
|
||||
throw new Error('Invalid permission mode');
|
||||
}
|
||||
return parsed.data as PermissionMode;
|
||||
};
|
||||
|
||||
session.rpcHandlerManager.registerHandler(RPC_METHODS.SetSessionConfig, async (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error('Invalid session config payload');
|
||||
}
|
||||
const config = payload as { permissionMode?: unknown };
|
||||
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = resolvePermissionMode(config.permissionMode);
|
||||
}
|
||||
|
||||
syncSessionMode();
|
||||
return { applied: { permissionMode: currentPermissionMode } };
|
||||
registerSessionConfigRpc<PermissionMode>({
|
||||
rpcHandlerManager: session.rpcHandlerManager,
|
||||
flavor: 'cursor',
|
||||
modelMode: 'ignore',
|
||||
appliedFallback: () => ({ permissionMode: currentPermissionMode }),
|
||||
onApply: (config) => {
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = config.permissionMode;
|
||||
}
|
||||
},
|
||||
onAfterApply: syncSessionMode
|
||||
});
|
||||
|
||||
let crashed = false;
|
||||
|
||||
+15
-41
@@ -12,9 +12,7 @@ import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } f
|
||||
import { startHookServer } from '@/claude/utils/startHookServer';
|
||||
import { cleanupHookSettingsFile, generateHookSettingsFile } from '@/modules/common/hooks/generateHookSettings';
|
||||
import { resolveGeminiRuntimeConfig } from './utils/config';
|
||||
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
|
||||
import { RPC_METHODS } from '@hapi/protocol/rpcMethods';
|
||||
import { PermissionModeSchema } from '@hapi/protocol/schemas';
|
||||
import { registerSessionConfigRpc } from '@/agent/sessionConfigRpc';
|
||||
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
|
||||
import { getInvokedCwd } from '@/utils/invokedCwd';
|
||||
|
||||
@@ -147,44 +145,20 @@ export async function runGemini(opts: {
|
||||
return removed;
|
||||
});
|
||||
|
||||
const resolvePermissionMode = (value: unknown): PermissionMode => {
|
||||
const parsed = PermissionModeSchema.safeParse(value);
|
||||
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, 'gemini')) {
|
||||
throw new Error('Invalid permission mode');
|
||||
}
|
||||
return parsed.data as PermissionMode;
|
||||
};
|
||||
|
||||
const resolveModel = (value: unknown): string | null => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
throw new Error('Invalid model');
|
||||
}
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
session.rpcHandlerManager.registerHandler(RPC_METHODS.SetSessionConfig, async (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error('Invalid session config payload');
|
||||
}
|
||||
const config = payload as { permissionMode?: unknown; model?: unknown };
|
||||
const applied: Record<string, unknown> = {};
|
||||
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = resolvePermissionMode(config.permissionMode);
|
||||
applied.permissionMode = currentPermissionMode;
|
||||
}
|
||||
|
||||
if (config.model !== undefined) {
|
||||
sessionModel = resolveModel(config.model);
|
||||
resolvedModel = sessionModel ?? machineDefault;
|
||||
applied.model = sessionModel;
|
||||
}
|
||||
|
||||
syncSessionMode();
|
||||
return { applied };
|
||||
registerSessionConfigRpc<PermissionMode>({
|
||||
rpcHandlerManager: session.rpcHandlerManager,
|
||||
flavor: 'gemini',
|
||||
modelMode: 'nullable',
|
||||
onApply: (config) => {
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = config.permissionMode;
|
||||
}
|
||||
if (config.model !== undefined) {
|
||||
sessionModel = config.model;
|
||||
resolvedModel = sessionModel ?? machineDefault;
|
||||
}
|
||||
},
|
||||
onAfterApply: syncSessionMode
|
||||
});
|
||||
|
||||
let crashed = false;
|
||||
|
||||
@@ -9,9 +9,7 @@ import type { OpencodeMode, PermissionMode } from './types';
|
||||
import { bootstrapExistingSession, bootstrapSession } from '@/agent/sessionFactory';
|
||||
import { registerLocalHandoffHandler } from '@/agent/localHandoff';
|
||||
import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle';
|
||||
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
|
||||
import { RPC_METHODS } from '@hapi/protocol/rpcMethods';
|
||||
import { PermissionModeSchema } from '@hapi/protocol/schemas';
|
||||
import { registerSessionConfigRpc } from '@/agent/sessionConfigRpc';
|
||||
import { startOpencodeHookServer } from './utils/startOpencodeHookServer';
|
||||
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
|
||||
import { getInvokedCwd } from '@/utils/invokedCwd';
|
||||
@@ -127,43 +125,19 @@ export async function runOpencode(opts: {
|
||||
return removed;
|
||||
});
|
||||
|
||||
const resolvePermissionMode = (value: unknown): PermissionMode => {
|
||||
const parsed = PermissionModeSchema.safeParse(value);
|
||||
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, 'opencode')) {
|
||||
throw new Error('Invalid permission mode');
|
||||
}
|
||||
return parsed.data as PermissionMode;
|
||||
};
|
||||
|
||||
const resolveModel = (value: unknown): string | null => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
throw new Error('Invalid model');
|
||||
}
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
session.rpcHandlerManager.registerHandler(RPC_METHODS.SetSessionConfig, async (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error('Invalid session config payload');
|
||||
}
|
||||
const config = payload as { permissionMode?: unknown; model?: unknown };
|
||||
const applied: Record<string, unknown> = {};
|
||||
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = resolvePermissionMode(config.permissionMode);
|
||||
applied.permissionMode = currentPermissionMode;
|
||||
}
|
||||
|
||||
if (config.model !== undefined) {
|
||||
sessionModel = resolveModel(config.model);
|
||||
applied.model = sessionModel;
|
||||
}
|
||||
|
||||
syncSessionMode();
|
||||
return { applied };
|
||||
registerSessionConfigRpc<PermissionMode>({
|
||||
rpcHandlerManager: session.rpcHandlerManager,
|
||||
flavor: 'opencode',
|
||||
modelMode: 'nullable',
|
||||
onApply: (config) => {
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = config.permissionMode;
|
||||
}
|
||||
if (config.model !== undefined) {
|
||||
sessionModel = config.model;
|
||||
}
|
||||
},
|
||||
onAfterApply: syncSessionMode
|
||||
});
|
||||
|
||||
let crashed = false;
|
||||
|
||||
Reference in New Issue
Block a user