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:
@@ -157,13 +157,30 @@ export class RpcGateway {
|
||||
effort?: string,
|
||||
permissionMode?: PermissionMode,
|
||||
serviceTier?: string,
|
||||
existingSessionId?: string
|
||||
existingSessionId?: string,
|
||||
collaborationMode?: CodexCollaborationMode
|
||||
): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> {
|
||||
try {
|
||||
const result = await this.machineRpc(
|
||||
machineId,
|
||||
RPC_METHODS.SpawnHappySession,
|
||||
{ type: 'spawn-in-directory', directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, resumeSessionId, effort, permissionMode, serviceTier, existingSessionId, sessionId: existingSessionId }
|
||||
{
|
||||
type: 'spawn-in-directory',
|
||||
directory,
|
||||
agent,
|
||||
model,
|
||||
modelReasoningEffort,
|
||||
yolo,
|
||||
sessionType,
|
||||
worktreeName,
|
||||
resumeSessionId,
|
||||
effort,
|
||||
permissionMode,
|
||||
serviceTier,
|
||||
existingSessionId,
|
||||
sessionId: existingSessionId,
|
||||
collaborationMode
|
||||
}
|
||||
)
|
||||
if (result && typeof result === 'object') {
|
||||
const obj = result as Record<string, unknown>
|
||||
|
||||
@@ -811,7 +811,8 @@ export class SyncEngine {
|
||||
effort?: string,
|
||||
permissionMode?: PermissionMode,
|
||||
serviceTier?: string,
|
||||
existingSessionId?: string
|
||||
existingSessionId?: string,
|
||||
collaborationMode?: CodexCollaborationMode
|
||||
): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> {
|
||||
return await this.rpcGateway.spawnSession(
|
||||
machineId,
|
||||
@@ -826,7 +827,8 @@ export class SyncEngine {
|
||||
effort,
|
||||
permissionMode,
|
||||
serviceTier,
|
||||
existingSessionId
|
||||
existingSessionId,
|
||||
collaborationMode
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1300,7 +1302,8 @@ export class SyncEngine {
|
||||
session.effort ?? undefined,
|
||||
preferredPermissionMode,
|
||||
session.serviceTier ?? undefined,
|
||||
access.sessionId
|
||||
access.sessionId,
|
||||
session.collaborationMode ?? undefined
|
||||
)
|
||||
|
||||
if (spawnResult.type !== 'success') {
|
||||
|
||||
@@ -857,6 +857,47 @@ describe('Codex Desktop import routes', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('applies selected Standard and Plan config before an imported session is resumed', async () => {
|
||||
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-config-test-'))
|
||||
const store = new Store(':memory:')
|
||||
const codexSessionId = '23232323-2323-4232-8232-232323232323'
|
||||
process.env.CODEX_HOME = codexHome
|
||||
|
||||
try {
|
||||
createTranscript(codexHome, codexSessionId, '/home/user/workspace/project')
|
||||
const engine = createImportSyncEngine(store, [
|
||||
createMachine('machine-1', ['/home/user/workspace'])
|
||||
])
|
||||
const applied: Array<{ sessionId: string; config: unknown }> = []
|
||||
;(engine as unknown as { applySessionConfig: (sessionId: string, config: unknown) => Promise<void> }).applySessionConfig = async (sessionId, config) => {
|
||||
applied.push({ sessionId, config })
|
||||
}
|
||||
|
||||
const result = await importSelectedCodexSessions({
|
||||
codexSessionIds: [codexSessionId],
|
||||
store,
|
||||
namespace: 'default',
|
||||
getSyncEngine: () => engine,
|
||||
serviceTier: 'standard',
|
||||
collaborationMode: 'plan'
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const importedSessionId = result.success ? result.hapiSessionIds?.[0] : undefined
|
||||
expect(importedSessionId).toBeDefined()
|
||||
if (!importedSessionId) {
|
||||
throw new Error('Imported session id missing')
|
||||
}
|
||||
expect(applied).toEqual([{
|
||||
sessionId: importedSessionId,
|
||||
config: { serviceTier: 'standard', collaborationMode: 'plan' }
|
||||
}])
|
||||
} finally {
|
||||
store.close()
|
||||
rmSync(codexHome, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('binds imported transcripts to the unique online machine that owns the cwd', async () => {
|
||||
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-machine-test-'))
|
||||
const store = new Store(':memory:')
|
||||
|
||||
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import { homedir, hostname, platform } from 'node:os'
|
||||
import { AGENT_MESSAGE_PAYLOAD_TYPE } from '@hapi/protocol'
|
||||
import type { CodexCollaborationMode } from '@hapi/protocol/types'
|
||||
import { Hono } from 'hono'
|
||||
import type { Machine, SyncEngine } from '../../sync/syncEngine'
|
||||
import type { Store, StoredMessage } from '../../store'
|
||||
@@ -131,6 +132,8 @@ type SyncSessionRequestParseResult = {
|
||||
machineId?: string | null
|
||||
model?: string | null
|
||||
modelReasoningEffort?: string | null
|
||||
serviceTier?: string | null
|
||||
collaborationMode?: CodexCollaborationMode
|
||||
yolo?: boolean
|
||||
error?: string
|
||||
}
|
||||
@@ -1701,7 +1704,7 @@ function parseSyncSessionRequest(body: unknown): SyncSessionRequestParseResult {
|
||||
return { sessionIds: [] }
|
||||
}
|
||||
|
||||
const bodyRecord = body as { sessionIds?: unknown; cwd?: unknown; machineId?: unknown; model?: unknown; modelReasoningEffort?: unknown; yolo?: unknown }
|
||||
const bodyRecord = body as { sessionIds?: unknown; cwd?: unknown; machineId?: unknown; model?: unknown; modelReasoningEffort?: unknown; serviceTier?: unknown; collaborationMode?: unknown; yolo?: unknown }
|
||||
const rawSessionIds = bodyRecord.sessionIds
|
||||
if (!Array.isArray(rawSessionIds)) {
|
||||
return { sessionIds: [], error: 'Invalid sessionIds' }
|
||||
@@ -1720,6 +1723,14 @@ function parseSyncSessionRequest(body: unknown): SyncSessionRequestParseResult {
|
||||
|
||||
const hasModel = Object.prototype.hasOwnProperty.call(bodyRecord, 'model')
|
||||
const hasModelReasoningEffort = Object.prototype.hasOwnProperty.call(bodyRecord, 'modelReasoningEffort')
|
||||
const hasServiceTier = Object.prototype.hasOwnProperty.call(bodyRecord, 'serviceTier')
|
||||
const hasCollaborationMode = Object.prototype.hasOwnProperty.call(bodyRecord, 'collaborationMode')
|
||||
if (hasServiceTier && bodyRecord.serviceTier !== null && bodyRecord.serviceTier !== 'fast' && bodyRecord.serviceTier !== 'standard') {
|
||||
return { sessionIds: [], error: 'Invalid serviceTier' }
|
||||
}
|
||||
if (hasCollaborationMode && bodyRecord.collaborationMode !== 'default' && bodyRecord.collaborationMode !== 'plan') {
|
||||
return { sessionIds: [], error: 'Invalid collaborationMode' }
|
||||
}
|
||||
|
||||
// 中文注释:前端允许多选,这里按 Codex thread 去重,避免重复导入同一条本地 transcript。
|
||||
return {
|
||||
@@ -1728,6 +1739,8 @@ function parseSyncSessionRequest(body: unknown): SyncSessionRequestParseResult {
|
||||
machineId: typeof bodyRecord.machineId === 'string' && bodyRecord.machineId.trim() ? bodyRecord.machineId.trim() : null,
|
||||
model: hasModel ? (typeof bodyRecord.model === 'string' && bodyRecord.model.trim() ? bodyRecord.model.trim() : null) : undefined,
|
||||
modelReasoningEffort: hasModelReasoningEffort ? (typeof bodyRecord.modelReasoningEffort === 'string' && bodyRecord.modelReasoningEffort.trim() ? bodyRecord.modelReasoningEffort.trim() : null) : undefined,
|
||||
serviceTier: hasServiceTier ? bodyRecord.serviceTier as 'fast' | 'standard' | null : undefined,
|
||||
collaborationMode: hasCollaborationMode ? bodyRecord.collaborationMode as CodexCollaborationMode : undefined,
|
||||
yolo: bodyRecord.yolo === true
|
||||
}
|
||||
}
|
||||
@@ -1952,6 +1965,8 @@ export async function importSelectedCodexSessions(options: {
|
||||
localSessions?: RemoteCodexSession[]
|
||||
model?: string | null
|
||||
modelReasoningEffort?: string | null
|
||||
serviceTier?: string | null
|
||||
collaborationMode?: CodexCollaborationMode
|
||||
yolo?: boolean
|
||||
machineId?: string | null
|
||||
}): Promise<ScriptLaunchResponse> {
|
||||
@@ -1976,6 +1991,23 @@ export async function importSelectedCodexSessions(options: {
|
||||
})
|
||||
results.push(result)
|
||||
|
||||
if (result.success && (options.serviceTier !== undefined || options.collaborationMode !== undefined)) {
|
||||
const importedSessionId = result.hapiSessionIds?.[0]
|
||||
const engine = options.getSyncEngine?.() ?? null
|
||||
if (!importedSessionId || !engine) {
|
||||
return createImportErrorResponse(codexSessionIds, 'Imported session config could not be applied before resume')
|
||||
}
|
||||
try {
|
||||
await engine.applySessionConfig(importedSessionId, {
|
||||
...(options.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}),
|
||||
...(options.collaborationMode !== undefined ? { collaborationMode: options.collaborationMode } : {})
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return createImportErrorResponse(codexSessionIds, `Failed to apply imported session config: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
...result,
|
||||
@@ -2110,6 +2142,8 @@ export function createCodexDesktopRoutes(options: {
|
||||
machineId: remote.machineId ?? null,
|
||||
model: parsed.model,
|
||||
modelReasoningEffort: parsed.modelReasoningEffort,
|
||||
serviceTier: parsed.serviceTier,
|
||||
collaborationMode: parsed.collaborationMode,
|
||||
yolo: parsed.yolo
|
||||
})
|
||||
return c.json({
|
||||
|
||||
@@ -51,7 +51,10 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho
|
||||
parsed.data.worktreeName,
|
||||
undefined,
|
||||
parsed.data.effort,
|
||||
parsed.data.permissionMode
|
||||
parsed.data.permissionMode,
|
||||
parsed.data.serviceTier,
|
||||
undefined,
|
||||
parsed.data.collaborationMode
|
||||
)
|
||||
return c.json(result)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user