pi-cursor-sdk 0.1.20 → 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 (88) hide show
  1. package/CHANGELOG.md +32 -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 +57 -17
  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 +235 -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 +138 -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-setting-sources.ts +7 -10
  76. package/src/cursor-state.ts +232 -28
  77. package/src/cursor-tool-lifecycle.ts +9 -8
  78. package/src/cursor-tool-manifest.ts +41 -0
  79. package/src/cursor-tool-names.ts +18 -106
  80. package/src/cursor-tool-presentation-registry.ts +556 -0
  81. package/src/cursor-tool-transcript.ts +1 -1
  82. package/src/cursor-tool-visibility.ts +3 -27
  83. package/src/cursor-transcript-tool-formatters.ts +0 -59
  84. package/src/cursor-transcript-tool-specs.ts +158 -233
  85. package/src/cursor-transcript-utils.ts +0 -44
  86. package/src/cursor-web-tool-activity.ts +10 -60
  87. package/src/cursor-web-tool-args.ts +39 -0
  88. package/src/index.ts +4 -10
@@ -505,18 +505,6 @@ export function getTodoTotalCount(args: Record<string, unknown>, result: Normali
505
505
  return getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
506
506
  }
507
507
 
508
- export function summarizeTodos(args: Record<string, unknown>, result: NormalizedResult): string {
509
- const todos = getTodoItems(args, result);
510
- const total = getTodoTotalCount(args, result, todos);
511
- const completed = todos.filter((todo) => todo.status === "completed").length;
512
- const inProgress = todos.filter((todo) => todo.status === "inProgress").length;
513
- const pending = todos.filter((todo) => todo.status === "pending").length;
514
- const parts = [`${completed}/${total} completed`];
515
- if (inProgress > 0) parts.push(`${inProgress} in progress`);
516
- if (pending > 0) parts.push(`${pending} pending`);
517
- return parts.join(", ");
518
- }
519
-
520
508
  function formatTodoStatus(status: string | undefined): string {
521
509
  if (status === "completed") return "✓";
522
510
  if (status === "inProgress") return "…";
@@ -532,12 +520,6 @@ export function formatTodos(args: Record<string, unknown>, result: NormalizedRes
532
520
  return joinSections(header, limitText(lines.join("\n"), options));
533
521
  }
534
522
 
535
- export function summarizePlan(args: Record<string, unknown>, result: NormalizedResult): string {
536
- const planText = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
537
- const firstLine = planText ? firstNonEmptyLine(planText) : undefined;
538
- return firstLine ? truncateArg(firstLine, 160) : summarizeTodos(args, result);
539
- }
540
-
541
523
  export function formatPlan(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
542
524
  if (result.status === "error") return joinSections("createPlan", formatError(result.error));
543
525
  const planText = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
@@ -578,13 +560,6 @@ export function formatTask(args: Record<string, unknown>, result: NormalizedResu
578
560
  return joinSections(`task ${description}`, limitText(taskText || stringifyUnknown(result.value), options));
579
561
  }
580
562
 
581
- export function summarizeTask(description: string, taskText: string): string {
582
- const firstLine = firstNonEmptyLine(taskText);
583
- if (!firstLine) return truncateArg(description);
584
- if (description === "task" || description === firstLine) return truncateArg(firstLine);
585
- return truncateArg(`${description}: ${firstLine}`, 160);
586
- }
587
-
588
563
  function getGenerateImageValue(result: NormalizedResult): Record<string, unknown> | undefined {
589
564
  return asRecord(result.value);
590
565
  }
@@ -642,16 +617,6 @@ function describeNonTextMcpContent(entry: unknown): string {
642
617
  return `[${type} omitted]`;
643
618
  }
644
619
 
645
- export function summarizeSemSearch(args: Record<string, unknown>): string {
646
- const query = getString(args, "query") ?? "semantic search";
647
- const targetDirectories = getArray(args, "targetDirectories");
648
- const dirHint =
649
- targetDirectories && targetDirectories.length > 0
650
- ? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})`
651
- : "";
652
- return truncateArg(`${query}${dirHint}`);
653
- }
654
-
655
620
  export function formatSemSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
656
621
  const query = getString(args, "query") ?? "semantic search";
657
622
  const header = `semSearch ${truncateArg(query)}`;
@@ -694,24 +659,6 @@ function formatRecordingDurationMs(ms: number | undefined): string | undefined {
694
659
  return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
695
660
  }
696
661
 
697
- export function summarizeRecordScreen(
698
- args: Record<string, unknown>,
699
- result: NormalizedResult,
700
- options: TranscriptOptions,
701
- ): string {
702
- const mode = getString(args, "mode");
703
- if (result.status === "error") return formatRecordScreenMode(mode);
704
- const value = asRecord(result.value);
705
- const path = getString(value, "path");
706
- const displayPath = path ? formatDisplayPath(path, options.cwd) : undefined;
707
- const duration = formatRecordingDurationMs(getNumber(value, "recordingDurationMs"));
708
- const modeLabel = formatRecordScreenMode(mode);
709
- if (displayPath && duration) return `${displayPath} · ${duration}`;
710
- if (displayPath) return displayPath;
711
- if (duration) return `${modeLabel} · ${duration}`;
712
- return modeLabel;
713
- }
714
-
715
662
  export function formatRecordScreen(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
716
663
  const mode = getString(args, "mode");
717
664
  const header = `recordScreen ${formatRecordScreenMode(mode)}`;
@@ -750,12 +697,6 @@ export function getMcpResultPreview(result: NormalizedResult): string | undefine
750
697
  return undefined;
751
698
  }
752
699
 
753
- export function summarizeMcp(args: Record<string, unknown>, result: NormalizedResult): string {
754
- const toolName = truncateArg(getString(args, "toolName") ?? "mcp");
755
- const preview = getMcpResultPreview(result);
756
- return preview && preview !== toolName ? `${toolName} · ${preview}` : toolName;
757
- }
758
-
759
700
  function formatWebToolBody(
760
701
  toolLabel: string,
761
702
  args: Record<string, unknown>,
@@ -1,11 +1,25 @@
1
1
  import {
2
- CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME,
2
+ CURSOR_KNOWN_NORMALIZED_TOOL_NAMES,
3
3
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
+ getCursorReplayActivityLabelKey,
4
5
  getCursorReplayActivityTitle,
6
+ getCursorReplayCallSummary,
5
7
  getCursorReplayDisplayLabel,
8
+ getCursorToolActivityReplaySpec,
9
+ getCursorToolGenerateImageReplaySpec,
10
+ type CursorNormalizedToolName,
11
+ type CursorReplayActivityToolName,
6
12
  type CursorReplayLegacyToolName,
7
- } from "./cursor-tool-names.js";
13
+ } from "./cursor-tool-presentation-registry.js";
8
14
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
15
+ import {
16
+ assembleCursorReplayActivityDetails,
17
+ assembleCursorReplayGenerateImageDetails,
18
+ buildCursorReplayNativeEditDetails,
19
+ buildCursorReplayNativeWriteDetails,
20
+ CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME,
21
+ type CursorReplayToolDetails,
22
+ } from "./cursor-replay-tool-details.js";
9
23
  import {
10
24
  asRecord,
11
25
  firstNonEmptyLine,
@@ -30,7 +44,6 @@ import {
30
44
  buildReadDisplayArgs,
31
45
  buildShellDisplayArgs,
32
46
  buildWriteDisplayArgs,
33
- collectTaskText,
34
47
  formatDelete,
35
48
  formatEdit,
36
49
  formatFallback,
@@ -52,27 +65,13 @@ import {
52
65
  formatWrite,
53
66
  formatNativeReadDisplayContent,
54
67
  getCursorWriteArgContent,
55
- getGenerateImageDisplayPath,
56
- getGenerateImagePath,
57
68
  getGlobBody,
58
69
  getGrepBody,
59
70
  getLsBody,
60
- getReadLintDiagnostics,
61
- getReadLintPaths,
62
71
  getShellOutput,
63
- getTaskDescription,
64
- getTodoItems,
65
- getTodoTotalCount,
66
- inferImageMimeType,
67
- summarizePlan,
68
- summarizeMcp,
69
- summarizeRecordScreen,
70
- summarizeSemSearch,
71
- summarizeTask,
72
- summarizeTodos,
73
72
  usesLocalReadPreview,
74
73
  } from "./cursor-transcript-tool-formatters.js";
75
- import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
74
+ import type { CursorReplaySummaryArgs } from "./cursor-replay-summary-args.js";
76
75
 
77
76
  export interface ToolDisplayContext {
78
77
  rawName: string;
@@ -82,17 +81,17 @@ export interface ToolDisplayContext {
82
81
  options: TranscriptOptions;
83
82
  }
84
83
 
85
- interface ActivityReplaySpec {
86
- labelKey: CursorReplayLegacyToolName;
87
- buildActivityArgs: (context: ToolDisplayContext) => Record<string, unknown>;
88
- buildActivitySummary: (context: ToolDisplayContext) => string | undefined;
89
- buildDetails: (context: ToolDisplayContext, contentText: string) => Record<string, unknown>;
90
- }
84
+ type NeutralActivityReplayToolName = Exclude<CursorReplayActivityToolName, "edit" | "write" | "generateImage">;
91
85
 
92
86
  interface ToolDisplaySpec {
93
87
  formatTranscript: (context: ToolDisplayContext) => string;
94
88
  buildPiToolDisplay: (context: ToolDisplayContext) => CursorPiToolDisplay;
95
- activityReplay?: ActivityReplaySpec;
89
+ }
90
+
91
+ function requireReplayActivityLabelKey(normalizedName: CursorReplayActivityToolName): CursorReplayLegacyToolName {
92
+ const labelKey = getCursorReplayActivityLabelKey(normalizedName);
93
+ if (!labelKey) throw new Error(`Missing replay activity label for ${normalizedName}`);
94
+ return labelKey;
96
95
  }
97
96
 
98
97
  function textToolResult(text: string, details?: unknown): PiToolDisplayResult {
@@ -112,22 +111,30 @@ function buildCursorActivityDisplayArgs(
112
111
  };
113
112
  }
114
113
 
114
+ function buildRegistryReplaySummary(
115
+ labelKey: CursorReplayLegacyToolName,
116
+ args: CursorReplaySummaryArgs,
117
+ ): string | undefined {
118
+ return getCursorReplayCallSummary(labelKey, args);
119
+ }
120
+
115
121
  function buildReplaySummaryDisplay(
116
122
  toolName: string,
117
123
  args: Record<string, unknown>,
118
124
  result: NormalizedResult,
119
125
  contentText: string,
120
- details: Record<string, unknown>,
126
+ details: CursorReplayToolDetails,
121
127
  ): CursorPiToolDisplay {
122
128
  const isError = result.status === "error";
123
- const summary = isError ? details.summary : (details.summary ?? firstNonEmptyLine(contentText));
129
+ const expandedText = details.expandedText ?? contentText;
130
+ const summary = details.summary;
124
131
  return {
125
132
  toolName,
126
133
  args,
127
134
  result: textToolResult(contentText, {
128
135
  ...details,
129
136
  summary,
130
- expandedText: details.expandedText ?? contentText,
137
+ expandedText,
131
138
  }),
132
139
  isError,
133
140
  };
@@ -137,30 +144,48 @@ function getCursorToolActivityTitle(toolName: string): string {
137
144
  return getCursorReplayActivityTitle(toolName) ?? buildGenericUnknownToolActivityTitle(toolName);
138
145
  }
139
146
 
140
- function buildActivityReplayDisplay(cursorToolName: string, spec: ToolDisplaySpec, context: ToolDisplayContext): CursorPiToolDisplay {
141
- const activity = spec.activityReplay;
142
- if (!activity) throw new Error(`Missing activity replay spec for ${cursorToolName}`);
143
- const activityTitle = getCursorReplayDisplayLabel(activity.labelKey);
144
- const activitySummary = activity.buildActivitySummary(context);
145
- const activityArgs = buildCursorActivityDisplayArgs(
146
- activity.buildActivityArgs(context),
147
+ function buildActivityReplayDisplay(
148
+ sourceToolName: NeutralActivityReplayToolName,
149
+ context: ToolDisplayContext,
150
+ ): CursorPiToolDisplay {
151
+ const spec = TOOL_DISPLAY_SPECS[sourceToolName];
152
+ const activity = getCursorToolActivityReplaySpec(sourceToolName);
153
+ if (!activity) throw new Error(`Missing activity replay spec for ${sourceToolName}`);
154
+ const labelKey = requireReplayActivityLabelKey(sourceToolName);
155
+ const activityTitle = getCursorReplayDisplayLabel(labelKey);
156
+ const replayArgs = activity.buildActivityArgs(context);
157
+ const activitySummary = buildRegistryReplaySummary(labelKey, replayArgs);
158
+ const activityArgs = buildCursorActivityDisplayArgs({ ...replayArgs }, activityTitle, activitySummary);
159
+ const contentText = spec.formatTranscript(context).trimEnd();
160
+ const activityFields = activity.buildDetails(context, contentText);
161
+ const details = assembleCursorReplayActivityDetails(
162
+ sourceToolName,
147
163
  activityTitle,
164
+ activityFields,
165
+ contentText,
166
+ context.result.status === "error",
148
167
  activitySummary,
149
168
  );
169
+ return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, context.result, contentText, details);
170
+ }
171
+
172
+ function buildGenerateImageReplayDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
173
+ const spec = TOOL_DISPLAY_SPECS.generateImage;
174
+ const replay = getCursorToolGenerateImageReplaySpec("generateImage");
175
+ if (!replay) throw new Error("Missing generate image replay spec");
176
+ const labelKey = requireReplayActivityLabelKey("generateImage");
177
+ const activityTitle = getCursorReplayDisplayLabel(labelKey);
178
+ const replayArgs = replay.buildActivityArgs(context);
179
+ const activitySummary = buildRegistryReplaySummary(labelKey, replayArgs);
180
+ const activityArgs = buildCursorActivityDisplayArgs({ ...replayArgs }, activityTitle, activitySummary);
150
181
  const contentText = spec.formatTranscript(context).trimEnd();
151
- const details = activity.buildDetails(context, contentText);
152
- return buildReplaySummaryDisplay(
153
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
154
- activityArgs,
155
- context.result,
182
+ const details = assembleCursorReplayGenerateImageDetails(
183
+ replay.buildDetails(context, contentText),
156
184
  contentText,
157
- {
158
- cursorToolName,
159
- title: activityTitle,
160
- summary: context.result.status === "error" ? undefined : details.summary ?? activitySummary,
161
- ...details,
162
- },
185
+ context.result.status === "error",
186
+ activitySummary,
163
187
  );
188
+ return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, context.result, contentText, details);
164
189
  }
165
190
 
166
191
  function buildGenericUnknownToolActivityTitle(displayName: string): string {
@@ -185,12 +210,35 @@ function buildGenericPiToolDisplay(context: ToolDisplayContext): CursorPiToolDis
185
210
  result.status === "error"
186
211
  ? undefined
187
212
  : activitySummary ?? truncateArg(displayName === "unknown" ? "tool" : displayName);
188
- return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, result, contentText, {
189
- cursorToolName: name,
190
- title: activityTitle,
191
- summary,
192
- expandedText: contentText,
193
- });
213
+ return buildReplaySummaryDisplay(
214
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
215
+ activityArgs,
216
+ result,
217
+ contentText,
218
+ assembleCursorReplayActivityDetails(
219
+ CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME,
220
+ activityTitle,
221
+ { summary, expandedText: contentText },
222
+ contentText,
223
+ result.status === "error",
224
+ activitySummary,
225
+ ),
226
+ );
227
+ }
228
+
229
+ function buildEditActivitySummary(
230
+ displayPath: string | undefined,
231
+ value: Record<string, unknown>,
232
+ ): string | undefined {
233
+ const path = displayPath ?? "replayed";
234
+ const linesAdded = getNumber(value, "linesAdded");
235
+ const linesRemoved = getNumber(value, "linesRemoved");
236
+ const parts = [
237
+ linesAdded ? `added ${linesAdded} line${linesAdded === 1 ? "" : "s"}` : undefined,
238
+ linesRemoved ? `removed ${linesRemoved} line${linesRemoved === 1 ? "" : "s"}` : undefined,
239
+ ].filter((part): part is string => Boolean(part));
240
+ if (parts.length > 0) return `${path} ${parts.join(", ")}`;
241
+ return path;
194
242
  }
195
243
 
196
244
  function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
@@ -204,15 +252,14 @@ function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDispla
204
252
  const activityTitle = getCursorToolActivityTitle("edit");
205
253
  const activityArgs = buildCursorActivityDisplayArgs(baseActivityArgs, activityTitle, displayPath);
206
254
  const contentText = formatEdit(activityArgs, result, options);
207
- const details = {
208
- cursorToolName: "edit",
255
+ const details = buildCursorReplayNativeEditDetails({
209
256
  path: displayPath,
210
257
  linesAdded: getNumber(value, "linesAdded"),
211
258
  linesRemoved: getNumber(value, "linesRemoved"),
212
259
  diffString: normalizedDiff,
213
260
  diff: normalizedDiff,
214
261
  firstChangedLine: getNumber(value, "firstChangedLine"),
215
- };
262
+ });
216
263
  if (nativeEditArgs) {
217
264
  return {
218
265
  toolName: "edit",
@@ -226,11 +273,22 @@ function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDispla
226
273
  activityArgs,
227
274
  result,
228
275
  contentText.trimEnd(),
229
- {
230
- ...details,
231
- title: activityTitle,
232
- summary: result.status === "error" ? undefined : displayPath ?? "replayed",
233
- },
276
+ assembleCursorReplayActivityDetails(
277
+ "edit",
278
+ activityTitle,
279
+ {
280
+ path: displayPath,
281
+ summary: result.status === "error" ? undefined : buildEditActivitySummary(displayPath, value ?? {}),
282
+ expandedText: contentText.trimEnd(),
283
+ diffString: normalizedDiff,
284
+ diff: normalizedDiff,
285
+ linesAdded: getNumber(value, "linesAdded"),
286
+ linesRemoved: getNumber(value, "linesRemoved"),
287
+ },
288
+ contentText.trimEnd(),
289
+ result.status === "error",
290
+ displayPath,
291
+ ),
234
292
  );
235
293
  }
236
294
 
@@ -241,14 +299,13 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
241
299
  const displayArgs = buildWriteDisplayArgs(args, options);
242
300
  const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
243
301
  const contentText = formatWrite(args, result, options).trimEnd();
244
- const details = {
245
- cursorToolName: "write",
302
+ const details = buildCursorReplayNativeWriteDetails({
246
303
  path: displayPath,
247
304
  linesCreated: getNumber(value, "linesCreated"),
248
305
  fileSize: getNumber(value, "fileSize"),
249
306
  fileContentAfterWrite: getString(value, "fileContentAfterWrite"),
250
307
  expandedText: contentText,
251
- };
308
+ });
252
309
  if (content === undefined) {
253
310
  const activityTitle = getCursorToolActivityTitle("write");
254
311
  return buildReplaySummaryDisplay(
@@ -256,11 +313,19 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
256
313
  buildCursorActivityDisplayArgs(displayArgs, activityTitle, displayPath ?? "file"),
257
314
  result,
258
315
  contentText,
259
- {
260
- ...details,
261
- title: activityTitle,
262
- summary: result.status === "error" ? undefined : displayPath ?? "wrote file",
263
- },
316
+ assembleCursorReplayActivityDetails(
317
+ "write",
318
+ activityTitle,
319
+ {
320
+ path: displayPath,
321
+ summary: result.status === "error" ? undefined : displayPath ?? "wrote file",
322
+ expandedText: contentText,
323
+ fileContentAfterWrite: getString(value, "fileContentAfterWrite"),
324
+ },
325
+ contentText,
326
+ result.status === "error",
327
+ displayPath ?? "file",
328
+ ),
264
329
  );
265
330
  }
266
331
  return {
@@ -271,7 +336,7 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
271
336
  };
272
337
  }
273
338
 
274
- const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
339
+ const TOOL_DISPLAY_IMPLEMENTATIONS: Record<CursorNormalizedToolName, ToolDisplaySpec> = {
275
340
  read: {
276
341
  formatTranscript: ({ args, result, options }) => formatRead(args, result, options),
277
342
  buildPiToolDisplay: ({ args, result, options }) => {
@@ -344,209 +409,69 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
344
409
  },
345
410
  delete: {
346
411
  formatTranscript: ({ args, result, options }) => formatDelete(args, result, options),
347
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("delete", TOOL_DISPLAY_SPECS.delete, context),
348
- activityReplay: {
349
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.delete,
350
- buildActivityArgs: ({ args, options }) => {
351
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
352
- return displayPath ? { path: displayPath } : {};
353
- },
354
- buildActivitySummary: ({ args, options }) => {
355
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
356
- return displayPath ?? "file";
357
- },
358
- buildDetails: ({ args, result, options }) => {
359
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
360
- const value = asRecord(result.value);
361
- return {
362
- path: displayPath,
363
- fileSize: getNumber(value, "fileSize"),
364
- summary: result.status === "error" ? undefined : displayPath ? `deleted ${displayPath}` : "deleted file",
365
- };
366
- },
367
- },
412
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("delete", context),
368
413
  },
369
414
  readLints: {
370
415
  formatTranscript: ({ args, result, options }) => formatReadLints(args, result, options),
371
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("readLints", TOOL_DISPLAY_SPECS.readLints, context),
372
- activityReplay: {
373
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.readLints,
374
- buildActivityArgs: ({ args, result, options }) => {
375
- const paths = getReadLintPaths(args, result, options);
376
- const diagnosticCount = getReadLintDiagnostics(result, options).length;
377
- return { paths, diagnosticCount };
378
- },
379
- buildActivitySummary: ({ args, result, options }) => {
380
- const paths = getReadLintPaths(args, result, options);
381
- const diagnosticCount = getReadLintDiagnostics(result, options).length;
382
- return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}${paths.length > 0 ? ` in ${paths.join(", ")}` : ""}`;
383
- },
384
- buildDetails: () => ({}),
385
- },
416
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("readLints", context),
386
417
  },
