mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* 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>
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
import { ApiClient, ApiSessionClient } from '@/lib';
|
|
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
|
import { AgentSessionBase } from '@/agent/sessionBase';
|
|
import type { EnhancedMode, PermissionMode } from './loop';
|
|
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
|
|
import type { CursorSessionProtocol } from './utils/cursorProtocol';
|
|
|
|
type LocalLaunchFailure = {
|
|
message: string;
|
|
exitReason: LocalLaunchExitReason;
|
|
};
|
|
|
|
type CursorModelApplyHandler = (model: string | null | undefined) => Promise<string | null>;
|
|
type CursorPermissionModeChangedHandler = (mode: PermissionMode) => void;
|
|
|
|
export class CursorSession extends AgentSessionBase<EnhancedMode> {
|
|
readonly cursorArgs?: string[];
|
|
/** Cursor-native `--worktree` name (`true` = flag without name). */
|
|
readonly cursorWorktree?: boolean | string;
|
|
/** Extra `--add-dir` roots for Cursor ACP spawn. */
|
|
readonly cursorAddDirs?: readonly string[];
|
|
model?: string;
|
|
readonly startedBy: 'runner' | 'terminal';
|
|
readonly startingMode: 'local' | 'remote';
|
|
localLaunchFailure: LocalLaunchFailure | null = null;
|
|
private modelApplyHandler: CursorModelApplyHandler | null = null;
|
|
private permissionModeChangedHandler: CursorPermissionModeChangedHandler | null = null;
|
|
|
|
constructor(opts: {
|
|
api: ApiClient;
|
|
client: ApiSessionClient;
|
|
path: string;
|
|
logPath: string;
|
|
sessionId: string | null;
|
|
messageQueue: MessageQueue2<EnhancedMode>;
|
|
onModeChange: (mode: 'local' | 'remote') => void;
|
|
mode?: 'local' | 'remote';
|
|
startedBy: 'runner' | 'terminal';
|
|
startingMode: 'local' | 'remote';
|
|
cursorArgs?: string[];
|
|
cursorWorktree?: boolean | string;
|
|
cursorAddDirs?: readonly string[];
|
|
model?: string;
|
|
permissionMode?: PermissionMode;
|
|
onPermissionModeChanged?: CursorPermissionModeChangedHandler;
|
|
}) {
|
|
super({
|
|
api: opts.api,
|
|
client: opts.client,
|
|
path: opts.path,
|
|
logPath: opts.logPath,
|
|
sessionId: opts.sessionId,
|
|
messageQueue: opts.messageQueue,
|
|
onModeChange: opts.onModeChange,
|
|
mode: opts.mode,
|
|
sessionLabel: 'CursorSession',
|
|
sessionIdLabel: 'Cursor',
|
|
applySessionIdToMetadata: (metadata, sessionId, extras) => ({
|
|
...metadata,
|
|
cursorSessionId: sessionId,
|
|
...extras
|
|
}),
|
|
permissionMode: opts.permissionMode
|
|
});
|
|
|
|
this.cursorArgs = opts.cursorArgs;
|
|
this.cursorWorktree = opts.cursorWorktree;
|
|
this.cursorAddDirs = opts.cursorAddDirs;
|
|
this.model = opts.model;
|
|
this.startedBy = opts.startedBy;
|
|
this.startingMode = opts.startingMode;
|
|
this.permissionMode = opts.permissionMode;
|
|
this.permissionModeChangedHandler = opts.onPermissionModeChanged ?? null;
|
|
}
|
|
|
|
setPermissionMode = (mode: PermissionMode): void => {
|
|
this.permissionMode = mode;
|
|
// Keep runCursor's enqueue source of truth in sync (CreatePlan accept, ACP mode sync).
|
|
this.permissionModeChangedHandler?.(mode);
|
|
};
|
|
|
|
setModel = (model: string | null | undefined): void => {
|
|
this.model = model ?? undefined;
|
|
};
|
|
|
|
registerModelApplyHandler = (handler: CursorModelApplyHandler): (() => void) => {
|
|
this.modelApplyHandler = handler;
|
|
return () => {
|
|
if (this.modelApplyHandler === handler) {
|
|
this.modelApplyHandler = null;
|
|
}
|
|
};
|
|
};
|
|
|
|
canApplyModelConfig = (): boolean => this.modelApplyHandler !== null;
|
|
|
|
applyModelConfig = async (model: string | null | undefined): Promise<string | null> => {
|
|
if (!this.modelApplyHandler) {
|
|
throw new Error('Cursor ACP session is not ready to apply model changes');
|
|
}
|
|
return await this.modelApplyHandler(model);
|
|
};
|
|
|
|
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
|
|
this.localLaunchFailure = { message, exitReason };
|
|
};
|
|
|
|
sendAgentMessage = (message: unknown): void => {
|
|
this.client.sendAgentMessage(message);
|
|
};
|
|
|
|
sendUserMessage = (text: string): void => {
|
|
this.client.sendUserMessage(text);
|
|
};
|
|
|
|
sendSessionEvent = (event: Parameters<ApiSessionClient['sendSessionEvent']>[0]): void => {
|
|
this.client.sendSessionEvent(event);
|
|
};
|
|
|
|
onSessionFoundWithProtocol = (sessionId: string, protocol: CursorSessionProtocol): void => {
|
|
this.onSessionFound(sessionId, { cursorSessionProtocol: protocol });
|
|
};
|
|
}
|