pi-cursor-sdk 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +23 -2
- package/docs/cursor-live-smoke-checklist.md +1 -1
- package/docs/cursor-model-ux-spec.md +5 -4
- package/docs/cursor-native-tool-replay.md +6 -4
- package/docs/cursor-testing-lessons.md +2 -2
- package/package.json +4 -2
- package/scripts/probe-mcp-coldstart.mjs +244 -0
- package/src/cursor-incomplete-tool-visibility.ts +51 -45
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-native-tool-display-replay.ts +2 -1
- package/src/cursor-provider-turn-coordinator.ts +29 -8
- package/src/cursor-provider.ts +55 -33
- package/src/cursor-sdk-event-debug.ts +6 -1
- package/src/cursor-session-agent.ts +262 -87
- package/src/cursor-tool-lifecycle.ts +9 -35
- package/src/cursor-tool-names.ts +27 -0
- package/src/cursor-tool-visibility.ts +63 -0
- package/src/cursor-transcript-tool-specs.ts +26 -14
|
@@ -26,22 +26,53 @@ export interface SessionCursorAgentSendState {
|
|
|
26
26
|
|
|
27
27
|
export interface SessionCursorAgentLease {
|
|
28
28
|
scopeKey: string;
|
|
29
|
+
poolKey: string;
|
|
30
|
+
instanceId: number;
|
|
29
31
|
agent: SDKAgent;
|
|
30
32
|
bridgeRun?: CursorPiToolBridgeRun;
|
|
31
33
|
sendState: SessionCursorAgentSendState;
|
|
32
34
|
created: boolean;
|
|
35
|
+
commitSend(context: Context, bootstrapped: boolean): void;
|
|
36
|
+
trackRunCompletion(completion: Promise<unknown>): void;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
interface
|
|
39
|
+
interface SessionCursorAgentPoolEntryBase {
|
|
36
40
|
poolKey: string;
|
|
41
|
+
instanceId: number;
|
|
37
42
|
scopeKey: string;
|
|
38
|
-
agent?: SDKAgent;
|
|
39
|
-
bridgeRun?: CursorPiToolBridgeRun;
|
|
40
43
|
sendState: SessionCursorAgentSendState;
|
|
41
|
-
creating?: Promise<SessionCursorAgentPoolEntry>;
|
|
42
|
-
creationGeneration?: number;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
interface SessionCursorAgentCreatingEntry extends SessionCursorAgentPoolEntryBase {
|
|
47
|
+
status: "creating";
|
|
48
|
+
creating: Promise<SessionCursorAgentReadyEntry>;
|
|
49
|
+
creationGeneration: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SessionCursorAgentReadyEntry extends SessionCursorAgentPoolEntryBase {
|
|
53
|
+
status: "ready";
|
|
54
|
+
agent: SDKAgent;
|
|
55
|
+
bridgeRun?: CursorPiToolBridgeRun;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
|
|
59
|
+
status: "busy";
|
|
60
|
+
agent: SDKAgent;
|
|
61
|
+
bridgeRun?: CursorPiToolBridgeRun;
|
|
62
|
+
completionSettled: Promise<void>;
|
|
63
|
+
pendingCompletion: Promise<void>;
|
|
64
|
+
releaseBusyWait: () => void;
|
|
65
|
+
busyGeneration: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type SessionCursorAgentActiveEntry = SessionCursorAgentReadyEntry | SessionCursorAgentBusyEntry;
|
|
69
|
+
type SessionCursorAgentPoolEntry =
|
|
70
|
+
| SessionCursorAgentCreatingEntry
|
|
71
|
+
| SessionCursorAgentReadyEntry
|
|
72
|
+
| SessionCursorAgentBusyEntry;
|
|
73
|
+
|
|
74
|
+
type SessionCursorAgentPoolState = { status: "empty" } | SessionCursorAgentPoolEntry;
|
|
75
|
+
|
|
45
76
|
class SessionCursorAgentCreationSupersededError extends Error {
|
|
46
77
|
constructor() {
|
|
47
78
|
super("Cursor session agent creation was superseded");
|
|
@@ -92,6 +123,20 @@ const sessionAgentsByScope = new Map<string, SessionCursorAgentPoolEntry>();
|
|
|
92
123
|
const invalidatedScopeKeys = new Set<string>();
|
|
93
124
|
const terminalDisposedScopeKeys = new Set<string>();
|
|
94
125
|
const scopeCreationGenerations = new Map<string, number>();
|
|
126
|
+
const EMPTY_POOL_STATE: SessionCursorAgentPoolState = { status: "empty" };
|
|
127
|
+
let nextSessionAgentInstanceId = 1;
|
|
128
|
+
|
|
129
|
+
function allocateSessionAgentInstanceId(): number {
|
|
130
|
+
return nextSessionAgentInstanceId++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getSessionCursorAgentPoolState(scopeKey: string): SessionCursorAgentPoolState {
|
|
134
|
+
return sessionAgentsByScope.get(scopeKey) ?? EMPTY_POOL_STATE;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isActivePoolEntry(entry: SessionCursorAgentPoolEntry | undefined): entry is SessionCursorAgentActiveEntry {
|
|
138
|
+
return entry?.status === "ready" || entry?.status === "busy";
|
|
139
|
+
}
|
|
95
140
|
|
|
96
141
|
function getScopeCreationGeneration(scopeKey: string): number {
|
|
97
142
|
return scopeCreationGenerations.get(scopeKey) ?? 0;
|
|
@@ -131,13 +176,13 @@ function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCr
|
|
|
131
176
|
}
|
|
132
177
|
|
|
133
178
|
async function disposePoolEntry(entry: SessionCursorAgentPoolEntry): Promise<void> {
|
|
179
|
+
if (!isActivePoolEntry(entry)) return;
|
|
134
180
|
entry.bridgeRun?.cancel("Cursor session agent disposed");
|
|
135
181
|
try {
|
|
136
182
|
await entry.bridgeRun?.dispose();
|
|
137
183
|
} catch {
|
|
138
184
|
// disposal failure should not block session replacement
|
|
139
185
|
}
|
|
140
|
-
if (!entry.agent) return;
|
|
141
186
|
try {
|
|
142
187
|
await entry.agent[Symbol.asyncDispose]();
|
|
143
188
|
} catch {
|
|
@@ -153,10 +198,12 @@ async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?:
|
|
|
153
198
|
const entry = sessionAgentsByScope.get(scopeKey);
|
|
154
199
|
invalidatedScopeKeys.delete(scopeKey);
|
|
155
200
|
if (!entry) return;
|
|
156
|
-
const orphanedCreating = entry.creating;
|
|
157
201
|
sessionAgentsByScope.delete(scopeKey);
|
|
158
|
-
if (entry.
|
|
159
|
-
|
|
202
|
+
if (entry.status === "busy") {
|
|
203
|
+
entry.releaseBusyWait();
|
|
204
|
+
}
|
|
205
|
+
if (entry.status === "creating") {
|
|
206
|
+
entry.creating.catch(() => {
|
|
160
207
|
// In-flight Agent.create was orphaned by scope disposal; active waiters surface errors elsewhere.
|
|
161
208
|
});
|
|
162
209
|
return;
|
|
@@ -169,14 +216,96 @@ function createInitialSendState(): SessionCursorAgentSendState {
|
|
|
169
216
|
}
|
|
170
217
|
|
|
171
218
|
function bindBridgeToolRequest(
|
|
172
|
-
entry:
|
|
219
|
+
entry: SessionCursorAgentActiveEntry,
|
|
173
220
|
onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void,
|
|
174
221
|
): void {
|
|
175
222
|
entry.bridgeRun?.setOnToolRequest(onBridgeToolRequest);
|
|
176
223
|
}
|
|
177
224
|
|
|
225
|
+
function commitSessionAgentSendForLease(
|
|
226
|
+
scopeKey: string,
|
|
227
|
+
poolKey: string,
|
|
228
|
+
instanceId: number,
|
|
229
|
+
context: Context,
|
|
230
|
+
bootstrapped: boolean,
|
|
231
|
+
): void {
|
|
232
|
+
const entry = sessionAgentsByScope.get(scopeKey);
|
|
233
|
+
if (!isActivePoolEntry(entry)) return;
|
|
234
|
+
if (entry.poolKey !== poolKey || entry.instanceId !== instanceId) return;
|
|
235
|
+
entry.sendState.bootstrapped = bootstrapped || entry.sendState.bootstrapped;
|
|
236
|
+
entry.sendState.contextFingerprint = computeCursorContextFingerprint(context);
|
|
237
|
+
if (bootstrapped) {
|
|
238
|
+
entry.sendState.incrementalSendCount = 0;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
entry.sendState.incrementalSendCount += 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeRunCompletion(completion: Promise<unknown>): Promise<void> {
|
|
245
|
+
return Promise.resolve(completion).then(
|
|
246
|
+
() => undefined,
|
|
247
|
+
() => undefined,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildBusyPoolEntry(
|
|
252
|
+
entry: SessionCursorAgentActiveEntry,
|
|
253
|
+
completionSettled: Promise<void>,
|
|
254
|
+
): SessionCursorAgentBusyEntry {
|
|
255
|
+
let releaseBusyWait = (): void => {};
|
|
256
|
+
const releaseSignal = new Promise<"released">((resolve) => {
|
|
257
|
+
releaseBusyWait = () => resolve("released");
|
|
258
|
+
});
|
|
259
|
+
const pendingCompletion = Promise.race([
|
|
260
|
+
completionSettled.then(() => "completed" as const),
|
|
261
|
+
releaseSignal,
|
|
262
|
+
]).then((outcome) => {
|
|
263
|
+
const current = sessionAgentsByScope.get(entry.scopeKey);
|
|
264
|
+
if (
|
|
265
|
+
outcome === "completed" &&
|
|
266
|
+
current?.status === "busy" &&
|
|
267
|
+
current.poolKey === entry.poolKey &&
|
|
268
|
+
current.instanceId === entry.instanceId &&
|
|
269
|
+
current.pendingCompletion === pendingCompletion
|
|
270
|
+
) {
|
|
271
|
+
sessionAgentsByScope.set(entry.scopeKey, { ...current, status: "ready" });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
...entry,
|
|
277
|
+
status: "busy",
|
|
278
|
+
completionSettled,
|
|
279
|
+
pendingCompletion,
|
|
280
|
+
releaseBusyWait,
|
|
281
|
+
busyGeneration: getScopeCreationGeneration(entry.scopeKey),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function trackSessionAgentRunCompletionForLease(
|
|
286
|
+
scopeKey: string,
|
|
287
|
+
poolKey: string,
|
|
288
|
+
instanceId: number,
|
|
289
|
+
completion: Promise<unknown>,
|
|
290
|
+
): void {
|
|
291
|
+
const entry = sessionAgentsByScope.get(scopeKey);
|
|
292
|
+
if (!isActivePoolEntry(entry)) return;
|
|
293
|
+
if (entry.poolKey !== poolKey || entry.instanceId !== instanceId) return;
|
|
294
|
+
|
|
295
|
+
const completionToTrack = normalizeRunCompletion(completion);
|
|
296
|
+
const completionSettled = (entry.status === "busy"
|
|
297
|
+
? Promise.all([entry.completionSettled, completionToTrack]).then(() => undefined)
|
|
298
|
+
: completionToTrack
|
|
299
|
+
);
|
|
300
|
+
if (entry.status === "busy") {
|
|
301
|
+
entry.releaseBusyWait();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
sessionAgentsByScope.set(scopeKey, buildBusyPoolEntry(entry, completionSettled));
|
|
305
|
+
}
|
|
306
|
+
|
|
178
307
|
function leaseFromEntry(
|
|
179
|
-
entry:
|
|
308
|
+
entry: SessionCursorAgentReadyEntry,
|
|
180
309
|
scopeKey: string,
|
|
181
310
|
params: SessionCursorAgentCreateParams,
|
|
182
311
|
created: boolean,
|
|
@@ -185,18 +314,54 @@ function leaseFromEntry(
|
|
|
185
314
|
entry.bridgeRun?.setDebugRecorder(params.debugRecorder);
|
|
186
315
|
return {
|
|
187
316
|
scopeKey,
|
|
188
|
-
|
|
317
|
+
poolKey: entry.poolKey,
|
|
318
|
+
instanceId: entry.instanceId,
|
|
319
|
+
agent: entry.agent,
|
|
189
320
|
bridgeRun: entry.bridgeRun,
|
|
190
321
|
sendState: entry.sendState,
|
|
191
322
|
created,
|
|
323
|
+
commitSend: (context, bootstrapped) => {
|
|
324
|
+
commitSessionAgentSendForLease(scopeKey, entry.poolKey, entry.instanceId, context, bootstrapped);
|
|
325
|
+
},
|
|
326
|
+
trackRunCompletion: (completion) => {
|
|
327
|
+
trackSessionAgentRunCompletionForLease(scopeKey, entry.poolKey, entry.instanceId, completion);
|
|
328
|
+
},
|
|
192
329
|
};
|
|
193
330
|
}
|
|
194
331
|
|
|
195
|
-
|
|
332
|
+
function getCurrentReadyPoolEntry(scopeKey: string, poolKey: string): SessionCursorAgentReadyEntry | undefined {
|
|
333
|
+
const current = sessionAgentsByScope.get(scopeKey);
|
|
334
|
+
if (current?.status !== "ready") return undefined;
|
|
335
|
+
if (current.poolKey !== poolKey) return undefined;
|
|
336
|
+
return current;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function tryLeaseReadyEntry(
|
|
340
|
+
entry: SessionCursorAgentActiveEntry,
|
|
196
341
|
scopeKey: string,
|
|
342
|
+
params: SessionCursorAgentCreateParams,
|
|
197
343
|
poolKey: string,
|
|
344
|
+
created: boolean,
|
|
345
|
+
): Promise<SessionCursorAgentLease | undefined> {
|
|
346
|
+
if (entry.status === "busy") {
|
|
347
|
+
await entry.pendingCompletion;
|
|
348
|
+
}
|
|
349
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
350
|
+
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
351
|
+
await disposePoolEntryForScope(scopeKey);
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const readyEntry = getCurrentReadyPoolEntry(scopeKey, poolKey);
|
|
355
|
+
if (!readyEntry) return undefined;
|
|
356
|
+
return leaseFromEntry(readyEntry, scopeKey, params, created);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function createSessionAgentEntry(
|
|
360
|
+
scopeKey: string,
|
|
361
|
+
instanceId: number,
|
|
362
|
+
sendState: SessionCursorAgentSendState,
|
|
198
363
|
params: SessionCursorAgentCreateParams,
|
|
199
|
-
): Promise<
|
|
364
|
+
): Promise<SessionCursorAgentReadyEntry> {
|
|
200
365
|
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
201
366
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
202
367
|
if (registeredBridge) {
|
|
@@ -233,11 +398,13 @@ async function createSessionAgentEntry(
|
|
|
233
398
|
}
|
|
234
399
|
|
|
235
400
|
return {
|
|
401
|
+
status: "ready",
|
|
236
402
|
poolKey: resolvedPoolKey,
|
|
403
|
+
instanceId,
|
|
237
404
|
scopeKey,
|
|
238
405
|
agent,
|
|
239
406
|
bridgeRun,
|
|
240
|
-
sendState
|
|
407
|
+
sendState,
|
|
241
408
|
};
|
|
242
409
|
}
|
|
243
410
|
|
|
@@ -249,95 +416,102 @@ export {
|
|
|
249
416
|
} from "./cursor-session-send-policy.js";
|
|
250
417
|
export { shouldBootstrapCursorContext, shouldBootstrapCursorSend } from "./context.js";
|
|
251
418
|
|
|
252
|
-
export function commitSessionAgentSend(scopeKey: string, context: Context, bootstrapped: boolean): void {
|
|
253
|
-
const entry = sessionAgentsByScope.get(scopeKey);
|
|
254
|
-
if (!entry) return;
|
|
255
|
-
entry.sendState.bootstrapped = bootstrapped || entry.sendState.bootstrapped;
|
|
256
|
-
entry.sendState.contextFingerprint = computeCursorContextFingerprint(context);
|
|
257
|
-
if (bootstrapped) {
|
|
258
|
-
entry.sendState.incrementalSendCount = 0;
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
entry.sendState.incrementalSendCount += 1;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
419
|
export function invalidateSessionAgent(scopeKey: string = getCursorSessionScopeKey()): void {
|
|
265
420
|
invalidatedScopeKeys.add(scopeKey);
|
|
266
421
|
}
|
|
267
422
|
|
|
268
423
|
export async function acquireSessionCursorAgent(params: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
|
|
269
424
|
const scopeKey = getCursorSessionScopeKey();
|
|
270
|
-
assertScopeAcceptsAcquire(scopeKey);
|
|
271
|
-
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
272
|
-
await disposePoolEntryForScope(scopeKey);
|
|
273
|
-
}
|
|
274
425
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
426
|
+
while (true) {
|
|
427
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
428
|
+
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
429
|
+
await disposePoolEntryForScope(scopeKey);
|
|
430
|
+
}
|
|
280
431
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
432
|
+
const poolKey = buildSessionAgentPoolKey(scopeKey, params);
|
|
433
|
+
const state = getSessionCursorAgentPoolState(scopeKey);
|
|
284
434
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
435
|
+
if ((state.status === "ready" || state.status === "busy") && state.poolKey !== poolKey) {
|
|
436
|
+
await disposePoolEntryForScope(scopeKey);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (state.status === "ready") {
|
|
441
|
+
return leaseFromEntry(state, scopeKey, params, false);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (state.status === "busy") {
|
|
445
|
+
const busyGeneration = state.busyGeneration;
|
|
446
|
+
await state.pendingCompletion;
|
|
447
|
+
if (busyGeneration !== getScopeCreationGeneration(scopeKey)) continue;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (state.status === "creating") {
|
|
452
|
+
if (state.poolKey !== poolKey) {
|
|
453
|
+
await disposePoolEntryForScope(scopeKey);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
await state.creating;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error instanceof SessionCursorAgentCreationSupersededError) {
|
|
460
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
461
|
+
rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
294
464
|
throw error;
|
|
295
465
|
}
|
|
466
|
+
continue;
|
|
296
467
|
}
|
|
297
|
-
entry = sessionAgentsByScope.get(scopeKey);
|
|
298
|
-
if (entry && entry.poolKey === poolKey && entry.agent && !entry.creating) {
|
|
299
|
-
return leaseFromEntry(entry, scopeKey, params, false);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
468
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
sendState
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
469
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
470
|
+
const creationGeneration = getScopeCreationGeneration(scopeKey);
|
|
471
|
+
const instanceId = allocateSessionAgentInstanceId();
|
|
472
|
+
const sendState = createInitialSendState();
|
|
473
|
+
let placeholder: SessionCursorAgentCreatingEntry;
|
|
474
|
+
const creating = createSessionAgentEntry(scopeKey, instanceId, sendState, params).then(async (createdEntry) => {
|
|
475
|
+
const stillCurrent =
|
|
476
|
+
sessionAgentsByScope.get(scopeKey) === placeholder &&
|
|
477
|
+
getScopeCreationGeneration(scopeKey) === placeholder.creationGeneration;
|
|
478
|
+
if (!stillCurrent) {
|
|
479
|
+
await disposePoolEntry(createdEntry);
|
|
480
|
+
if (sessionAgentsByScope.get(scopeKey) === placeholder) {
|
|
481
|
+
sessionAgentsByScope.delete(scopeKey);
|
|
482
|
+
}
|
|
483
|
+
throw new SessionCursorAgentCreationSupersededError();
|
|
484
|
+
}
|
|
485
|
+
sessionAgentsByScope.set(scopeKey, createdEntry);
|
|
486
|
+
return createdEntry;
|
|
487
|
+
});
|
|
488
|
+
placeholder = {
|
|
489
|
+
status: "creating",
|
|
490
|
+
poolKey,
|
|
491
|
+
instanceId,
|
|
492
|
+
scopeKey,
|
|
493
|
+
sendState,
|
|
494
|
+
creationGeneration,
|
|
495
|
+
creating,
|
|
496
|
+
};
|
|
497
|
+
sessionAgentsByScope.set(scopeKey, placeholder);
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const createdEntry = await creating;
|
|
501
|
+
const lease = await tryLeaseReadyEntry(createdEntry, scopeKey, params, poolKey, true);
|
|
502
|
+
if (lease) return lease;
|
|
503
|
+
continue;
|
|
504
|
+
} catch (error) {
|
|
317
505
|
if (sessionAgentsByScope.get(scopeKey) === placeholder) {
|
|
318
506
|
sessionAgentsByScope.delete(scopeKey);
|
|
319
507
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
sessionAgentsByScope.set(scopeKey, placeholder);
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
const createdEntry = await creating;
|
|
330
|
-
return leaseFromEntry(createdEntry, scopeKey, params, true);
|
|
331
|
-
} catch (error) {
|
|
332
|
-
if (sessionAgentsByScope.get(scopeKey) === placeholder) {
|
|
333
|
-
sessionAgentsByScope.delete(scopeKey);
|
|
334
|
-
}
|
|
335
|
-
if (error instanceof SessionCursorAgentCreationSupersededError) {
|
|
336
|
-
assertScopeAcceptsAcquire(scopeKey);
|
|
337
|
-
rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
|
|
338
|
-
return acquireSessionCursorAgent(params);
|
|
508
|
+
if (error instanceof SessionCursorAgentCreationSupersededError) {
|
|
509
|
+
assertScopeAcceptsAcquire(scopeKey);
|
|
510
|
+
rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
339
514
|
}
|
|
340
|
-
throw error;
|
|
341
515
|
}
|
|
342
516
|
}
|
|
343
517
|
|
|
@@ -383,6 +557,7 @@ export function registerCursorSessionAgent(_pi: CursorSessionAgentExtensionApi):
|
|
|
383
557
|
|
|
384
558
|
export const __testUtils = {
|
|
385
559
|
sessionAgentsByScope,
|
|
560
|
+
getSessionCursorAgentPoolState,
|
|
386
561
|
invalidateSessionAgent,
|
|
387
562
|
disposeSessionCursorAgent,
|
|
388
563
|
resetSessionCursorAgent,
|
|
@@ -1,44 +1,19 @@
|
|
|
1
1
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
2
|
-
import { getCursorReplayDisplayLabel, type CursorReplayLegacyToolName } from "./cursor-tool-names.js";
|
|
3
2
|
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
4
|
-
import { extractWebSearchQuery
|
|
5
|
-
import { firstNonEmptyLine, getArray, getString,
|
|
3
|
+
import { extractWebSearchQuery } from "./cursor-web-tool-activity.js";
|
|
4
|
+
import { firstNonEmptyLine, getArray, getString, truncateArg } from "./cursor-transcript-utils.js";
|
|
5
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
6
6
|
|
|
7
7
|
/** Defer pending lifecycle lines so fast start+complete pairs coalesce into the completed replay card only. */
|
|
8
8
|
export const CURSOR_TOOL_LIFECYCLE_DEFER_MS = 75;
|
|
9
9
|
|
|
10
|
-
const LIFECYCLE_ELIGIBLE_TOOLS = new Set(
|
|
11
|
-
["task", "shell", "mcp", "generateImage", "recordScreen", "semSearch", "webSearch", "webFetch", "createPlan", "updateTodos"].map(
|
|
12
|
-
(name) => name.toLowerCase(),
|
|
13
|
-
),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
const LIFECYCLE_TITLE_KEYS: Partial<Record<string, CursorReplayLegacyToolName>> = {
|
|
17
|
-
task: "cursor_task",
|
|
18
|
-
mcp: "cursor_mcp",
|
|
19
|
-
generateimage: "cursor_generate_image",
|
|
20
|
-
recordscreen: "cursor_record_screen",
|
|
21
|
-
semsearch: "cursor_sem_search",
|
|
22
|
-
websearch: "cursor_web_search",
|
|
23
|
-
webfetch: "cursor_web_fetch",
|
|
24
|
-
createplan: "cursor_create_plan",
|
|
25
|
-
updatetodos: "cursor_update_todos",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
10
|
export function isCursorToolLifecycleEligible(toolCall: unknown): boolean {
|
|
29
|
-
|
|
30
|
-
const name = resolveTranscriptToolName(getToolName(toolCall), args);
|
|
31
|
-
return LIFECYCLE_ELIGIBLE_TOOLS.has(normalizeToolName(name).toLowerCase());
|
|
11
|
+
return classifyCursorToolVisibility(toolCall).lifecycleEligible;
|
|
32
12
|
}
|
|
33
13
|
|
|
34
14
|
function getCursorToolLifecycleTitle(toolCall: unknown): string {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const normalized = normalizeToolName(name).toLowerCase();
|
|
38
|
-
const labelKey = LIFECYCLE_TITLE_KEYS[normalized];
|
|
39
|
-
if (labelKey) return getCursorReplayDisplayLabel(labelKey);
|
|
40
|
-
if (normalized === "shell") return "Cursor shell";
|
|
41
|
-
return `Cursor ${normalizeToolName(name)}`;
|
|
15
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
16
|
+
return visibility.lifecycleTitle ?? `Cursor ${visibility.normalizedName}`;
|
|
42
17
|
}
|
|
43
18
|
|
|
44
19
|
/** Prefixes that commonly introduce path/URI values in free-text pending lifecycle details. */
|
|
@@ -61,11 +36,10 @@ function scrubLifecycleDetail(value: string | undefined, apiKey?: string): strin
|
|
|
61
36
|
}
|
|
62
37
|
|
|
63
38
|
export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string): string | undefined {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const normalized = normalizeToolName(name).toLowerCase();
|
|
39
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
40
|
+
const args = visibility.args;
|
|
67
41
|
|
|
68
|
-
switch (
|
|
42
|
+
switch (visibility.normalizedKey) {
|
|
69
43
|
case "task": {
|
|
70
44
|
return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
|
|
71
45
|
}
|
package/src/cursor-tool-names.ts
CHANGED
|
@@ -51,6 +51,24 @@ const CURSOR_REPLAY_PROMPT_LABELS = {
|
|
|
51
51
|
cursor_web_fetch: "Cursor web fetch",
|
|
52
52
|
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
53
53
|
|
|
54
|
+
export const CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME = {
|
|
55
|
+
edit: "cursor_edit",
|
|
56
|
+
write: "cursor_write",
|
|
57
|
+
readLints: "cursor_read_lints",
|
|
58
|
+
delete: "cursor_delete",
|
|
59
|
+
updateTodos: "cursor_update_todos",
|
|
60
|
+
task: "cursor_task",
|
|
61
|
+
createPlan: "cursor_create_plan",
|
|
62
|
+
generateImage: "cursor_generate_image",
|
|
63
|
+
mcp: "cursor_mcp",
|
|
64
|
+
semSearch: "cursor_sem_search",
|
|
65
|
+
recordScreen: "cursor_record_screen",
|
|
66
|
+
webSearch: "cursor_web_search",
|
|
67
|
+
webFetch: "cursor_web_fetch",
|
|
68
|
+
} as const satisfies Record<string, CursorReplayLegacyToolName>;
|
|
69
|
+
|
|
70
|
+
export type CursorReplayActivityToolName = keyof typeof CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME;
|
|
71
|
+
|
|
54
72
|
export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
|
|
55
73
|
return CURSOR_REPLAY_LEGACY_TOOL_NAMES.some((legacyToolName) => legacyToolName === toolName);
|
|
56
74
|
}
|
|
@@ -77,3 +95,12 @@ export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): str
|
|
|
77
95
|
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
|
|
78
96
|
return CURSOR_REPLAY_PROMPT_LABELS[toolName];
|
|
79
97
|
}
|
|
98
|
+
|
|
99
|
+
export function getCursorReplayActivityLabelKey(toolName: string): CursorReplayLegacyToolName | undefined {
|
|
100
|
+
return CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME[toolName as CursorReplayActivityToolName];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getCursorReplayActivityTitle(toolName: string): string | undefined {
|
|
104
|
+
const labelKey = getCursorReplayActivityLabelKey(toolName);
|
|
105
|
+
return labelKey ? getCursorReplayDisplayLabel(labelKey) : undefined;
|
|
106
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getCursorReplayActivityTitle } from "./cursor-tool-names.js";
|
|
2
|
+
import { getToolArgs, getToolName, normalizeToolName } from "./cursor-transcript-utils.js";
|
|
3
|
+
import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
|
|
4
|
+
|
|
5
|
+
interface CursorToolVisibilityConfig {
|
|
6
|
+
incompleteTitle?: string;
|
|
7
|
+
lifecycleTitle?: string;
|
|
8
|
+
lifecycleEligible?: boolean;
|
|
9
|
+
fastLocalDiscovery?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CursorToolVisibility {
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
displayName: string;
|
|
15
|
+
normalizedName: string;
|
|
16
|
+
normalizedKey: string;
|
|
17
|
+
activityTitle?: string;
|
|
18
|
+
incompleteTitle?: string;
|
|
19
|
+
lifecycleTitle?: string;
|
|
20
|
+
lifecycleEligible: boolean;
|
|
21
|
+
fastLocalDiscovery: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CURSOR_TOOL_VISIBILITY_BY_NAME: Record<string, CursorToolVisibilityConfig> = {
|
|
25
|
+
read: { incompleteTitle: "Cursor read", fastLocalDiscovery: true },
|
|
26
|
+
grep: { incompleteTitle: "Cursor grep", fastLocalDiscovery: true },
|
|
27
|
+
glob: { incompleteTitle: "Cursor find", fastLocalDiscovery: true },
|
|
28
|
+
ls: { incompleteTitle: "Cursor ls", fastLocalDiscovery: true },
|
|
29
|
+
shell: { incompleteTitle: "Cursor shell", lifecycleTitle: "Cursor shell", lifecycleEligible: true },
|
|
30
|
+
task: { lifecycleEligible: true },
|
|
31
|
+
mcp: { lifecycleEligible: true },
|
|
32
|
+
generateimage: { lifecycleEligible: true },
|
|
33
|
+
recordscreen: { lifecycleEligible: true },
|
|
34
|
+
semsearch: { lifecycleEligible: true },
|
|
35
|
+
websearch: { lifecycleEligible: true },
|
|
36
|
+
webfetch: { lifecycleEligible: true },
|
|
37
|
+
createplan: { lifecycleEligible: true },
|
|
38
|
+
updatetodos: { lifecycleEligible: true },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function classifyCursorToolVisibility(toolCall: unknown): CursorToolVisibility {
|
|
42
|
+
const args = getToolArgs(toolCall);
|
|
43
|
+
const displayName = resolveTranscriptToolName(getToolName(toolCall), args);
|
|
44
|
+
const normalizedName = normalizeToolName(displayName);
|
|
45
|
+
const normalizedKey = normalizedName.toLowerCase();
|
|
46
|
+
const config = CURSOR_TOOL_VISIBILITY_BY_NAME[normalizedKey];
|
|
47
|
+
const replayActivityTitle = getCursorReplayActivityTitle(normalizedName);
|
|
48
|
+
return {
|
|
49
|
+
args,
|
|
50
|
+
displayName,
|
|
51
|
+
normalizedName,
|
|
52
|
+
normalizedKey,
|
|
53
|
+
activityTitle: replayActivityTitle ?? config?.incompleteTitle ?? config?.lifecycleTitle,
|
|
54
|
+
incompleteTitle: replayActivityTitle ?? config?.incompleteTitle,
|
|
55
|
+
lifecycleTitle: replayActivityTitle ?? config?.lifecycleTitle,
|
|
56
|
+
lifecycleEligible: config?.lifecycleEligible ?? false,
|
|
57
|
+
fastLocalDiscovery: config?.fastLocalDiscovery ?? false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isFastLocalDiscoveryTool(toolCall: unknown): boolean {
|
|
62
|
+
return classifyCursorToolVisibility(toolCall).fastLocalDiscovery;
|
|
63
|
+
}
|