387
418
  updateTodos: {
388
419
  formatTranscript: ({ args, result, options }) => formatTodos(args, result, options, "updateTodos"),
389
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("updateTodos", TOOL_DISPLAY_SPECS.updateTodos, context),
390
- activityReplay: {
391
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.updateTodos,
392
- buildActivityArgs: ({ args, result }) => {
393
- const todos = getTodoItems(args, result);
394
- return { totalCount: getTodoTotalCount(args, result, todos) };
395
- },
396
- buildActivitySummary: ({ args, result }) => summarizeTodos(args, result),
397
- buildDetails: () => ({}),
398
- },
420
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("updateTodos", context),
399
421
  },
400
422
  createPlan: {
401
423
  formatTranscript: ({ args, result, options }) => formatPlan(args, result, options),
402
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("createPlan", TOOL_DISPLAY_SPECS.createPlan, context),
403
- activityReplay: {
404
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.createPlan,
405
- buildActivityArgs: ({ args, result }) => {
406
- const todos = getTodoItems(args, result);
407
- return { totalCount: getTodoTotalCount(args, result, todos) };
408
- },
409
- buildActivitySummary: ({ args, result }) => summarizePlan(args, result),
410
- buildDetails: () => ({}),
411
- },
424
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("createPlan", context),
412
425
  },
