pi-cursor-sdk 0.1.18 → 0.1.20

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 (49) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +59 -1
  3. package/docs/cursor-live-smoke-checklist.md +4 -1
  4. package/docs/cursor-model-ux-spec.md +7 -5
  5. package/docs/cursor-native-tool-replay.md +99 -3
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +10 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  11. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  12. package/scripts/probe-mcp-coldstart.mjs +244 -0
  13. package/scripts/validate-smoke-jsonl.mjs +27 -3
  14. package/src/context.ts +45 -32
  15. package/src/cursor-agent-message-web-tools.ts +172 -0
  16. package/src/cursor-agents-context.ts +176 -0
  17. package/src/cursor-incomplete-tool-visibility.ts +124 -0
  18. package/src/cursor-live-run-coordinator.ts +18 -7
  19. package/src/cursor-mcp-timeout-override.ts +66 -11
  20. package/src/cursor-model.ts +12 -0
  21. package/src/cursor-native-tool-display-registration.ts +1 -4
  22. package/src/cursor-native-tool-display-replay.ts +65 -6
  23. package/src/cursor-native-tool-display-tools.ts +20 -0
  24. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  25. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  26. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  27. package/src/cursor-provider-errors.ts +96 -0
  28. package/src/cursor-provider-live-run-drain.ts +181 -62
  29. package/src/cursor-provider-turn-coordinator.ts +220 -33
  30. package/src/cursor-provider.ts +302 -93
  31. package/src/cursor-question-tool.ts +1 -4
  32. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  33. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  34. package/src/cursor-sdk-event-debug-session.ts +163 -0
  35. package/src/cursor-sdk-event-debug.ts +602 -0
  36. package/src/cursor-sensitive-text.ts +27 -7
  37. package/src/cursor-session-agent.ts +279 -82
  38. package/src/cursor-session-send-policy.ts +43 -0
  39. package/src/cursor-setting-sources.ts +29 -0
  40. package/src/cursor-state.ts +1 -5
  41. package/src/cursor-tool-lifecycle.ts +85 -0
  42. package/src/cursor-tool-names.ts +39 -0
  43. package/src/cursor-tool-transcript.ts +4 -2
  44. package/src/cursor-tool-visibility.ts +63 -0
  45. package/src/cursor-transcript-tool-formatters.ts +228 -5
  46. package/src/cursor-transcript-tool-specs.ts +135 -24
  47. package/src/cursor-transcript-utils.ts +12 -0
  48. package/src/cursor-web-tool-activity.ts +84 -0
  49. package/src/index.ts +4 -1
@@ -10,6 +10,10 @@ export const CURSOR_REPLAY_LEGACY_TOOL_NAMES = [
10
10
  "cursor_create_plan",
11
11
  "cursor_generate_image",
12
12
  "cursor_mcp",
13
+ "cursor_sem_search",
14
+ "cursor_record_screen",
15
+ "cursor_web_search",
16
+ "cursor_web_fetch",
13
17
  ] as const;
14
18
 
15
19
  export type CursorReplayLegacyToolName = (typeof CURSOR_REPLAY_LEGACY_TOOL_NAMES)[number];
@@ -25,6 +29,10 @@ const CURSOR_REPLAY_SOURCE_TOOL_NAMES = {
25
29
  cursor_create_plan: "createPlan",
26
30
  cursor_generate_image: "generateImage",
27
31
  cursor_mcp: "MCP",
32
+ cursor_sem_search: "semSearch",
33
+ cursor_record_screen: "recordScreen",
34
+ cursor_web_search: "web search",
35
+ cursor_web_fetch: "web fetch",
28
36
  } as const satisfies Record<CursorReplayLegacyToolName, string>;
29
37
 
