pi-cursor-sdk 0.2.0 → 0.3.7

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 (177) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +55 -51
  3. package/dist/bundled-context-windows.js +39 -0
  4. package/dist/context-window-cache.js +92 -0
  5. package/dist/context.js +353 -0
  6. package/dist/cursor-active-tools.js +3 -0
  7. package/dist/cursor-agent-message-web-tools.js +165 -0
  8. package/dist/cursor-agents-context-registration.js +17 -0
  9. package/dist/cursor-agents-context.js +127 -0
  10. package/dist/cursor-api-key.js +34 -0
  11. package/dist/cursor-bridge-contract.js +27 -0
  12. package/dist/cursor-cloud-lifecycle.js +650 -0
  13. package/dist/cursor-cloud-local-state.js +460 -0
  14. package/dist/cursor-cloud-options.js +145 -0
  15. package/dist/cursor-cloud-reporting.js +222 -0
  16. package/dist/cursor-compact-tool-summary.js +75 -0
  17. package/dist/cursor-config.js +613 -0
  18. package/dist/cursor-context-tools.js +6 -0
  19. package/dist/cursor-display-only-trace.js +11 -0
  20. package/dist/cursor-display-text.js +18 -0
  21. package/dist/cursor-durable-fs.js +53 -0
  22. package/dist/cursor-edit-diff.js +13 -0
  23. package/dist/cursor-env-boolean.js +19 -0
  24. package/dist/cursor-fallback-models.generated.js +7760 -0
  25. package/dist/cursor-fallback-warning.js +15 -0
  26. package/dist/cursor-http1.js +45 -0
  27. package/dist/cursor-incomplete-tool-visibility.js +83 -0
  28. package/dist/cursor-live-run-accounting.js +55 -0
  29. package/dist/cursor-live-run-coordinator.js +409 -0
  30. package/dist/cursor-mcp-timeout-override.js +116 -0
  31. package/dist/cursor-model-lifecycle.js +32 -0
  32. package/dist/cursor-model.js +5 -0
  33. package/dist/cursor-native-replay-routing.js +35 -0
  34. package/dist/cursor-native-replay-trace.js +28 -0
  35. package/dist/cursor-native-tool-display-registration.js +102 -0
  36. package/dist/cursor-native-tool-display-replay.js +428 -0
  37. package/dist/cursor-native-tool-display-state.js +70 -0
  38. package/dist/cursor-native-tool-display-tools.js +123 -0
  39. package/dist/cursor-native-tool-names.js +11 -0
  40. package/dist/cursor-partial-content-emitter.js +130 -0
  41. package/dist/cursor-pi-context.js +30 -0
  42. package/dist/cursor-pi-tool-bridge-abort.js +114 -0
  43. package/dist/cursor-pi-tool-bridge-constants.js +6 -0
  44. package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
  45. package/dist/cursor-pi-tool-bridge-env.js +18 -0
  46. package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
  47. package/dist/cursor-pi-tool-bridge-run.js +355 -0
  48. package/dist/cursor-pi-tool-bridge-server.js +163 -0
  49. package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
  50. package/dist/cursor-pi-tool-bridge-types.js +1 -0
  51. package/dist/cursor-pi-tool-bridge.js +123 -0
  52. package/dist/cursor-provider-errors.js +311 -0
  53. package/dist/cursor-provider-lazy.js +39 -0
  54. package/dist/cursor-provider-live-run-drain.js +420 -0
  55. package/dist/cursor-provider-overflow.js +90 -0
  56. package/dist/cursor-provider-run-finalizer.js +176 -0
  57. package/dist/cursor-provider-run-outcome.js +76 -0
  58. package/dist/cursor-provider-turn-coordinator.js +293 -0
  59. package/dist/cursor-provider-turn-display-router.js +158 -0
  60. package/dist/cursor-provider-turn-emit.js +41 -0
  61. package/dist/cursor-provider-turn-finalize.js +154 -0
  62. package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
  63. package/dist/cursor-provider-turn-prepare.js +379 -0
  64. package/dist/cursor-provider-turn-runner.js +146 -0
  65. package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
  66. package/dist/cursor-provider-turn-send.js +138 -0
  67. package/dist/cursor-provider-turn-shell-output.js +117 -0
  68. package/dist/cursor-provider-turn-tool-ledger.js +99 -0
  69. package/dist/cursor-provider-turn-types.js +1 -0
  70. package/dist/cursor-provider.js +72 -0
  71. package/dist/cursor-question-tool.js +194 -0
  72. package/dist/cursor-record-utils.js +61 -0
  73. package/dist/cursor-replay-activity-builders.js +114 -0
  74. package/dist/cursor-replay-source-names.js +27 -0
  75. package/dist/cursor-replay-summary-args.js +105 -0
  76. package/dist/cursor-replay-tool-details.js +167 -0
  77. package/dist/cursor-ripgrep-path.js +27 -0
  78. package/dist/cursor-run-final-text.js +63 -0
  79. package/dist/cursor-runtime-state.js +380 -0
  80. package/dist/cursor-sdk-billed-usage.js +86 -0
  81. package/dist/cursor-sdk-event-debug-constants.js +33 -0
  82. package/dist/cursor-sdk-event-debug-session.js +107 -0
  83. package/dist/cursor-sdk-event-debug.js +461 -0
  84. package/dist/cursor-sdk-output-filter.js +1 -0
  85. package/dist/cursor-sdk-process-error-guard.js +221 -0
  86. package/dist/cursor-sdk-runtime.js +3 -0
  87. package/dist/cursor-sensitive-text.js +23 -0
  88. package/dist/cursor-session-agent-cleanup.js +311 -0
  89. package/dist/cursor-session-agent-lifecycle.js +32 -0
  90. package/dist/cursor-session-agent-lineage.js +103 -0
  91. package/dist/cursor-session-agent-resume.js +417 -0
  92. package/dist/cursor-session-agent.js +507 -0
  93. package/dist/cursor-session-compaction-prep.js +21 -0
  94. package/dist/cursor-session-scope.js +113 -0
  95. package/dist/cursor-session-send-policy.js +18 -0
  96. package/dist/cursor-session-store.js +105 -0
  97. package/dist/cursor-session-turn-queue.js +50 -0
  98. package/dist/cursor-setting-sources.js +15 -0
  99. package/dist/cursor-skill-tool.js +230 -0
  100. package/dist/cursor-state.js +519 -0
  101. package/dist/cursor-task-presentation.js +59 -0
  102. package/dist/cursor-tool-lifecycle.js +94 -0
  103. package/dist/cursor-tool-manifest.js +34 -0
  104. package/dist/cursor-tool-presentation-registry.js +305 -0
  105. package/dist/cursor-tool-result-display-readers.js +158 -0
  106. package/dist/cursor-tool-transcript.js +32 -0
  107. package/dist/cursor-tool-visibility.js +28 -0
  108. package/dist/cursor-transcript-tool-formatters.js +618 -0
  109. package/dist/cursor-transcript-tool-specs.js +284 -0
  110. package/dist/cursor-transcript-utils.js +170 -0
  111. package/dist/cursor-usage-accounting.js +166 -0
  112. package/dist/cursor-web-tool-activity.js +23 -0
  113. package/dist/cursor-web-tool-args.js +12 -0
  114. package/dist/index.js +82 -0
  115. package/dist/model-discovery.js +305 -0
  116. package/dist/model-list-cache.js +156 -0
  117. package/docs/cursor-dogfood-checklist.md +2 -1
  118. package/docs/cursor-live-smoke-checklist.md +13 -12
  119. package/docs/cursor-model-ux-spec.md +27 -6
  120. package/docs/cursor-native-tool-replay.md +13 -13
  121. package/docs/cursor-native-tool-visual-audit.md +6 -3
  122. package/docs/cursor-testing-lessons.md +14 -8
  123. package/docs/cursor-tool-surfaces.md +5 -5
  124. package/docs/platform-smoke.md +3 -3
  125. package/package.json +19 -7
  126. package/platform-smoke.config.mjs +1 -1
  127. package/scripts/build.mjs +126 -0
  128. package/scripts/cloud-runtime-smoke.mjs +3 -1
  129. package/scripts/debug-provider-events.mjs +3 -1
  130. package/scripts/debug-sdk-events.mjs +1 -1
  131. package/scripts/isolated-cursor-smoke.sh +5 -4
  132. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
  133. package/scripts/lib/cursor-visual-render.mjs +1 -1
  134. package/scripts/lib/ensure-built.mjs +16 -0
  135. package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
  136. package/scripts/local-resume-smoke.mjs +3 -1
  137. package/scripts/platform-smoke/card-detect.mjs +1 -1
  138. package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
  139. package/scripts/platform-smoke/render-ansi.mjs +1 -1
  140. package/scripts/platform-smoke/targets.mjs +2 -2
  141. package/scripts/prepare.mjs +102 -0
  142. package/scripts/steering-rpc-smoke.mjs +4 -2
  143. package/scripts/tmux-live-smoke.sh +11 -11
  144. package/scripts/visual-tui-smoke-self-test.mjs +4 -1
  145. package/scripts/visual-tui-smoke.mjs +51 -7
  146. package/src/bundled-context-windows.ts +4 -1
  147. package/src/context.ts +27 -4
  148. package/src/cursor-agents-context-registration.ts +2 -2
  149. package/src/cursor-agents-context.ts +24 -10
  150. package/src/cursor-api-key.ts +5 -4
  151. package/src/cursor-cloud-reporting.ts +23 -2
  152. package/src/cursor-context-tools.ts +3 -1
  153. package/src/cursor-fallback-models.generated.ts +592 -204
  154. package/src/cursor-live-run-coordinator.ts +11 -6
  155. package/src/cursor-native-tool-display-registration.ts +10 -11
  156. package/src/cursor-partial-content-emitter.ts +13 -0
  157. package/src/cursor-pi-context.ts +44 -0
  158. package/src/cursor-provider-errors.ts +1 -1
  159. package/src/cursor-provider-lazy.ts +5 -4
  160. package/src/cursor-provider-live-run-drain.ts +14 -9
  161. package/src/cursor-provider-run-finalizer.ts +1 -0
  162. package/src/cursor-provider-turn-coordinator.ts +15 -0
  163. package/src/cursor-provider-turn-emit.ts +6 -0
  164. package/src/cursor-provider-turn-finalize.ts +12 -0
  165. package/src/cursor-provider-turn-prepare.ts +10 -5
  166. package/src/cursor-provider-turn-types.ts +2 -0
  167. package/src/cursor-sdk-billed-usage.ts +107 -0
  168. package/src/cursor-sdk-process-error-guard.ts +6 -4
  169. package/src/cursor-session-agent-lifecycle.ts +8 -9
  170. package/src/cursor-session-agent-resume.ts +23 -0
  171. package/src/cursor-session-compaction-prep.ts +4 -0
  172. package/src/cursor-skill-tool.ts +1 -1
  173. package/src/cursor-tool-manifest.ts +1 -1
  174. package/src/cursor-usage-accounting.ts +70 -9
  175. package/src/index.ts +1 -1
  176. package/tsconfig.build.json +8 -0
  177. package/tsconfig.json +16 -0