413
426
  task: {
414
427
  formatTranscript: ({ args, result, options }) => formatTask(args, result, options),
415
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("task", TOOL_DISPLAY_SPECS.task, context),
416
- activityReplay: {
417
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.task,
418
- buildActivityArgs: ({ args, result }) => {
419
- const description = getTaskDescription(args, result);
420
- return { description: truncateArg(description) };
421
- },
422
- buildActivitySummary: ({ args, result }) => {
423
- const description = getTaskDescription(args, result);
424
- return summarizeTask(description, collectTaskText(result));
425
- },
426
- buildDetails: () => ({}),
427
- },
428
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("task", context),
428
429
  },
429
430
  generateImage: {
430
431
  formatTranscript: ({ args, result, options }) => formatGenerateImage(args, result, options),
431
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("generateImage", TOOL_DISPLAY_SPECS.generateImage, context),
432
- activityReplay: {
433
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.generateImage,
434
- buildActivityArgs: ({ args }) => {
435
- const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
436
- return { prompt: truncateArg(prompt) };
437
- },
438
- buildActivitySummary: ({ args, result, options }) => {
439
- const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
440
- const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
441
- return imageDisplayPath ?? truncateArg(prompt);
442
- },
443
- buildDetails: ({ args, result, options }, contentText) => {
444
- const imagePath = getGenerateImagePath(args, result);
445
- const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
446
- return {
447
- imagePath,
448
- imageDisplayPath,
449
- imageMimeType: inferImageMimeType(imagePath),
450
- summary: result.status === "error" ? undefined : imageDisplayPath ? `saved ${imageDisplayPath}` : "image generated",
451
- expandedText: contentText,
452
- };
453
- },
454
- },
432
+ buildPiToolDisplay: (context) => buildGenerateImageReplayDisplay(context),
455
433
  },
