import type { AgentModeOption } from "@cursor/sdk"; import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent"; import { buildCursorToolManifestText, resolveCursorToolManifestEnabled, } from "./tool-manifest.js"; import { CURSOR_HTTP1_ENTRY_TYPE, getStoredCursorHttp1Enabled, isCursorHttp1EntryData, setCursorHttp1GlobalPreferenceAuthoritative, setStoredCursorHttp1Enabled, type CursorHttp1EntryData, } from "./http1.js"; import { buildCursorPiToolBridgeSnapshot } from "./pi-tool-bridge-snapshot.js"; import { resolveCursorPiToolBridgeConfig } from "./pi-tool-bridge-config.js"; import { getEffectiveCursorSettingSources } from "./setting-sources.js"; import { isCursorModel, registerCursorModelLifecycle } from "./model.js"; import { asRecord } from "./record-utils.js"; import { getCursorSessionScopeKey } from "./session-scope.js"; import { refreshSessionCursorAgentConfig } from "./session-agent.js"; import { getCursorModelMetadata } from "./model-discovery.js"; import { buildCursorBridgeExcludeToolNames, cursorFastDefaultsFromConfig, getCursorSdkUserConfigPath, loadCursorSdkUserConfig, mergeCursorSdkConfigForUpdate, resolveCursorFastDefault, updateCursorSdkConfig, type CursorSdkConfig, } from "./config.js"; import { consumeCursorLocalForceOverride, formatCursorStatus, getCursorCliConfig, getCursorSessionConfig, registerCursorLocalRuntimeFlags, resetCursorRuntimeStateForTests, resolveCursorStatusRuntime, resolveEffectiveCursorConfigForContext, restoreCursorCliState, type CursorRuntimeStateExtensionApi, } from "./runtime-state.js"; export { consumeCursorLocalForceOverride, getCursorCliConfig, getCursorSessionConfig, } from "./runtime-state.js"; const FAST_ENTRY_TYPE = "cursor-fast-state"; const MODE_ENTRY_TYPE = "cursor-mode-state"; export type CursorAgentMode = AgentModeOption; const DEFAULT_CURSOR_AGENT_MODE: AgentModeOption = "agent"; interface CursorFastEntryData { modelId?: string; // Read session entries written before fast preferences adopted the canonical modelId field. baseModelId?: string; fast: boolean; } interface CursorModeEntryData { mode: AgentModeOption; } type CursorRuntimeControlsExtensionApi = Pick< ExtensionAPI, "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on" | "getActiveTools" | "getAllTools" > & CursorRuntimeStateExtensionApi; type CursorCliModeState = | { kind: "unset" } | { kind: "valid"; mode: AgentModeOption } | { kind: "invalid"; raw: string; message: string }; const sessionFastPreferences = new Map(); const authoritativeGlobalFastPreferenceIds = new Set(); let globalFastPreferences = new Map(); let cliForceFast = false; let cliForceNoFast = false; let sessionCursorAgentMode: AgentModeOption | undefined; let cliCursorModeState: CursorCliModeState = { kind: "unset" }; const invalidCursorModeNotifiedSessionScopeKeys = new Set(); export function isCursorAgentMode(value: unknown): value is AgentModeOption { return value === "agent" || value === "plan"; } export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined { if (typeof raw !== "string") return undefined; const mode = raw.trim(); return isCursorAgentMode(mode) ? mode : undefined; } function isCursorFastEntryData(value: unknown): value is CursorFastEntryData { const record = asRecord(value); if (!record) return false; return (typeof record.modelId === "string" || typeof record.baseModelId === "string") && typeof record.fast === "boolean"; } function getCursorFastEntryModelId(data: CursorFastEntryData): string { return data.modelId ?? data.baseModelId ?? ""; } function isCursorModeEntryData(value: unknown): value is CursorModeEntryData { return isCursorAgentMode(asRecord(value)?.mode); } function getConfigPath(): string { return getCursorSdkUserConfigPath(); } function loadGlobalFastPreferences(): Map { return cursorFastDefaultsFromConfig(loadCursorSdkUserConfig()); } function saveGlobalFastPreference(modelId: string, fast: boolean): void { updateCursorSdkConfig( getConfigPath(), (current) => { const models = asRecord(current.models); const fastDefaults = { ...asRecord(models?.fastDefaults), [modelId]: fast }; return { ...current, models: { ...models, fastDefaults: Object.fromEntries( Object.entries(fastDefaults).sort(([a], [b]) => a.localeCompare(b)), ), }, }; }, { newFileMode: 0o600 }, ); } function saveGlobalCursorHttp1Enabled(enabled: boolean): void { updateCursorSdkConfig( getConfigPath(), (current) => mergeCursorSdkConfigForUpdate(current, { local: { transport: enabled ? "http1" : "default" }, }), { newFileMode: 0o600 }, ); } function lastBranchCustomData( branch: readonly SessionEntry[], customType: string, guard: (value: unknown) => value is T, ): T | undefined { let last: T | undefined; for (const entry of branch) { if (entry.type !== "custom" || entry.customType !== customType) continue; if (guard(entry.data)) last = entry.data; } return last; } function restoreSessionFastPreferences(branch: readonly SessionEntry[]): void { sessionFastPreferences.clear(); for (const entry of branch) { if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue; if (isCursorFastEntryData(entry.data)) { const modelId = getCursorFastEntryModelId(entry.data); if (modelId) sessionFastPreferences.set(modelId, entry.data.fast); } } } function restoreSessionCursorMode(branch: readonly SessionEntry[]): void { sessionCursorAgentMode = lastBranchCustomData(branch, MODE_ENTRY_TYPE, isCursorModeEntryData)?.mode; } function restoreSessionCursorPreferences(ctx: { sessionManager: Pick }): void { const branch = ctx.sessionManager.getBranch(); restoreSessionFastPreferences(branch); restoreSessionCursorMode(branch); restoreSessionCursorHttp1(branch); } function restoreSessionCursorHttp1(branch: readonly SessionEntry[]): void { setStoredCursorHttp1Enabled(lastBranchCustomData(branch, CURSOR_HTTP1_ENTRY_TYPE, isCursorHttp1EntryData)?.enabled); } function getMapFastPreference( map: Map, metadata: NonNullable>, ): boolean | undefined { return map.get(metadata.piModelId); } function getEffectiveFast(modelId: string): boolean | undefined { const metadata = getCursorModelMetadata(modelId); if (!metadata?.supportsFast) return undefined; return resolveCursorFastDefault({ cliForceNoFast, cliForceFast, sessionValue: authoritativeGlobalFastPreferenceIds.has(metadata.piModelId) ? undefined : getMapFastPreference(sessionFastPreferences, metadata), userValue: getMapFastPreference(globalFastPreferences, metadata), modelDefault: metadata.defaultFast, }).value; } function formatInvalidCursorMode(raw: string): string { return `Invalid --cursor-mode "${raw}". Use "agent" or "plan".`; } export type CursorAgentModeResolution = | { kind: "valid"; mode: AgentModeOption } | { kind: "invalid"; raw: string; message: string }; export function getStoredCursorAgentMode(): AgentModeOption { return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE; } export function resolveCursorAgentMode(): CursorAgentModeResolution { switch (cliCursorModeState.kind) { case "valid": return { kind: "valid", mode: cliCursorModeState.mode }; case "invalid": return { kind: "invalid", raw: cliCursorModeState.raw, message: cliCursorModeState.message }; case "unset": return { kind: "valid", mode: getStoredCursorAgentMode() }; } } export function getCursorProviderAgentModeOrThrow(): AgentModeOption { const resolution = resolveCursorAgentMode(); if (resolution.kind === "invalid") throw new Error(resolution.message); return resolution.mode; } type CursorStatusContext = Pick & Partial>; function updateCursorStatus(ctx: CursorStatusContext & Pick, model = ctx.model): void { if (!model || !isCursorModel(model)) { ctx.ui.setStatus("cursor", undefined); return; } const metadata = getCursorModelMetadata(model.id); const resolution = resolveCursorStatusRuntime(ctx); const modeResolution = resolveCursorAgentMode(); const mode = modeResolution.kind === "invalid" ? "invalid" : modeResolution.mode; if (resolution.kind === "invalid") { ctx.ui.setStatus("cursor", formatCursorStatus(undefined, "invalid")); return; } const fast = metadata?.supportsFast ? getEffectiveFast(model.id) : undefined; ctx.ui.setStatus( "cursor", formatCursorStatus(fast, mode, resolution.transport.value), ); } function getCurrentCursorMetadata(ctx: Pick) { const model = ctx.model; if (!model || !isCursorModel(model)) return undefined; return getCursorModelMetadata(model.id); } function restoreMapValue(map: Map, key: string, previous: boolean | undefined): void { if (previous === undefined) { map.delete(key); } else { map.set(key, previous); } } function persistSessionGlobalPreference(params: { apply: () => void; revert: () => void; save: () => void; append: () => void; markAppendFailed: () => void; }): unknown | undefined { params.apply(); try { params.save(); } catch (error) { params.revert(); throw error; } try { params.append(); return undefined; } catch (error) { params.markAppendFailed(); return error; } } function persistFastPreference( pi: Pick, modelId: string, fast: boolean, ): unknown | undefined { const previousSession = sessionFastPreferences.get(modelId); const previousGlobal = globalFastPreferences.get(modelId); return persistSessionGlobalPreference({ apply: () => { sessionFastPreferences.set(modelId, fast); globalFastPreferences.set(modelId, fast); }, revert: () => { restoreMapValue(sessionFastPreferences, modelId, previousSession); restoreMapValue(globalFastPreferences, modelId, previousGlobal); }, save: () => { saveGlobalFastPreference(modelId, fast); }, append: () => { pi.appendEntry(FAST_ENTRY_TYPE, { modelId, fast }); authoritativeGlobalFastPreferenceIds.delete(modelId); }, markAppendFailed: () => { authoritativeGlobalFastPreferenceIds.add(modelId); }, }); } function persistCursorModePreference(pi: Pick, mode: AgentModeOption): void { const previousMode = sessionCursorAgentMode; sessionCursorAgentMode = mode; try { pi.appendEntry(MODE_ENTRY_TYPE, { mode }); } catch (error) { sessionCursorAgentMode = previousMode; throw error; } } function persistCursorHttp1Preference( pi: Pick, enabled: boolean, ): unknown | undefined { const previousSession = getStoredCursorHttp1Enabled(); return persistSessionGlobalPreference({ apply: () => { setStoredCursorHttp1Enabled(enabled); }, revert: () => { setStoredCursorHttp1Enabled(previousSession); }, save: () => { saveGlobalCursorHttp1Enabled(enabled); }, append: () => { pi.appendEntry(CURSOR_HTTP1_ENTRY_TYPE, { enabled }); setCursorHttp1GlobalPreferenceAuthoritative(false); }, markAppendFailed: () => { setCursorHttp1GlobalPreferenceAuthoritative(true); }, }); } function applyPersistedPreference( ctx: CursorStatusContext & Pick, persist: () => unknown | undefined, messages: { saveFailed: string; appendFailed: string; success: string }, ): void { let appendError: unknown; try { appendError = persist(); } catch (error) { updateCursorStatus(ctx); ctx.ui.notify(`${messages.saveFailed}: ${error instanceof Error ? error.message : String(error)}`, "error"); return; } updateCursorStatus(ctx); if (appendError !== undefined) { ctx.ui.notify( `${messages.appendFailed}: ${appendError instanceof Error ? appendError.message : String(appendError)}`, "error", ); return; } ctx.ui.notify(messages.success, "info"); } function restoreCliCursorMode(raw: boolean | string | undefined): void { cliCursorModeState = { kind: "unset" }; if (raw === undefined || raw === "" || raw === false) return; const parsed = parseCursorAgentMode(raw); if (parsed) { cliCursorModeState = { kind: "valid", mode: parsed }; return; } const rawText = String(raw); const message = formatInvalidCursorMode(rawText); cliCursorModeState = { kind: "invalid", raw: rawText, message }; } function notifyInvalidCursorModeIfCursorActive(ctx: Pick): void { const modeResolution = resolveCursorAgentMode(); if (modeResolution.kind !== "invalid" || !ctx.hasUI || ctx.mode !== "tui") return; const scopeKey = getCursorSessionScopeKey(); if (invalidCursorModeNotifiedSessionScopeKeys.has(scopeKey)) return; invalidCursorModeNotifiedSessionScopeKeys.add(scopeKey); ctx.ui.notify(modeResolution.message, "error"); } function formatEffectiveCursorSettingSourcesLabel(config: CursorSdkConfig): string { return getEffectiveCursorSettingSources(config).join(",") || "none"; } export function formatCursorToolsDebugReport( pi: Pick, excludedToolNames?: ReadonlySet, config: CursorSdkConfig = loadCursorSdkUserConfig(), ): string { const bridgeConfig = resolveCursorPiToolBridgeConfig(config); const bridgeEnabled = bridgeConfig.enabled; const manifestEnabled = resolveCursorToolManifestEnabled(config); const lines = [ "Cursor tool surfaces (current session):", `tools.bridge.enabled: ${bridgeEnabled ? "enabled" : "disabled"}`, `tools.manifest: ${manifestEnabled ? "enabled" : "disabled"}`, `local.settingSources: ${formatEffectiveCursorSettingSourcesLabel(config)}`, ]; let bridgeSnapshot; if (bridgeEnabled) { try { bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi, { exposeOverlappingBuiltins: bridgeConfig.exposeBuiltins, excludedToolNames, }); } catch { lines.push("Pi bridge snapshot: unavailable (extension tool APIs required)."); } } lines.push(buildCursorToolManifestText({ bridgeSnapshot, piBridgeEnabled: bridgeEnabled })); return lines.join("\n"); } function emitCursorToolsDebugReport( pi: Pick, ctx: Pick, ): void { let excludedToolNames: ReadonlySet | undefined; try { excludedToolNames = buildCursorBridgeExcludeToolNames(resolveEffectiveCursorConfigForContext(ctx)); } catch { // Config load failure must not break the debug report; fall back to the unfiltered snapshot. } const report = formatCursorToolsDebugReport(pi, excludedToolNames); if (ctx.hasUI) { ctx.ui.notify(report, "info"); return; } console.log(report); } export function getEffectiveFastForModelId(modelId: string): boolean | undefined { return getEffectiveFast(modelId); } export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void { registerCursorLocalRuntimeFlags(pi); pi.registerFlag("cursor-fast", { description: "Force Cursor fast mode for this run when the selected Cursor model supports it", type: "boolean", default: false, }); pi.registerFlag("cursor-no-fast", { description: "Force Cursor fast mode off for this run when the selected Cursor model supports it", type: "boolean", default: false, }); pi.registerFlag("cursor-mode", { description: "Set Cursor SDK conversation mode for this run: agent or plan", type: "string", default: "", }); pi.registerCommand("cursor-fast", { description: "Toggle Cursor fast mode for the selected Cursor model", handler: async (_args, ctx) => { const metadata = getCurrentCursorMetadata(ctx); if (!metadata?.supportsFast || !ctx.model) { const modelName = ctx.model?.id ?? "current model"; ctx.ui.notify(`Fast mode not supported by ${modelName}`, "info"); return; } if (cliForceNoFast) { ctx.ui.notify("Cursor fast is forced off by --cursor-no-fast", "info"); return; } if (cliForceFast) { ctx.ui.notify("Cursor fast is forced by --cursor-fast", "info"); return; } const preferenceModelId = metadata.piModelId; const current = getEffectiveFast(metadata.piModelId) ?? false; const next = !current; applyPersistedPreference(ctx, () => persistFastPreference(pi, preferenceModelId, next), { saveFailed: "Failed to save Cursor fast preference", appendFailed: `Cursor fast ${next ? "enabled" : "disabled"} was saved globally, but persisting the session entry failed`, success: `Cursor fast ${next ? "enabled" : "disabled"}`, }); }, }); pi.registerCommand("cursor-tools", { description: "Show live Cursor tool surfaces for this session (maintainer debug)", handler: async (_args, ctx) => { emitCursorToolsDebugReport(pi, ctx); }, }); pi.registerCommand("cursor-http", { description: "Toggle Cursor SDK HTTP/1.1/SSE transport compatibility mode", handler: async (args, ctx) => { const usage = "Usage: /cursor-http [on|off|toggle]"; const normalized = args.trim().toLowerCase(); const resolution = resolveCursorStatusRuntime(ctx); if (resolution.kind === "invalid") { ctx.ui.notify(resolution.message, "error"); return; } const setting = resolution.transport; const enabled = setting.value === "http1"; if (!normalized) { ctx.ui.notify( `Cursor HTTP/1.1/SSE transport is ${enabled ? "enabled" : "disabled"} (source: ${setting.source}). ${usage}`, "info", ); return; } const next = normalized === "on" ? true : normalized === "off" ? false : normalized === "toggle" ? !enabled : undefined; if (next === undefined) { ctx.ui.notify( `Invalid Cursor HTTP transport mode "${args.trim()}". ${usage}`, "error", ); return; } applyPersistedPreference(ctx, () => persistCursorHttp1Preference(pi, next), { saveFailed: "Failed to save Cursor HTTP/1.1 preference", appendFailed: "Cursor HTTP/1.1 preference was saved globally, but persisting the session entry failed", success: `Cursor HTTP/1.1/SSE transport ${next ? "enabled" : "disabled"}`, }); }, }); pi.registerCommand("cursor-refresh-config", { description: "Refresh filesystem Cursor config in the current pooled SDK agent", handler: async (_args, ctx) => { if (!isCursorModel(ctx.model)) { ctx.ui.notify("Cursor config refresh is available only for Cursor models.", "info"); return; } try { const result = await refreshSessionCursorAgentConfig(); const messages: Record = { reloaded: "Cursor SDK agent config refreshed.", "no-agent": "No Cursor SDK agent exists yet; config will load on the next Cursor run.", busy: "Cursor SDK agent is still finalizing a run; retry /cursor-refresh-config after it finishes.", unsupported: "Current Cursor SDK agent does not support config reload.", }; ctx.ui.notify(messages[result], result === "reloaded" ? "info" : "warning"); } catch (error) { ctx.ui.notify(`Failed to refresh Cursor config: ${error instanceof Error ? error.message : String(error)}`, "error"); } }, }); pi.registerCommand("cursor-mode", { description: "Set Cursor SDK conversation mode: agent or plan", handler: async (args, ctx) => { const usage = "Usage: /cursor-mode agent|plan"; const mode = parseCursorAgentMode(args); if (!args.trim()) { const modeResolution = resolveCursorAgentMode(); if (modeResolution.kind === "invalid") { ctx.ui.notify(`${modeResolution.message} ${usage}`, "error"); } else { ctx.ui.notify(`Cursor mode is ${modeResolution.mode}. ${usage}`, "info"); } return; } if (!mode) { ctx.ui.notify(`Invalid Cursor mode "${args.trim()}". ${usage}`, "error"); return; } if (cliCursorModeState.kind === "valid") { ctx.ui.notify(`Cursor mode is forced to ${cliCursorModeState.mode} by --cursor-mode`, "info"); return; } const clearedInvalidCliMode = cliCursorModeState.kind === "invalid"; try { persistCursorModePreference(pi, mode); if (clearedInvalidCliMode) cliCursorModeState = { kind: "unset" }; } catch (error) { updateCursorStatus(ctx); ctx.ui.notify(`Failed to save Cursor mode preference: ${error instanceof Error ? error.message : String(error)}`, "error"); return; } updateCursorStatus(ctx); ctx.ui.notify( clearedInvalidCliMode ? `Cursor mode set to ${mode}; cleared invalid --cursor-mode override` : `Cursor mode set to ${mode}`, "info", ); }, }); pi.on("session_tree", (_event, ctx) => { restoreSessionCursorPreferences(ctx); updateCursorStatus(ctx); }); registerCursorModelLifecycle(pi, { sessionStart: (_event, ctx) => { authoritativeGlobalFastPreferenceIds.clear(); setCursorHttp1GlobalPreferenceAuthoritative(false); globalFastPreferences = loadGlobalFastPreferences(); cliForceFast = pi.getFlag("cursor-fast") === true; cliForceNoFast = pi.getFlag("cursor-no-fast") === true; restoreCursorCliState(pi); restoreSessionCursorPreferences(ctx); restoreCliCursorMode(pi.getFlag("cursor-mode")); }, sync: (ctx) => { if (isCursorModel(ctx.model)) notifyInvalidCursorModeIfCursorActive(ctx); updateCursorStatus(ctx); }, }); } function resetCursorModeStateForTests(): void { sessionCursorAgentMode = undefined; setCursorHttp1GlobalPreferenceAuthoritative(false); setStoredCursorHttp1Enabled(undefined); cliCursorModeState = { kind: "unset" }; resetCursorRuntimeStateForTests(); invalidCursorModeNotifiedSessionScopeKeys.clear(); authoritativeGlobalFastPreferenceIds.clear(); } export const __testUtils = { FAST_ENTRY_TYPE, MODE_ENTRY_TYPE, CURSOR_HTTP1_ENTRY_TYPE, DEFAULT_CURSOR_AGENT_MODE, getConfigPath, loadGlobalFastPreferences, sessionFastPreferences, getSessionCursorAgentMode: () => sessionCursorAgentMode, getCliCursorAgentMode: () => (cliCursorModeState.kind === "valid" ? cliCursorModeState.mode : undefined), getCliCursorModeState: () => cliCursorModeState, resetCursorModeStateForTests, };