pi-cursor-sdk 0.1.19 → 0.1.21

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +72 -11
  3. package/docs/cursor-dogfood-checklist.md +57 -0
  4. package/docs/cursor-live-smoke-checklist.md +116 -10
  5. package/docs/cursor-model-ux-spec.md +60 -19
  6. package/docs/cursor-native-tool-replay.md +21 -11
  7. package/docs/cursor-native-tool-visual-audit.md +104 -59
  8. package/docs/cursor-testing-lessons.md +10 -5
  9. package/docs/cursor-tool-surfaces.md +69 -0
  10. package/package.json +37 -11
  11. package/scripts/debug-provider-events.d.mts +59 -0
  12. package/scripts/debug-provider-events.mjs +70 -175
  13. package/scripts/debug-sdk-events.d.mts +90 -0
  14. package/scripts/debug-sdk-events.mjs +36 -98
  15. package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
  16. package/scripts/isolated-cursor-smoke.sh +264 -102
  17. package/scripts/lib/cursor-child-process.d.mts +10 -0
  18. package/scripts/lib/cursor-child-process.mjs +50 -0
  19. package/scripts/lib/cursor-cli-args.d.mts +63 -0
  20. package/scripts/lib/cursor-cli-args.mjs +129 -0
  21. package/scripts/lib/cursor-script-fail.d.mts +1 -0
  22. package/scripts/lib/cursor-script-fail.mjs +13 -0
  23. package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
  24. package/scripts/lib/cursor-smoke-env.d.mts +38 -0
  25. package/scripts/lib/cursor-smoke-env.mjs +81 -0
  26. package/scripts/lib/cursor-smoke-shell.sh +174 -0
  27. package/scripts/lib/cursor-visual-render.d.mts +15 -0
  28. package/scripts/lib/cursor-visual-render.mjs +131 -0
  29. package/scripts/probe-mcp-coldstart.mjs +226 -0
  30. package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
  31. package/scripts/steering-rpc-smoke.mjs +170 -65
  32. package/scripts/tmux-live-smoke.sh +152 -98
  33. package/scripts/visual-tui-smoke.mjs +659 -0
  34. package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
  35. package/shared/cursor-sdk-event-debug-env.mjs +13 -0
  36. package/shared/cursor-sensitive-text.d.mts +1 -0
  37. package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
  38. package/shared/cursor-setting-sources.d.mts +5 -0
  39. package/shared/cursor-setting-sources.mjs +22 -0
  40. package/src/context.ts +21 -12
  41. package/src/cursor-bridge-contract.ts +1 -3
  42. package/src/cursor-incomplete-tool-visibility.ts +72 -49
  43. package/src/cursor-mcp-timeout-override.ts +66 -11
  44. package/src/cursor-native-tool-display-registration.ts +63 -27
  45. package/src/cursor-native-tool-display-replay.ts +246 -143
  46. package/src/cursor-native-tool-display-state.ts +2 -0
  47. package/src/cursor-native-tool-display-tools.ts +149 -41
  48. package/src/cursor-provider-live-run-drain.ts +1 -52
  49. package/src/cursor-provider-run-finalizer.ts +235 -0
  50. package/src/cursor-provider-run-outcome.ts +149 -0
  51. package/src/cursor-provider-turn-api-key.ts +8 -0
  52. package/src/cursor-provider-turn-coordinator.ts +113 -440
  53. package/src/cursor-provider-turn-display-router.ts +216 -0
  54. package/src/cursor-provider-turn-emit.ts +59 -0
  55. package/src/cursor-provider-turn-finalize.ts +119 -0
  56. package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
  57. package/src/cursor-provider-turn-message-offset.ts +15 -0
  58. package/src/cursor-provider-turn-prepare.ts +216 -0
  59. package/src/cursor-provider-turn-runner.ts +138 -0
  60. package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
  61. package/src/cursor-provider-turn-send.ts +103 -0
  62. package/src/cursor-provider-turn-shell-output.ts +107 -0
  63. package/src/cursor-provider-turn-tool-ledger.ts +126 -0
  64. package/src/cursor-provider-turn-types.ts +87 -0
  65. package/src/cursor-provider.ts +16 -482
  66. package/src/cursor-replay-activity-builders.ts +276 -0
  67. package/src/cursor-replay-source-names.ts +33 -0
  68. package/src/cursor-replay-summary-args.ts +191 -0
  69. package/src/cursor-replay-tool-details.ts +464 -0
  70. package/src/cursor-run-final-text.ts +56 -0
  71. package/src/cursor-sdk-abort-error-guard.ts +4 -0
  72. package/src/cursor-sdk-event-debug-constants.ts +14 -5
  73. package/src/cursor-sdk-event-debug.ts +8 -2
  74. package/src/cursor-sensitive-text.ts +3 -36
  75. package/src/cursor-session-agent.ts +265 -88
  76. package/src/cursor-setting-sources.ts +7 -10
  77. package/src/cursor-state.ts +232 -28
  78. package/src/cursor-tool-lifecycle.ts +17 -42
  79. package/src/cursor-tool-manifest.ts +41 -0
  80. package/src/cursor-tool-names.ts +18 -79
  81. package/src/cursor-tool-presentation-registry.ts +556 -0
  82. package/src/cursor-tool-transcript.ts +1 -1
  83. package/src/cursor-tool-visibility.ts +39 -0
  84. package/src/cursor-transcript-tool-formatters.ts +0 -59
  85. package/src/cursor-transcript-tool-specs.ts +169 -232
  86. package/src/cursor-transcript-utils.ts +0 -44
  87. package/src/cursor-web-tool-activity.ts +10 -60
  88. package/src/cursor-web-tool-args.ts +39 -0
  89. package/src/index.ts +4 -10