456
434
  mcp: {
457
435
  formatTranscript: ({ args, result, options }) => formatMcp(args, result, options),
458
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("mcp", TOOL_DISPLAY_SPECS.mcp, context),
459
- activityReplay: {
460
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.mcp,
461
- buildActivityArgs: ({ args }) => {
462
- const toolName = getString(args, "toolName") ?? "mcp";
463
- return { toolName: truncateArg(toolName) };
464
- },
465
- buildActivitySummary: ({ args, result }) => summarizeMcp(args, result),
466
- buildDetails: ({ args, result }) => ({
467
- summary: result.status === "error" ? undefined : summarizeMcp(args, result),
468
- }),
469
- },
436
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("mcp", context),
470
437
  },
471
438
  semSearch: {
472
439
  formatTranscript: ({ args, result, options }) => formatSemSearch(args, result, options),
473
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("semSearch", TOOL_DISPLAY_SPECS.semSearch, context),
474
- activityReplay: {
475
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.semSearch,
476
- buildActivityArgs: ({ args }) => {
477
- const query = getString(args, "query") ?? "semantic search";
478
- return { query: truncateArg(query) };
479
- },
480
- buildActivitySummary: ({ args }) => summarizeSemSearch(args),
481
- buildDetails: ({ result }, contentText) => ({
482
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "semantic search results captured",
483
- }),
484
- },
440
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("semSearch", context),
485
441
  },
