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
@@ -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,5 +1,25 @@
1
- import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayDisplayLabel, type CursorReplayLegacyToolName } from "./cursor-tool-names.js";
1
+ import {
2
+ CURSOR_KNOWN_NORMALIZED_TOOL_NAMES,
3
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
4
+ getCursorReplayActivityLabelKey,
5
+ getCursorReplayActivityTitle,
6
+ getCursorReplayCallSummary,
7
+ getCursorReplayDisplayLabel,
8
+ getCursorToolActivityReplaySpec,
9
+ getCursorToolGenerateImageReplaySpec,
10
+ type CursorNormalizedToolName,
11
+ type CursorReplayActivityToolName,
12
+ type CursorReplayLegacyToolName,
13
+ } from "./cursor-tool-presentation-registry.js";
2
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";
3
23
  import {
4
24
  asRecord,
5
25
  firstNonEmptyLine,
@@ -24,7 +44,6 @@ import {
24
44
  buildReadDisplayArgs,
25
45
  buildShellDisplayArgs,
26
46
  buildWriteDisplayArgs,
27
- collectTaskText,
28
47
  formatDelete,
29
48
  formatEdit,
30
49
  formatFallback,
@@ -46,27 +65,13 @@ import {
46
65
  formatWrite,
47
66
  formatNativeReadDisplayContent,
48
67
  getCursorWriteArgContent,
49
- getGenerateImageDisplayPath,
50
- getGenerateImagePath,
51
68
  getGlobBody,
52
69
  getGrepBody,
53
70
  getLsBody,
54
- getReadLintDiagnostics,
55
- getReadLintPaths,
56
71
  getShellOutput,
57
- getTaskDescription,
58
- getTodoItems,
59
- getTodoTotalCount,
60
- inferImageMimeType,
61
- summarizePlan,
62
- summarizeMcp,
63
- summarizeRecordScreen,
64
- summarizeSemSearch,
65
- summarizeTask,
66
- summarizeTodos,
67
72
  usesLocalReadPreview,
68
73
  } from "./cursor-transcript-tool-formatters.js";
69
- import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
74
+ import type { CursorReplaySummaryArgs } from "./cursor-replay-summary-args.js";
70
75
 
71
76
  export interface ToolDisplayContext {
72
77
  rawName: string;
@@ -76,17 +81,17 @@ export interface ToolDisplayContext {
76
81
  options: TranscriptOptions;
77
82
  }
78
83
 
79
- interface ActivityReplaySpec {
80
- labelKey: CursorReplayLegacyToolName;
81
- buildActivityArgs: (context: ToolDisplayContext) => Record<string, unknown>;
82
- buildActivitySummary: (context: ToolDisplayContext) => string | undefined;
83
- buildDetails: (context: ToolDisplayContext, contentText: string) => Record<string, unknown>;
84
- }
84
+ type NeutralActivityReplayToolName = Exclude<CursorReplayActivityToolName, "edit" | "write" | "generateImage">;
85
85
 
86
86
  interface ToolDisplaySpec {
87
87
  formatTranscript: (context: ToolDisplayContext) => string;
88
88
  buildPiToolDisplay: (context: ToolDisplayContext) => CursorPiToolDisplay;
89
- 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;
90
95
  }
91
96
 
92
97
  function textToolResult(text: string, details?: unknown): PiToolDisplayResult {
@@ -106,51 +111,81 @@ function buildCursorActivityDisplayArgs(
106
111
  };
107
112
  }
108
113
 
114
+ function buildRegistryReplaySummary(
115
+ labelKey: CursorReplayLegacyToolName,
116
+ args: CursorReplaySummaryArgs,
117
+ ): string | undefined {
118
+ return getCursorReplayCallSummary(labelKey, args);
119
+ }
120
+
109
121
  function buildReplaySummaryDisplay(
110
122
  toolName: string,
111
123
  args: Record<string, unknown>,
112
124
  result: NormalizedResult,
113
125
  contentText: string,
114
- details: Record<string, unknown>,
126
+ details: CursorReplayToolDetails,
115
127
  ): CursorPiToolDisplay {
116
128
  const isError = result.status === "error";
117
- const summary = isError ? details.summary : (details.summary ?? firstNonEmptyLine(contentText));
129
+ const expandedText = details.expandedText ?? contentText;
130
+ const summary = details.summary;
118
131
  return {
119
132
  toolName,
120
133
  args,
121
134
  result: textToolResult(contentText, {
122
135
  ...details,
123
136
  summary,
124
- expandedText: details.expandedText ?? contentText,
137
+ expandedText,
125
138
  }),
126
139
  isError,
127
140
  };
128
141
  }
129
142
 
130
- function buildActivityReplayDisplay(cursorToolName: string, spec: ToolDisplaySpec, context: ToolDisplayContext): CursorPiToolDisplay {
131
- const activity = spec.activityReplay;
132
- if (!activity) throw new Error(`Missing activity replay spec for ${cursorToolName}`);
133
- const activityTitle = getCursorReplayDisplayLabel(activity.labelKey);
134
- const activitySummary = activity.buildActivitySummary(context);
135
- const activityArgs = buildCursorActivityDisplayArgs(
136
- activity.buildActivityArgs(context),
143
+ function getCursorToolActivityTitle(toolName: string): string {
144
+ return getCursorReplayActivityTitle(toolName) ?? buildGenericUnknownToolActivityTitle(toolName);
145
+ }
146
+
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,
137
163
  activityTitle,
164
+ activityFields,
165
+ contentText,
166
+ context.result.status === "error",
138
167
  activitySummary,
139
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);
140
181
  const contentText = spec.formatTranscript(context).trimEnd();
141
- const details = activity.buildDetails(context, contentText);
142
- return buildReplaySummaryDisplay(
143
- CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
144
- activityArgs,
145
- context.result,
182
+ const details = assembleCursorReplayGenerateImageDetails(
183
+ replay.buildDetails(context, contentText),
146
184
  contentText,
147
- {
148
- cursorToolName,
149
- title: activityTitle,
150
- summary: context.result.status === "error" ? undefined : details.summary ?? activitySummary,
151
- ...details,
152
- },
185
+ context.result.status === "error",
186
+ activitySummary,
153
187
  );
188
+ return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, context.result, contentText, details);
154
189
  }
155
190
 
156
191
  function buildGenericUnknownToolActivityTitle(displayName: string): string {
@@ -175,12 +210,35 @@ function buildGenericPiToolDisplay(context: ToolDisplayContext): CursorPiToolDis
175
210
  result.status === "error"
176
211
  ? undefined
177
212
  : activitySummary ?? truncateArg(displayName === "unknown" ? "tool" : displayName);
178
- return buildReplaySummaryDisplay(CURSOR_REPLAY_ACTIVITY_TOOL_NAME, activityArgs, result, contentText, {
179
- cursorToolName: name,
180
- title: activityTitle,
181
- summary,
182
- expandedText: contentText,
183
- });
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;
184
242
  }
185
243
 
186
244
  function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDisplay {
@@ -191,18 +249,17 @@ function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDispla
191
249
  const nativeEditArgs = buildNativeEditDisplayArgs(rawName, args, options);
192
250
  const baseActivityArgs = buildCursorEditActivityDisplayArgs(args, options);
193
251
  const displayPath = typeof baseActivityArgs.path === "string" ? baseActivityArgs.path : undefined;
194
- const activityTitle = getCursorReplayDisplayLabel("cursor_edit");
252
+ const activityTitle = getCursorToolActivityTitle("edit");
195
253
  const activityArgs = buildCursorActivityDisplayArgs(baseActivityArgs, activityTitle, displayPath);
196
254
  const contentText = formatEdit(activityArgs, result, options);
197
- const details = {
198
- cursorToolName: "edit",
255
+ const details = buildCursorReplayNativeEditDetails({
199
256
  path: displayPath,
200
257
  linesAdded: getNumber(value, "linesAdded"),
201
258
  linesRemoved: getNumber(value, "linesRemoved"),
202
259
  diffString: normalizedDiff,
203
260
  diff: normalizedDiff,
204
261
  firstChangedLine: getNumber(value, "firstChangedLine"),
205
- };
262
+ });
206
263
  if (nativeEditArgs) {
207
264
  return {
208
265
  toolName: "edit",
@@ -216,11 +273,22 @@ function buildEditPiToolDisplay(context: ToolDisplayContext): CursorPiToolDispla
216
273
  activityArgs,
217
274
  result,
218
275
  contentText.trimEnd(),
219
- {
220
- ...details,
221
- title: activityTitle,
222
- summary: result.status === "error" ? undefined : displayPath ?? "replayed",
223
- },
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
+ ),
224
292
  );
225
293
  }
226
294
 
@@ -231,26 +299,33 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
231
299
  const displayArgs = buildWriteDisplayArgs(args, options);
232
300
  const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
233
301
  const contentText = formatWrite(args, result, options).trimEnd();
234
- const details = {
235
- cursorToolName: "write",
302
+ const details = buildCursorReplayNativeWriteDetails({
236
303
  path: displayPath,
237
304
  linesCreated: getNumber(value, "linesCreated"),
238
305
  fileSize: getNumber(value, "fileSize"),
239
306
  fileContentAfterWrite: getString(value, "fileContentAfterWrite"),
240
307
  expandedText: contentText,
241
- };
308
+ });
242
309
  if (content === undefined) {
243
- const activityTitle = getCursorReplayDisplayLabel("cursor_write");
310
+ const activityTitle = getCursorToolActivityTitle("write");
244
311
  return buildReplaySummaryDisplay(
245
312
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
246
313
  buildCursorActivityDisplayArgs(displayArgs, activityTitle, displayPath ?? "file"),
247
314
  result,
248
315
  contentText,
249
- {
250
- ...details,
251
- title: activityTitle,
252
- summary: result.status === "error" ? undefined : displayPath ?? "wrote file",
253
- },
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
+ ),
254
329
  );
255
330
  }
256
331
  return {
@@ -261,7 +336,7 @@ function buildWritePiToolDisplay(context: ToolDisplayContext): CursorPiToolDispl
261
336
  };
262
337
  }
263
338
 
264
- const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
339
+ const TOOL_DISPLAY_IMPLEMENTATIONS: Record<CursorNormalizedToolName, ToolDisplaySpec> = {
265
340
  read: {
266
341
  formatTranscript: ({ args, result, options }) => formatRead(args, result, options),
267
342
  buildPiToolDisplay: ({ args, result, options }) => {
@@ -334,207 +409,69 @@ const TOOL_DISPLAY_SPECS: Record<string, ToolDisplaySpec> = {
334
409
  },
335
410
  delete: {
336
411
  formatTranscript: ({ args, result, options }) => formatDelete(args, result, options),
337
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("delete", TOOL_DISPLAY_SPECS.delete, context),
338
- activityReplay: {
339
- labelKey: "cursor_delete",
340
- buildActivityArgs: ({ args, options }) => {
341
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
342
- return displayPath ? { path: displayPath } : {};
343
- },
344
- buildActivitySummary: ({ args, options }) => {
345
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
346
- return displayPath ?? "file";
347
- },
348
- buildDetails: ({ args, result, options }) => {
349
- const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
350
- const value = asRecord(result.value);
351
- return {
352
- path: displayPath,
353
- fileSize: getNumber(value, "fileSize"),
354
- summary: result.status === "error" ? undefined : displayPath ? `deleted ${displayPath}` : "deleted file",
355
- };
356
- },
357
- },
412
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("delete", context),
358
413
  },
359
414
  readLints: {
360
415
  formatTranscript: ({ args, result, options }) => formatReadLints(args, result, options),
361
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("readLints", TOOL_DISPLAY_SPECS.readLints, context),
362
- activityReplay: {
363
- labelKey: "cursor_read_lints",
364
- buildActivityArgs: ({ args, result, options }) => {
365
- const paths = getReadLintPaths(args, result, options);
366
- const diagnosticCount = getReadLintDiagnostics(result, options).length;
367
- return { paths, diagnosticCount };
368
- },
369
- buildActivitySummary: ({ args, result, options }) => {
370
- const paths = getReadLintPaths(args, result, options);
371
- const diagnosticCount = getReadLintDiagnostics(result, options).length;
372
- return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}${paths.length > 0 ? ` in ${paths.join(", ")}` : ""}`;
373
- },
374
- buildDetails: () => ({}),
375
- },
416
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("readLints", context),
376
417
  },
377
418
  updateTodos: {
378
419
  formatTranscript: ({ args, result, options }) => formatTodos(args, result, options, "updateTodos"),
379
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("updateTodos", TOOL_DISPLAY_SPECS.updateTodos, context),
380
- activityReplay: {
381
- labelKey: "cursor_update_todos",
382
- buildActivityArgs: ({ args, result }) => {
383
- const todos = getTodoItems(args, result);
384
- return { totalCount: getTodoTotalCount(args, result, todos) };
385
- },
386
- buildActivitySummary: ({ args, result }) => summarizeTodos(args, result),
387
- buildDetails: () => ({}),
388
- },
420
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("updateTodos", context),
389
421
  },
390
422
  createPlan: {
391
423
  formatTranscript: ({ args, result, options }) => formatPlan(args, result, options),
392
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("createPlan", TOOL_DISPLAY_SPECS.createPlan, context),
393
- activityReplay: {
394
- labelKey: "cursor_create_plan",
395
- buildActivityArgs: ({ args, result }) => {
396
- const todos = getTodoItems(args, result);
397
- return { totalCount: getTodoTotalCount(args, result, todos) };
398
- },
399
- buildActivitySummary: ({ args, result }) => summarizePlan(args, result),
400
- buildDetails: () => ({}),
401
- },
424
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("createPlan", context),
402
425
  },
403
426
  task: {
404
427
  formatTranscript: ({ args, result, options }) => formatTask(args, result, options),
405
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("task", TOOL_DISPLAY_SPECS.task, context),
406
- activityReplay: {
407
- labelKey: "cursor_task",
408
- buildActivityArgs: ({ args, result }) => {
409
- const description = getTaskDescription(args, result);
410
- return { description: truncateArg(description) };
411
- },
412
- buildActivitySummary: ({ args, result }) => {
413
- const description = getTaskDescription(args, result);
414
- return summarizeTask(description, collectTaskText(result));
415
- },
416
- buildDetails: () => ({}),
417
- },
428
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("task", context),
418
429
  },
419
430
  generateImage: {
420
431
  formatTranscript: ({ args, result, options }) => formatGenerateImage(args, result, options),
421
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("generateImage", TOOL_DISPLAY_SPECS.generateImage, context),
422
- activityReplay: {
423
- labelKey: "cursor_generate_image",
424
- buildActivityArgs: ({ args }) => {
425
- const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
426
- return { prompt: truncateArg(prompt) };
427
- },
428
- buildActivitySummary: ({ args, result, options }) => {
429
- const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
430
- const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
431
- return imageDisplayPath ?? truncateArg(prompt);
432
- },
433
- buildDetails: ({ args, result, options }, contentText) => {
434
- const imagePath = getGenerateImagePath(args, result);
435
- const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
436
- return {
437
- imagePath,
438
- imageDisplayPath,
439
- imageMimeType: inferImageMimeType(imagePath),
440
- summary: result.status === "error" ? undefined : imageDisplayPath ? `saved ${imageDisplayPath}` : "image generated",
441
- expandedText: contentText,
442
- };
443
- },
444
- },
432
+ buildPiToolDisplay: (context) => buildGenerateImageReplayDisplay(context),
445
433
  },
446
434
  mcp: {
447
435
  formatTranscript: ({ args, result, options }) => formatMcp(args, result, options),
448
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("mcp", TOOL_DISPLAY_SPECS.mcp, context),
449
- activityReplay: {
450
- labelKey: "cursor_mcp",
451
- buildActivityArgs: ({ args }) => {
452
- const toolName = getString(args, "toolName") ?? "mcp";
453
- return { toolName: truncateArg(toolName) };
454
- },
455
- buildActivitySummary: ({ args, result }) => summarizeMcp(args, result),
456
- buildDetails: ({ args, result }) => ({
457
- summary: result.status === "error" ? undefined : summarizeMcp(args, result),
458
- }),
459
- },
436
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("mcp", context),
460
437
  },
461
438
  semSearch: {
462
439
  formatTranscript: ({ args, result, options }) => formatSemSearch(args, result, options),
463
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("semSearch", TOOL_DISPLAY_SPECS.semSearch, context),
464
- activityReplay: {
465
- labelKey: "cursor_sem_search",
466
- buildActivityArgs: ({ args }) => {
467
- const query = getString(args, "query") ?? "semantic search";
468
- return { query: truncateArg(query) };
469
- },
470
- buildActivitySummary: ({ args }) => summarizeSemSearch(args),
471
- buildDetails: ({ result }, contentText) => ({
472
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "semantic search results captured",
473
- }),
474
- },
440
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("semSearch", context),
475
441
  },
476
442
  recordScreen: {
477
443
  formatTranscript: ({ args, result, options }) => formatRecordScreen(args, result, options),
478
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("recordScreen", TOOL_DISPLAY_SPECS.recordScreen, context),
479
- activityReplay: {
480
- labelKey: "cursor_record_screen",
481
- buildActivityArgs: ({ args, result, options }) => {
482
- const mode = getString(args, "mode");
483
- const path = getString(asRecord(result.value), "path");
484
- return {
485
- ...(mode ? { mode } : {}),
486
- ...(path ? { path: formatDisplayPath(path, options.cwd) } : {}),
487
- };
488
- },
489
- buildActivitySummary: ({ args, result, options }) => summarizeRecordScreen(args, result, options),
490
- buildDetails: ({ args, result, options }, contentText) => ({
491
- summary:
492
- result.status === "error"
493
- ? undefined
494
- : summarizeRecordScreen(args, result, options) ?? firstNonEmptyLine(contentText) ?? "screen recording updated",
495
- }),
496
- },
444
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("recordScreen", context),
497
445
  },
498
446
  webSearch: {
499
447
  formatTranscript: ({ args, result, options }) => formatWebSearch(args, result, options),
500
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("webSearch", TOOL_DISPLAY_SPECS.webSearch, context),
501
- activityReplay: {
502
- labelKey: "cursor_web_search",
503
- buildActivityArgs: ({ args }) => {
504
- const query = extractWebSearchQuery(args);
505
- return query ? { query: truncateArg(query) } : {};
506
- },
507
- buildActivitySummary: ({ args }) => truncateArg(extractWebSearchQuery(args) ?? "web search"),
508
- buildDetails: ({ result }, contentText) => ({
509
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web search result captured",
510
- }),
511
- },
448
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webSearch", context),
512
449
  },
513
450
  webFetch: {
514
451
  formatTranscript: ({ args, result, options }) => formatWebFetch(args, result, options),
515
- buildPiToolDisplay: (context) => buildActivityReplayDisplay("webFetch", TOOL_DISPLAY_SPECS.webFetch, context),
516
- activityReplay: {
517
- labelKey: "cursor_web_fetch",
518
- buildActivityArgs: ({ args }) => {
519
- const target = extractWebFetchTarget(args);
520
- return target ? { url: truncateArg(target) } : {};
521
- },
522
- buildActivitySummary: ({ args }) => truncateArg(extractWebFetchTarget(args) ?? "web fetch"),
523
- buildDetails: ({ result }, contentText) => ({
524
- summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "web fetch result captured",
525
- }),
526
- },
452
+ buildPiToolDisplay: (context) => buildActivityReplayDisplay("webFetch", context),
527
453
  },
528
454
  };
529
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
+
530
467
  export function formatCursorToolTranscriptFromSpec(context: ToolDisplayContext): string {
531
- const spec = TOOL_DISPLAY_SPECS[context.name];
468
+ const spec = getToolDisplaySpec(context.name);
532
469
  if (spec) return spec.formatTranscript(context);
533
470
  return formatFallback(context.name, context.args, context.result, context.options);
534
471
  }
535
472
 
536
473
  export function buildCursorPiToolDisplayFromSpec(context: ToolDisplayContext): CursorPiToolDisplay {
537
- const spec = TOOL_DISPLAY_SPECS[context.name];
474
+ const spec = getToolDisplaySpec(context.name);
538
475
  if (spec) return spec.buildPiToolDisplay(context);
539
476
  return buildGenericPiToolDisplay(context);
540
477
  }