30
38
  const CURSOR_REPLAY_PROMPT_LABELS = {
@@ -37,8 +45,30 @@ const CURSOR_REPLAY_PROMPT_LABELS = {
37
45
  cursor_create_plan: "Cursor plan",
38
46
  cursor_generate_image: "Cursor image generation",
39
47
  cursor_mcp: "Cursor MCP",
48
+ cursor_sem_search: "Cursor semantic search",
49
+ cursor_record_screen: "Cursor screen recording",
50
+ cursor_web_search: "Cursor web search",
51
+ cursor_web_fetch: "Cursor web fetch",
40
52
  } as const satisfies Record<CursorReplayLegacyToolName, string>;
41
53
 
54
+ export const CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME = {
55
+ edit: "cursor_edit",
56
+ write: "cursor_write",
57
+ readLints: "cursor_read_lints",
58
+ delete: "cursor_delete",
59
+ updateTodos: "cursor_update_todos",
60
+ task: "cursor_task",
61
+ createPlan: "cursor_create_plan",
62
+ generateImage: "cursor_generate_image",
63
+ mcp: "cursor_mcp",
64
+ semSearch: "cursor_sem_search",
65
+ recordScreen: "cursor_record_screen",
66
+ webSearch: "cursor_web_search",
67
+ webFetch: "cursor_web_fetch",
68
+ } as const satisfies Record<string, CursorReplayLegacyToolName>;
69
+
70
+ export type CursorReplayActivityToolName = keyof typeof CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME;
71
+
42
72
  export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
43
73
  return CURSOR_REPLAY_LEGACY_TOOL_NAMES.some((legacyToolName) => legacyToolName === toolName);
44
74
  }
@@ -65,3 +95,12 @@ export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): str
65
95
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
66
96
  return CURSOR_REPLAY_PROMPT_LABELS[toolName];
67
97
  }
98
+
99
+ export function getCursorReplayActivityLabelKey(toolName: string): CursorReplayLegacyToolName | undefined {
100
+ return CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME[toolName as CursorReplayActivityToolName];
101
+ }
102
+
103
+ export function getCursorReplayActivityTitle(toolName: string): string | undefined {
104
+ const labelKey = getCursorReplayActivityLabelKey(toolName);
105
+ return labelKey ? getCursorReplayDisplayLabel(labelKey) : undefined;
106
+ }
@@ -14,6 +14,7 @@ import {
14
14
  formatCursorToolTranscriptFromSpec,
15
15
  type ToolDisplayContext,
16
16
  } from "./cursor-transcript-tool-specs.js";
17
+ import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
17
18
 
18
19
  export type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
19
20
 
@@ -29,10 +30,11 @@ export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
29
30
 