486
442
  recordScreen: {
487
443
  formatTranscript: ({ args, result, options }) => formatRecordScreen(args, result, options),
488
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("recordScreen", TOOL_DISPLAY_SPECS.recordScreen, context),
489
- activityReplay: {
490
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.recordScreen,
491
- buildActivityArgs: ({ args, result, options }) => {
492
- const mode = getString(args, "mode");
493
- const path = getString(asRecord(result.value), "path");
494
- return {
495
- ...(mode ? { mode } : {}),
496
- ...(path ? { path: formatDisplayPath(path, options.cwd) } : {}),
497
- };
498
- },
499
- buildActivitySummary: ({ args, result, options }) => summarizeRecordScreen(args, result, options),
500
- buildDetails: ({ args, result, options }, contentText) => ({
501
- summary:
502
- result.status === "error"
503
- ? undefined
504
- : summarizeRecordScreen(args, result, options) ?? firstNonEmptyLine(contentText) ?? "screen recording updated",
505
- }),
506
- },
444
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("recordScreen", context),
507
445
  },
508
446
  webSearch: {
509
447
  formatTranscript: ({ args, result, options }) => formatWebSearch(args, result, options),
510
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("webSearch", TOOL_DISPLAY_SPECS.webSearch, context),
511
- activityReplay: {
512
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.webSearch,
513
- buildActivityArgs: ({ args }) => {
514
- const query = extractWebSearchQuery(args);
515
- return query ? { query: truncateArg(query) } : {};
516
- },
517
- buildActivitySummary: ({ args }) => truncateArg(extractWebSearchQuery(args) ?? "web search"),
518
- buildDetails: ({ result }, contentText) => ({
519
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web search result captured",
520
- collapseDetailsByDefault: true,
521
- }),
522
- },
448
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webSearch", context),
523
449
  },
