Files
hapi/cli/src/cursor/loop.ts
T
e4cc3916c8 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>
2026-07-27 12:57:31 +08:00

70 lines
2.4 KiB
TypeScript

import { logger } from '@/ui/logger';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { runLocalRemoteSession } from '@/agent/loopBase';
import { CursorSession } from './session';
import { cursorLocalLauncher } from './cursorLocalLauncher';
import { cursorRemoteLauncher } from './cursorRemoteLauncher';
import type { Metadata } from '@hapi/protocol/schemas';
import { ApiClient, ApiSessionClient } from '@/lib';
import type { CursorPermissionMode } from '@hapi/protocol/types';
export type PermissionMode = CursorPermissionMode;
export interface EnhancedMode {
permissionMode: PermissionMode;
model?: string;
}
interface LoopOptions {
path: string;
startingMode?: 'local' | 'remote';
startedBy?: 'runner' | 'terminal';
onModeChange: (mode: 'local' | 'remote') => void;
messageQueue: MessageQueue2<EnhancedMode>;
session: ApiSessionClient;
api: ApiClient;
cursorArgs?: string[];
cursorWorktree?: boolean | string;
cursorAddDirs?: readonly string[];
permissionMode?: PermissionMode;
resumeSessionId?: string;
model?: string;
sessionMetadata?: Metadata | null;
onSessionReady?: (session: CursorSession) => void;
/** Keep runCursor's enqueue mode in sync when the session leaves plan/ask. */
onPermissionModeChanged?: (mode: PermissionMode) => void;
}
export async function loop(opts: LoopOptions): Promise<void> {
const logPath = logger.getLogPath();
const startedBy = opts.startedBy ?? 'terminal';
const startingMode = opts.startingMode ?? 'local';
const session = new CursorSession({
api: opts.api,
client: opts.session,
path: opts.path,
sessionId: opts.resumeSessionId ?? null,
logPath,
messageQueue: opts.messageQueue,
onModeChange: opts.onModeChange,
mode: startingMode,
startedBy,
startingMode,
cursorArgs: opts.cursorArgs,
cursorWorktree: opts.cursorWorktree,
cursorAddDirs: opts.cursorAddDirs,
model: opts.model,
permissionMode: opts.permissionMode ?? 'default',
onPermissionModeChanged: opts.onPermissionModeChanged
});
await runLocalRemoteSession({
session,
startingMode: opts.startingMode,
logTag: 'cursor-loop',
runLocal: cursorLocalLauncher,
runRemote: (session) => cursorRemoteLauncher(session, opts.sessionMetadata),
onSessionReady: opts.onSessionReady
});
}