@@ -7,10 +7,47 @@ import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
7
7
  import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
8
8
  import {
9
9
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
10
- getCursorReplayDisplayLabel,
11
- getCursorReplaySourceToolName,
10
+ getCursorReplayCallSummary,
11
+ getCursorReplaySideEffectDescription,
12
+ getCursorReplayOperationLabel,
13
+ getCursorReplayWrapperLabel,
12
14
  type CursorReplayToolName,
13
- } from "./cursor-tool-names.js";
15
+ } from "./cursor-tool-presentation-registry.js";
16
+ import {
17
+ CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE,
18
+ type CursorReplayNativeEditDetails,
19
+ type CursorReplayGenerateImageDetails,
20
+ type CursorReplayActivityDetails,
21
+ type CursorReplayToolDetails,
22
+ type CursorReplayNativeWriteDetails,
23
+ isCursorReplayNativeEditDetails,
24
+ isCursorReplayGenerateImageDetails,
25
+ isCursorReplayActivityDetails,
26
+ parseCursorReplayToolDetails,
27
+ } from "./cursor-replay-tool-details.js";
28
+
29
+ export type {
30
+ CursorReplayNativeEditDetails,
31
+ CursorReplayEditDetails,
32
+ CursorReplayGenerateImageDetails,
33
+ CursorReplayGenericFallbackDetails,
34
+ CursorReplayActivityDetails,
35
+ CursorReplayTitledActivityDetails,
36
+ CursorReplayToolDetails,
37
+ CursorReplayNativeWriteDetails,
38
+ CursorReplayWriteDetails,
39
+ } from "./cursor-replay-tool-details.js";
40
+ export {
41
+ asCursorReplayToolDetails,
42
+ isCursorReplayNativeEditDetails,
43
+ isCursorReplayEditDetails,
44
+ isCursorReplayGenerateImageDetails,
45
+ isCursorReplayActivityDetails,
46
+ isCursorReplayTitledActivityDetails,
47
+ isCursorReplayNativeWriteDetails,
48
+ isCursorReplayWriteDetails,
49
+ parseCursorReplayToolDetails,
50
+ } from "./cursor-replay-tool-details.js";
14
51
 
15
52
  export const CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES = 8;