30
31
  function buildToolDisplayContext(toolCall: unknown, options: TranscriptOptions): ToolDisplayContext {
31
32
  const rawName = getToolName(toolCall);
33
+ const args = getToolArgs(toolCall);
32
34
  return {
33
35
  rawName,
34
- name: normalizeToolName(rawName),
35
- args: getToolArgs(toolCall),
36
+ name: resolveTranscriptToolName(rawName, args),
37
+ args,
36
38
  result: normalizeResult(getToolResult(toolCall)),
37
39
  options,
38
40
  };
@@ -0,0 +1,63 @@
1
+ import { getCursorReplayActivityTitle } from "./cursor-tool-names.js";
2
+ import { getToolArgs, getToolName, normalizeToolName } from "./cursor-transcript-utils.js";
3
+ import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
4
+
5
+ interface CursorToolVisibilityConfig {
6
+ incompleteTitle?: string;
7
+ lifecycleTitle?: string;
8
+ lifecycleEligible?: boolean;
9
+ fastLocalDiscovery?: boolean;
10
+ }
11
+
12
+ export interface CursorToolVisibility {
13
+ args: Record<string, unknown>;
14
+ displayName: string;
15
+ normalizedName: string;
16
+ normalizedKey: string;
17
+ activityTitle?: string;
18
+ incompleteTitle?: string;
19
+ lifecycleTitle?: string;
20
+ lifecycleEligible: boolean;
21
+ fastLocalDiscovery: boolean;
22
+ }
23
+
24
+ const CURSOR_TOOL_VISIBILITY_BY_NAME: Record<string, CursorToolVisibilityConfig> = {
25
+ read: { incompleteTitle: "Cursor read", fastLocalDiscovery: true },
26
+ grep: { incompleteTitle: "Cursor grep", fastLocalDiscovery: true },
27
+ glob: { incompleteTitle: "Cursor find", fastLocalDiscovery: true },
28
+ ls: { incompleteTitle: "Cursor ls", fastLocalDiscovery: true },
29
+ shell: { incompleteTitle: "Cursor shell", lifecycleTitle: "Cursor shell", lifecycleEligible: true },
30
+ task: { lifecycleEligible: true },
31
+ mcp: { lifecycleEligible: true },
32
+ generateimage: { lifecycleEligible: true },
33
+ recordscreen: { lifecycleEligible: true },
34
+ semsearch: { lifecycleEligible: true },
35
+ websearch: { lifecycleEligible: true },
36
+ webfetch: { lifecycleEligible: true },
37
+ createplan: { lifecycleEligible: true },
38
+ updatetodos: { lifecycleEligible: true },
39
+ };
40
+
41
+ export function classifyCursorToolVisibility(toolCall: unknown): CursorToolVisibility {
42
+ const args = getToolArgs(toolCall);
43
+ const displayName = resolveTranscriptToolName(getToolName(toolCall), args);
44
+ const normalizedName = normalizeToolName(displayName);
45
+ const normalizedKey = normalizedName.toLowerCase();
46
+ const config = CURSOR_TOOL_VISIBILITY_BY_NAME[normalizedKey];
47
+ const replayActivityTitle = getCursorReplayActivityTitle(normalizedName);
48
+ return {
49
+ args,
50
+ displayName,
51
+ normalizedName,
52
+ normalizedKey,
53
+ activityTitle: replayActivityTitle ?? config?.incompleteTitle ?? config?.lifecycleTitle,
54
+ incompleteTitle: replayActivityTitle ?? config?.incompleteTitle,
55
+ lifecycleTitle: replayActivityTitle ?? config?.lifecycleTitle,
56
+ lifecycleEligible: config?.lifecycleEligible ?? false,
57
+ fastLocalDiscovery: config?.fastLocalDiscovery ?? false,
58
+ };
59
+ }
60
+
61
+ export function isFastLocalDiscoveryTool(toolCall: unknown): boolean {
62
+ return classifyCursorToolVisibility(toolCall).fastLocalDiscovery;
63
+ }
@@ -1,4 +1,6 @@
1
1
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
2
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
+ import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
2
4
  import { getFirstStringByKeys } from "./cursor-record-utils.js";
3
5
  import {
4
6
  asRecord,
@@ -27,6 +29,21 @@ import {
27
29
  type TranscriptOptions,
28
30
  } from "./cursor-transcript-utils.js";
29
31
 
32
+ export function usesLocalReadPreview(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): boolean {
33
+ if (result.status === "error") return false;
34
+ const value = asRecord(result.value);
35
+ const resultContent = getString(value, "content");
36
+ if (resultContent && resultContent.length > 0) return false;
37
+ const rawPath = typeof args.path === "string" ? args.path : undefined;
38
+ if (!rawPath) return false;
39
+ const readOptions = {
40
+ ...options,
41
+ maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
42
+ maxLines: options.maxLines ?? DEFAULT_READ_TRANSCRIPT_LINES,
43
+ };
44
+ return readFilePreview(rawPath, readOptions) !== undefined;
45
+ }
46
+
30
47
  function getReadContent(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
31
48
  const rawPath = typeof args.path === "string" ? args.path : undefined;
32
49
  const readOptions = {
@@ -57,9 +74,17 @@ export function formatRead(args: Record<string, unknown>, result: NormalizedResu
57
74
  return joinSections(`read ${path}`, limitText(getReadContent(args, result, options), readOptions, totalLines));
58
75
  }
59
76
 
60
- export function buildReadDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
77
+ export function buildReadDisplayArgs(
78
+ args: Record<string, unknown>,
79
+ options: TranscriptOptions,
80
+ result?: NormalizedResult,
81
+ ): Record<string, unknown> {
61
82
  const rawPath = typeof args.path === "string" ? args.path : undefined;
62
- return rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
83
+ const displayArgs = rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
84
+ if (result && usesLocalReadPreview(args, result, options)) {
85
+ return { ...displayArgs, localReadPreview: true };
86
+ }
87
+ return displayArgs;
63
88
  }
64
89
 
65
90
  function buildPathDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
@@ -617,6 +642,153 @@ function describeNonTextMcpContent(entry: unknown): string {
617
642
  return `[${type} omitted]`;
618
643
  }
619
644
 
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
+ export function formatSemSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
656
+ const query = getString(args, "query") ?? "semantic search";
657
+ const header = `semSearch ${truncateArg(query)}`;
658
+ if (result.status === "error") return joinSections(header, formatError(result.error));
659
+
660
+ const value = asRecord(result.value);
661
+ const results = getString(value, "results");
662
+ const targetDirectories = getArray(args, "targetDirectories");
663
+ const explanation = getString(args, "explanation");
664
+ const lines: string[] = [];
665
+ if (explanation?.trim()) lines.push(`Explanation: ${explanation.trim()}`);
666
+ if (targetDirectories && targetDirectories.length > 0) {
667
+ const dirs = targetDirectories
668
+ .map((entry) => (typeof entry === "string" ? entry : stringifyUnknown(entry)))
669
+ .join(", ");
670
+ lines.push(`Scope: ${dirs}`);
671
+ }
672
+ if (results?.trim()) lines.push(results.trim());
673
+ const body = lines.length > 0 ? lines.join("\n\n") : stringifyUnknown(result.value);
674
+ return joinSections(header, limitText(body, options));
675
+ }
676
+
677
+ function formatRecordScreenMode(mode: string | undefined): string {
678
+ switch (mode) {
679
+ case "START_RECORDING":
680
+ return "start recording";
681
+ case "SAVE_RECORDING":
682
+ return "save recording";
683
+ case "DISCARD_RECORDING":
684
+ return "discard recording";
685
+ default:
686
+ return mode ?? "record screen";
687
+ }
688
+ }
689
+
690
+ function formatRecordingDurationMs(ms: number | undefined): string | undefined {
691
+ if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
692
+ if (ms < 1000) return `${Math.round(ms)}ms`;
693
+ const seconds = ms / 1000;
694
+ return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
695
+ }
696
+
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
+ export function formatRecordScreen(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
716
+ const mode = getString(args, "mode");
717
+ const header = `recordScreen ${formatRecordScreenMode(mode)}`;
718
+ if (result.status === "error") return joinSections(header, formatError(result.error));
719
+
720
+ const value = asRecord(result.value);
721
+ const path = getString(value, "path");
722
+ const displayPath = path ? formatDisplayPath(path, options.cwd) : undefined;
723
+ const duration = formatRecordingDurationMs(getNumber(value, "recordingDurationMs"));
724
+ const wasCancelled = getBoolean(value, "wasPriorRecordingCancelled");
725
+ const lines: string[] = [];
726
+ if (displayPath) lines.push(`Recording: ${displayPath}`);
727
+ if (duration) lines.push(`Duration: ${duration}`);
728
+ if (wasCancelled === true) lines.push("Prior recording cancelled.");
729
+ if (lines.length === 0) {
730
+ if (mode === "START_RECORDING") lines.push("Recording started.");
731
+ else if (mode === "DISCARD_RECORDING") lines.push("Recording discarded.");
732
+ else lines.push("Screen recording updated.");
733
+ }
734
+ return joinSections(header, lines.join("\n"));
735
+ }
736
+
737
+ export function getMcpResultPreview(result: NormalizedResult): string | undefined {
738
+ if (result.status === "error") return undefined;
739
+ const value = asRecord(result.value);
740
+ const content = getArray(value, "content") ?? [];
741
+ for (const entry of content) {
742
+ const text = getMcpContentText(entry);
743
+ if (text) {
744
+ const line = firstNonEmptyLine(text);
745
+ if (line) return truncateArg(scrubSensitiveText(line), 120);
746
+ }
747
+ const summary = describeNonTextMcpContent(entry);
748
+ if (summary) return summary;
749
+ }
750
+ return undefined;
751
+ }
752
+
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
+ function formatWebToolBody(
760
+ toolLabel: string,
761
+ args: Record<string, unknown>,
762
+ result: NormalizedResult,
763
+ options: TranscriptOptions,
764
+ summaryArg: string | undefined,
765
+ ): string {
766
+ if (result.status === "error") return joinSections(toolLabel, formatError(result.error));
767
+ const summary = summaryArg ? `${summaryArg}\n\n` : "";
768
+ const value = asRecord(result.value);
769
+ const isError = getBoolean(value, "isError");
770
+ const content = getArray(value, "content") ?? [];
771
+ const text = content
772
+ .map((entry) => getMcpContentText(entry))
773
+ .filter((entry): entry is string => Boolean(entry))
774
+ .join("\n");
775
+ const contentSummary = content.length > 0 ? content.map(describeNonTextMcpContent).join("\n") : stringifyUnknown(result.value);
776
+ const body = `${isError ? "[tool error]\n" : ""}${text || contentSummary}`;
777
+ return joinSections(toolLabel, limitText(`${summary}${body}`.trim(), options));
778
+ }
779
+
780
+ export function formatWebSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
781
+ const query = extractWebSearchQuery(args);
782
+ const header = query ? `web search ${query}` : "web search";
783
+ return formatWebToolBody(header, args, result, options, undefined);
784
+ }
785
+
786
+ export function formatWebFetch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
787
+ const target = extractWebFetchTarget(args);
788
+ const header = target ? `web fetch ${target}` : "web fetch";
789
+ return formatWebToolBody(header, args, result, options, undefined);
790
+ }
791
+
620
792
  export function formatMcp(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
621
793
  const toolName = typeof args.toolName === "string" ? args.toolName : "mcp";
622
794
  if (result.status === "error") return joinSections(toolName, formatError(result.error));
@@ -633,9 +805,60 @@ export function formatMcp(args: Record<string, unknown>, result: NormalizedResul
633
805
  return joinSections(toolName, limitText(body, options));
634
806
  }
635
807
 
808
+ const UNKNOWN_TOOL_FALLBACK_MAX_ARGS = 8;
809
+ const UNKNOWN_TOOL_FALLBACK_MAX_CHARS = 240;
810
+ const UNKNOWN_TOOL_FALLBACK_MAX_LINES = 6;
811
+
812
+ function summarizeUnknownToolArgValue(value: unknown): string {
813
+ if (typeof value === "string") return truncateArg(value);
814
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
815
+ if (Array.isArray(value)) {
816
+ const preview = value.slice(0, 3).map((entry) => summarizeUnknownToolArgValue(entry)).join(", ");
817
+ const omitted = value.length - Math.min(value.length, 3);
818
+ return omitted > 0 ? `[${preview}, +${omitted} more]` : `[${preview}]`;
819
+ }
820
+ return truncateArg(stringifyUnknown(value).replace(/\s+/g, " "));
821
+ }
822
+
823
+ function summarizeUnknownToolArgs(args: Record<string, unknown>): string {
824
+ const entries = Object.entries(args).slice(0, UNKNOWN_TOOL_FALLBACK_MAX_ARGS);
825
+ if (entries.length === 0) return "";
826
+ const parts = entries.map(([key, value]) => `${key}=${summarizeUnknownToolArgValue(value)}`);
827
+ const omitted = Object.keys(args).length - entries.length;
828
+ const body = parts.join(", ");
829
+ return omitted > 0 ? `${body} (+${omitted} more)` : body;
830
+ }
831
+
832
+ function summarizeUnknownToolResult(value: unknown, options: TranscriptOptions): string {
833
+ const text = stringifyUnknown(value).trim();
834
+ if (!text) return "";
835
+ return limitText(text.replace(/\s+/g, " "), {
836
+ ...options,
837
+ maxChars: options.maxChars ?? UNKNOWN_TOOL_FALLBACK_MAX_CHARS,
838
+ maxLines: options.maxLines ?? UNKNOWN_TOOL_FALLBACK_MAX_LINES,
839
+ });
840
+ }
841
+
842
+ function summarizeUnknownToolError(error: unknown, options: TranscriptOptions): string {
843
+ const text = formatError(error).trim();
844
+ if (!text) return "Error";
845
+ return limitText(text, {
846
+ ...options,
847
+ maxChars: options.maxChars ?? UNKNOWN_TOOL_FALLBACK_MAX_CHARS,
848
+ maxLines: options.maxLines ?? UNKNOWN_TOOL_FALLBACK_MAX_LINES,
849
+ });
850
+ }
851
+
636
852
  export function formatFallback(name: string, args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
637
853
  const header = name === "unknown" ? "Cursor tool" : name;
638
- if (result.status === "error") return joinSections(header, formatError(result.error));
639
- const argsText = Object.keys(args).length > 0 ? `${stringifyUnknown(args)}\n\n` : "";
640
- return joinSections(header, limitText(`${argsText}${stringifyUnknown(result.value)}`.trim(), options));
854
+ if (result.status === "error") {
855
+ const argsSummary = summarizeUnknownToolArgs(args);
856
+ const errorSummary = summarizeUnknownToolError(result.error, options);
857
+ const body = [argsSummary, errorSummary].filter(Boolean).join("\n\n");
858
+ return joinSections(header, body ? limitText(body, options) : undefined);
859
+ }
860
+ const argsSummary = summarizeUnknownToolArgs(args);
861
+ const resultSummary = summarizeUnknownToolResult(result.value, options);
862
+ const body = [argsSummary, resultSummary].filter(Boolean).join("\n\n");
863
+ return joinSections(header, body ? limitText(body, options) : undefined);
641
864
  }