pi-cursor-sdk 0.1.14 → 0.1.16

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.
@@ -1,11 +1,13 @@
1
1
  import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
+ import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayDisplayLabel } from "./cursor-tool-names.js";
3
4
 
4
5
  const DEFAULT_MAX_TRANSCRIPT_CHARS = 24000;
5
6
  const DEFAULT_MAX_TRANSCRIPT_LINES = 800;
6
7
  const DEFAULT_MAX_LIST_ITEMS = 200;
7
8
  const DEFAULT_READ_TRANSCRIPT_CHARS = 4000;
8
9
  const DEFAULT_READ_TRANSCRIPT_LINES = 12;
10
+ const DEFAULT_NATIVE_READ_DISPLAY_LINES = 20;
9
11
  const LOCAL_READ_PREVIEW_NOTICE =
10
12
  "[local file preview at transcript time; Cursor read result content was unavailable]";
11
13
 
@@ -79,7 +81,8 @@ function getToolResult(toolCall: unknown): unknown {
79
81
 
80
82
  function normalizeToolName(name: string): string {
81
83
  const normalized = name.replace(/\s+/g, " ").trim();
82
- switch (normalized) {
84
+ const normalizedKey = normalized.toLowerCase();
85
+ switch (normalizedKey) {
83
86
  case "read_file":
84
87
  return "read";
85
88
  case "list_dir":
@@ -87,12 +90,26 @@ function normalizeToolName(name: string): string {
87
90
  case "run_terminal_cmd":
88
91
  case "terminal":
89
92
  case "bash":
93
+ case "shell":
90
94
  return "shell";
91
95
  case "grep_search":
92
96
  case "search":
93
97
  return "grep";
94
98
  case "file_search":
95
99
  return "glob";
100
+ case "write_file":
101
+ case "writefile":
102
+ return "write";
103
+ case "strreplace":
104
+ case "str_replace":
105
+ case "str-replace":
106
+ case "edit_file":
107
+ case "editfile":
108
+ case "edit_notebook":
109
+ case "editnotebook":
110
+ case "notebook_edit":
111
+ case "notebookedit":
112
+ return "edit";
96
113
  default:
97
114
  return normalized || "unknown";
98
115
  }
@@ -167,6 +184,27 @@ function formatDisplayPath(path: string, cwd = process.cwd()): string {
167
184
  return relativePath;
168
185
  }
169
186
 
187
+ function formatDiffPath(path: string, cwd = process.cwd()): string {
188
+ if (path === "/dev/null") return path;
189
+ return formatDisplayPath(path, cwd);
190
+ }
191
+
192
+ function formatDiffHeaderLine(line: string, options: TranscriptOptions): string {
193
+ const match = /^(---|\+\+\+)\s+((?:[ab]\/)?)(.+)$/.exec(line);
194
+ if (!match) return line;
195
+ const [, marker, prefix, rawPath] = match;
196
+ if (!prefix && rawPath !== "/dev/null") return line;
197
+ const displayPath = formatDiffPath(rawPath, options.cwd);
198
+ return `${marker} ${prefix}${displayPath}`;
199
+ }
200
+
201
+ function formatDiffString(diff: string | undefined, options: TranscriptOptions): string | undefined {
202
+ return diff
203
+ ?.split("\n")
204
+ .map((line) => formatDiffHeaderLine(line, options))
205
+ .join("\n");
206
+ }
207
+
170
208
  function resolveFilePath(path: string, cwd = process.cwd()): string {
171
209
  return isAbsolute(path) ? path : resolve(cwd, path);
172
210
  }
@@ -255,16 +293,139 @@ function formatRead(args: Record<string, unknown>, result: NormalizedResult, opt
255
293
  return joinSections(`read ${path}`, limitText(getReadContent(args, result, options), readOptions, totalLines));
256
294
  }
257
295
 
258
- function getShellOutput(result: NormalizedResult): { text: string; exitCode: number | undefined } {
296
+ function buildReadDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
297
+ const rawPath = typeof args.path === "string" ? args.path : undefined;
298
+ return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
299
+ }
300
+
301
+ function buildPathDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
302
+ const rawPath = typeof args.path === "string" ? args.path : undefined;
303
+ return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
304
+ }
305
+
306
+ function buildWriteDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
307
+ const displayArgs = buildPathDisplayArgs(args, options);
308
+ const content = getCursorWriteArgContent(args);
309
+ return content === undefined ? displayArgs : { ...displayArgs, content };
310
+ }
311
+
312
+ type NativeEditReplacement = { oldText: string; newText: string };
313
+ type NativeEditDisplayArgs = { path: string; edits: NativeEditReplacement[] };
314
+
315
+ const CURSOR_EDIT_PATH_KEYS = ["path", "filePath", "file_path"] as const;
316
+ const CURSOR_EDIT_OLD_TEXT_KEYS = ["oldText", "old_text", "oldString", "old_string", "oldStr", "old_str"] as const;
317
+ const CURSOR_EDIT_NEW_TEXT_KEYS = ["newText", "new_text", "newString", "new_string", "newStr", "new_str"] as const;
318
+ const CURSOR_NOTEBOOK_EDIT_ARG_KEYS = ["cellId", "cell_id", "cellIndex", "cell_index", "cellType", "cell_type", "notebookPath", "notebook_path"] as const;
319
+
320
+ function getFirstStringByKeys(record: Record<string, unknown>, keys: readonly string[]): string | undefined {
321
+ for (const key of keys) {
322
+ const value = record[key];
323
+ if (typeof value === "string") return value;
324
+ }
325
+ return undefined;
326
+ }
327
+
328
+ function getCursorEditPathArg(args: Record<string, unknown>): string | undefined {
329
+ const path = getFirstStringByKeys(args, CURSOR_EDIT_PATH_KEYS);
330
+ return path?.trim() ? path : undefined;
331
+ }
332
+
333
+ function isCursorNotebookEditToolName(toolName: string): boolean {
334
+ const normalized = toolName.replace(/[\s_-]+/g, "").toLowerCase();
335
+ return normalized === "editnotebook" || normalized === "notebookedit";
336
+ }
337
+
338
+ function isCursorStrReplaceToolName(toolName: string): boolean {
339
+ const normalized = toolName.replace(/[\s_-]+/g, "").toLowerCase();
340
+ return normalized === "strreplace";
341
+ }
342
+
343
+ function hasAnyKey(record: Record<string, unknown>, keys: readonly string[]): boolean {
344
+ return keys.some((key) => record[key] !== undefined);
345
+ }
346
+
347
+ function isNotebookPath(path: string | undefined): boolean {
348
+ return path?.toLowerCase().endsWith(".ipynb") === true;
349
+ }
350
+
351
+ function isCursorNotebookEditActivity(rawToolName: string, args: Record<string, unknown>): boolean {
352
+ if (isCursorNotebookEditToolName(rawToolName)) return true;
353
+ if (hasAnyKey(args, CURSOR_NOTEBOOK_EDIT_ARG_KEYS)) return true;
354
+ return !isCursorStrReplaceToolName(rawToolName) && isNotebookPath(getCursorEditPathArg(args));
355
+ }
356
+
357
+ function asNativeEditReplacement(value: unknown): NativeEditReplacement | undefined {
358
+ const record = asRecord(value);
359
+ const oldText = record ? getFirstStringByKeys(record, CURSOR_EDIT_OLD_TEXT_KEYS) : undefined;
360
+ const newText = record ? getFirstStringByKeys(record, CURSOR_EDIT_NEW_TEXT_KEYS) : undefined;
361
+ if (typeof oldText !== "string" || oldText.length === 0 || typeof newText !== "string") return undefined;
362
+ return { oldText, newText };
363
+ }
364
+
365
+ function getNativeEditReplacementsFromArgs(args: Record<string, unknown>): NativeEditReplacement[] | undefined {
366
+ const edits = getArray(args, "edits")?.map(asNativeEditReplacement);
367
+ if (edits && edits.length > 0 && edits.every((edit): edit is NativeEditReplacement => edit !== undefined)) return edits;
368
+
369
+ const singleEdit = asNativeEditReplacement(args);
370
+ return singleEdit ? [singleEdit] : undefined;
371
+ }
372
+
373
+ function buildNativeEditDisplayArgs(rawToolName: string, args: Record<string, unknown>, options: TranscriptOptions): NativeEditDisplayArgs | undefined {
374
+ if (isCursorNotebookEditActivity(rawToolName, args)) return undefined;
375
+ const rawPath = getCursorEditPathArg(args);
376
+ const edits = getNativeEditReplacementsFromArgs(args);
377
+ if (!rawPath || !edits) return undefined;
378
+ return { path: formatDisplayPath(rawPath, options.cwd), edits };
379
+ }
380
+
381
+ function buildCursorEditActivityDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
382
+ const rawPath = getCursorEditPathArg(args);
383
+ return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
384
+ }
385
+
386
+ function formatNativeReadDisplayContent(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
387
+ const value = asRecord(result.value);
388
+ const totalLines = getNumber(value, "totalLines");
389
+ const readOptions = {
390
+ ...options,
391
+ maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
392
+ maxLines: options.maxLines ?? DEFAULT_NATIVE_READ_DISPLAY_LINES,
393
+ };
394
+ const content = getReadContent(args, result, readOptions);
395
+ if (totalLines === undefined) return limitText(content, readOptions);
396
+
397
+ const maxLines = readOptions.maxLines ?? DEFAULT_NATIVE_READ_DISPLAY_LINES;
398
+ const lines = content.split("\n");
399
+ const visible = lines.slice(0, maxLines).join("\n");
400
+ if (totalLines <= maxLines && lines.length <= maxLines) return visible;
401
+ if (visible.length > (readOptions.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS)) return limitText(content, readOptions, totalLines);
402
+ return `${visible}\n\n[${Math.max(totalLines - maxLines, 0)} more lines in file. Use offset=${maxLines + 1} to continue.]`;
403
+ }
404
+
405
+ function getShellOutput(result: NormalizedResult, args: Record<string, unknown> = {}): { text: string; exitCode: number | undefined; timedOut: boolean } {
259
406
  const value = asRecord(result.value);
260
407
  const stdout = getString(value, "stdout") ?? "";
261
408
  const stderr = getString(value, "stderr") ?? "";
262
409
  const exitCode = getNumber(value, "exitCode");
410
+ const timeoutMs = getNumber(args, "timeout");
411
+ const executionTimeMs = getNumber(value, "executionTime");
412
+ const timedOut = timeoutMs !== undefined && executionTimeMs !== undefined && executionTimeMs >= timeoutMs;
263
413
  const outputParts: string[] = [];
264
414
  if (stdout) outputParts.push(stdout.trimEnd());
265
415
  if (stderr) outputParts.push(stderr.trimEnd());
266
416
  if (exitCode !== undefined && exitCode !== 0) outputParts.push(`Command exited with code ${exitCode}`);
267
- return { text: outputParts.filter(Boolean).join("\n\n") || "(no output)", exitCode };
417
+ if (timedOut) outputParts.push(`Command backgrounded after ${(timeoutMs / 1000).toFixed(0)} second timeout`);
418
+ return { text: outputParts.filter(Boolean).join("\n\n") || "(no output)", exitCode, timedOut };
419
+ }
420
+
421
+ function buildShellDisplayArgs(args: Record<string, unknown>): Record<string, unknown> {
422
+ const command = typeof args.command === "string" ? args.command : undefined;
423
+ const timeoutMs = getNumber(args, "timeout");
424
+ const displayArgs: Record<string, unknown> = command ? { command } : { ...args };
425
+ if (timeoutMs !== undefined) {
426
+ displayArgs.timeout = timeoutMs / 1000;
427
+ }
428
+ return displayArgs;
268
429
  }
269
430
 
270
431
  function formatShell(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
@@ -273,7 +434,7 @@ function formatShell(args: Record<string, unknown>, result: NormalizedResult, op
273
434
 
274
435
  const value = asRecord(result.value);
275
436
  const executionTime = getNumber(value, "executionTime");
276
- const outputParts = [getShellOutput(result).text];
437
+ const outputParts = [getShellOutput(result, args).text];
277
438
  if (executionTime !== undefined) outputParts.push(`Took ${(executionTime / 1000).toFixed(1)}s`);
278
439
  return joinSections(`$ ${command || "shell"}`, limitText(outputParts.filter(Boolean).join("\n\n"), options));
279
440
  }
@@ -304,17 +465,17 @@ function formatLs(args: Record<string, unknown>, result: NormalizedResult, optio
304
465
  }
305
466
 
306
467
  function formatGlob(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
307
- const pattern = typeof args.globPattern === "string" ? args.globPattern : "*";
308
- const targetDirectory = typeof args.targetDirectory === "string" ? formatDisplayPath(args.targetDirectory, options.cwd) : undefined;
309
- const header = targetDirectory ? `glob ${pattern} in ${targetDirectory}` : `glob ${pattern}`;
468
+ const header = `$ ${synthesizeGlobBashCommand(args, options)}`;
310
469
  if (result.status === "error") return joinSections(header, formatError(result.error));
470
+ return joinSections(header, getGlobBody(result, options));
471
+ }
311
472
 
312
- const value = asRecord(result.value);
313
- const files = getArray(value, "files")?.filter((entry): entry is string => typeof entry === "string") ?? [];
314
- if (files.length === 0) return joinSections(header, stringifyUnknown(result.value));
315
- const limited = limitItems(files, options);
316
- const body = limited.omitted > 0 ? `${limited.items.join("\n")}\n... (${limited.omitted} more files truncated)` : limited.items.join("\n");
317
- return joinSections(header, body);
473
+ function formatSearchCount(totalMatches: number): string {
474
+ return totalMatches === 1 ? "1 match" : `${totalMatches} matches`;
475
+ }
476
+
477
+ function formatSearchFile(file: string): string {
478
+ return file.endsWith(":") ? file.slice(0, -1) : file;
318
479
  }
319
480
 
320
481
  function collectSearchResults(value: unknown): string[] {
@@ -327,46 +488,140 @@ function collectSearchResults(value: unknown): string[] {
327
488
  if (outputs.length === 0) outputs.push(value);
328
489
 
329
490
  const lines: string[] = [];
491
+ let sawExplicitNoMatches = false;
330
492
  for (const outputValue of outputs) {
331
493
  const outputRecord = asRecord(outputValue);
332
494
  const type = getString(outputRecord, "type");
333
495
  const output = getRecord(outputRecord, "output");
334
496
  if (type === "content") {
335
497
  const matches = getArray(output, "matches") ?? [];
498
+ if (matches.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
336
499
  for (const match of matches) {
337
500
  const matchRecord = asRecord(match);
338
- const file = getString(matchRecord, "file") ?? "";
501
+ const file = formatSearchFile(getString(matchRecord, "file") ?? "");
339
502
  const lineNumber = getNumber(matchRecord, "lineNumber");
340
503
  const line = getString(matchRecord, "line") ?? "";
341
- lines.push(`${file}${lineNumber !== undefined ? `:${lineNumber}` : ""}: ${line}`.trim());
504
+ if (lineNumber === undefined && !line.trim()) {
505
+ if (file) lines.push(file);
506
+ continue;
507
+ }
508
+ const location = `${file}${lineNumber !== undefined ? `:${lineNumber}` : ""}`;
509
+ lines.push(line ? `${location}: ${line}` : location);
342
510
  }
343
511
  } else if (type === "files") {
344
512
  const files = getArray(output, "files") ?? [];
345
- lines.push(...files.filter((entry): entry is string => typeof entry === "string"));
513
+ if (files.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
514
+ lines.push(...files.filter((entry): entry is string => typeof entry === "string").map(formatSearchFile));
346
515
  } else if (type === "count") {
347
516
  const counts = getArray(output, "counts") ?? [];
517
+ if (counts.length === 0 && getNumber(output, "totalMatches") === 0) sawExplicitNoMatches = true;
348
518
  for (const count of counts) {
349
519
  const countRecord = asRecord(count);
350
520
  lines.push(`${getString(countRecord, "file") ?? ""}: ${getNumber(countRecord, "count") ?? 0}`.trim());
351
521
  }
352
522
  } else {
523
+ const totalMatches = getNumber(outputRecord, "totalMatches");
524
+ if (totalMatches !== undefined) {
525
+ if (totalMatches === 0) {
526
+ sawExplicitNoMatches = true;
527
+ continue;
528
+ }
529
+ lines.push(formatSearchCount(totalMatches));
530
+ continue;
531
+ }
353
532
  lines.push(stringifyUnknown(outputValue));
354
533
  }
355
534
  }
535
+
536
+ const topLevelTotalMatches = getNumber(record, "totalMatches");
537
+ if (lines.length === 0 && topLevelTotalMatches !== undefined) {
538
+ return topLevelTotalMatches === 0 ? ["(no matches)"] : [formatSearchCount(topLevelTotalMatches)];
539
+ }
540
+ if (lines.length === 0 && sawExplicitNoMatches) return ["(no matches)"];
356
541
  return lines.filter(Boolean);
357
542
  }
358
543
 
359
- function formatGrep(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
544
+ function synthesizeGrepBashCommand(args: Record<string, unknown>, options: TranscriptOptions): string {
360
545
  const pattern = typeof args.pattern === "string" ? args.pattern : "";
361
546
  const path = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
362
547
  const glob = typeof args.glob === "string" ? args.glob : undefined;
363
- const header = ["grep", pattern && JSON.stringify(pattern), path ?? glob].filter(Boolean).join(" ");
364
- if (result.status === "error") return joinSections(header, formatError(result.error));
548
+ return ["grep", pattern && JSON.stringify(pattern), path ?? glob].filter(Boolean).join(" ");
549
+ }
550
+
551
+ function buildGrepDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
552
+ const displayArgs: Record<string, unknown> = {};
553
+ const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
554
+ const path = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
555
+ const glob = typeof args.glob === "string" ? args.glob : undefined;
556
+ const ignoreCase = getBoolean(args, "caseInsensitive");
557
+ const context = getNumber(args, "context") ?? getNumber(args, "contextBefore") ?? getNumber(args, "contextAfter");
558
+ const limit = getNumber(args, "headLimit");
559
+ if (pattern !== undefined) displayArgs.pattern = pattern;
560
+ if (path !== undefined) displayArgs.path = path;
561
+ if (glob !== undefined) displayArgs.glob = glob;
562
+ if (ignoreCase !== undefined) displayArgs.ignoreCase = ignoreCase;
563
+ if (context !== undefined) displayArgs.context = context;
564
+ if (limit !== undefined) displayArgs.limit = limit;
565
+ return Object.keys(displayArgs).length > 0 ? displayArgs : args;
566
+ }
567
+
568
+ function getGlobPattern(args: Record<string, unknown>): string {
569
+ return typeof args.globPattern === "string" ? args.globPattern : typeof args.pattern === "string" ? args.pattern : "*";
570
+ }
571
+
572
+ function getGlobTargetDirectory(args: Record<string, unknown>, options: TranscriptOptions): string | undefined {
573
+ const rawPath = typeof args.targetDirectory === "string" ? args.targetDirectory : typeof args.path === "string" ? args.path : undefined;
574
+ return rawPath ? formatDisplayPath(rawPath, options.cwd) : undefined;
575
+ }
576
+
577
+ function synthesizeGlobBashCommand(args: Record<string, unknown>, options: TranscriptOptions): string {
578
+ const pattern = getGlobPattern(args);
579
+ const targetDirectory = getGlobTargetDirectory(args, options);
580
+ return targetDirectory ? `glob ${pattern} in ${targetDirectory}` : `glob ${pattern}`;
581
+ }
582
+
583
+ function buildFindDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
584
+ const displayArgs: Record<string, unknown> = { pattern: getGlobPattern(args) };
585
+ const targetDirectory = getGlobTargetDirectory(args, options);
586
+ const limit = getNumber(args, "limit") ?? getNumber(args, "headLimit");
587
+ if (targetDirectory !== undefined) displayArgs.path = targetDirectory;
588
+ if (limit !== undefined) displayArgs.limit = limit;
589
+ return displayArgs;
590
+ }
365
591
 
592
+ function getGrepBody(result: NormalizedResult, options: TranscriptOptions): string {
366
593
  const lines = collectSearchResults(result.value);
367
594
  const limited = limitItems(lines, options);
368
595
  const body = limited.omitted > 0 ? `${limited.items.join("\n")}\n... (${limited.omitted} more matches truncated)` : limited.items.join("\n");
369
- return joinSections(header, limitText(body || stringifyUnknown(result.value), options));
596
+ return limitText(body || stringifyUnknown(result.value), options);
597
+ }
598
+
599
+ function getGlobBody(result: NormalizedResult, options: TranscriptOptions): string {
600
+ const value = asRecord(result.value);
601
+ const files = getArray(value, "files")?.filter((entry): entry is string => typeof entry === "string") ?? [];
602
+ if (files.length === 0) {
603
+ const totalMatches = getNumber(value, "totalMatches");
604
+ const totalFiles = getNumber(value, "totalFiles");
605
+ if (totalMatches === 0 || totalFiles === 0) return "No files found matching pattern";
606
+ return stringifyUnknown(result.value);
607
+ }
608
+ const limited = limitItems(files, options);
609
+ const body = limited.omitted > 0 ? `${limited.items.join("\n")}\n... (${limited.omitted} more files truncated)` : limited.items.join("\n");
610
+ return limitText(body, options);
611
+ }
612
+
613
+ function formatGrep(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
614
+ const header = `$ ${synthesizeGrepBashCommand(args, options)}`;
615
+ if (result.status === "error") return joinSections(header, formatError(result.error));
616
+ return joinSections(header, getGrepBody(result, options));
617
+ }
618
+
619
+ function getCursorWriteArgContent(args: Record<string, unknown>): string | undefined {
620
+ return getString(args, "content") ?? getString(args, "fileContent") ?? getString(args, "contents");
621
+ }
622
+
623
+ function getCursorWriteRecordedContent(args: Record<string, unknown>, resultValue: Record<string, unknown> | undefined): string | undefined {
624
+ return getCursorWriteArgContent(args) ?? getString(resultValue, "fileContentAfterWrite");
370
625
  }
371
626
 
372
627
  function formatWrite(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
@@ -376,7 +631,7 @@ function formatWrite(args: Record<string, unknown>, result: NormalizedResult, op
376
631
  const value = asRecord(result.value);
377
632
  const linesCreated = getNumber(value, "linesCreated");
378
633
  const fileSize = getNumber(value, "fileSize");
379
- const fileContentAfterWrite = getString(value, "fileContentAfterWrite");
634
+ const fileContentAfterWrite = getCursorWriteRecordedContent(args, value);
380
635
  const parts = [
381
636
  linesCreated !== undefined ? `Created ${linesCreated} lines` : undefined,
382
637
  fileSize !== undefined ? `File size: ${fileSize} bytes` : undefined,
@@ -390,7 +645,7 @@ function formatEdit(args: Record<string, unknown>, result: NormalizedResult, opt
390
645
  if (result.status === "error") return joinSections(`edit ${path}`, formatError(result.error));
391
646
 
392
647
  const value = asRecord(result.value);
393
- const diff = getString(value, "diffString");
648
+ const diff = formatDiffString(getString(value, "diffString") ?? getString(value, "diff") ?? getString(value, "unifiedDiff"), options);
394
649
  const linesAdded = getNumber(value, "linesAdded");
395
650
  const linesRemoved = getNumber(value, "linesRemoved");
396
651
  const stats = [
@@ -409,13 +664,19 @@ function formatDelete(args: Record<string, unknown>, result: NormalizedResult, o
409
664
  return joinSections(`delete ${path}`, fileSize !== undefined ? `Deleted ${fileSize} bytes` : stringifyUnknown(result.value));
410
665
  }
411
666
 
412
- function formatReadLints(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
413
- const paths = Array.isArray(args.paths)
414
- ? args.paths.filter((entry): entry is string => typeof entry === "string").map((entry) => formatDisplayPath(entry, options.cwd))
415
- : [];
416
- const header = `readLints${paths.length > 0 ? ` ${paths.join(" ")}` : ""}`;
417
- if (result.status === "error") return joinSections(header, formatError(result.error));
667
+ function getReadLintPaths(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string[] {
668
+ const explicitPaths = Array.isArray(args.paths)
669
+ ? args.paths.filter((entry): entry is string => typeof entry === "string")
670
+ : typeof args.path === "string"
671
+ ? [args.path]
672
+ : [];
673
+ const resultPaths = (getArray(asRecord(result.value), "fileDiagnostics") ?? [])
674
+ .map((file) => getString(asRecord(file), "path"))
675
+ .filter((entry): entry is string => Boolean(entry));
676
+ return [...new Set([...explicitPaths, ...resultPaths].map((entry) => formatDisplayPath(entry, options.cwd)))];
677
+ }
418
678
 
679
+ function getReadLintDiagnostics(result: NormalizedResult, options: TranscriptOptions): string[] {
419
680
  const value = asRecord(result.value);
420
681
  const files = getArray(value, "fileDiagnostics") ?? [];
421
682
  const lines: string[] = [];
@@ -432,9 +693,172 @@ function formatReadLints(args: Record<string, unknown>, result: NormalizedResult
432
693
  lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
433
694
  }
434
695
  }
696
+ return lines;
697
+ }
698
+
699
+ function formatReadLints(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
700
+ const paths = getReadLintPaths(args, result, options);
701
+ const header = `readLints${paths.length > 0 ? ` ${paths.join(" ")}` : ""}`;
702
+ if (result.status === "error") return joinSections(header, formatError(result.error));
703
+
704
+ const lines = getReadLintDiagnostics(result, options);
705
+ if (lines.length === 0 && paths.length > 0) return joinSections(header, `No diagnostics in ${paths.join(", ")}`);
435
706
  return joinSections(header, limitText(lines.join("\n") || stringifyUnknown(result.value), options));
436
707
  }
437
708
 
709
+ function getTodoItems(args: Record<string, unknown>, result: NormalizedResult): Array<{ content: string; status?: string }> {
710
+ const value = asRecord(result.value);
711
+ const rawTodos = getArray(value, "todos") ?? getArray(args, "todos") ?? [];
712
+ const todos: Array<{ content: string; status?: string }> = [];
713
+ for (const todo of rawTodos) {
714
+ const record = asRecord(todo);
715
+ const content = getString(record, "content");
716
+ if (!content) continue;
717
+ const status = getString(record, "status");
718
+ todos.push(status ? { content, status } : { content });
719
+ }
720
+ return todos;
721
+ }
722
+
723
+ function getTodoTotalCount(args: Record<string, unknown>, result: NormalizedResult, todos: Array<{ content: string; status?: string }>): number {
724
+ return getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
725
+ }
726
+
727
+ function summarizeTodos(args: Record<string, unknown>, result: NormalizedResult): string {
728
+ const todos = getTodoItems(args, result);
729
+ const total = getTodoTotalCount(args, result, todos);
730
+ const completed = todos.filter((todo) => todo.status === "completed").length;
731
+ const inProgress = todos.filter((todo) => todo.status === "inProgress").length;
732
+ const pending = todos.filter((todo) => todo.status === "pending").length;
733
+ const parts = [`${completed}/${total} completed`];
734
+ if (inProgress > 0) parts.push(`${inProgress} in progress`);
735
+ if (pending > 0) parts.push(`${pending} pending`);
736
+ return parts.join(", ");
737
+ }
738
+
739
+ function formatTodoStatus(status: string | undefined): string {
740
+ if (status === "completed") return "✓";
741
+ if (status === "inProgress") return "…";
742
+ if (status === "pending") return "○";
743
+ return "•";
744
+ }
745
+
746
+ function formatTodos(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions, header: string): string {
747
+ if (result.status === "error") return joinSections(header, formatError(result.error));
748
+ const todos = getTodoItems(args, result);
749
+ if (todos.length === 0) return joinSections(header, limitText(stringifyUnknown(result.value), options));
750
+ const lines = todos.map((todo) => `${formatTodoStatus(todo.status)} ${todo.content}${todo.status ? ` (${todo.status})` : ""}`);
751
+ return joinSections(header, limitText(lines.join("\n"), options));
752
+ }
753
+
754
+ export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
755
+ const name = normalizeToolName(getToolName(toolCall));
756
+ if (name !== "createPlan") return undefined;
757
+ const args = getToolArgs(toolCall);
758
+ const result = normalizeResult(getToolResult(toolCall));
759
+ const plan = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
760
+ const trimmed = plan?.trim();
761
+ return trimmed || undefined;
762
+ }
763
+
764
+ function summarizePlan(args: Record<string, unknown>, result: NormalizedResult): string {
765
+ const planText = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
766
+ const firstLine = planText ? firstNonEmptyLine(planText) : undefined;
767
+ return firstLine ? truncateArg(firstLine, 160) : summarizeTodos(args, result);
768
+ }
769
+
770
+ function formatPlan(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
771
+ if (result.status === "error") return joinSections("createPlan", formatError(result.error));
772
+ const planText = getString(args, "plan") ?? getString(asRecord(result.value), "plan");
773
+ if (planText?.trim()) return joinSections("createPlan", limitText(planText, options));
774
+ return formatTodos(args, result, options, "createPlan");
775
+ }
776
+
777
+ function getTaskDescription(args: Record<string, unknown>, result: NormalizedResult): string {
778
+ return getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
779
+ }
780
+
781
+ function getNestedRecord(record: Record<string, unknown> | undefined, ...keys: string[]): Record<string, unknown> | undefined {
782
+ let current = record;
783
+ for (const key of keys) {
784
+ current = getRecord(current, key);
785
+ if (!current) return undefined;
786
+ }
787
+ return current;
788
+ }
789
+
790
+ function collectTaskText(result: NormalizedResult): string {
791
+ const value = asRecord(result.value);
792
+ const success = getNestedRecord(value, "result", "success");
793
+ const command = getString(success, "command");
794
+ const stdout = getString(success, "stdout");
795
+ const interleavedOutput = getString(success, "interleavedOutput");
796
+ const assistantMessages = (getArray(value, "conversationSteps") ?? [])
797
+ .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
798
+ .filter((entry): entry is string => Boolean(entry));
799
+ const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
800
+ return parts.join("\n");
801
+ }
802
+
803
+ function formatTask(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
804
+ const description = getTaskDescription(args, result);
805
+ if (result.status === "error") return joinSections(`task ${description}`, formatError(result.error));
806
+ const taskText = collectTaskText(result);
807
+ return joinSections(`task ${description}`, limitText(taskText || stringifyUnknown(result.value), options));
808
+ }
809
+
810
+ function summarizeTask(description: string, taskText: string): string {
811
+ const firstLine = firstNonEmptyLine(taskText);
812
+ if (!firstLine) return truncateArg(description);
813
+ if (description === "task" || description === firstLine) return truncateArg(firstLine);
814
+ return truncateArg(`${description}: ${firstLine}`, 160);
815
+ }
816
+
817
+ function getGenerateImageValue(result: NormalizedResult): Record<string, unknown> | undefined {
818
+ return asRecord(result.value);
819
+ }
820
+
821
+ function getGenerateImagePath(args: Record<string, unknown>, result: NormalizedResult): string | undefined {
822
+ const value = getGenerateImageValue(result);
823
+ return getString(value, "filePath") ?? getString(args, "filePath") ?? getString(args, "path");
824
+ }
825
+
826
+ function getGenerateImageDisplayPath(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string | undefined {
827
+ const path = getGenerateImagePath(args, result);
828
+ return path ? formatDisplayPath(path, options.cwd) : undefined;
829
+ }
830
+
831
+ function inferImageMimeType(path: string | undefined): string | undefined {
832
+ const lower = path?.toLowerCase();
833
+ if (!lower) return undefined;
834
+ if (lower.endsWith(".png")) return "image/png";
835
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
836
+ if (lower.endsWith(".gif")) return "image/gif";
837
+ if (lower.endsWith(".webp")) return "image/webp";
838
+ return undefined;
839
+ }
840
+
841
+ function formatGenerateImage(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
842
+ const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
843
+ if (result.status === "error") return joinSections(`generateImage ${prompt}`, formatError(result.error));
844
+ const value = getGenerateImageValue(result);
845
+ const displayPath = getGenerateImageDisplayPath(args, result, options);
846
+ const hasImageData = typeof value?.imageData === "string" && value.imageData.length > 0;
847
+ const lines = [displayPath ? `Saved image: ${displayPath}` : undefined, hasImageData ? "Image data returned by Cursor SDK." : undefined].filter(
848
+ (line): line is string => Boolean(line),
849
+ );
850
+ if (lines.length > 0) return joinSections(`generateImage ${prompt}`, lines.join("\n"));
851
+ return joinSections(`generateImage ${prompt}`, limitText(stringifyUnknown(result.value), options));
852
+ }
853
+
854
+ function getMcpContentText(entry: unknown): string | undefined {
855
+ const record = asRecord(entry);
856
+ const directText = getString(record, "text");
857
+ if (directText) return directText;
858
+ const nestedText = getRecord(record, "text");
859
+ return getString(nestedText, "text");
860
+ }
861
+
438
862
  function formatMcp(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
439
863
  const toolName = typeof args.toolName === "string" ? args.toolName : "mcp";
440
864
  if (result.status === "error") return joinSections(toolName, formatError(result.error));
@@ -443,7 +867,7 @@ function formatMcp(args: Record<string, unknown>, result: NormalizedResult, opti
443
867
  const isError = getBoolean(value, "isError");
444
868
  const content = getArray(value, "content") ?? [];
445
869
  const text = content
446
- .map((entry) => getString(asRecord(entry), "text"))
870
+ .map((entry) => getMcpContentText(entry))
447
871
  .filter((entry): entry is string => Boolean(entry))
448
872
  .join("\n");
449
873
  const body = `${isError ? "[tool error]\n" : ""}${text || stringifyUnknown(result.value)}`;
@@ -481,6 +905,14 @@ export function formatCursorToolTranscript(toolCall: unknown, options: Transcrip
481
905
  return formatDelete(args, result, options);
482
906
  case "readLints":
483
907
  return formatReadLints(args, result, options);
908
+ case "updateTodos":
909
+ return formatTodos(args, result, options, "updateTodos");
910
+ case "createPlan":
911
+ return formatPlan(args, result, options);
912
+ case "task":
913
+ return formatTask(args, result, options);
914
+ case "generateImage":
915
+ return formatGenerateImage(args, result, options);
484
916
  case "mcp":
485
917
  return formatMcp(args, result, options);
486
918
  default:
@@ -502,39 +934,96 @@ function buildGenericPiToolDisplay(name: string, args: Record<string, unknown>,
502
934
  };
503
935
  }
504
936
 
937
+ function firstNonEmptyLine(text: string): string | undefined {
938
+ return text.split("\n").find((line) => line.trim())?.trim();
939
+ }
940
+
941
+ function buildReplaySummaryDisplay(
942
+ toolName: string,
943
+ args: Record<string, unknown>,
944
+ result: NormalizedResult,
945
+ contentText: string,
946
+ details: Record<string, unknown>,
947
+ ): CursorPiToolDisplay {
948
+ const isError = result.status === "error";
949
+ const summary = isError ? formatError(result.error) : firstNonEmptyLine(contentText);
950
+ return {
951
+ toolName,
952
+ args,
953
+ result: textToolResult(contentText, {
954
+ ...details,
955
+ summary: details.summary ?? summary,
956
+ expandedText: details.expandedText ?? contentText,
957
+ }),
958
+ isError,
959
+ };
960
+ }
961
+
962
+ function truncateArg(value: string, maxLength = 120): string {
963
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
964
+ }
965
+
966
+ function buildCursorActivityDisplayArgs(
967
+ args: Record<string, unknown>,
968
+ activityTitle: string,
969
+ activitySummary: string | undefined,
970
+ ): Record<string, unknown> {
971
+ const trimmedSummary = activitySummary?.trim();
972
+ return {
973
+ ...args,
974
+ activityTitle,
975
+ ...(trimmedSummary ? { activitySummary: trimmedSummary } : {}),
976
+ };
977
+ }
978
+
505
979
  export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptOptions = {}): CursorPiToolDisplay {
506
- const name = normalizeToolName(getToolName(toolCall));
980
+ const rawName = getToolName(toolCall);
981
+ const name = normalizeToolName(rawName);
507
982
  const args = getToolArgs(toolCall);
508
983
  const result = normalizeResult(getToolResult(toolCall));
509
984
 
510
985
  if (name === "read") {
511
986
  const isError = result.status === "error";
512
- const value = asRecord(result.value);
513
- const totalLines = getNumber(value, "totalLines");
514
- const readOptions = {
515
- ...options,
516
- maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
517
- maxLines: options.maxLines ?? DEFAULT_READ_TRANSCRIPT_LINES,
518
- };
519
987
  return {
520
988
  toolName: "read",
521
- args,
522
- result: textToolResult(isError ? formatError(result.error) : limitText(getReadContent(args, result, options), readOptions, totalLines)),
989
+ args: buildReadDisplayArgs(args, options),
990
+ result: textToolResult(isError ? formatError(result.error) : formatNativeReadDisplayContent(args, result, options)),
523
991
  isError,
524
992
  };
525
993
  }
526
994
 
527
995
  if (name === "shell") {
528
- const shellOutput = getShellOutput(result);
529
- const isError = result.status === "error" || (shellOutput.exitCode !== undefined && shellOutput.exitCode !== 0);
996
+ const shellOutput = getShellOutput(result, args);
997
+ const isError = result.status === "error" || shellOutput.timedOut || (shellOutput.exitCode !== undefined && shellOutput.exitCode !== 0);
530
998
  return {
531
999
  toolName: "bash",
532
- args,
1000
+ args: buildShellDisplayArgs(args),
533
1001
  result: textToolResult(result.status === "error" ? formatError(result.error) : limitText(shellOutput.text, options)),
534
1002
  isError,
535
1003
  };
536
1004
  }
537
1005
 
1006
+ if (name === "grep") {
1007
+ const isError = result.status === "error";
1008
+ const outputText = isError ? formatError(result.error) : getGrepBody(result, options);
1009
+ return {
1010
+ toolName: "grep",
1011
+ args: buildGrepDisplayArgs(args, options),
1012
+ result: textToolResult(outputText),
1013
+ isError,
1014
+ };
1015
+ }
1016
+
1017
+ if (name === "glob") {
1018
+ const isError = result.status === "error";
1019
+ return {
1020
+ toolName: "find",
1021
+ args: buildFindDisplayArgs(args, options),
1022
+ result: textToolResult(isError ? formatError(result.error) : getGlobBody(result, options)),
1023
+ isError,
1024
+ };
1025
+ }
1026
+
538
1027
  if (name === "ls") {
539
1028
  return {
540
1029
  toolName: "ls",
@@ -546,35 +1035,215 @@ export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptO
546
1035
 
547
1036
  if (name === "edit") {
548
1037
  const value = asRecord(result.value);
549
- return {
550
- toolName: "cursor_edit",
551
- args,
552
- result: textToolResult(formatEdit(args, result, options), {
553
- cursorToolName: "edit",
554
- path: typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined,
555
- linesAdded: getNumber(value, "linesAdded"),
556
- linesRemoved: getNumber(value, "linesRemoved"),
557
- diffString: getString(value, "diffString"),
558
- }),
559
- isError: result.status === "error",
1038
+ const rawDiff = getString(value, "diffString") ?? getString(value, "diff") ?? getString(value, "unifiedDiff");
1039
+ const normalizedDiff = formatDiffString(rawDiff, options);
1040
+ const nativeEditArgs = buildNativeEditDisplayArgs(rawName, args, options);
1041
+ const baseActivityArgs = buildCursorEditActivityDisplayArgs(args, options);
1042
+ const displayPath = typeof baseActivityArgs.path === "string" ? baseActivityArgs.path : undefined;
1043
+ const activityTitle = getCursorReplayDisplayLabel("cursor_edit");
1044
+ const activityArgs = buildCursorActivityDisplayArgs(baseActivityArgs, activityTitle, displayPath);
1045
+ const contentText = formatEdit(activityArgs, result, options);
1046
+ const details = {
1047
+ cursorToolName: "edit",
1048
+ path: displayPath,
1049
+ linesAdded: getNumber(value, "linesAdded"),
1050
+ linesRemoved: getNumber(value, "linesRemoved"),
1051
+ diffString: normalizedDiff,
1052
+ diff: normalizedDiff,
1053
+ firstChangedLine: getNumber(value, "firstChangedLine"),
560
1054
  };
1055
+ if (nativeEditArgs) {
1056
+ return {
1057
+ toolName: "edit",
1058
+ args: nativeEditArgs,
1059
+ result: textToolResult(contentText, details),
1060
+ isError: result.status === "error",
1061
+ };
1062
+ }
1063
+ return buildReplaySummaryDisplay(
1064
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1065
+ activityArgs,
1066
+ result,
1067
+ contentText.trimEnd(),
1068
+ {
1069
+ ...details,
1070
+ title: activityTitle,
1071
+ summary: result.status === "error" ? undefined : displayPath ?? "replayed",
1072
+ },
1073
+ );
561
1074
  }
562
1075
 
563
1076
  if (name === "write") {
564
1077
  const value = asRecord(result.value);
1078
+ const content = getCursorWriteArgContent(args);
1079
+ const displayArgs = buildWriteDisplayArgs(args, options);
1080
+ const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
1081
+ const contentText = formatWrite(args, result, options).trimEnd();
1082
+ const details = {
1083
+ cursorToolName: "write",
1084
+ path: displayPath,
1085
+ linesCreated: getNumber(value, "linesCreated"),
1086
+ fileSize: getNumber(value, "fileSize"),
1087
+ fileContentAfterWrite: getString(value, "fileContentAfterWrite"),
1088
+ expandedText: contentText,
1089
+ };
1090
+ if (content === undefined) {
1091
+ const activityTitle = getCursorReplayDisplayLabel("cursor_write");
1092
+ return buildReplaySummaryDisplay(
1093
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1094
+ buildCursorActivityDisplayArgs(displayArgs, activityTitle, displayPath ?? "file"),
1095
+ result,
1096
+ contentText,
1097
+ {
1098
+ ...details,
1099
+ title: activityTitle,
1100
+ summary: result.status === "error" ? undefined : displayPath ?? "wrote file",
1101
+ },
1102
+ );
1103
+ }
565
1104
  return {
566
- toolName: "cursor_write",
567
- args,
568
- result: textToolResult(formatWrite(args, result, options), {
569
- cursorToolName: "write",
570
- path: typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined,
571
- linesCreated: getNumber(value, "linesCreated"),
572
- fileSize: getNumber(value, "fileSize"),
573
- }),
1105
+ toolName: "write",
1106
+ args: displayArgs,
1107
+ result: textToolResult(contentText, details),
574
1108
  isError: result.status === "error",
575
1109
  };
576
1110
  }
577
1111
 
1112
+ if (name === "delete") {
1113
+ const value = asRecord(result.value);
1114
+ const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
1115
+ const activityTitle = getCursorReplayDisplayLabel("cursor_delete");
1116
+ const contentText = formatDelete(args, result, options).trimEnd();
1117
+ return buildReplaySummaryDisplay(
1118
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1119
+ buildCursorActivityDisplayArgs(displayPath ? { path: displayPath } : {}, activityTitle, displayPath ?? "file"),
1120
+ result,
1121
+ contentText,
1122
+ {
1123
+ cursorToolName: "delete",
1124
+ title: activityTitle,
1125
+ path: displayPath,
1126
+ summary: result.status === "error" ? undefined : displayPath ? `deleted ${displayPath}` : "deleted file",
1127
+ fileSize: getNumber(value, "fileSize"),
1128
+ },
1129
+ );
1130
+ }
1131
+
1132
+ if (name === "readLints") {
1133
+ const paths = getReadLintPaths(args, result, options);
1134
+ const diagnosticCount = getReadLintDiagnostics(result, options).length;
1135
+ const activityTitle = getCursorReplayDisplayLabel("cursor_read_lints");
1136
+ const diagnosticSummary = `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}${paths.length > 0 ? ` in ${paths.join(", ")}` : ""}`;
1137
+ const contentText = formatReadLints(args, result, options).trimEnd();
1138
+ return buildReplaySummaryDisplay(
1139
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1140
+ buildCursorActivityDisplayArgs({ paths, diagnosticCount }, activityTitle, diagnosticSummary),
1141
+ result,
1142
+ contentText,
1143
+ {
1144
+ cursorToolName: "readLints",
1145
+ title: activityTitle,
1146
+ summary: result.status === "error" ? undefined : diagnosticSummary,
1147
+ },
1148
+ );
1149
+ }
1150
+
1151
+ if (name === "updateTodos") {
1152
+ const todos = getTodoItems(args, result);
1153
+ const totalCount = getTodoTotalCount(args, result, todos);
1154
+ const activityTitle = getCursorReplayDisplayLabel("cursor_update_todos");
1155
+ const todoSummary = summarizeTodos(args, result);
1156
+ const contentText = formatTodos(args, result, options, "updateTodos").trimEnd();
1157
+ return buildReplaySummaryDisplay(
1158
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1159
+ buildCursorActivityDisplayArgs({ totalCount }, activityTitle, todoSummary),
1160
+ result,
1161
+ contentText,
1162
+ {
1163
+ cursorToolName: "updateTodos",
1164
+ title: activityTitle,
1165
+ summary: result.status === "error" ? undefined : todoSummary,
1166
+ },
1167
+ );
1168
+ }
1169
+
1170
+ if (name === "createPlan") {
1171
+ const todos = getTodoItems(args, result);
1172
+ const totalCount = getTodoTotalCount(args, result, todos);
1173
+ const activityTitle = getCursorReplayDisplayLabel("cursor_create_plan");
1174
+ const planSummary = summarizePlan(args, result);
1175
+ const contentText = formatPlan(args, result, options).trimEnd();
1176
+ return buildReplaySummaryDisplay(
1177
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1178
+ buildCursorActivityDisplayArgs({ totalCount }, activityTitle, planSummary),
1179
+ result,
1180
+ contentText,
1181
+ {
1182
+ cursorToolName: "createPlan",
1183
+ title: activityTitle,
1184
+ summary: result.status === "error" ? undefined : planSummary,
1185
+ },
1186
+ );
1187
+ }
1188
+
1189
+ if (name === "task") {
1190
+ const description = getTaskDescription(args, result);
1191
+ const contentText = formatTask(args, result, options).trimEnd();
1192
+ const taskText = collectTaskText(result);
1193
+ const activityTitle = getCursorReplayDisplayLabel("cursor_task");
1194
+ const taskSummary = summarizeTask(description, taskText);
1195
+ return buildReplaySummaryDisplay(
1196
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1197
+ buildCursorActivityDisplayArgs({ description: truncateArg(description) }, activityTitle, taskSummary),
1198
+ result,
1199
+ contentText,
1200
+ {
1201
+ cursorToolName: "task",
1202
+ title: activityTitle,
1203
+ summary: result.status === "error" ? undefined : taskSummary,
1204
+ },
1205
+ );
1206
+ }
1207
+
1208
+ if (name === "generateImage") {
1209
+ const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
1210
+ const contentText = formatGenerateImage(args, result, options).trimEnd();
1211
+ const imagePath = getGenerateImagePath(args, result);
1212
+ const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
1213
+ const activityTitle = getCursorReplayDisplayLabel("cursor_generate_image");
1214
+ return buildReplaySummaryDisplay(
1215
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1216
+ buildCursorActivityDisplayArgs({ prompt: truncateArg(prompt) }, activityTitle, imageDisplayPath ?? truncateArg(prompt)),
1217
+ result,
1218
+ contentText,
1219
+ {
1220
+ cursorToolName: "generateImage",
1221
+ title: activityTitle,
1222
+ summary: result.status === "error" ? undefined : imageDisplayPath ? `saved ${imageDisplayPath}` : "image generated",
1223
+ imagePath,
1224
+ imageDisplayPath,
1225
+ imageMimeType: inferImageMimeType(imagePath),
1226
+ },
1227
+ );
1228
+ }
1229
+
1230
+ if (name === "mcp") {
1231
+ const toolName = getString(args, "toolName") ?? "mcp";
1232
+ const activityTitle = getCursorReplayDisplayLabel("cursor_mcp");
1233
+ const contentText = formatMcp(args, result, options).trimEnd();
1234
+ return buildReplaySummaryDisplay(
1235
+ CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
1236
+ buildCursorActivityDisplayArgs({ toolName: truncateArg(toolName) }, activityTitle, truncateArg(toolName)),
1237
+ result,
1238
+ contentText,
1239
+ {
1240
+ cursorToolName: "mcp",
1241
+ title: activityTitle,
1242
+ summary: result.status === "error" ? undefined : firstNonEmptyLine(contentText) ?? "MCP result captured",
1243
+ },
1244
+ );
1245
+ }
1246
+
578
1247
  return buildGenericPiToolDisplay(name, args, result, options);
579
1248
  }
580
1249