16
53
  export const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
@@ -18,29 +55,6 @@ export const CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS = 240;
18
55
  const CURSOR_REPLAY_HIGHLIGHT_MAX_CHARS = 12000;
19
56
  export const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
20
57
 
21
- export interface CursorReplayToolDetails {
22
- cursorToolName?: string;
23
- title?: string;
24
- summary?: string;
25
- path?: string;
26
- imagePath?: string;
27
- imageDisplayPath?: string;
28
- imageMimeType?: string;
29
- linesAdded?: number;
30
- linesRemoved?: number;
31
- linesCreated?: number;
32
- fileSize?: number;
33
- fileContentAfterWrite?: string;
34
- diffString?: string;
35
- diff?: string;
36
- firstChangedLine?: number;
37
- expandedText?: string;
38
- }
39
-
40
- export function asCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
41
- return value && typeof value === "object" ? (value as CursorReplayToolDetails) : undefined;
42
- }
43
-
44
58
  type CursorReplayRenderCall = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderCall"]>;
45
59
  type CursorReplayRenderResult = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderResult"]>;
46
60
  export type CursorReplayRenderTheme = Parameters<CursorReplayRenderCall>[1];
@@ -86,13 +100,10 @@ function buildImageReplayComponent(text: string, imageData: string, mimeType: st
86
100
  };
87
101
  }
88
102
 
89
- function getCursorReplayToolLabel(toolName: CursorReplayToolName): string {
90
- if (toolName === "cursor_edit") return "edit";
91
- if (toolName === "cursor_write") return "write";
92
- return getCursorReplayDisplayLabel(toolName);
93
- }
94
-
95
- export function getCursorReplayPath(args: Record<string, unknown> | undefined, details: CursorReplayToolDetails | undefined): string {
103
+ export function getCursorReplayPath(
104
+ args: Record<string, unknown> | undefined,
105
+ details: Pick<CursorReplayNativeEditDetails, "path"> | Pick<CursorReplayNativeWriteDetails, "path"> | undefined,
106
+ ): string {
96
107
  const argPath = args?.path;
97
108
  return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
98
109
  }
@@ -211,6 +222,93 @@ function formatMutedBlock(text: string, theme: CursorReplayRenderTheme): string
211
222
  return text.split("\n").map((line) => theme.fg("muted", line)).join("\n");
212
223
  }
213
224
 
