mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): expose Codex Fast and Plan on Create Session (#1017)
* fix(web): expose Codex Fast and Plan on Create Session Wire serviceTier and collaborationMode through spawn so Create can set the same Codex options chat Settings already supports (#1015). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): forward collaborationMode through machine spawn RPC Create Session Plan was accepted by the hub but dropped in apiMachine before buildCliArgs; also preserve collaborationMode on resume spawn. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): correct stopSession mock type in spawn RPC test Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep Fast mode across Create draft restore while models load Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): preserve pending Fast selection * fix: apply Fast and Plan to imported Codex sessions * test: narrow imported Codex session id * fix: forward explicit Standard service tier * fix: integrate create-session controls with current main * test: close Codex RPC suite * fix: preserve existing session spawn field * fix(web): integrate Codex controls with current New Session form * fix(web): reconcile draft types and submit state * fix(hub): integrate spawn arguments with current resume flow * test(cli): isolate spawn RPC suite --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -421,6 +421,66 @@ describe('ApiMachineClient Codex transcript handlers', () => {
|
||||
client.shutdown()
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('ApiMachineClient SpawnHappySession handler', () => {
|
||||
let workspaceRoot: string
|
||||
|
||||
beforeEach(() => {
|
||||
ioMock.mockReset()
|
||||
workspaceRoot = mkdtempSync(join(tmpdir(), 'hapi-machine-spawn-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(workspaceRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function callSpawnHappySession(
|
||||
client: ApiMachineClient,
|
||||
machineId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
const manager = (client as unknown as {
|
||||
rpcHandlerManager: { handleRequest: (req: { method: string; params: string }) => Promise<string> }
|
||||
}).rpcHandlerManager
|
||||
const raw = await manager.handleRequest({
|
||||
method: `${machineId}:spawn-happy-session`,
|
||||
params: JSON.stringify(params)
|
||||
})
|
||||
return JSON.parse(raw) as unknown
|
||||
}
|
||||
|
||||
it('forwards collaborationMode and serviceTier to spawnSession', async () => {
|
||||
const machine = makeMachine('machine-spawn-1')
|
||||
const client = new ApiMachineClient('cli-token', machine, [workspaceRoot])
|
||||
const spawnSession = vi.fn(async () => ({ type: 'success' as const, sessionId: 'session-1' }))
|
||||
|
||||
client.setRPCHandlers({
|
||||
spawnSession,
|
||||
stopSession: vi.fn(() => true),
|
||||
requestShutdown: vi.fn()
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await callSpawnHappySession(client, machine.id, {
|
||||
directory: workspaceRoot,
|
||||
agent: 'codex',
|
||||
serviceTier: 'fast',
|
||||
collaborationMode: 'plan'
|
||||
})
|
||||
|
||||
expect(result).toEqual({ type: 'success', sessionId: 'session-1' })
|
||||
expect(spawnSession).toHaveBeenCalledWith(expect.objectContaining({
|
||||
directory: workspaceRoot,
|
||||
agent: 'codex',
|
||||
serviceTier: 'fast',
|
||||
collaborationMode: 'plan'
|
||||
}))
|
||||
} finally {
|
||||
client.shutdown()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiMachineClient keepAlive lifecycle', () => {
|
||||
|
||||
@@ -357,7 +357,7 @@ export class ApiMachineClient {
|
||||
|
||||
setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void {
|
||||
this.rpcHandlerManager.registerHandler(RPC_METHODS.SpawnHappySession, async (params: any) => {
|
||||
const { directory, sessionId, existingSessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, effort, modelReasoningEffort, yolo, permissionMode, serviceTier, token, sessionType, worktreeName } = params || {}
|
||||
const { directory, sessionId, existingSessionId, resumeSessionId, machineId, approvedNewDirectoryCreation, agent, model, effort, modelReasoningEffort, yolo, permissionMode, serviceTier, collaborationMode, token, sessionType, worktreeName } = params || {}
|
||||
|
||||
if (!directory) {
|
||||
throw new Error('Directory is required')
|
||||
@@ -382,6 +382,7 @@ export class ApiMachineClient {
|
||||
yolo,
|
||||
permissionMode,
|
||||
serviceTier,
|
||||
collaborationMode,
|
||||
token,
|
||||
sessionType,
|
||||
worktreeName
|
||||
|
||||
@@ -112,6 +112,15 @@ describe('codexCommand', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards a valid --collaboration-mode to runCodex', async () => {
|
||||
await codexCommand.run(createCommandContext(['--started-by', 'runner', '--collaboration-mode', 'plan']))
|
||||
|
||||
expect(runCodexMock).toHaveBeenCalledWith({
|
||||
startedBy: 'runner',
|
||||
collaborationMode: 'plan'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an unsupported --service-tier value', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { maybeAutoStartServer } from '@/utils/autoStartServer'
|
||||
import type { CommandDefinition } from './types'
|
||||
import { CODEX_PERMISSION_MODES } from '@hapi/protocol/modes'
|
||||
import type { CodexPermissionMode } from '@hapi/protocol/types'
|
||||
import { CodexCollaborationModeSchema } from '@hapi/protocol/schemas'
|
||||
import type { ReasoningEffort } from '@/codex/appServerTypes'
|
||||
import { assertCodexLocalSupported } from '@/codex/utils/codexVersion'
|
||||
import { parseReasoningEffortValue } from '@/codex/utils/reasoningEffort'
|
||||
@@ -19,6 +20,14 @@ function parseServiceTier(value: string): 'fast' | 'standard' {
|
||||
throw new Error('Invalid --service-tier value')
|
||||
}
|
||||
|
||||
function parseCollaborationMode(value: string): 'default' | 'plan' {
|
||||
const parsed = CodexCollaborationModeSchema.safeParse(value.trim().toLowerCase())
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid --collaboration-mode value')
|
||||
}
|
||||
return parsed.data
|
||||
}
|
||||
|
||||
export const codexCommand: CommandDefinition = {
|
||||
name: 'codex',
|
||||
requiresRuntimeAssets: true,
|
||||
@@ -35,6 +44,7 @@ export const codexCommand: CommandDefinition = {
|
||||
model?: string
|
||||
modelReasoningEffort?: ReasoningEffort
|
||||
serviceTier?: string
|
||||
collaborationMode?: 'default' | 'plan'
|
||||
} = {}
|
||||
const unknownArgs: string[] = []
|
||||
let hasExplicitPermissionMode = false
|
||||
@@ -88,6 +98,12 @@ export const codexCommand: CommandDefinition = {
|
||||
throw new Error('Missing --service-tier value')
|
||||
}
|
||||
options.serviceTier = parseServiceTier(tier)
|
||||
} else if (arg === '--collaboration-mode') {
|
||||
const mode = commandArgs[++i]
|
||||
if (!mode) {
|
||||
throw new Error('Missing --collaboration-mode value')
|
||||
}
|
||||
options.collaborationMode = parseCollaborationMode(mode)
|
||||
} else {
|
||||
unknownArgs.push(arg)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface SpawnSessionOptions {
|
||||
yolo?: boolean
|
||||
permissionMode?: string
|
||||
serviceTier?: string
|
||||
collaborationMode?: 'default' | 'plan'
|
||||
token?: string
|
||||
sessionType?: 'simple' | 'worktree'
|
||||
worktreeName?: string
|
||||
|
||||
@@ -84,6 +84,23 @@ describe('buildCliArgs', () => {
|
||||
expect(args).toContain('fast')
|
||||
})
|
||||
|
||||
it('passes --collaboration-mode through for codex Plan mode', () => {
|
||||
const args = buildCliArgs('codex', {
|
||||
directory: '/tmp',
|
||||
collaborationMode: 'plan',
|
||||
})
|
||||
expect(args).toContain('--collaboration-mode')
|
||||
expect(args).toContain('plan')
|
||||
})
|
||||
|
||||
it('omits --collaboration-mode for default collaboration mode', () => {
|
||||
const args = buildCliArgs('codex', {
|
||||
directory: '/tmp',
|
||||
collaborationMode: 'default',
|
||||
})
|
||||
expect(args).not.toContain('--collaboration-mode')
|
||||
})
|
||||
|
||||
it('does not pass --service-tier for non-codex agents', () => {
|
||||
const args = buildCliArgs('claude', {
|
||||
directory: '/tmp',
|
||||
@@ -143,6 +160,14 @@ describe('buildCliArgs', () => {
|
||||
expect(args).toContain('cursor-csid-1')
|
||||
})
|
||||
|
||||
it('does not pass --collaboration-mode for non-codex agents', () => {
|
||||
const args = buildCliArgs('claude', {
|
||||
directory: '/tmp',
|
||||
collaborationMode: 'plan',
|
||||
})
|
||||
expect(args).not.toContain('--collaboration-mode')
|
||||
})
|
||||
|
||||
it('validates all known permission modes', () => {
|
||||
for (const mode of ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan', 'ask', 'debug', 'autoReview', 'read-only', 'safe-yolo', 'yolo']) {
|
||||
const args = buildCliArgs('claude', {
|
||||
|
||||
@@ -1136,6 +1136,9 @@ export function buildCliArgs(
|
||||
if (options.serviceTier && agent === 'codex') {
|
||||
args.push('--service-tier', options.serviceTier);
|
||||
}
|
||||
if (options.collaborationMode && options.collaborationMode !== 'default' && agent === 'codex') {
|
||||
args.push('--collaboration-mode', options.collaborationMode);
|
||||
}
|
||||
// Pi RPC mode has no permission switching; never pass these flags to it
|
||||
// (the Pi parser rejects --permission-mode and ignores --yolo).
|
||||
if (agent !== 'pi') {
|
||||
|
||||
Reference in New Issue
Block a user