pi-cursor-sdk 0.1.20 → 0.1.22

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 +38 -0
  2. package/README.md +49 -9
  3. package/docs/cursor-dogfood-checklist.md +57 -0
  4. package/docs/cursor-live-smoke-checklist.md +115 -9
  5. package/docs/cursor-model-ux-spec.md +58 -18
  6. package/docs/cursor-native-tool-replay.md +15 -7
  7. package/docs/cursor-native-tool-visual-audit.md +104 -59
  8. package/docs/cursor-testing-lessons.md +8 -3
  9. package/docs/cursor-tool-surfaces.md +69 -0
  10. package/package.json +34 -10
  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 +20 -38
  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 +22 -5
  43. package/src/cursor-native-tool-display-registration.ts +63 -27
  44. package/src/cursor-native-tool-display-replay.ts +246 -144
  45. package/src/cursor-native-tool-display-state.ts +2 -0
  46. package/src/cursor-native-tool-display-tools.ts +149 -41
  47. package/src/cursor-provider-live-run-drain.ts +1 -52
  48. package/src/cursor-provider-run-finalizer.ts +237 -0
  49. package/src/cursor-provider-run-outcome.ts +149 -0
  50. package/src/cursor-provider-turn-api-key.ts +8 -0
  51. package/src/cursor-provider-turn-coordinator.ts +98 -446
  52. package/src/cursor-provider-turn-display-router.ts +216 -0
  53. package/src/cursor-provider-turn-emit.ts +59 -0
  54. package/src/cursor-provider-turn-finalize.ts +119 -0
  55. package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
  56. package/src/cursor-provider-turn-message-offset.ts +15 -0
  57. package/src/cursor-provider-turn-prepare.ts +216 -0
  58. package/src/cursor-provider-turn-runner.ts +140 -0
  59. package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
  60. package/src/cursor-provider-turn-send.ts +103 -0
  61. package/src/cursor-provider-turn-shell-output.ts +107 -0
  62. package/src/cursor-provider-turn-tool-ledger.ts +126 -0
  63. package/src/cursor-provider-turn-types.ts +87 -0
  64. package/src/cursor-provider.ts +16 -504
  65. package/src/cursor-replay-activity-builders.ts +276 -0
  66. package/src/cursor-replay-source-names.ts +33 -0
  67. package/src/cursor-replay-summary-args.ts +191 -0
  68. package/src/cursor-replay-tool-details.ts +464 -0
  69. package/src/cursor-run-final-text.ts +56 -0
  70. package/src/cursor-sdk-abort-error-guard.ts +4 -0
  71. package/src/cursor-sdk-event-debug-constants.ts +14 -5
  72. package/src/cursor-sdk-event-debug.ts +2 -1
  73. package/src/cursor-sensitive-text.ts +3 -36
  74. package/src/cursor-session-agent.ts +3 -1
  75. package/src/cursor-session-compaction-prep.ts +19 -0
  76. package/src/cursor-setting-sources.ts +7 -10
  77. package/src/cursor-state.ts +232 -28
  78. package/src/cursor-tool-lifecycle.ts +9 -8
  79. package/src/cursor-tool-manifest.ts +41 -0
  80. package/src/cursor-tool-names.ts +18 -106
  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 +3 -27
  84. package/src/cursor-transcript-tool-formatters.ts +0 -59
  85. package/src/cursor-transcript-tool-specs.ts +158 -233
  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 +8 -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,30 +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
- collapseDetailsByDefault?: boolean;
39
- }
40
-
41
- export function asCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
42
- return value && typeof value === "object" ? (value as CursorReplayToolDetails) : undefined;
43
- }
44
-
45
58
  type CursorReplayRenderCall = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderCall"]>;
46
59
  type CursorReplayRenderResult = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderResult"]>;
47
60
  export type CursorReplayRenderTheme = Parameters<CursorReplayRenderCall>[1];
@@ -87,13 +100,10 @@ function buildImageReplayComponent(text: string, imageData: string, mimeType: st
87
100
  };
88
101
  }
89
102
 
