import type { ExtensionHandler, SessionInfoChangedEvent, SessionStartEvent } from "@earendil-works/pi-coding-agent"; import { truncateCursorDisplayLine } from "./display-text.js"; interface CursorSessionScopeExtensionApi { on(event: "session_start", handler: ExtensionHandler): void; on(event: "session_info_changed", handler: ExtensionHandler): void; } const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__"; const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:"; export const MAX_CURSOR_SESSION_NAME_LENGTH = 100; type CursorSessionScopeChangeHandler = (previousScopeKey: string) => Promise | void; const state = { sessionCwd: process.cwd(), sessionFile: undefined as string | undefined, sessionId: undefined as string | undefined, sessionName: undefined as string | undefined, sessionGeneration: 0, }; const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration]]); let nextSessionGeneration = 1; let scopeChangeHandler: CursorSessionScopeChangeHandler | undefined; /** * Pi session file when known; used to scope reused Cursor SDK agents to one pi session. */ export function getCursorSessionFile(): string | undefined { return state.sessionFile; } /** * Stable scope key for session-agent pooling. Falls back to a process-local anonymous key * before the first session_start (tests and early startup). */ export function getCursorSessionScopeKey(): string { if (state.sessionFile) return state.sessionFile; if (state.sessionId) return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${state.sessionId}`; return ANONYMOUS_SESSION_SCOPE_KEY; } export function getCursorSessionScopeGeneration(scopeKey: string = getCursorSessionScopeKey()): number { return scopeGenerations.get(scopeKey) ?? 0; } /** * Pi session cwd when known; falls back to process.cwd() before session_start. * Updated on session_start only until pi threads cwd into streamSimple—mid-session cwd * changes without a new session_start event are not reflected here. */ export function getCursorSessionCwd(): string { return state.sessionCwd; } export function getCursorSessionName(): string | undefined { return state.sessionName; } function normalizeCursorSessionName(name: string | undefined): string | undefined { if (name === undefined) return undefined; return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined; } function setCursorSessionScope( cwd: string, sessionFile: string | undefined, sessionId?: string, sessionName?: string, ): void { state.sessionCwd = cwd; state.sessionFile = sessionFile; state.sessionId = sessionId; state.sessionName = normalizeCursorSessionName(sessionName); state.sessionGeneration = nextSessionGeneration; nextSessionGeneration += 1; scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration); } function resetCursorSessionScope(): void { state.sessionCwd = process.cwd(); state.sessionFile = undefined; state.sessionId = undefined; state.sessionName = undefined; state.sessionGeneration = 0; nextSessionGeneration = 1; scopeGenerations.clear(); scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration); } export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeHandler): void { scopeChangeHandler = handler; } export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi): void { pi.on("session_start", async (_event, ctx) => { const previousScopeKey = getCursorSessionScopeKey(); setCursorSessionScope( ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined, ctx.sessionManager?.getSessionId?.() ?? undefined, ctx.sessionManager?.getSessionName?.() ?? undefined, ); if (previousScopeKey !== getCursorSessionScopeKey()) { await scopeChangeHandler?.(previousScopeKey); } }); pi.on("session_info_changed", (event) => { state.sessionName = normalizeCursorSessionName(event.name); }); } export const __testUtils = { ANONYMOUS_SESSION_SCOPE_KEY, EPHEMERAL_SESSION_SCOPE_PREFIX, set: setCursorSessionScope, reset: resetCursorSessionScope, };