225
+ function hasUnifiedDiffHunk(text: string): boolean {
226
+ return text.split("\n").some((line) => Boolean(parseUnifiedDiffHunkHeader(line)));
227
+ }
228
+
229
+ /** First unified-diff marker (`---`/`+++`/`@@`) so collapsed previews budget diff lines, not transcript preamble. */
230
+ function extractUnifiedDiffSection(text: string): string | undefined {
231
+ const lines = text.split("\n");
232
+ const markerIndex = lines.findIndex((line) => line.startsWith("--- ") || line.startsWith("+++ "));
233
+ const hunkIndex = lines.findIndex((line) => Boolean(parseUnifiedDiffHunkHeader(line)));
234
+ const start = markerIndex >= 0 ? markerIndex : hunkIndex;
235
+ if (start < 0) return undefined;
236
+ return lines.slice(start).join("\n");
237
+ }
238
+
239
+ function formatCursorReplayActivityDiffPreview(
240
+ text: string,
241
+ theme: CursorReplayRenderTheme,
242
+ maxLines: number,
243
+ stripHeader: boolean,
244
+ ): string | undefined {
245
+ const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
246
+ const diffSection = body ? extractUnifiedDiffSection(body) : undefined;
247
+ if (!diffSection || !hasUnifiedDiffHunk(diffSection)) return undefined;
248
+ // Legacy-only shim (old JSONL with no diffString on the activity details).
249
+ // All actual diff coloring now lives in the single `formatCursorReplayDiff` renderer.
250
+ // This path exists solely so pre-structured sessions still get colored diffs via the same
251
+ // high-quality implementation used for nativeEdit and new structured activity cases.
252
+ return formatCursorReplayDiff(diffSection, theme, maxLines);
253
+ }
254
+
255
+ function formatCursorReplayActivityEditPreview(
256
+ details: CursorReplayExpandableResultDetails,
257
+ text: string,
258
+ theme: CursorReplayRenderTheme,
259
+ maxLines: number,
260
+ stripHeader: boolean,
261
+ ): string | undefined {
262
+ const structuredDiff = details.diffString ?? details.diff;
263
+ if (structuredDiff) {
264
+ return formatCursorReplayDiff(structuredDiff, theme, maxLines);
265
+ }
266
+ const diffPreview = formatCursorReplayActivityDiffPreview(text, theme, maxLines, stripHeader);
267
+ if (diffPreview) return diffPreview;
268
+ return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
269
+ }
270
+
271
+ function formatCursorReplayActivityWritePreview(
272
+ details: CursorReplayExpandableResultDetails,
273
+ text: string,
274
+ theme: CursorReplayRenderTheme,
275
+ maxLines: number,
276
+ stripHeader: boolean,
277
+ ): string | undefined {
278
+ const structuredDiff = details.diffString ?? details.diff;
279
+ if (structuredDiff) {
280
+ return formatCursorReplayDiff(structuredDiff, theme, maxLines);
281
+ }
282
+ if (details.fileContentAfterWrite) {
283
+ return formatCursorReplayFilePreview(
284
+ details.fileContentAfterWrite,
285
+ details.path,
286
+ theme,
287
+ maxLines,
288
+ false,
289
+ );
290
+ }
291
+ const diffPreview = formatCursorReplayActivityDiffPreview(text, theme, maxLines, stripHeader);
292
+ if (diffPreview) return diffPreview;
293
+ return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
294
+ }
295
+
296
+ function formatCursorReplayActivityPreview(
297
+ details: CursorReplayExpandableResultDetails,
298
+ text: string,
299
+ theme: CursorReplayRenderTheme,
300
+ maxLines: number,
301
+ stripHeader: boolean,
302
+ ): string | undefined {
303
+ if (details.sourceToolName === "edit") {
304
+ return formatCursorReplayActivityEditPreview(details, text, theme, maxLines, stripHeader);
305
+ }
306
+ if (details.sourceToolName === "write") {
307
+ return formatCursorReplayActivityWritePreview(details, text, theme, maxLines, stripHeader);
308
+ }
309
+ return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
310
+ }
311
+
214
312
  export function formatCursorReplayPreview(
215
313
  text: string,
216
314
  theme: CursorReplayRenderTheme,
@@ -256,69 +354,7 @@ function getCursorReplayActivityTitle(toolName: CursorReplayToolName, args: Reco
256
354
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && typeof args?.activityTitle === "string" && args.activityTitle.trim()) {
257
355
  return args.activityTitle.trim();
258
356
  }
259
- return getCursorReplayToolLabel(toolName);
260
- }
261
-
262
- function formatReplayRecordingDurationMs(ms: number | undefined): string | undefined {
263
- if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
264
- if (ms < 1000) return `${Math.round(ms)}ms`;
265
- const seconds = ms / 1000;
266
- return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
267
- }
268
-
269
- function formatReplaySemSearchQuery(args: Record<string, unknown> | undefined): string | undefined {
270
- const query = typeof args?.query === "string" ? args.query.trim() : undefined;
271
- if (!query) return undefined;
272
- const targetDirectories = Array.isArray(args?.targetDirectories)
273
- ? args.targetDirectories.filter((entry): entry is string => typeof entry === "string")
274
- : [];
275
- const dirHint =
276
- targetDirectories.length > 0 ? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})` : "";
277
- return `${query}${dirHint}`;
278
- }
279
-
280
- function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string | undefined {
281
- const activitySummary = typeof args?.activitySummary === "string" && args.activitySummary.trim() ? args.activitySummary.trim() : undefined;
282
- if (activitySummary) return activitySummary;
283
-
284
- const path = typeof args?.path === "string" ? args.path : undefined;
285
- const description = typeof args?.description === "string" ? args.description : undefined;
286
- const prompt = typeof args?.prompt === "string" ? args.prompt : undefined;
287
- const totalCount = typeof args?.totalCount === "number" ? args.totalCount : undefined;
288
- const diagnosticCount = typeof args?.diagnosticCount === "number" ? args.diagnosticCount : undefined;
289
- const paths = Array.isArray(args?.paths) ? args.paths.filter((entry): entry is string => typeof entry === "string") : [];
290
-
291
- if (toolName === "cursor_edit" || toolName === "cursor_write" || toolName === "cursor_delete") return path ?? "unknown";
292
- if (toolName === "cursor_read_lints") {
293
- const target = paths.length > 0 ? paths.join(", ") : path;
294
- if (target && diagnosticCount !== undefined) {
295
- return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"} in ${target}`;
296
- }
297
- return target;
298
- }
299
- if (toolName === "cursor_update_todos" || toolName === "cursor_create_plan") {
300
- return totalCount !== undefined ? `${totalCount} item${totalCount === 1 ? "" : "s"}` : undefined;
301
- }
302
- if (toolName === "cursor_task") return description;
303
- if (toolName === "cursor_generate_image") return path ?? prompt;
304
- if (toolName === "cursor_mcp") return typeof args?.toolName === "string" ? args.toolName : undefined;
305
- if (toolName === "cursor_sem_search") return formatReplaySemSearchQuery(args);
306
- if (toolName === "cursor_record_screen") {
307
- const duration = formatReplayRecordingDurationMs(
308
- typeof args?.recordingDurationMs === "number" ? args.recordingDurationMs : undefined,
309
- );
310
- if (path && duration) return `${path} · ${duration}`;
311
- if (path) return path;
312
- if (typeof args?.mode === "string") return args.mode;
313
- }
314
- if (toolName === "cursor_web_search") return typeof args?.query === "string" ? args.query : undefined;
315
- if (toolName === "cursor_web_fetch") return typeof args?.url === "string" ? args.url : undefined;
316
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
317
- if (path) return path;
318
- if (typeof args?.toolName === "string") return args.toolName;
319
- return formatReplaySemSearchQuery(args);
320
- }
321
- return undefined;
357
+ return getCursorReplayWrapperLabel(toolName);
322
358
  }