524
450
  webFetch: {
525
451
  formatTranscript: ({ args, result, options }) => formatWebFetch(args, result, options),
526
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("webFetch", TOOL_DISPLAY_SPECS.webFetch, context),
527
- activityReplay: {
528
- labelKey: CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME.webFetch,
529
- buildActivityArgs: ({ args }) => {
530
- const target = extractWebFetchTarget(args);
531
- return target ? { url: truncateArg(target) } : {};
532
- },
533
- buildActivitySummary: ({ args }) => truncateArg(extractWebFetchTarget(args) ?? "web fetch"),
534
- buildDetails: ({ result }, contentText) => ({
535
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web fetch result captured",
536
- collapseDetailsByDefault: true,
537
- }),
538
- },
452
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webFetch", context),
539
453
  },
540
454
  };
541
455
 
456
+ export const CURSOR_TOOL_DISPLAY_SPEC_KEYS = CURSOR_KNOWN_NORMALIZED_TOOL_NAMES;
457
+
458
+ const TOOL_DISPLAY_SPECS = Object.fromEntries(
459
+ CURSOR_KNOWN_NORMALIZED_TOOL_NAMES.map((name) => [name, TOOL_DISPLAY_IMPLEMENTATIONS[name]]),
460
+ ) as Record<CursorNormalizedToolName, ToolDisplaySpec>;
461
+
462
+ function getToolDisplaySpec(name: string): ToolDisplaySpec | undefined {
463
+ if (Object.hasOwn(TOOL_DISPLAY_SPECS, name)) return TOOL_DISPLAY_SPECS[name as CursorNormalizedToolName];
464
+ return undefined;
465
+ }
466
+
542
467
  export function formatCursorToolTranscriptFromSpec(context: ToolDisplayContext): string {
543
- const spec = TOOL_DISPLAY_SPECS[context.name];
468
+ const spec = getToolDisplaySpec(context.name);
544
469
  if (spec) return spec.formatTranscript(context);
545
470
  return formatFallback(context.name, context.args, context.result, context.options);
546
471
  }
547
472
 
548
473
  export function buildCursorPiToolDisplayFromSpec(context: ToolDisplayContext): CursorPiToolDisplay {
549
- const spec = TOOL_DISPLAY_SPECS[context.name];
474
+ const spec = getToolDisplaySpec(context.name);
550
475
  if (spec) return spec.buildPiToolDisplay(context);
551
476
  return buildGenericPiToolDisplay(context);
552
477
  }