90
- function getCursorReplayToolLabel(toolName: CursorReplayToolName): string {
91
- if (toolName === "cursor_edit") return "edit";
92
- if (toolName === "cursor_write") return "write";
93
- return getCursorReplayDisplayLabel(toolName);
94
- }
95
-
96
- 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 {
97
107
  const argPath = args?.path;
98
108
  return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
99
109
  }
@@ -212,6 +222,93 @@ function formatMutedBlock(text: string, theme: CursorReplayRenderTheme): string
212
222
  return text.split("\n").map((line) => theme.fg("muted", line)).join("\n");
213
223
  }
214
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
+
215
312
  export function formatCursorReplayPreview(
216
313
  text: string,
217
314
  theme: CursorReplayRenderTheme,
@@ -257,69 +354,7 @@ function getCursorReplayActivityTitle(toolName: CursorReplayToolName, args: Reco
257
354
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && typeof args?.activityTitle === "string" && args.activityTitle.trim()) {
258
355
  return args.activityTitle.trim();
259
356
  }
260
- return getCursorReplayToolLabel(toolName);
261
- }
262
-
263
- function formatReplayRecordingDurationMs(ms: number | undefined): string | undefined {
264
- if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
265
- if (ms < 1000) return `${Math.round(ms)}ms`;
266
- const seconds = ms / 1000;
267
- return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
268
- }
269
-
270
- function formatReplaySemSearchQuery(args: Record<string, unknown> | undefined): string | undefined {
271
- const query = typeof args?.query === "string" ? args.query.trim() : undefined;
272
- if (!query) return undefined;
273
- const targetDirectories = Array.isArray(args?.targetDirectories)
274
- ? args.targetDirectories.filter((entry): entry is string => typeof entry === "string")
275
- : [];
276
- const dirHint =
277
- targetDirectories.length > 0 ? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})` : "";
278
- return `${query}${dirHint}`;
279
- }
280
-
281
- function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string | undefined {
282
- const activitySummary = typeof args?.activitySummary === "string" && args.activitySummary.trim() ? args.activitySummary.trim() : undefined;
283
- if (activitySummary) return activitySummary;
284
-
285
- const path = typeof args?.path === "string" ? args.path : undefined;
286
- const description = typeof args?.description === "string" ? args.description : undefined;
287
- const prompt = typeof args?.prompt === "string" ? args.prompt : undefined;
288
- const totalCount = typeof args?.totalCount === "number" ? args.totalCount : undefined;
289
- const diagnosticCount = typeof args?.diagnosticCount === "number" ? args.diagnosticCount : undefined;
290
- const paths = Array.isArray(args?.paths) ? args.paths.filter((entry): entry is string => typeof entry === "string") : [];
291
-
292
- if (toolName === "cursor_edit" || toolName === "cursor_write" || toolName === "cursor_delete") return path ?? "unknown";
293
- if (toolName === "cursor_read_lints") {
294
- const target = paths.length > 0 ? paths.join(", ") : path;
295
- if (target && diagnosticCount !== undefined) {
296
- return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"} in ${target}`;
297
- }
298
- return target;
299
- }
300
- if (toolName === "cursor_update_todos" || toolName === "cursor_create_plan") {
301
- return totalCount !== undefined ? `${totalCount} item${totalCount === 1 ? "" : "s"}` : undefined;
302
- }
303
- if (toolName === "cursor_task") return description;
304
- if (toolName === "cursor_generate_image") return path ?? prompt;
305
- if (toolName === "cursor_mcp") return typeof args?.toolName === "string" ? args.toolName : undefined;
306
- if (toolName === "cursor_sem_search") return formatReplaySemSearchQuery(args);
307
- if (toolName === "cursor_record_screen") {
308
- const duration = formatReplayRecordingDurationMs(
309
- typeof args?.recordingDurationMs === "number" ? args.recordingDurationMs : undefined,
310
- );
311
- if (path && duration) return `${path} · ${duration}`;
312
- if (path) return path;
313
- if (typeof args?.mode === "string") return args.mode;
314
- }
315
- if (toolName === "cursor_web_search") return typeof args?.query === "string" ? args.query : undefined;
316
- if (toolName === "cursor_web_fetch") return typeof args?.url === "string" ? args.url : undefined;
317
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
318
- if (path) return path;
319
- if (typeof args?.toolName === "string") return args.toolName;
320
- return formatReplaySemSearchQuery(args);
321
- }
322
- return undefined;
357
+ return getCursorReplayWrapperLabel(toolName);
323
358
  }