323
359
 
324
360
  export function renderCursorReplayCall(
@@ -360,15 +396,15 @@ function pluralize(count: number, noun: string): string {
360
396
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
361
397
  }
362
398
 
363
- function getCursorEditDiff(details: CursorReplayToolDetails): string | undefined {
399
+ function getCursorEditDiff(details: CursorReplayNativeEditDetails): string | undefined {
364
400
  return resolveCursorEditDiff(details);
365
401
  }
366
402
 
367
- function hasCursorEditChanges(details: CursorReplayToolDetails): boolean {
403
+ function hasCursorEditChanges(details: CursorReplayNativeEditDetails): boolean {
368
404
  return Boolean(getCursorEditDiff(details)) || Boolean(details.linesAdded) || Boolean(details.linesRemoved);
369
405
  }
370
406
 
371
- function classifyCursorEditOperation(details: CursorReplayToolDetails): "created" | "deleted" | "updated" | "unchanged" {
407
+ function classifyCursorEditOperation(details: CursorReplayNativeEditDetails): "created" | "deleted" | "updated" | "unchanged" {
372
408
  if (!hasCursorEditChanges(details)) return "unchanged";
373
409
  const diff = getCursorEditDiff(details);
374
410
  if (diff?.startsWith("--- /dev/null")) return "created";
@@ -376,7 +412,7 @@ function classifyCursorEditOperation(details: CursorReplayToolDetails): "created
376
412
  return "updated";
377
413
  }
378
414
 
379
- function formatCursorEditSummary(details: CursorReplayToolDetails): string {
415
+ function formatCursorEditSummary(details: CursorReplayNativeEditDetails): string {
380
416
  const operation = classifyCursorEditOperation(details);
381
417
  if (operation === "unchanged") return "no changes needed";
382
418
  if (operation === "created" && details.linesAdded !== undefined) return `created ${pluralize(details.linesAdded, "line")}`;
@@ -393,24 +429,52 @@ function firstContentText(result: Parameters<CursorReplayRenderResult>[0]): stri
393
429
  return content?.type === "text" ? content.text : "";
394
430
  }
395
431
 
432
+ type CursorReplayExpandableResultDetails = {
433
+ summary?: string;
434
+ expandedText?: string;
435
+ collapseDetailsByDefault?: boolean;
436
+ imagePath?: string;
437
+ imageMimeType?: string;
438
+ sourceToolName?: CursorReplayActivityDetails["sourceToolName"];
439
+ path?: string;
440
+ /** Structured diff fields populated on activity edit/write details for canonical coloring (primary over text parse). */
441
+ diffString?: string;
442
+ diff?: string;
443
+ linesAdded?: number;
444
+ linesRemoved?: number;
445
+ /** Structured post-write content for activity write fallbacks; drives canonical file preview (mirrors nativeWrite). */
446
+ fileContentAfterWrite?: string;
447
+ };
448
+
449
+ function hasCursorReplayDisplayTitle(details: CursorReplayToolDetails | undefined): boolean {
450
+ if (!details) return false;
451
+ return isCursorReplayActivityDetails(details) || isCursorReplayGenerateImageDetails(details);
452
+ }
453
+
396
454
  function renderExpandableCursorReplayResult(
397
455
  title: string,
456
+ details: CursorReplayExpandableResultDetails,
398
457
  result: Parameters<CursorReplayRenderResult>[0],
399
458
  options: Parameters<CursorReplayRenderResult>[1],
400
459
  theme: Parameters<CursorReplayRenderResult>[2],
401
460
  context: Parameters<CursorReplayRenderResult>[3],
402
461
  isError: boolean,
403
462
  ): Component {
404
- const details = asCursorReplayToolDetails(result.details);
405
463
  const text = firstContentText(result);
406
- const summary = details?.summary ?? text.split("\n").find((line) => line.trim()) ?? "completed";
464
+ const summary = details.summary ?? text.split("\n").find((line) => line.trim()) ?? "completed";
407
465
  let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg(isError ? "error" : "success", summary)}`;
408
- const expandedText = details?.expandedText ?? (text.includes("\n") ? text : undefined);
409
- if (expandedText) {
410
- const preview = options.expanded ? formatMutedBlock(expandedText, theme) : formatCursorReplayPreview(expandedText, theme);
466
+ const expandedText = details.expandedText ?? (text.includes("\n") ? text : undefined);
467
+ if (expandedText && (options.expanded || !details.collapseDetailsByDefault)) {
468
+ const preview = formatCursorReplayActivityPreview(
469
+ details,
470
+ expandedText,
471
+ theme,
472
+ options.expanded ? Number.POSITIVE_INFINITY : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
473
+ !options.expanded,
474
+ );
411
475
  if (preview) rendered += `\n${preview}`;
412
476
  }
413
- if (details?.cursorToolName === "generateImage" && !isError && context.showImages) {
477
+ if (details.imagePath && !isError && context.showImages) {
414
478
  const imageData = readImageFileForReplay(details.imagePath);
415
479
  const mimeType = details.imageMimeType ?? inferImageMimeTypeFromPath(details.imagePath);
416
480
  if (imageData && mimeType) return buildImageReplayComponent(rendered, imageData, mimeType, basename(details.imagePath ?? "generated-image"), theme);
@@ -418,14 +482,81 @@ function renderExpandableCursorReplayResult(
418
482
  return new Text(rendered, 0, 0);
419
483
  }
420
484
 
485
+ function renderCursorReplayEditResult(
486
+ details: CursorReplayNativeEditDetails,
487
+ options: Parameters<CursorReplayRenderResult>[1],
488
+ theme: Parameters<CursorReplayRenderResult>[2],
489
+ ): Component {
490
+ const summary = formatCursorEditSummary(details);
491
+ let rendered = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
492
+ const diff = getCursorEditDiff(details);
493
+ if (diff) rendered += `\n${formatCursorReplayDiff(diff, theme, options.expanded ? 40 : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES)}`;
494
+ return new Text(rendered, 0, 0);
495
+ }
496
+
497
+ function renderCursorReplayWriteResult(
498
+ details: CursorReplayNativeWriteDetails,
499
+ result: Parameters<CursorReplayRenderResult>[0],
500
+ options: Parameters<CursorReplayRenderResult>[1],
501
+ theme: Parameters<CursorReplayRenderResult>[2],
502
+ ): Component {
503
+ const text = firstContentText(result);
504
+ const parts = [
505
+ details.linesCreated !== undefined ? `${details.linesCreated} line${details.linesCreated === 1 ? "" : "s"}` : undefined,
506
+ details.fileSize !== undefined ? `${details.fileSize} bytes` : undefined,
507
+ ].filter(Boolean);
508
+ const summary = parts.length > 0 ? parts.join(", ") : "written";
509
+ let rendered = `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
510
+ const previewSource = details.fileContentAfterWrite ?? details.expandedText ?? text;
511
+ const preview = formatCursorReplayFilePreview(
512
+ previewSource,
513
+ getCursorReplayPath(undefined, details),
514
+ theme,
515
+ CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
516
+ details.fileContentAfterWrite === undefined,
517
+ );
518
+ if (preview) rendered += `\n${preview}`;
519
+ return new Text(rendered, 0, 0);
520
+ }
521
+
421
522
  function renderCursorGenerateImageResult(
523
+ details: CursorReplayGenerateImageDetails,
422
524
  result: Parameters<CursorReplayRenderResult>[0],
423
525
  options: Parameters<CursorReplayRenderResult>[1],
424
526
  theme: Parameters<CursorReplayRenderResult>[2],
425
527
  context: Parameters<CursorReplayRenderResult>[3],
426
528
  isError: boolean,
427
529
  ): Component {
428
- return renderExpandableCursorReplayResult("Cursor generateImage", result, options, theme, context, isError);
530
+ const title = CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE;
531
+ return renderExpandableCursorReplayResult(title, details, result, options, theme, context, isError);
532
+ }
533
+
534
+ function renderCursorReplayDetails(
535
+ details: CursorReplayToolDetails,
536
+ result: Parameters<CursorReplayRenderResult>[0],
537
+ options: Parameters<CursorReplayRenderResult>[1],
538
+ theme: Parameters<CursorReplayRenderResult>[2],
539
+ context: Parameters<CursorReplayRenderResult>[3],
540
+ isError: boolean,
541
+ text: string,
542
+ ): Component {
543
+ switch (details.variant) {
544
+ case "nativeEdit":
545
+ return renderCursorReplayEditResult(details, options, theme);
546
+ case "nativeWrite":
547
+ return renderCursorReplayWriteResult(details, result, options, theme);
548
+ case "generateImage":
549
+ return renderCursorGenerateImageResult(details, result, options, theme, context, isError);
550
+ case "activity":
551
+ return renderExpandableCursorReplayResult(details.title, details, result, options, theme, context, isError);
552
+ case "genericFallback":
553
+ break;
554
+ default: {
555
+ const _exhaustive: never = details;
556
+ return _exhaustive;
557
+ }
558
+ }
559
+ return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
429
560
  }
430
561
 
431
562
  export function renderCursorReplayResult(
@@ -436,41 +567,13 @@ export function renderCursorReplayResult(
436
567
  isError: boolean,
437
568
  ): Component {
438
569
  if (options.isPartial) return new Text(theme.fg("warning", "Replaying Cursor tool result..."), 0, 0);
439
- const details = asCursorReplayToolDetails(result.details);
570
+ const details = parseCursorReplayToolDetails(result.details);
440
571
  const text = firstContentText(result);
441
- if (isError && !details?.title) return new Text(theme.fg("error", text.split("\n")[0] || "Cursor replay failed"), 0, 0);
442
-
443
- if (details?.cursorToolName === "edit" && hasCursorEditChanges(details)) {
444
- const summary = formatCursorEditSummary(details);
445
- const title = details.title ?? "edit";
446
- let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
447
- const diff = getCursorEditDiff(details);
448
- if (diff) rendered += `\n${formatCursorReplayDiff(diff, theme, options.expanded ? 40 : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES)}`;
449
- return new Text(rendered, 0, 0);
572
+ if (isError && !hasCursorReplayDisplayTitle(details)) {
573
+ return new Text(theme.fg("error", text.split("\n")[0] || "Cursor replay failed"), 0, 0);
450
574
  }
451
-
452
- if (details?.cursorToolName === "write") {
453
- const parts = [
454
- details.linesCreated !== undefined ? `${details.linesCreated} line${details.linesCreated === 1 ? "" : "s"}` : undefined,
455
- details.fileSize !== undefined ? `${details.fileSize} bytes` : undefined,
456
- ].filter(Boolean);
457
- const summary = parts.length > 0 ? parts.join(", ") : "written";
458
- let rendered = `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
459
- const previewSource = details.fileContentAfterWrite ?? details.expandedText ?? text;
460
- const preview = formatCursorReplayFilePreview(
461
- previewSource,
462
- getCursorReplayPath(undefined, details),
463
- theme,
464
- CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
465
- details.fileContentAfterWrite === undefined,
466
- );
467
- if (preview) rendered += `\n${preview}`;
468
- return new Text(rendered, 0, 0);
469
- }
470
-
471
- if (details?.cursorToolName === "generateImage") return renderCursorGenerateImageResult(result, options, theme, context, isError);
472
- if (details?.title) return renderExpandableCursorReplayResult(details.title, result, options, theme, context, isError);
473
- return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
575
+ if (!details) return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
576
+ return renderCursorReplayDetails(details, result, options, theme, context, isError, text);
474
577
  }
475
578
 
476
579
  export function renderNativeLookingCursorReadReplayResult(
@@ -499,11 +602,11 @@ export function renderNativeLookingCursorReadReplayResult(
499
602
  }
500
603
 
501
604
  export function createCursorReplayOnlyToolDefinition(toolName: CursorReplayToolName): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
502
- const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplaySourceToolName(toolName);
503
- const sideEffectDescription = toolName === "cursor_edit" || toolName === "cursor_write" || toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "file mutations" : "real tool work";
605
+ const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplayOperationLabel(toolName);
606
+ const sideEffectDescription = getCursorReplaySideEffectDescription(toolName);
504
607
  return {
505
608
  name: toolName,
506
- label: getCursorReplayToolLabel(toolName),
609
+ label: getCursorReplayWrapperLabel(toolName),
507
610
  description: `Replay display for a Cursor SDK ${cursorToolName} operation. This tool only returns recorded Cursor results and never executes ${sideEffectDescription} directly.`,
508
611
  promptSnippet: `Render a recorded Cursor SDK ${cursorToolName} operation without executing ${sideEffectDescription}.`,
509
612
  promptGuidelines: [
@@ -10,6 +10,7 @@ export const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
10
10
  export const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
11
11
 
12
12
  export const registeredNativeToolNames = new Set<string>();
13
+ export const skippedNativeToolNames = new Set<string>();
13
14
  export const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
14
15
 
15
16
  export function readBooleanEnv(name: string, env: Record<string, string | undefined> = process.env): boolean | undefined {
@@ -73,6 +74,7 @@ export const __testUtils = {
73
74
  },
74
75
  reset(): void {
75
76
  registeredNativeToolNames.clear();
77
+ skippedNativeToolNames.clear();
76
78
  nativeToolResults.clear();
77
79
  },
78
80
  };