fix(cursor): CreatePlan Yes accepts and continues task (nested outcome + plan→execute) (#1097)

* fix(cursor): nest ACP extension outcome so plan approvals aren't cancelled

Cursor's ACP blocking extension methods (cursor/ask_question,
cursor/create_plan) expect the JSON-RPC result to nest the outcome under
an `outcome` key, e.g. `{ outcome: { outcome: "accepted" } }`. The
adapter returned it flat (`{ outcome: "accepted" }`), so Cursor read
`response.outcome.outcome` as undefined and fell back to a cancellation
— an approved plan was relayed to the agent as `User cancelled`, making
plan mode unusable over HAPI for Cursor sessions.

Wrap every ask_question / create_plan response in the nested envelope
and add regression tests asserting the exact wire shape for the
affirmative approval -> CreatePlan path (plus approved_for_session,
reject, and abort).

Fixes #79

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): continue task after CreatePlan Yes (plan→execute)

Nested ACP accept alone is not a complete fix: Yes unblocked create_plan
but the prompt turn still ended with the plan "done" and no execution.
Mirror Claude ExitPlanMode: on accept, leave plan/ask for an executable
mode and queue a continue prompt so the original user task keeps going.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): sync runCursor enqueue mode after CreatePlan accept

Codex Major on #1097: setPermissionMode alone left runCursor's
currentPermissionMode stale, so the next user message could re-enter
plan/ask after Yes. Notify onPermissionModeChanged from CursorSession
so the enqueue source of truth stays aligned with the session.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-07-27 12:57:31 +08:00
committed by GitHub
co-authored by Cursor Debian
parent 311e0cef55
commit e4cc3916c8
10 changed files with 245 additions and 27 deletions
+35 -1
View File
@@ -24,8 +24,10 @@ import {
applyCursorAcpMode,
applyCursorAcpModel,
isCursorAutoReviewMode,
resolveCursorModeAfterPlanApproval,
wireIdForCursorSessionState
} from './utils/cursorModeConfig';
import { CURSOR_PLAN_CONTINUE } from './utils/cursorPlanContinue';
import { cursorPassThroughStatusMessage, parseCursorSpecialCommand } from './cursorSpecialCommands';
import { buildCursorModelsSeedPayload, seedCursorModelsCache } from '@/modules/common/cursorModels';
import { readSharedCursorModelsCache } from '@/modules/common/cursorModelsSharedCache';
@@ -34,6 +36,7 @@ import { registerAcpSessionTitleSync } from '@/agent/acpSessionTitle';
class CursorAcpRemoteLauncher extends RemoteLauncherBase {
private readonly session: CursorSession;
private backend: ReturnType<typeof createCursorAcpBackend> | null = null;
private acpSessionId: string | null = null;
private permissionAdapter: PermissionAdapter | null = null;
private extensionAdapter: CursorExtensionAdapter | null = null;
private happyServer: { stop: () => void } | null = null;
@@ -115,7 +118,8 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
const extensionAdapter = new CursorExtensionAdapter(
session.client,
backend,
(message) => this.handleAgentMessage(message)
(message) => this.handleAgentMessage(message),
() => this.handleCreatePlanAccepted()
);
this.extensionAdapter = extensionAdapter;
@@ -155,6 +159,7 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
mcpServers: mcpServerList
});
}
this.acpSessionId = acpSessionId;
if (acpSessionId !== resumeSessionId) {
session.onSessionFoundWithProtocol(acpSessionId, 'acp');
@@ -304,6 +309,35 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase {
setCursorAcpModelsSnapshot(null);
}
private handleCreatePlanAccepted(): void {
const backend = this.backend;
const acpSessionId = this.acpSessionId;
if (!backend || !acpSessionId) {
logger.warn('[cursor-acp] CreatePlan accepted but ACP session is not ready; skip continue handoff');
return;
}
const session = this.session;
const executeMode = resolveCursorModeAfterPlanApproval(
session.getPermissionMode() as PermissionMode
) as PermissionMode;
// Leave plan/ask for an executable mode, then queue a continue prompt so
// Yes means "keep going on the user task" (Claude ExitPlanMode parallel).
session.setPermissionMode(executeMode);
void applyCursorAcpMode(backend, acpSessionId, executeMode).then(() => {
this.applyDisplayMode(executeMode);
});
session.queue.unshiftIsolated(CURSOR_PLAN_CONTINUE, {
permissionMode: executeMode,
model: session.model
});
logger.debug('[cursor-acp] CreatePlan accepted — queued continue prompt', {
executeMode
});
}
private handleAgentMessage(message: AgentMessage): void {
const converted = convertAgentMessage(message);
if (converted) {