@@ -0,0 +1,428 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { getLanguageFromPath, highlightCode, keyHint } from "@earendil-works/pi-coding-agent";
4
+ import { Image, Text } from "@earendil-works/pi-tui";
5
+ import { Type } from "typebox";
6
+ import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
7
+ import { inferImageMimeType } from "./cursor-tool-result-display-readers.js";
8
+ import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
9
+ import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayCallSummary, shouldShowCursorReplayCollapsedExpandHint, } from "./cursor-tool-presentation-registry.js";
10
+ import { CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE, isCursorReplayGenerateImageDetails, isCursorReplayActivityDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
11
+ export { isCursorReplayNativeEditDetails, isCursorReplayGenerateImageDetails, isCursorReplayActivityDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
12
+ export const CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES = 8;
13
+ export const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
14
+ export const CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS = 240;
15
+ const CURSOR_REPLAY_HIGHLIGHT_MAX_CHARS = 12000;
16
+ export const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
17
+ function readImageFileForReplay(path) {
18
+ if (!path)
19
+ return undefined;
20
+ try {
21
+ const stat = statSync(path);
22
+ if (!stat.isFile() || stat.size <= 0 || stat.size > 25 * 1024 * 1024)
23
+ return undefined;
24
+ return readFileSync(path).toString("base64");
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ function buildImageReplayComponent(text, imageData, mimeType, filename, theme) {
31
+ const textComponent = new Text(text, 0, 0);
32
+ const imageComponent = new Image(imageData, mimeType, { fallbackColor: (value) => theme.fg("muted", value) }, { filename, maxWidthCells: 40, maxHeightCells: 16 });
33
+ return {
34
+ render(width) {
35
+ return [...textComponent.render(width), ...imageComponent.render(width)];
36
+ },
37
+ invalidate() {
38
+ textComponent.invalidate();
39
+ imageComponent.invalidate();
40
+ },
41
+ };
42
+ }
43
+ export function getCursorReplayPath(args, details) {
44
+ const argPath = args?.path;
45
+ return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
46
+ }
47
+ function parseUnifiedDiffHunkHeader(line) {
48
+ const match = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/.exec(line);
49
+ if (!match)
50
+ return undefined;
51
+ return { oldLine: Number(match[1]), newLine: Number(match[2]) };
52
+ }
53
+ function replaceCursorReplayTabs(text) {
54
+ return text.replace(/\t/g, " ");
55
+ }
56
+ function truncateCursorReplayLine(text, maxChars = CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS) {
57
+ return text.length > maxChars ? `${text.slice(0, Math.max(maxChars - 1, 0))}…` : text;
58
+ }
59
+ function sliceCursorReplayPreview(text, maxLines, maxChars = CURSOR_REPLAY_PREVIEW_MAX_CHARS) {
60
+ const lines = text.split("\n");
61
+ const visible = [];
62
+ let usedChars = 0;
63
+ let omittedChars = 0;
64
+ for (const line of lines) {
65
+ if (visible.length >= maxLines) {
66
+ omittedChars += line.length + 1;
67
+ continue;
68
+ }
69
+ const normalizedLine = replaceCursorReplayTabs(line);
70
+ const lineBudget = Math.max(Math.min(CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS, maxChars - usedChars), 0);
71
+ if (lineBudget <= 0) {
72
+ omittedChars += normalizedLine.length + 1;
73
+ continue;
74
+ }
75
+ const truncatedLine = truncateCursorReplayLine(normalizedLine, lineBudget);
76
+ visible.push(truncatedLine);
77
+ usedChars += truncatedLine.length + 1;
78
+ omittedChars += Math.max(normalizedLine.length - truncatedLine.length, 0);
79
+ }
80
+ return {
81
+ text: visible.join("\n"),
82
+ omittedLines: Math.max(lines.length - visible.length, 0),
83
+ omittedChars,
84
+ };
85
+ }
86
+ function formatCursorReplayOmission(slice) {
87
+ const parts = [];
88
+ if (slice.omittedLines > 0)
89
+ parts.push(`${slice.omittedLines} more lines`);
90
+ if (slice.omittedChars > 0)
91
+ parts.push(`${slice.omittedChars} more chars`);
92
+ return parts.length > 0 ? `... (${parts.join(", ")} truncated)` : undefined;
93
+ }
94
+ function formatCursorReplayDiffLine(prefix, lineNumber, content, theme) {
95
+ const rendered = `${prefix}${lineNumber} ${truncateCursorReplayLine(replaceCursorReplayTabs(content))}`;
96
+ if (prefix === "+")
97
+ return theme.fg("toolDiffAdded", rendered);
98
+ if (prefix === "-")
99
+ return theme.fg("toolDiffRemoved", rendered);
100
+ return theme.fg("toolDiffContext", rendered);
101
+ }
102
+ export function formatCursorReplayDiff(diff, theme, maxLines) {
103
+ const lines = diff.split("\n");
104
+ const oldFileIsNull = lines.some((line) => line === "--- /dev/null");
105
+ const newFileIsNull = lines.some((line) => line === "+++ /dev/null");
106
+ const rendered = [];
107
+ let oldLine = 1;
108
+ let newLine = 1;
109
+ for (const line of lines) {
110
+ if (!line || line.startsWith("--- ") || line.startsWith("+++ "))
111
+ continue;
112
+ const hunk = parseUnifiedDiffHunkHeader(line);
113
+ if (hunk) {
114
+ oldLine = hunk.oldLine;
115
+ newLine = hunk.newLine;
116
+ continue;
117
+ }
118
+ if (line.startsWith("+")) {
119
+ if (newFileIsNull)
120
+ continue;
121
+ rendered.push(formatCursorReplayDiffLine("+", newLine, line.slice(1), theme));
122
+ newLine += 1;
123
+ }
124
+ else if (line.startsWith("-")) {
125
+ if (oldFileIsNull && line === "-")
126
+ continue;
127
+ rendered.push(formatCursorReplayDiffLine("-", oldLine, line.slice(1), theme));
128
+ oldLine += 1;
129
+ }
130
+ else if (line.startsWith(" ")) {
131
+ rendered.push(formatCursorReplayDiffLine(" ", newLine, line.slice(1), theme));
132
+ oldLine += 1;
133
+ newLine += 1;
134
+ }
135
+ else {
136
+ rendered.push(theme.fg("toolDiffContext", replaceCursorReplayTabs(line)));
137
+ }
138
+ }
139
+ const visible = rendered.slice(0, maxLines);
140
+ if (rendered.length > maxLines)
141
+ visible.push(theme.fg("muted", `... (${rendered.length - maxLines} more diff lines hidden)`));
142
+ return visible.join("\n");
143
+ }
144
+ function stripCursorReplayHeader(text) {
145
+ const lines = text.trimEnd().split("\n");
146
+ return lines.length > 2 && lines[1]?.trim() === "" ? lines.slice(2).join("\n") : lines.join("\n");
147
+ }
148
+ function formatMutedBlock(text, theme) {
149
+ return text.split("\n").map((line) => theme.fg("muted", line)).join("\n");
150
+ }
151
+ function hasUnifiedDiffHunk(text) {
152
+ return text.split("\n").some((line) => Boolean(parseUnifiedDiffHunkHeader(line)));
153
+ }
154
+ /** First unified-diff marker (`---`/`+++`/`@@`) so collapsed previews budget diff lines, not transcript preamble. */
155
+ function extractUnifiedDiffSection(text) {
156
+ const lines = text.split("\n");
157
+ const markerIndex = lines.findIndex((line) => line.startsWith("--- ") || line.startsWith("+++ "));
158
+ const hunkIndex = lines.findIndex((line) => Boolean(parseUnifiedDiffHunkHeader(line)));
159
+ const start = markerIndex >= 0 ? markerIndex : hunkIndex;
160
+ if (start < 0)
161
+ return undefined;
162
+ return lines.slice(start).join("\n");
163
+ }
164
+ function formatCursorReplayActivityDiffPreview(text, theme, maxLines, stripHeader) {
165
+ const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
166
+ const diffSection = body ? extractUnifiedDiffSection(body) : undefined;
167
+ if (!diffSection || !hasUnifiedDiffHunk(diffSection))
168
+ return undefined;
169
+ // Fallback for unstructured activity details that carry a unified diff only in expanded text.
170
+ // All actual diff coloring lives in the single `formatCursorReplayDiff` renderer.
171
+ return formatCursorReplayDiff(diffSection, theme, maxLines);
172
+ }
173
+ function formatCursorReplayActivityEditPreview(details, text, theme, maxLines, stripHeader) {
174
+ const structuredDiff = details.diffString ?? details.diff;
175
+ if (structuredDiff) {
176
+ return formatCursorReplayDiff(structuredDiff, theme, maxLines);
177
+ }
178
+ const diffPreview = formatCursorReplayActivityDiffPreview(text, theme, maxLines, stripHeader);
179
+ if (diffPreview)
180
+ return diffPreview;
181
+ return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
182
+ }
183
+ function formatCursorReplayActivityWritePreview(details, text, theme, maxLines, stripHeader) {
184
+ const structuredDiff = details.diffString ?? details.diff;
185
+ if (structuredDiff) {
186
+ return formatCursorReplayDiff(structuredDiff, theme, maxLines);
187
+ }
188
+ if (details.fileContentAfterWrite) {
189
+ return formatCursorReplayFilePreview(details.fileContentAfterWrite, details.path, theme, maxLines, false);
190
+ }
191
+ const diffPreview = formatCursorReplayActivityDiffPreview(text, theme, maxLines, stripHeader);
192
+ if (diffPreview)
193
+ return diffPreview;
194
+ return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
195
+ }
196
+ function formatCursorReplayActivityPreview(details, text, theme, maxLines, stripHeader) {
197
+ if (details.sourceToolName === "edit") {
198
+ return formatCursorReplayActivityEditPreview(details, text, theme, maxLines, stripHeader);
199
+ }
200
+ if (details.sourceToolName === "write") {
201
+ return formatCursorReplayActivityWritePreview(details, text, theme, maxLines, stripHeader);
202
+ }
203
+ return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
204
+ }
205
+ export function formatCursorReplayPreview(text, theme, maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, stripHeader = true) {
206
+ const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
207
+ if (!body)
208
+ return undefined;
209
+ const slice = sliceCursorReplayPreview(body, maxLines);
210
+ const omission = formatCursorReplayOmission(slice);
211
+ const preview = omission ? `${slice.text}\n${omission}` : slice.text;
212
+ return formatMutedBlock(preview, theme);
213
+ }
214
+ function safeHighlightCursorReplayCode(text, path) {
215
+ const lang = path ? getLanguageFromPath(path) : undefined;
216
+ if (!lang)
217
+ return undefined;
218
+ try {
219
+ return highlightCode(replaceCursorReplayTabs(text), lang);
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
224
+ }
225
+ export function formatCursorReplayFilePreview(text, path, theme, maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, stripHeader = true) {
226
+ const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
227
+ if (!body)
228
+ return undefined;
229
+ const slice = sliceCursorReplayPreview(body, maxLines);
230
+ const highlightedLines = slice.text.length <= CURSOR_REPLAY_HIGHLIGHT_MAX_CHARS ? safeHighlightCursorReplayCode(slice.text, path) : undefined;
231
+ const renderedLines = highlightedLines ?? slice.text.split("\n").map((line) => theme.fg("toolOutput", line));
232
+ const omission = formatCursorReplayOmission(slice);
233
+ if (omission)
234
+ renderedLines.push(theme.fg("muted", omission));
235
+ return renderedLines.join("\n");
236
+ }
237
+ function getCursorReplayCardTitle(toolName, args) {
238
+ if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && typeof args?.activityTitle === "string" && args.activityTitle.trim()) {
239
+ return args.activityTitle.trim();
240
+ }
241
+ return "Cursor activity";
242
+ }
243
+ export function renderCursorReplayCall(toolName, args, theme, isPartial) {
244
+ if (!isPartial)
245
+ return new Text("", 0, 0);
246
+ let text = theme.fg("toolTitle", theme.bold(`${getCursorReplayCardTitle(toolName, args)} `));
247
+ const summary = getCursorReplayCallSummary(toolName, args);
248
+ if (summary)
249
+ text += theme.fg("accent", summary);
250
+ return new Text(text.trimEnd(), 0, 0);
251
+ }
252
+ function countDisplayLines(text) {
253
+ const withoutFinalNewline = text.endsWith("\n") ? text.slice(0, -1) : text;
254
+ return withoutFinalNewline ? withoutFinalNewline.split("\n").length : 0;
255
+ }
256
+ export function renderNativeLookingCursorFileMutationCall(toolName, args, theme, isPartial) {
257
+ if (!isPartial)
258
+ return new Text("", 0, 0);
259
+ let text = theme.fg("toolTitle", theme.bold(`${toolName} `));
260
+ const path = typeof args?.path === "string" && args.path.trim() ? args.path : "unknown";
261
+ text += theme.fg("accent", path);
262
+ if (toolName === "write" && typeof args?.content === "string" && args.content.length > 0) {
263
+ const lineCount = countDisplayLines(args.content);
264
+ text += theme.fg("dim", ` (${pluralize(lineCount, "line")})`);
265
+ }
266
+ return new Text(text.trimEnd(), 0, 0);
267
+ }
268
+ function pluralize(count, noun) {
269
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
270
+ }
271
+ function getCursorEditDiff(details) {
272
+ return resolveCursorEditDiff(details);
273
+ }
274
+ function hasCursorEditChanges(details) {
275
+ return Boolean(getCursorEditDiff(details)) || Boolean(details.linesAdded) || Boolean(details.linesRemoved);
276
+ }
277
+ function classifyCursorEditOperation(details) {
278
+ if (!hasCursorEditChanges(details))
279
+ return "unchanged";
280
+ const diff = getCursorEditDiff(details);
281
+ if (diff?.startsWith("--- /dev/null"))
282
+ return "created";
283
+ if (diff?.includes("\n+++ /dev/null"))
284
+ return "deleted";
285
+ return "updated";
286
+ }
287
+ function formatCursorEditSummary(details) {
288
+ const operation = classifyCursorEditOperation(details);
289
+ if (operation === "unchanged")
290
+ return "no changes needed";
291
+ if (operation === "created" && details.linesAdded !== undefined)
292
+ return `created ${pluralize(details.linesAdded, "line")}`;
293
+ if (operation === "deleted" && details.linesRemoved !== undefined)
294
+ return `deleted ${pluralize(details.linesRemoved, "line")}`;
295
+ const parts = [
296
+ details.linesAdded ? `added ${pluralize(details.linesAdded, "line")}` : undefined,
297
+ details.linesRemoved ? `removed ${pluralize(details.linesRemoved, "line")}` : undefined,
298
+ ].filter((part) => Boolean(part));
299
+ return parts.length > 0 ? parts.join(", ") : "updated file";
300
+ }
301
+ function firstContentText(result) {
302
+ const content = result.content[0];
303
+ return content?.type === "text" ? content.text : "";
304
+ }
305
+ function hasCursorReplayDisplayTitle(details) {
306
+ if (!details)
307
+ return false;
308
+ return isCursorReplayActivityDetails(details) || isCursorReplayGenerateImageDetails(details);
309
+ }
310
+ function formatCursorReplayExpandHint() {
311
+ try {
312
+ return keyHint("app.tools.expand", "to expand");
313
+ }
314
+ catch {
315
+ return "Ctrl+O to expand";
316
+ }
317
+ }
318
+ function renderExpandableCursorReplayResult(title, details, result, options, theme, context, isError) {
319
+ const text = firstContentText(result);
320
+ const summary = details.summary ?? text.split("\n").find((line) => line.trim()) ?? "completed";
321
+ const expandedText = details.expandedText ?? (text.includes("\n") ? text : undefined);
322
+ const showExpandHint = expandedText && !options.expanded && shouldShowCursorReplayCollapsedExpandHint(details.sourceToolName);
323
+ const expandHint = showExpandHint ? theme.fg("dim", ` (${formatCursorReplayExpandHint()})`) : "";
324
+ let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg(isError ? "error" : "success", summary)}${expandHint}`;
325
+ if (expandedText && (options.expanded || !details.collapseDetailsByDefault)) {
326
+ const preview = formatCursorReplayActivityPreview(details, expandedText, theme, options.expanded ? Number.POSITIVE_INFINITY : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, !options.expanded);
327
+ if (preview)
328
+ rendered += `\n${preview}`;
329
+ }
330
+ if (details.imagePath && !isError && context.showImages) {
331
+ const imageData = readImageFileForReplay(details.imagePath);
332
+ const mimeType = details.imageMimeType ?? inferImageMimeType(details.imagePath);
333
+ if (imageData && mimeType)
334
+ return buildImageReplayComponent(rendered, imageData, mimeType, basename(details.imagePath ?? "generated-image"), theme);
335
+ }
336
+ return new Text(rendered, 0, 0);
337
+ }
338
+ function renderCursorReplayEditResult(details, options, theme) {
339
+ const summary = formatCursorEditSummary(details);
340
+ let rendered = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
341
+ const diff = getCursorEditDiff(details);
342
+ if (diff)
343
+ rendered += `\n${formatCursorReplayDiff(diff, theme, options.expanded ? 40 : CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES)}`;
344
+ return new Text(rendered, 0, 0);
345
+ }
346
+ function renderCursorReplayWriteResult(details, result, theme) {
347
+ const text = firstContentText(result);
348
+ const parts = [
349
+ details.linesCreated !== undefined ? `${details.linesCreated} line${details.linesCreated === 1 ? "" : "s"}` : undefined,
350
+ details.fileSize !== undefined ? `${details.fileSize} bytes` : undefined,
351
+ ].filter(Boolean);
352
+ const summary = parts.length > 0 ? parts.join(", ") : "written";
353
+ let rendered = `${theme.fg("toolTitle", theme.bold("write"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
354
+ const previewSource = details.fileContentAfterWrite ?? details.expandedText ?? text;
355
+ const preview = formatCursorReplayFilePreview(previewSource, getCursorReplayPath(undefined, details), theme, CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, details.fileContentAfterWrite === undefined);
356
+ if (preview)
357
+ rendered += `\n${preview}`;
358
+ return new Text(rendered, 0, 0);
359
+ }
360
+ function renderCursorGenerateImageResult(details, result, options, theme, context, isError) {
361
+ const title = CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE;
362
+ return renderExpandableCursorReplayResult(title, details, result, options, theme, context, isError);
363
+ }
364
+ function renderCursorReplayDetails(details, result, options, theme, context, isError, text) {
365
+ switch (details.variant) {
366
+ case "nativeEdit":
367
+ return renderCursorReplayEditResult(details, options, theme);
368
+ case "nativeWrite":
369
+ return renderCursorReplayWriteResult(details, result, theme);
370
+ case "generateImage":
371
+ return renderCursorGenerateImageResult(details, result, options, theme, context, isError);
372
+ case "activity":
373
+ return renderExpandableCursorReplayResult(details.title, details, result, options, theme, context, isError);
374
+ case "genericFallback":
375
+ break;
376
+ default: {
377
+ const _exhaustive = details;
378
+ return _exhaustive;
379
+ }
380
+ }
381
+ return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
382
+ }
383
+ export function renderCursorReplayResult(result, options, theme, context, isError) {
384
+ if (options.isPartial)
385
+ return new Text(theme.fg("warning", "Replaying Cursor tool result..."), 0, 0);
386
+ const details = parseCursorReplayToolDetails(result.details);
387
+ const text = firstContentText(result);
388
+ if (isError && !hasCursorReplayDisplayTitle(details)) {
389
+ return new Text(theme.fg("error", text.split("\n")[0] || "Cursor replay failed"), 0, 0);
390
+ }
391
+ if (!details)
392
+ return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
393
+ return renderCursorReplayDetails(details, result, options, theme, context, isError, text);
394
+ }
395
+ export function renderNativeLookingCursorReadReplayResult(result, options, theme, context, renderBase) {
396
+ const base = renderBase?.() ?? new Text("", 0, 0);
397
+ const readArgs = context.args;
398
+ const replayDetails = result.details;
399
+ const usesLocalPreview = readArgs?.localReadPreview === true ||
400
+ replayDetails?.localReadPreview === true ||
401
+ isLocalReadPreviewContent(firstContentText(result));
402
+ if (usesLocalPreview && !options.expanded && !context.isError) {
403
+ const noticeText = `\n${theme.fg("warning", LOCAL_READ_PREVIEW_NOTICE)}`;
404
+ if (base instanceof Text) {
405
+ base.setText(noticeText);
406
+ return base;
407
+ }
408
+ return new Text(noticeText, 0, 0);
409
+ }
410
+ return base;
411
+ }
412
+ export function createCursorReplayOnlyToolDefinition(toolName) {
413
+ return {
414
+ name: toolName,
415
+ label: "Cursor activity",
416
+ description: "Display recorded Cursor SDK tool activity. This tool only returns recorded Cursor results and never executes work directly.",
417
+ parameters: cursorReplayToolSchema,
418
+ async execute() {
419
+ throw new Error("No recorded Cursor activity result was available. This replay-only tool does not execute work directly.");
420
+ },
421
+ renderCall(args, theme, context) {
422
+ return renderCursorReplayCall(toolName, args, theme, context.isPartial);
423
+ },
424
+ renderResult(result, options, theme, context) {
425
+ return renderCursorReplayResult(result, options, theme, context, context.isError);
426
+ },
427
+ };
428
+ }
@@ -0,0 +1,70 @@
1
+ import { parseOptionalEnvBoolean } from "./cursor-env-boolean.js";
2
+ export const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
3
+ export const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
4
+ export const registeredNativeToolNames = new Set();
5
+ export const skippedNativeToolNames = new Set();
6
+ export const nativeToolResults = new Map();
7
+ let nativeToolDisplayRuntimeRequested = false;
8
+ export function readBooleanEnv(name, env = process.env) {
9
+ return parseOptionalEnvBoolean(env[name]);
10
+ }
11
+ export function isCursorNativeToolDisplayRequested(mode) {
12
+ const override = readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV);
13
+ if (override !== undefined)
14
+ return override;
15
+ if (mode)
16
+ return mode === "tui" || mode === "json" || mode === "rpc";
17
+ return process.stdout.isTTY === true;
18
+ }
19
+ export function isCursorNativeToolRegistrationRequested(mode) {
20
+ return mode !== "print" && readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested(mode);
21
+ }
22
+ export function setCursorNativeToolDisplayRuntimeRequested(requested) {
23
+ nativeToolDisplayRuntimeRequested = requested;
24
+ }
25
+ export function isCursorNativeToolDisplayEnabled() {
26
+ return registeredNativeToolNames.size > 0;
27
+ }
28
+ export function isCursorNativeToolDisplayRuntimeEnabled() {
29
+ return nativeToolDisplayRuntimeRequested && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== false && registeredNativeToolNames.size > 0;
30
+ }
31
+ export function canRenderCursorToolNatively(toolName) {
32
+ return registeredNativeToolNames.has(toolName);
33
+ }
34
+ export function isRegisteredCursorNativeToolName(toolName) {
35
+ return registeredNativeToolNames.has(toolName);
36
+ }
37
+ export function recordCursorNativeToolDisplay(item) {
38
+ if (!canRenderCursorToolNatively(item.toolName))
39
+ return false;
40
+ nativeToolResults.set(item.id, item);
41
+ return true;
42
+ }
43
+ export function deleteCursorNativeToolDisplay(id) {
44
+ nativeToolResults.delete(id);
45
+ }
46
+ export function consumeCursorNativeToolDisplay(id) {
47
+ const item = nativeToolResults.get(id);
48
+ if (item)
49
+ nativeToolResults.delete(id);
50
+ return item;
51
+ }
52
+ export function isCursorReplayToolCallId(toolCallId) {
53
+ return toolCallId.startsWith("cursor-replay-");
54
+ }
55
+ export function isCursorFileMutationToolName(toolName) {
56
+ return toolName === "edit" || toolName === "write";
57
+ }
58
+ export const __testUtils = {
59
+ nativeToolResultCount: () => nativeToolResults.size,
60
+ registerNativeToolNameForTests(toolName) {
61
+ nativeToolDisplayRuntimeRequested = true;
62
+ registeredNativeToolNames.add(toolName);
63
+ },
64
+ reset() {
65
+ nativeToolDisplayRuntimeRequested = false;
66
+ registeredNativeToolNames.clear();
67
+ skippedNativeToolNames.clear();
68
+ nativeToolResults.clear();
69
+ },
70
+ };
@@ -0,0 +1,123 @@
1
+ import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { getCursorSessionCwd } from "./cursor-session-scope.js";
4
+ import { CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES, CURSOR_REPLAY_TOOL_NAMES, } from "./cursor-native-tool-names.js";
5
+ import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
6
+ import { createCursorReplayOnlyToolDefinition, isCursorReplayNativeEditDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, renderCursorReplayResult, renderNativeLookingCursorFileMutationCall, renderNativeLookingCursorReadReplayResult, } from "./cursor-native-tool-display-replay.js";
7
+ import { consumeCursorNativeToolDisplay, isCursorReplayToolCallId, } from "./cursor-native-tool-display-state.js";
8
+ function emptyText() {
9
+ return new Text("", 0, 0);
10
+ }
11
+ function renderReadReplayCall(args, theme, context, renderBase) {
12
+ const rendered = renderBase();
13
+ if (args.localReadPreview !== true || context.expanded)
14
+ return rendered;
15
+ const baseText = rendered.render(120).join("\n").trimEnd();
16
+ const labeled = `${baseText}${theme.fg("muted", " · local file preview")}`;
17
+ if (rendered instanceof Text) {
18
+ rendered.setText(labeled);
19
+ return rendered;
20
+ }
21
+ return new Text(labeled, 0, 0);
22
+ }
23
+ function renderReadReplayResult(result, options, theme, context, renderBase) {
24
+ return renderNativeLookingCursorReadReplayResult(result, options, theme, context, renderBase);
25
+ }
26
+ function renderEditReplayResult(result, options, theme, context, renderBase) {
27
+ const details = parseCursorReplayToolDetails(result.details);
28
+ return details && isCursorReplayNativeEditDetails(details)
29
+ ? renderCursorReplayResult(result, options, theme, context, context.isError)
30
+ : renderBase();
31
+ }
32
+ function renderWriteReplayResult(result, options, theme, context, renderBase) {
33
+ const details = parseCursorReplayToolDetails(result.details);
34
+ return details && isCursorReplayNativeWriteDetails(details)
35
+ ? renderCursorReplayResult(result, options, theme, context, context.isError)
36
+ : renderBase();
37
+ }
38
+ const NATIVE_CURSOR_TOOL_STRATEGIES = {
39
+ read: {
40
+ createDefinition: (cwd) => createReadToolDefinition(cwd),
41
+ renderReplayCall: renderReadReplayCall,
42
+ renderReplayResult: renderReadReplayResult,
43
+ },
44
+ bash: { createDefinition: (cwd) => createBashToolDefinition(cwd) },
45
+ edit: {
46
+ createDefinition: (cwd) => createEditToolDefinition(cwd),
47
+ missingReplayPolicy: "block-file-mutation",
48
+ renderReplayCall: (args, theme, context) => renderNativeLookingCursorFileMutationCall("edit", args, theme, context.isPartial),
49
+ renderReplayResult: renderEditReplayResult,
50
+ },
51
+ write: {
52
+ createDefinition: (cwd) => createWriteToolDefinition(cwd),
53
+ missingReplayPolicy: "block-file-mutation",
54
+ renderReplayCall: (args, theme, context) => renderNativeLookingCursorFileMutationCall("write", args, theme, context.isPartial),
55
+ renderReplayResult: renderWriteReplayResult,
56
+ },
57
+ grep: { createDefinition: (cwd) => createGrepToolDefinition(cwd) },
58
+ find: { createDefinition: (cwd) => createFindToolDefinition(cwd) },
59
+ ls: { createDefinition: (cwd) => createLsToolDefinition(cwd) },
60
+ };
61
+ function getNativeReplayStrategy(toolName) {
62
+ return Object.hasOwn(NATIVE_CURSOR_TOOL_STRATEGIES, toolName)
63
+ ? NATIVE_CURSOR_TOOL_STRATEGIES[toolName]
64
+ : undefined;
65
+ }
66
+ export function wrapNativeCursorTool(definition, getCurrentDefinition) {
67
+ const strategy = getNativeReplayStrategy(definition.name);
68
+ return {
69
+ ...definition,
70
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
71
+ const cursorDisplay = consumeCursorNativeToolDisplay(toolCallId);
72
+ if (cursorDisplay) {
73
+ if (cursorDisplay.isError) {
74
+ const text = cursorDisplay.result.content
75
+ .map((entry) => (entry.type === "text" ? entry.text : undefined))
76
+ .filter((entry) => Boolean(entry))
77
+ .join("\n");
78
+ throw new Error(text || "Cursor tool replay failed");
79
+ }
80
+ return {
81
+ content: cursorDisplay.result.content,
82
+ details: cursorDisplay.result.details,
83
+ terminate: cursorDisplay.terminate ?? true,
84
+ };
85
+ }
86
+ if (strategy?.missingReplayPolicy === "block-file-mutation" && isCursorReplayToolCallId(toolCallId)) {
87
+ throw new Error(`No recorded Cursor ${definition.name} result was available. This replay-only call does not execute file mutations.`);
88
+ }
89
+ return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx);
90
+ },
91
+ renderCall(args, theme, context) {
92
+ const currentRenderCall = getCurrentDefinition().renderCall;
93
+ const renderBase = () => currentRenderCall?.(args, theme, context) ?? emptyText();
94
+ const isReplayCall = typeof context.toolCallId === "string" && isCursorReplayToolCallId(context.toolCallId);
95
+ if (isReplayCall && strategy?.renderReplayCall) {
96
+ return strategy.renderReplayCall(args, theme, context, renderBase);
97
+ }
98
+ return renderBase();
99
+ },
100
+ renderResult(result, options, theme, context) {
101
+ const currentRenderResult = getCurrentDefinition().renderResult;
102
+ const renderBase = () => currentRenderResult?.(result, options, theme, context) ?? emptyText();
103
+ const isReplayCall = typeof context.toolCallId === "string" && isCursorReplayToolCallId(context.toolCallId);
104
+ if (isReplayCall && strategy?.renderReplayResult) {
105
+ return strategy.renderReplayResult(result, options, theme, context, renderBase);
106
+ }
107
+ return renderBase();
108
+ },
109
+ };
110
+ }
111
+ export function createNativeCursorToolDefinition(toolName, cwd) {
112
+ const strategy = getNativeReplayStrategy(toolName);
113
+ if (strategy)
114
+ return strategy.createDefinition(cwd);
115
+ if (isCursorReplayToolName(toolName))
116
+ return createCursorReplayOnlyToolDefinition(toolName);
117
+ throw new Error(`Unsupported Cursor native replay tool: ${toolName}`);
118
+ }
119
+ export function registerNativeCursorTool(pi, toolName) {
120
+ const definition = createNativeCursorToolDefinition(toolName, getCursorSessionCwd());
121
+ pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, getCursorSessionCwd())));
122
+ }
123
+ export { CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES, CURSOR_REPLAY_TOOL_NAMES };
@@ -0,0 +1,11 @@
1
+ import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME } from "./cursor-tool-presentation-registry.js";
2
+ export const CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME];
3
+ export const CURSOR_REPLAY_TOOL_NAMES = [CURSOR_REPLAY_ACTIVITY_TOOL_NAME];
4
+ export const BUILTIN_NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
5
+ export const NATIVE_CURSOR_TOOL_NAMES = [
6
+ ...BUILTIN_NATIVE_CURSOR_TOOL_NAMES,
7
+ ...CURSOR_REPLAY_TOOL_NAMES,
8
+ ];
9
+ export function isNativeCursorToolName(toolName) {
10
+ return NATIVE_CURSOR_TOOL_NAMES.some((nativeToolName) => nativeToolName === toolName);
11
+ }