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.
@@ -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 SessionCursorAgentPoolEntry {
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.creating || !entry.agent) {
159
- orphanedCreating?.catch(() => {
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: SessionCursorAgentPoolEntry,
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: SessionCursorAgentPoolEntry,
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
- agent: entry.agent!,
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
- async function createSessionAgentEntry(
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<SessionCursorAgentPoolEntry> {
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: createInitialSendState(),
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
- const poolKey = buildSessionAgentPoolKey(scopeKey, params);
276
- const existing = sessionAgentsByScope.get(scopeKey);
277
- if (existing?.poolKey === poolKey && !existing.creating) {
278
- return leaseFromEntry(existing, scopeKey, params, false);
279
- }
426
+ while (true) {
427
+ assertScopeAcceptsAcquire(scopeKey);
428
+ if (invalidatedScopeKeys.has(scopeKey)) {
429
+ await disposePoolEntryForScope(scopeKey);
430
+ }
280
431
 
281
- if (existing && existing.poolKey !== poolKey) {
282
- await disposePoolEntryForScope(scopeKey);
283
- }
432
+ const poolKey = buildSessionAgentPoolKey(scopeKey, params);
433
+ const state = getSessionCursorAgentPoolState(scopeKey);
284
434
 
285
- let entry = sessionAgentsByScope.get(scopeKey);
286
- if (entry?.creating) {
287
- try {
288
- await entry.creating;
289
- } catch (error) {
290
- if (error instanceof SessionCursorAgentCreationSupersededError) {
291
- assertScopeAcceptsAcquire(scopeKey);
292
- rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
293
- } else {
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
- assertScopeAcceptsAcquire(scopeKey);
304
- const creationGeneration = getScopeCreationGeneration(scopeKey);
305
- const placeholder: SessionCursorAgentPoolEntry = {
306
- poolKey,
307
- scopeKey,
308
- sendState: createInitialSendState(),
309
- creationGeneration,
310
- };
311
- const creating = createSessionAgentEntry(scopeKey, poolKey, params).then(async (createdEntry) => {
312
- const stillCurrent =
313
- sessionAgentsByScope.get(scopeKey) === placeholder &&
314
- getScopeCreationGeneration(scopeKey) === placeholder.creationGeneration;
315
- if (!stillCurrent) {
316
- await disposePoolEntry(createdEntry);
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
- throw new SessionCursorAgentCreationSupersededError();
321
- }
322
- sessionAgentsByScope.set(scopeKey, createdEntry);
323
- return createdEntry;
324
- });
325
- placeholder.creating = creating;
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, resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
5
- import { firstNonEmptyLine, getArray, getString, getToolArgs, getToolName, normalizeToolName, truncateArg } from "./cursor-transcript-utils.js";
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
- const args = getToolArgs(toolCall);
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 args = getToolArgs(toolCall);
36
- const name = resolveTranscriptToolName(getToolName(toolCall), args);
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 args = getToolArgs(toolCall);
65
- const name = resolveTranscriptToolName(getToolName(toolCall), args);
66
- const normalized = normalizeToolName(name).toLowerCase();
39
+ const visibility = classifyCursorToolVisibility(toolCall);
40
+ const args = visibility.args;
67
41
 
68
- switch (normalized) {
42
+ switch (visibility.normalizedKey) {
69
43
  case "task": {
70
44
  return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
71
45
  }
@@ -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
+ }