Skip to content

Commit d8cc2f6

Browse files
committed
fix(codex): auto-execute after plan confirmation
1 parent 852fb03 commit d8cc2f6

3 files changed

Lines changed: 87 additions & 6 deletions

File tree

cli/src/codex/codexRemoteLauncher.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ function shouldUseAppServer(): boolean {
3939
return !useMcpServer;
4040
}
4141

42+
const AUTO_EXECUTE_PLAN_PROMPT = 'Plan approved. Exit plan mode and start implementing now.';
43+
4244
class CodexRemoteLauncher extends RemoteLauncherBase {
4345
private readonly session: CodexSession;
4446
private readonly useAppServer: boolean;
@@ -313,6 +315,11 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
313315
return rows.length > 0 ? rows.join('\n') : null;
314316
};
315317

318+
let activeTurnMode: EnhancedMode | null = null;
319+
let planAutoExecutePending: {
320+
turnId: string | null;
321+
} | null = null;
322+
316323
const permissionHandler = new CodexPermissionHandler(session.client, {
317324
getPermissionMode: () => session.getPermissionMode() as EnhancedMode['permissionMode'] | undefined,
318325
onRequest: ({ id, toolName, input }) => {
@@ -476,10 +483,23 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
476483
sendReady();
477484
}
478485

486+
if (msgType === 'tool_call_end') {
487+
const toolName = asString(msg.name);
488+
if (toolName === 'ExitPlanMode' || toolName === 'exit_plan_mode') {
489+
if (session.getCollaborationMode() === 'plan') {
490+
planAutoExecutePending = {
491+
turnId: eventTurnId ?? this.currentTurnId
492+
};
493+
messageBuffer.addMessage('Plan approved. Preparing auto-execution...', 'status');
494+
}
495+
}
496+
}
497+
479498
if (msgType === 'task_started') {
480499
setTurnInFlight(true);
481500
}
482501
if (isTerminalTurnEvent) {
502+
const terminalTurnId = eventTurnId ?? this.currentTurnId;
483503
setTurnInFlight(false);
484504

485505
const summaryStatus = msgType === 'task_complete'
@@ -505,6 +525,31 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
505525
id: randomUUID()
506526
});
507527

528+
const planAutoExecuteMatchedTurn = Boolean(
529+
planAutoExecutePending
530+
&& (!planAutoExecutePending.turnId || !terminalTurnId || planAutoExecutePending.turnId === terminalTurnId)
531+
);
532+
if (msgType === 'task_complete' && planAutoExecuteMatchedTurn) {
533+
const fallbackPermissionMode = session.getPermissionMode() as EnhancedMode['permissionMode'] | undefined;
534+
const executeMode: EnhancedMode = {
535+
...(activeTurnMode ?? { permissionMode: fallbackPermissionMode ?? 'default' }),
536+
collaborationMode: undefined,
537+
routeContext: undefined
538+
};
539+
session.setCollaborationMode(undefined, { syncMetadata: true });
540+
session.queue.unshift(AUTO_EXECUTE_PLAN_PROMPT, executeMode, {
541+
deferUserMessageUntilDequeue: true
542+
});
543+
session.sendSessionEvent({
544+
type: 'message',
545+
message: 'Plan 已确认,自动切换到默认模式并开始执行。'
546+
});
547+
planAutoExecutePending = null;
548+
} else if (planAutoExecuteMatchedTurn) {
549+
planAutoExecutePending = null;
550+
}
551+
activeTurnMode = null;
552+
508553
diffProcessor.reset();
509554
this.turnChangeTracker.reset();
510555
appServerEventConverter?.reset();
@@ -839,6 +884,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
839884
currentModeHash = message.hash;
840885

841886
try {
887+
activeTurnMode = message.mode;
842888
if (!wasCreated) {
843889
if (useAppServer && appServerClient) {
844890
const threadParams = buildThreadStartParams({
@@ -968,6 +1014,8 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
9681014
const isAbortError = error instanceof Error && error.name === 'AbortError';
9691015
clearLiveActivity();
9701016
setTurnInFlight(false);
1017+
activeTurnMode = null;
1018+
planAutoExecutePending = null;
9711019

9721020
if (isAbortError) {
9731021
messageBuffer.addMessage('Aborted by user', 'status');

cli/src/codex/runCodex.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ export async function runCodex(opts: {
9494
? (metadataCollaborationMode === 'plan' ? 'plan' : undefined)
9595
: undefined;
9696

97+
const getCurrentCollaborationMode = (): EnhancedMode['collaborationMode'] => {
98+
return sessionWrapperRef.current?.getCollaborationMode() ?? currentCollaborationMode;
99+
};
100+
97101
const syncRuntimeMetadata = (): void => {
102+
const collaborationMode = getCurrentCollaborationMode();
98103
session.updateMetadata((currentMetadata) => {
99104
const {
100105
thinkEffort: _previousThinkEffort,
@@ -106,12 +111,12 @@ export async function runCodex(opts: {
106111
...metadataWithoutRuntime,
107112
model: currentModel,
108113
thinkEffort: currentEffort,
109-
...(currentCollaborationMode ? { collaborationMode: currentCollaborationMode } : {})
114+
...(collaborationMode ? { collaborationMode } : {})
110115
}
111116
: {
112117
...metadataWithoutRuntime,
113118
model: currentModel,
114-
...(currentCollaborationMode ? { collaborationMode: currentCollaborationMode } : {})
119+
...(collaborationMode ? { collaborationMode } : {})
115120
};
116121
});
117122
};
@@ -186,7 +191,7 @@ export async function runCodex(opts: {
186191
mode: sessionInstance?.mode ?? startingMode,
187192
sessionId: sessionInstance?.sessionId ?? null,
188193
permissionMode: currentPermissionMode,
189-
collaborationMode: currentCollaborationMode,
194+
collaborationMode: getCurrentCollaborationMode(),
190195
queueSnapshot: getCodexQueueSnapshot()
191196
});
192197
};
@@ -223,7 +228,7 @@ export async function runCodex(opts: {
223228
permissionMode: messagePermissionMode ?? 'default',
224229
model: currentModel,
225230
effort: currentEffort,
226-
collaborationMode: currentCollaborationMode,
231+
collaborationMode: getCurrentCollaborationMode(),
227232
routeContext: message.meta?.routeContext
228233
};
229234
const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments);
@@ -373,6 +378,7 @@ export async function runCodex(opts: {
373378

374379
if (config.collaborationMode !== undefined) {
375380
currentCollaborationMode = resolveCollaborationMode(config.collaborationMode);
381+
sessionWrapperRef.current?.setCollaborationMode(currentCollaborationMode);
376382
syncRuntimeMetadata();
377383
}
378384

@@ -385,7 +391,7 @@ export async function runCodex(opts: {
385391
return {
386392
applied: {
387393
permissionMode: currentPermissionMode,
388-
collaborationMode: currentCollaborationMode,
394+
collaborationMode: getCurrentCollaborationMode(),
389395
thinkEffort: currentEffort
390396
}
391397
};
@@ -418,7 +424,7 @@ export async function runCodex(opts: {
418424
permissionMode: currentPermissionMode ?? 'default',
419425
model: currentModel,
420426
effort: currentEffort,
421-
collaborationMode: currentCollaborationMode,
427+
collaborationMode: getCurrentCollaborationMode(),
422428
routeContext: parsed.routeContext
423429
};
424430
messageQueue.push(formattedText, enhancedMode, {
@@ -498,6 +504,7 @@ export async function runCodex(opts: {
498504
onSessionReady: (instance) => {
499505
sessionWrapperRef.current = instance;
500506
syncSessionMode();
507+
instance.setCollaborationMode(currentCollaborationMode);
501508
}
502509
});
503510
} catch (error) {

cli/src/codex/session.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
1717
readonly startedBy: 'runner' | 'terminal';
1818
readonly startingMode: 'local' | 'remote';
1919
localLaunchFailure: LocalLaunchFailure | null = null;
20+
private collaborationMode: EnhancedMode['collaborationMode'];
2021

2122
constructor(opts: {
2223
api: ApiClient;
@@ -32,6 +33,7 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
3233
codexArgs?: string[];
3334
codexCliOverrides?: CodexCliOverrides;
3435
permissionMode?: PermissionMode;
36+
collaborationMode?: EnhancedMode['collaborationMode'];
3537
}) {
3638
super({
3739
api: opts.api,
@@ -56,12 +58,36 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
5658
this.startedBy = opts.startedBy;
5759
this.startingMode = opts.startingMode;
5860
this.permissionMode = opts.permissionMode;
61+
this.collaborationMode = opts.collaborationMode;
5962
}
6063

6164
setPermissionMode = (mode: PermissionMode): void => {
6265
this.permissionMode = mode;
6366
};
6467

68+
setCollaborationMode = (
69+
mode: EnhancedMode['collaborationMode'],
70+
options?: { syncMetadata?: boolean }
71+
): void => {
72+
this.collaborationMode = mode;
73+
74+
if (options?.syncMetadata) {
75+
this.client.updateMetadata((metadata) => {
76+
const next = { ...metadata };
77+
if (mode) {
78+
next.collaborationMode = mode;
79+
} else {
80+
delete next.collaborationMode;
81+
}
82+
return next;
83+
});
84+
}
85+
};
86+
87+
getCollaborationMode = (): EnhancedMode['collaborationMode'] => {
88+
return this.collaborationMode;
89+
};
90+
6591
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
6692
this.localLaunchFailure = { message, exitReason };
6793
};

0 commit comments

Comments
 (0)