324
359
 
325
360
  export function renderCursorReplayCall(
@@ -361,15 +396,15 @@ function pluralize(count: number, noun: string): string {
361
396
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
362
397
  }
363
398
 
364
- function getCursorEditDiff(details: CursorReplayToolDetails): string | undefined {
399
+ function getCursorEditDiff(details: CursorReplayNativeEditDetails): string | undefined {
365
400
  return resolveCursorEditDiff(details);
366
401
  }
367
402
 
368
- function hasCursorEditChanges(details: CursorReplayToolDetails): boolean {
403
+ function hasCursorEditChanges(details: CursorReplayNativeEditDetails): boolean {
369
404
  return Boolean(getCursorEditDiff(details)) || Boolean(details.linesAdded) || Boolean(details.linesRemoved);
370
405
  }
371
406
 
372
- function classifyCursorEditOperation(details: CursorReplayToolDetails): "created" | "deleted" | "updated" | "unchanged" {
407
+ function classifyCursorEditOperation(details: CursorReplayNativeEditDetails): "created" | "deleted" | "updated" | "unchanged" {
373
408
  if (!hasCursorEditChanges(details)) return "unchanged";
374
409
  const diff = getCursorEditDiff(details);
375
410
  if (diff?.startsWith("--- /dev/null")) return "created";
@@ -377,7 +412,7 @@ function classifyCursorEditOperation(details: CursorReplayToolDetails): "created
377
412
  return "updated";
378
413
  }
379
414
 
380
- function formatCursorEditSummary(details: CursorReplayToolDetails): string {
415
+ function formatCursorEditSummary(details: CursorReplayNativeEditDetails): string {
381
416
  const operation = classifyCursorEditOperation(details);
382
417
  if (operation === "unchanged") return "no changes needed";
383
418
  if (operation === "created" && details.linesAdded !== undefined) return `created ${pluralize(details.linesAdded, "line")}`;
@@ -394,24 +429,52 @@ function firstContentText(result: Parameters<CursorReplayRenderResult>[0]): stri
394
429
  return content?.type === "text" ? content.text : "";
395
430
  }
396
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
+
397
454
  function renderExpandableCursorReplayResult(
398
455
  title: string,
456
+ details: CursorReplayExpandableResultDetails,
399
457
  result: Parameters<CursorReplayRenderResult>[0],
400
458
  options: Parameters<CursorReplayRenderResult>[1],
401
459
  theme: Parameters<CursorReplayRenderResult>[2],
402
460
  context: Parameters<CursorReplayRenderResult>[3],
403
461
  isError: boolean,
404
462
  ): Component {
405
- const details = asCursorReplayToolDetails(result.details);
406
463
  const text = firstContentText(result);
407
- 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";
408
465
  let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg(isError ? "error" : "success", summary)}`;
409
- const expandedText = details?.expandedText ?? (text.includes("\n") ? text : undefined);
410
- if (expandedText && (options.expanded || !details?.collapseDetailsByDefault)) {
411
- 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
+ );
412
475
  if (preview) rendered += `\n${preview}`;
413
476
  }
414
- if (details?.cursorToolName === "generateImage" && !isError && context.showImages) {
477
+ if (details.imagePath && !isError && context.showImages) {
415
478
  const imageData = readImageFileForReplay(details.imagePath);
416
479
  const mimeType = details.imageMimeType ?? inferImageMimeTypeFromPath(details.imagePath);
417
480
  if (imageData && mimeType) return buildImageReplayComponent(rendered, imageData, mimeType, basename(details.imagePath ?? "generated-image"), theme);
@@ -419,14 +482,81 @@ function renderExpandableCursorReplayResult(
419
482
  return new Text(rendered, 0, 0);
420
483
  }
421
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
+
422
522
  function renderCursorGenerateImageResult(
523
+ details: CursorReplayGenerateImageDetails,
524
+ result: Parameters<CursorReplayRenderResult>[0],
525
+ options: Parameters<CursorReplayRenderResult>[1],
526
+ theme: Parameters<CursorReplayRenderResult>[2],
527
+ context: Parameters<CursorReplayRenderResult>[3],
528
+ isError: boolean,
529
+ ): Component {
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,
423
536
  result: Parameters<CursorReplayRenderResult>[0],
424
537
  options: Parameters<CursorReplayRenderResult>[1],
425
538
  theme: Parameters<CursorReplayRenderResult>[2],
426
539
  context: Parameters<CursorReplayRenderResult>[3],
427
540
  isError: boolean,
541
+ text: string,
428
542
  ): Component {
429
- return renderExpandableCursorReplayResult("Cursor generateImage", result, options, theme, context, isError);
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);
430
560
  }
431
561
 
432
562
  export function renderCursorReplayResult(
@@ -437,41 +567,13 @@ export function renderCursorReplayResult(
437
567
  isError: boolean,
438
568
  ): Component {
439
569
  if (options.isPartial) return new Text(theme.fg("warning", "Replaying Cursor tool result..."), 0, 0);
440
- const details = asCursorReplayToolDetails(result.details);
570
+ const details = parseCursorReplayToolDetails(result.details);
441
571
  const text = firstContentText(result);
442
- if (isError && !details?.title) return new Text(theme.fg("error", text.split("\n")[0] || "Cursor replay failed"), 0, 0);
443
-
444
- if (details?.cursorToolName === "edit" && hasCursorEditChanges(details)) {
445
- const summary = formatCursorEditSummary(details);
446
- const title = details.title ?? "edit";
447
- let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
448
- const diff = getCursorEditDiff(details);
449
- if (diff) rendered += `\n${formatCursorReplayDiff(diff, theme, options.expanded ? 40 : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES)}`;
450
- 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);
451
574
  }
452
-
453
- if (details?.cursorToolName === "write") {
454
- const parts = [
455
- details.linesCreated !== undefined ? `${details.linesCreated} line${details.linesCreated === 1 ? "" : "s"}` : undefined,
456
- details.fileSize !== undefined ? `${details.fileSize} bytes` : undefined,
457
- ].filter(Boolean);
458
- const summary = parts.length > 0 ? parts.join(", ") : "written";
459
- let rendered = `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
460
- const previewSource = details.fileContentAfterWrite ?? details.expandedText ?? text;
461
- const preview = formatCursorReplayFilePreview(
462
- previewSource,
463
- getCursorReplayPath(undefined, details),
464
- theme,
465
- CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
466
- details.fileContentAfterWrite === undefined,
467
- );
468
- if (preview) rendered += `\n${preview}`;
469
- return new Text(rendered, 0, 0);
470
- }
471
-
472
- if (details?.cursorToolName === "generateImage") return renderCursorGenerateImageResult(result, options, theme, context, isError);
473
- if (details?.title) return renderExpandableCursorReplayResult(details.title, result, options, theme, context, isError);
474
- 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);
475
577
  }
476
578
 
477
579
  export function renderNativeLookingCursorReadReplayResult(
@@ -500,11 +602,11 @@ export function renderNativeLookingCursorReadReplayResult(
500
602
  }
501
603
 
502
604
  export function createCursorReplayOnlyToolDefinition(toolName: CursorReplayToolName): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
503
- const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplaySourceToolName(toolName);
504
- 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);
505
607
  return {
506
608
  name: toolName,
507
- label: getCursorReplayToolLabel(toolName),
609
+ label: getCursorReplayWrapperLabel(toolName),
508
610
  description: `Replay display for a Cursor SDK ${cursorToolName} operation. This tool only returns recorded Cursor results and never executes ${sideEffectDescription} directly.`,
509
611
  promptSnippet: `Render a recorded Cursor SDK ${cursorToolName} operation without executing ${sideEffectDescription}.`,
510
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
  };